[
  {
    "path": ".claude/commands/add-tests.md",
    "content": "---\ndescription: 为代码添加单元测试（支持增量和存量代码测试补齐）\n---\n\n# 单元测试生成\n\n## 用法\n- `/add-tests [dir]` - 为指定模块或文件添加单元测试\n  - `[dir]` 可选参数：包名（以 @ 开头）、文件路径或目录路径\n  - 不指定 `[dir]` 则默认为全代码库（会提示用户确认）\n  - 执行后会询问用户选择：增量代码测试（基于 git diff）或存量代码测试补齐\n\n## 命令说明\n\n此命令用于自动生成和补充单元测试，确保代码质量。FlowGram 使用 **Vitest** 作为测试框架。\n\n### 测试覆盖率目标\n\n根据包的类型和重要性，测试覆盖率要求如下：\n\n- **核心引擎层**（canvas-engine、node-engine、variable-engine、runtime）\n  - 覆盖率目标：≥ 85%\n  - 包括：@flowgram.ai/core、@flowgram.ai/form、@flowgram.ai/variable-core、@flowgram.ai/runtime-js 等\n\n- **插件和客户端层**（plugins、client）\n  - 覆盖率目标：≥ 60%\n  - 包括：@flowgram.ai/editor、各类 plugin 包、@flowgram.ai/fixed-layout-editor 等\n\n- **工具和示例**（common、apps）\n  - 覆盖率目标：尽可能覆盖关键逻辑\n  - 包括：@flowgram.ai/utils、demo 应用等\n\n### 测试文件组织\n\n- 测试文件位置：\n  - `__tests__/` 目录（推荐）\n  - 或与源文件同级的 `*.test.ts`/`*.test.tsx` 文件\n- 命名规范：\n  - 对于 `src/core/utils.ts`，测试文件为 `__tests__/core/utils.test.ts` 或 `src/core/utils.test.ts`\n\n## 测试生成流程\n\n### 0. 命令执行和用户确认\n\n1. **确认范围**：\n   - 如果未指定 `[dir]`，询问用户是否要对全代码库操作，还是指定具体目录\n   - 全代码库操作工作量巨大，需要用户明确确认\n\n2. **选择模式**：\n   - 询问用户选择测试模式：\n     - **增量代码测试**：仅为 git diff 中的新增/修改代码添加测试\n     - **存量代码测试补齐**：扫描所有代码，补齐缺失或覆盖率不足的测试\n\n### 1. 识别待测代码\n\n**增量代码模式**：\n```bash\n# 检查 git diff 获取所有修改的文件\ngit diff --name-only\ngit diff <file>  # 查看具体变更\n```\n\n**存量代码模式**：\n- 扫描指定目录下所有源文件（排除已有完整测试的文件）\n- 查找缺少测试或覆盖率不足的文件\n- 优先处理核心引擎层的文件\n\n### 2. 确定包信息和覆盖率目标\n\n1. 从最近的 `package.json` 获取包名\n2. 使用包名在 `rush.json` 中查找包的分类（projectFolder）\n3. 根据包所在目录确定覆盖率目标：\n   - `packages/canvas-engine/`、`packages/node-engine/`、`packages/variable-engine/`、`packages/runtime/` → 85%\n   - `packages/plugins/`、`packages/client/` → 60%\n   - `packages/common/`、`apps/` → 尽可能覆盖\n\n### 3. 生成测试代码\n\n**测试重点**：\n- 新增或修改的函数、方法、类\n- 分支逻辑（if/else、switch/case）\n- 边界条件和异常处理\n- 依赖注入容器（inversify）的模拟\n- 响应式状态（ReactiveState）的行为验证\n\n**Vitest 最佳实践**：\n```typescript\nimport { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';\nimport { render, screen } from '@testing-library/react';\n\ndescribe('ModuleName', () => {\n  beforeEach(() => {\n    // 初始化\n  });\n\n  it('should handle specific case', () => {\n    // 测试逻辑\n    expect(result).toBe(expected);\n  });\n});\n```\n\n**React 组件测试**：\n- 使用 `@testing-library/react` 进行组件测试\n- 为关键元素添加 `data-testid` 属性\n- 测试用户交互和状态变化\n\n**依赖注入测试**：\n- 使用 `vi.mock()` 模拟依赖\n- 创建测试容器来验证服务注册\n\n### 4. 执行测试验证\n\n**单包测试**：\n```bash\ncd packages/canvas-engine/core\nrushx test          # 运行测试\nrushx test:cov      # 生成覆盖率报告\n```\n\n**全局测试**：\n```bash\nrush test           # 运行所有包的测试\nrush test:cov       # 生成所有包的覆盖率报告\n```\n\n**每次添加测试后**：\n1. 立即运行测试确保通过\n2. 检查覆盖率是否达到目标\n3. 修复失败的测试或调整测试用例\n4. 继续处理下一个文件\n\n### 5. 输出测试文件\n\n- 将测试文件保存到 `__tests__/` 目录（优先）或源文件同级\n- 保持目录结构与源代码一致\n- 添加必要的导入和类型声明\n\n## 示例命令\n\n```bash\n# 为某个包添加测试（执行后会询问增量或存量）\n/add_tests @flowgram.ai/core\n\n# 为特定目录添加测试\n/add_tests packages/node-engine/form\n\n# 为单个文件添加测试\n/add_tests packages/canvas-engine/core/src/core/utils.ts\n\n# 全代码库测试（会先确认范围，再询问增量或存量）\n/add_tests\n```\n\n### 典型使用场景\n\n**场景 1：为新功能添加测试**\n```bash\n/add_tests packages/plugins/my-new-plugin\n# 选择：增量代码测试\n# 结果：仅为 git diff 中的新代码生成测试\n```\n\n**场景 2：提升现有包的测试覆盖率**\n```bash\n/add_tests @flowgram.ai/variable-core\n# 选择：存量代码测试补齐\n# 结果：扫描所有代码，补齐缺失的测试，目标 85% 覆盖率\n```\n\n**场景 3：全面测试检查**\n```bash\n/add_tests\n# 确认：选择要处理的目录或全代码库\n# 选择：存量代码测试补齐\n# 结果：系统性地补齐整个项目的测试\n```\n\n## 注意事项\n\n1. **优先级**：优先为核心引擎层包编写高质量测试\n2. **隔离性**：每个测试应该独立，不依赖其他测试的执行顺序\n3. **可读性**：测试用例命名应清晰描述测试场景（使用中文或英文皆可）\n4. **Mock 策略**：\n   - 外部依赖（网络请求、文件系统）必须 mock\n   - 内部复杂模块可以考虑 mock\n   - 简单工具函数可以直接使用\n5. **快照测试**：谨慎使用快照测试，仅用于稳定的 UI 或数据结构\n6. **异步测试**：使用 async/await 处理异步操作，确保 Promise 正确解决\n\n## 工作流程总结\n\n1. **接收命令**：用户执行 `/add_tests [dir]`\n2. **确认范围**：如果未指定 dir，询问用户要处理全代码库还是指定目录\n3. **选择模式**：询问用户选择增量代码测试或存量代码测试补齐\n4. **分析代码**：根据选择的模式识别待测代码\n5. **确定目标**：根据包的分类确定覆盖率目标\n6. **生成测试**：逐文件生成测试用例\n7. **运行验证**：每生成一批测试后立即运行验证\n8. **修复问题**：修复失败的测试或调整测试用例\n9. **检查覆盖率**：查看覆盖率报告是否达标\n10. **继续迭代**：直到达到目标覆盖率或所有文件都有测试\n\n开始生成测试吧！\n"
  },
  {
    "path": ".claude/skills/create-node/SKILL.md",
    "content": "---\nskill_name: create-node\ndescription: 用于在 FlowGram demo-free-layout 中创建新的自定义节点，支持简单节点（自动表单）和复杂节点（自定义 UI）\nversion: 1.0.0\ntags: [flowgram, node, workflow, custom-node]\n---\n\n# FlowGram Custom Node Development\n\n## 概述\n\n本 SKILL 用于指导在 FlowGram 项目的 `apps/demo-free-layout/src/nodes` 目录下创建新的自定义工作流节点。\n\n## 核心概念\n\n### 节点数据结构\n\n节点数据在保存时会存储到后端，基本结构如下：\n\n```typescript\n{\n  id: 'node_xxxxx',           // 节点 ID\n  type: 'node_type',          // 节点类型\n  data: {\n    title: 'Node Title',      // 节点标题\n    inputsValues: { ... },    // 节点表单字段的初始值（实际的值）\n    inputs: { ... },          // 节点表单的 JSON Schema（定义表单结构）\n    outputs: { ... },         // 节点输出的 JSON Schema（工作流执行时的输出）\n    // ... 其他自定义字段\n  }\n}\n```\n\n### 三个核心字段\n\n#### 1. `data.inputsValues` - 节点表单字段的初始值\n\n存储表单中各个字段的实际值，每个字段值包含 `type` 和 `content` 两个属性：\n\n```typescript\ninputsValues: {\n  url: {\n    type: 'constant',          // 常量类型\n    content: 'https://...',    // 实际的值\n  },\n  prompt: {\n    type: 'template',          // 模板类型（支持变量引用）\n    content: 'Hello {var}',    // 可以引用变量\n  },\n}\n```\n\n**`type` 的可选值**：\n- `'constant'`：常量值，不支持变量引用\n- `'template'`：模板值，支持 `{variableName}` 语法引用变量\n- `'variable'`：变量引用\n\n#### 2. `data.inputs` - 节点表单的 JSON Schema\n\n使用 JSON Schema 定义表单的结构，系统会根据这个 Schema 自动生成表单界面：\n\n```typescript\ninputs: {\n  type: 'object',\n  required: ['url'],           // 必填字段\n  properties: {\n    url: {\n      type: 'string',\n    },\n    timeout: {\n      type: 'number',\n      minimum: 0,\n      maximum: 60000,\n    },\n    prompt: {\n      type: 'string',\n      extra: {\n        formComponent: 'prompt-editor',  // 指定自定义组件\n      },\n    },\n  },\n}\n```\n\n#### 3. `data.outputs` - 节点输出的 JSON Schema\n\n定义节点在工作流执行时的输出数据结构，供下游节点使用：\n\n```typescript\noutputs: {\n  type: 'object',\n  properties: {\n    body: { type: 'string' },\n    statusCode: { type: 'number' },\n    headers: { type: 'object' },\n  },\n}\n```\n\n### 三者的关系\n\n```\ninputs (JSON Schema)     →  定义表单结构\ninputsValues (实际值)    →  存储表单数据\n[节点执行]\noutputs (JSON Schema)    →  定义输出结构\n```\n\n### 字段类型与自动组件映射\n\n在简单节点中，字段类型会自动匹配对应的表单组件：\n\n| 字段类型 | `extra.formComponent` | 默认组件 |\n|---------|---------------------|---------|\n| `string` | - | Input |\n| `string` | `'prompt-editor'` | PromptEditorWithVariables |\n| `number` | - | InputNumber |\n| `boolean` | - | Switch |\n| `object` | - | JsonCodeEditor |\n| `array` | - | JsonCodeEditor |\n\n## 节点开发模式\n\n### 1. 简单节点（自动表单模式）\n\n- **适用场景**：节点配置较为简单，不需要复杂的自定义 UI\n- **特点**：根据 `inputs` Schema 自动生成表单\n- **示例**：LLM 节点\n- **文件结构**：只需要 `index.ts` 文件\n- **模板位置**：`./templates/simple-node/index.ts`\n\n### 2. 复杂节点（自定义 UI 模式）\n\n- **适用场景**：需要自定义表单布局、特殊交互或复杂的 UI 组件\n- **特点**：完全控制表单渲染和交互逻辑\n- **示例**：HTTP 节点\n- **文件结构**：\n  ```\n  {节点名}/\n  ├── index.tsx                # 节点注册配置\n  ├── form-meta.tsx            # 自定义表单渲染\n  ├── types.tsx                # TypeScript 类型定义\n  └── components/              # 自定义组件\n      └── *.tsx\n  ```\n- **模板位置**：`./templates/complex-node/`\n\n## 开发流程\n\n### Step 1: 规划节点\n\n确定节点的核心信息：\n- **节点类型 ID**：唯一标识，如 `database`、`webhook`\n- **节点功能**：明确节点要做什么\n- **输入参数**：节点需要哪些配置项\n- **输出数据**：节点执行后返回什么数据\n- **UI 复杂度**：是否需要自定义 UI\n\n### Step 2: 选择开发模式\n\n```\n是否需要自定义 UI？\n├─ 否 → 使用简单节点模式（复制 templates/simple-node/）\n└─ 是 → 使用复杂节点模式（复制 templates/complex-node/）\n```\n\n### Step 3: 复制模板并修改\n\n#### 简单节点\n\n```bash\n# 复制模板\ncp .claude/skills/create-node/templates/simple-node/index.ts \\\n   apps/demo-free-layout/src/nodes/{节点名}/index.ts\n\n# 修改模板中的 TODO 标记\n# - {NODE_NAME} → 节点名（PascalCase）\n# - {NODE_TYPE} → 节点类型枚举值\n# - {node_name} → 节点名（kebab-case）\n# - {node_type} → 节点类型（小写）\n```\n\n#### 复杂节点\n\n```bash\n# 复制模板目录\ncp -r .claude/skills/create-node/templates/complex-node \\\n      apps/demo-free-layout/src/nodes/{节点名}\n\n# 修改所有文件中的 TODO 标记\n```\n\n### Step 4: 添加节点类型常量\n\n编辑 `apps/demo-free-layout/src/nodes/constants.ts`：\n\n```typescript\nexport enum WorkflowNodeType {\n  // ... 现有节点\n  {节点类型} = '{节点类型}',\n}\n```\n\n### Step 5: 注册节点\n\n编辑 `apps/demo-free-layout/src/nodes/index.ts`：\n\n```typescript\n// 导入节点\nexport { {节点名}NodeRegistry } from './{节点名}';\n\n// 添加到注册列表\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  // ... 现有节点\n  {节点名}NodeRegistry,\n];\n```\n\n### Step 6: 准备节点图标\n\n在 `apps/demo-free-layout/src/assets/` 目录下添加节点图标（SVG 或 JPG 格式）：\n\n```\napps/demo-free-layout/src/assets/icon-{节点名}.svg\n```\n\n### Step 7: 测试验证\n\n```bash\n# 启动开发服务器\nrush dev:demo-free-layout\n\n# 在浏览器中测试节点功能\n```\n\n## 常用组件和工具\n\n### FlowGram 组件\n\n从 `@flowgram.ai/form-materials` 导入：\n\n```typescript\nimport {\n  PromptEditorWithVariables,  // 带变量的提示词编辑器\n  VariableSelector,            // 变量选择器\n  JsonCodeEditor,              // JSON 代码编辑器\n  CodeEditor,                  // 代码编辑器\n  DisplayOutputs,              // 输出字段展示\n  DynamicValueInput,           // 动态值输入\n  createInferInputsPlugin,     // 输入推断插件\n} from '@flowgram.ai/form-materials';\n```\n\n### Semi UI 组件\n\n从 `@douyinfe/semi-ui` 导入：\n\n```typescript\nimport {\n  Input,\n  InputNumber,\n  Select,\n  Switch,\n  Button,\n  Divider,\n} from '@douyinfe/semi-ui';\n```\n\n### 表单工具\n\n```typescript\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { FormItem, FormHeader, FormContent } from '../../form-components';\nimport { useNodeRenderContext } from '../../hooks';\n```\n\n## 最佳实践\n\n### 1. 节点设计\n\n- **单一职责**：一个节点只做一件事\n- **清晰的 Schema**：明确定义 inputs 和 outputs\n- **合理的默认值**：提供有意义的初始配置\n- **友好的描述**：为节点和字段提供清晰的描述\n\n### 2. Schema 设计\n\n```typescript\n// ✅ 好的做法：清晰的 Schema\ninputs: {\n  type: 'object',\n  required: ['url', 'method'],\n  properties: {\n    url: {\n      type: 'string',\n      description: 'API endpoint URL',\n    },\n    method: {\n      type: 'string',\n      enum: ['GET', 'POST', 'PUT', 'DELETE'],\n    },\n  },\n}\n\n// ❌ 不好的做法：缺少约束\ninputs: {\n  type: 'object',\n  properties: {\n    url: { type: 'string' },\n    method: { type: 'string' },\n  },\n}\n```\n\n### 3. 表单组件使用\n\n```typescript\n// ✅ 好的做法：使用 Field 绑定表单状态\n<Field<string> name=\"api.url\">\n  {({ field }) => (\n    <Input\n      value={field.value}\n      onChange={(value) => field.onChange(value)}\n    />\n  )}\n</Field>\n\n// ❌ 不好的做法：手动管理状态\nconst [url, setUrl] = useState('');\n<Input value={url} onChange={setUrl} />\n```\n\n### 4. 只读状态处理\n\n```typescript\nexport function CustomComponent() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <Input disabled={readonly} {...props} />\n  );\n}\n```\n\n## 常见问题\n\n### Q1: 如何选择简单节点还是复杂节点？\n\n**判断标准**：\n- 字段简单 + 默认布局满足需求 → 简单节点\n- 需要自定义布局/特殊交互 → 复杂节点\n\n### Q2: 如何使用变量功能？\n\n在 `inputs` Schema 中使用 `formComponent: 'prompt-editor'`，并在 `inputsValues` 中使用 `type: 'template'`。\n\n### Q3: 如何定义必填字段？\n\n在 `inputs` Schema 的 `required` 数组中列出必填字段名。\n\n### Q4: `inputsValues` 和 `inputs` 必须一致吗？\n\n是的。`inputsValues` 中的字段必须在 `inputs.properties` 中有对应的定义。\n\n### Q5: 节点图标支持什么格式？\n\n支持 SVG、JPG、PNG 格式，推荐使用 SVG。\n\n### Q6: 如何调试节点？\n\n1. 使用浏览器开发者工具查看 console.log\n2. 在 FormRender 组件中添加 `console.log(form.getValues())`\n3. 使用 React DevTools 查看组件状态\n\n## 参考资源\n\n### 代码示例\n\n- **简单节点**: `apps/demo-free-layout/src/nodes/llm/`\n- **复杂节点**: `apps/demo-free-layout/src/nodes/http/`\n- **表单组件**: `apps/demo-free-layout/src/form-components/`\n- **默认表单**: `apps/demo-free-layout/src/nodes/default-form-meta.tsx`\n\n### 模板文件\n\n- **简单节点模板**: `.claude/skills/create-node/templates/simple-node/`\n- **复杂节点模板**: `.claude/skills/create-node/templates/complex-node/`\n\n### 相关文档\n\n- FlowGram 官方文档: https://flowgram.ai\n- JSON Schema 规范: https://json-schema.org/\n- Semi UI 组件库: https://semi.design/\n\n### 开发命令\n\n```bash\n# 启动开发服务器\nrush dev:demo-free-layout\n\n# 构建项目\nrush build\n\n# 类型检查\nrush ts-check\n\n# 代码检查\nrush lint\n```\n\n## 快速开始检查清单\n\n创建新节点时，按照此检查清单执行：\n\n- [ ] 规划节点功能和数据结构\n- [ ] 选择开发模式（简单 vs 复杂）\n- [ ] 复制对应的模板文件\n- [ ] 修改模板中的 TODO 标记\n- [ ] 在 `constants.ts` 中添加节点类型\n- [ ] 在 `index.ts` 中注册节点\n- [ ] 准备节点图标文件\n- [ ] 启动开发服务器测试\n- [ ] 验证节点功能正常\n"
  },
  {
    "path": ".claude/skills/create-node/templates/README.md",
    "content": "# Node Templates\n\n这些是创建新节点的模板文件，使用时需要替换其中的占位符。\n\n## 占位符说明\n\n在使用模板时，需要将以下占位符替换为实际值：\n\n| 占位符 | 说明 | 示例 |\n|-------|------|------|\n| `{NODE_NAME}` | 节点名称（PascalCase） | `Database`, `Webhook`, `EmailSender` |\n| `{NODE_TYPE}` | 节点类型枚举值（SCREAMING_SNAKE_CASE） | `DATABASE`, `WEBHOOK`, `EMAIL_SENDER` |\n| `{node_name}` | 节点名称（kebab-case，用于 ID 前缀） | `database`, `webhook`, `email_sender` |\n| `{node_type}` | 节点类型（小写，用于 type 字段） | `database`, `webhook`, `email_sender` |\n| `{节点功能描述}` | 节点的功能描述（中文） | `发送邮件`, `查询数据库`, `调用 Webhook` |\n\n## 使用方法\n\n### 简单节点\n\n```bash\n# 1. 复制模板\ncp .claude/skills/create-node/templates/simple-node/index.ts \\\n   apps/demo-free-layout/src/nodes/database/index.ts\n\n# 2. 替换占位符\n# {NODE_NAME} → Database\n# {NODE_TYPE} → DATABASE\n# {node_name} → database\n# {node_type} → database\n# {节点功能描述} → 查询数据库\n```\n\n### 复杂节点\n\n```bash\n# 1. 复制模板目录\ncp -r .claude/skills/create-node/templates/complex-node \\\n      apps/demo-free-layout/src/nodes/webhook\n\n# 2. 替换所有文件中的占位符\n# {NODE_NAME} → Webhook\n# {NODE_TYPE} → WEBHOOK\n# {node_name} → webhook\n# {node_type} → webhook\n# {节点功能描述} → 调用 Webhook\n```\n\n## 快速替换脚本（可选）\n\n如果需要批量替换，可以使用以下命令（macOS/Linux）：\n\n```bash\n# 设置变量\nNODE_NAME=\"Database\"\nNODE_TYPE=\"DATABASE\"\nnode_name=\"database\"\nnode_type=\"database\"\ndescription=\"查询数据库\"\n\n# 批量替换\nfind apps/demo-free-layout/src/nodes/database -type f -name \"*.ts*\" -exec sed -i '' \\\n  -e \"s/{NODE_NAME}/$NODE_NAME/g\" \\\n  -e \"s/{NODE_TYPE}/$NODE_TYPE/g\" \\\n  -e \"s/{node_name}/$node_name/g\" \\\n  -e \"s/{node_type}/$node_type/g\" \\\n  -e \"s/{节点功能描述}/$description/g\" \\\n  {} +\n```\n"
  },
  {
    "path": ".claude/skills/create-node/templates/complex-node/components/custom-component.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { Input, Select } from '@douyinfe/semi-ui';\n\nimport { useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\n/**\n * 自定义表单组件\n */\nexport function CustomComponent() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <div>\n      <FormItem name=\"配置项名称\" required vertical type=\"string\">\n        <Field<string> name=\"customConfig.key\" defaultValue=\"\">\n          {({ field }) => (\n            <Input\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              disabled={readonly}\n              placeholder=\"请输入...\"\n            />\n          )}\n        </Field>\n      </FormItem>\n\n      {/* TODO: 添加更多表单字段 */}\n      <FormItem name=\"选择器示例\" vertical type=\"string\">\n        <Field<string> name=\"customConfig.option\" defaultValue=\"option1\">\n          {({ field }) => (\n            <Select\n              value={field.value}\n              onChange={(value) => field.onChange(value as string)}\n              disabled={readonly}\n              style={{ width: '100%' }}\n              optionList={[\n                { label: '选项 1', value: 'option1' },\n                { label: '选项 2', value: 'option2' },\n                { label: '选项 3', value: 'option3' },\n              ]}\n            />\n          )}\n        </Field>\n      </FormItem>\n    </div>\n  );\n}\n"
  },
  {
    "path": ".claude/skills/create-node/templates/complex-node/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { DisplayOutputs } from '@flowgram.ai/form-materials';\nimport { Divider } from '@douyinfe/semi-ui';\n\nimport { FormHeader, FormContent } from '../../form-components';\nimport { {NODE_NAME}NodeJSON } from './types';\nimport { CustomComponent } from './components/custom-component';\nimport { defaultFormMeta } from '../default-form-meta';\n\n/**\n * 表单渲染组件\n */\nexport const FormRender = ({ form }: FormRenderProps<{NODE_NAME}NodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      {/* TODO: 添加自定义组件 */}\n      <CustomComponent />\n      <Divider />\n      {/* 显示节点输出 */}\n      <DisplayOutputs displayFromScope />\n    </FormContent>\n  </>\n);\n\n/**\n * 表单配置\n */\nexport const formMeta: FormMeta = {\n  render: (props) => <FormRender {...props} />,\n  effect: defaultFormMeta.effect,\n  plugins: [\n    // TODO: 根据需要添加插件\n    // createInferInputsPlugin({ sourceKey: 'xxxValues', targetKey: 'xxx' }),\n  ],\n};\n"
  },
  {
    "path": ".claude/skills/create-node/templates/complex-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconNode from '../../assets/icon-{NODE_NAME}.svg'; // TODO: 准备图标文件\nimport { formMeta } from './form-meta';\n\nlet index = 0;\n\nexport const {NODE_NAME}NodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.{NODE_TYPE}, // TODO: 在 constants.ts 中定义\n  info: {\n    icon: iconNode,\n    description: '{节点功能描述}', // TODO: 修改描述\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    return {\n      id: `{node_name}_${nanoid(5)}`, // TODO: 修改前缀\n      type: '{node_type}', // TODO: 与 WorkflowNodeType 保持一致\n      data: {\n        title: `{NODE_NAME}_${++index}`, // TODO: 修改标题前缀\n\n        // TODO: 根据实际需求定义自定义字段\n        customConfig: {\n          key: 'value',\n        },\n\n        // 节点输出数据的 JSON Schema\n        outputs: {\n          type: 'object',\n          properties: {\n            // TODO: 定义节点执行后的输出结构\n            result: { type: 'string' },\n            status: { type: 'number' },\n          },\n        },\n      },\n    };\n  },\n  formMeta: formMeta, // 引入自定义表单\n};\n"
  },
  {
    "path": ".claude/skills/create-node/templates/complex-node/types.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeJSON } from '../../typings';\n\n/**\n * 节点数据类型定义\n */\nexport interface {NODE_NAME}NodeJSON extends FlowNodeJSON {\n  data: FlowNodeJSON['data'] & {\n    // TODO: 根据实际需求定义自定义字段类型\n    customConfig?: {\n      key: string;\n      // 其他自定义字段\n    };\n  };\n}\n"
  },
  {
    "path": ".claude/skills/create-node/templates/simple-node/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconNode from '../../assets/icon-{NODE_NAME}.svg'; // TODO: 准备图标文件\n\nlet index = 0;\n\nexport const {NODE_NAME}NodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.{NODE_TYPE}, // TODO: 在 constants.ts 中定义\n  info: {\n    icon: iconNode,\n    description: '{节点功能描述}', // TODO: 修改描述\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    // 返回节点的初始数据，这些数据会被保存到后端\n    return {\n      id: `{node_name}_${nanoid(5)}`, // TODO: 修改前缀\n      type: '{node_type}', // TODO: 与 WorkflowNodeType 保持一致\n      data: {\n        title: `{NODE_NAME}_${++index}`, // TODO: 修改标题前缀\n\n        // 节点表单字段的初始值\n        inputsValues: {\n          // TODO: 根据实际需求定义字段初始值\n          field1: {\n            type: 'constant',      // 常量类型\n            content: '默认值',      // 字段的初始值\n          },\n          field2: {\n            type: 'constant',\n            content: 100,\n          },\n          promptField: {\n            type: 'template',      // 支持变量的模板类型\n            content: '',\n          },\n        },\n\n        // 节点表单的 JSON Schema（定义表单结构）\n        inputs: {\n          type: 'object',\n          required: ['field1'], // TODO: 定义必填字段\n          properties: {\n            // TODO: 根据实际需求定义字段 Schema\n            field1: {\n              type: 'string',\n              // 使用默认 Input 组件\n            },\n            field2: {\n              type: 'number',\n              minimum: 0,\n              maximum: 100,\n            },\n            promptField: {\n              type: 'string',\n              // 使用 PromptEditorWithVariables 组件\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            booleanField: {\n              type: 'boolean',\n              // 使用 Switch 组件\n            },\n            objectField: {\n              type: 'object',\n              // 使用 JsonCodeEditor 组件\n            },\n          },\n        },\n\n        // 节点输出数据的 JSON Schema（工作流执行时的输出）\n        outputs: {\n          type: 'object',\n          properties: {\n            // TODO: 定义节点执行后的输出结构\n            result: { type: 'string' },\n            status: { type: 'number' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": ".claude/skills/material-component-dev/SKILL.md",
    "content": "---\nskill_name: material-component-dev\ndescription: FlowGram 物料组件开发指南 - 用于在 form-materials 包中创建新的物料组件\nversion: 1.0.0\ntags: [flowgram, material, component, development]\n---\n\n# FlowGram Material Component Development\n\n## 概述\n\n本 SKILL 用于指导在 FlowGram 项目的 `@flowgram.ai/form-materials` 包中创建新的物料组件。\n\n## 核心原则\n\n### 1. 组件位置\n- ✅ **在现有包中创建**：直接在 `packages/materials/form-materials/src/components/` 下创建组件目录\n- ❌ **不要单独拆包**：不创建新的 npm 包，保持简洁\n\n### 2. 代码质量\n- ✅ **使用 named export**：所有导出使用 named export 提高 tree shake 性能\n- ❌ **不写单元测试**：通过 Storybook 进行手动测试\n- ✅ **通过类型检查**：必须通过 `yarn ts-check`\n- ✅ **符合代码规范**：遵循项目 ESLint 规则\n\n### 3. 物料设计\n- ✅ **保持精简**：只保留必要的 props，不添加非核心功能配置项\n- ✅ **功能单一**：一个物料只做一件事\n- ✅ **使用内部依赖**：优先使用 FlowGram 内部的组件和类型\n\n### 4. 技术栈\n- **UI 组件库**：`@douyinfe/semi-ui`\n- **代码编辑器**：`JsonCodeEditor`, `CodeEditor` 等来自 `../code-editor`\n- **类型定义**：`IJsonSchema` 来自 `@flowgram.ai/json-schema`（不使用外部的 `json-schema` 包）\n- **React**：必须显式 `import React` 避免 UMD 全局引用错误\n\n## 开发流程\n\n### Step 1: 规划组件结构\n\n确定组件的：\n- **功能**：组件要解决什么问题\n- **Props 接口**：只保留核心必需的 props\n- **命名**：使用 PascalCase，清晰描述功能\n\n### Step 2: 创建目录结构\n\n```bash\nmkdir -p packages/materials/form-materials/src/components/{组件名}/utils\n```\n\n典型结构：\n```\npackages/materials/form-materials/src/components/{组件名}/\n├── index.tsx                 # 导出文件 (named export)\n├── {组件名}.tsx              # 主组件\n├── {辅助组件}.tsx            # 可选的辅助组件\n└── utils/                    # 可选的工具函数\n    └── *.ts\n```\n\n### Step 3: 实现组件\n\n#### 3.1 工具函数（如需要）\n\n```typescript\n// utils/helper.ts\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function helperFunction(input: string): Output {\n  // 实现逻辑\n}\n```\n\n#### 3.2 辅助组件（如需要）\n\n```typescript\n// modal.tsx\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\nimport { Modal, Typography } from '@douyinfe/semi-ui';\n\ninterface ModalProps {\n  visible: boolean;\n  onClose: () => void;\n  onConfirm: (data: SomeType) => void;\n}\n\nexport function MyModal({ visible, onClose, onConfirm }: ModalProps) {\n  // 实现\n}\n```\n\n#### 3.3 主组件\n\n```typescript\n// my-component.tsx\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\nimport { Button } from '@douyinfe/semi-ui';\nimport type { IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport interface MyComponentProps {\n  /** 核心功能的回调 */\n  onSomething?: (data: SomeType) => void;\n}\n\n// 使用 named export，不使用 default export\nexport function MyComponent({ onSomething }: MyComponentProps) {\n  const [visible, setVisible] = useState(false);\n\n  return (\n    <>\n      <Button onClick={() => setVisible(true)}>\n        操作文本\n      </Button>\n      {/* 其他组件 */}\n    </>\n  );\n}\n```\n\n**关键点**：\n- ✅ 显式 `import React`\n- ✅ 使用 Semi UI 组件\n- ✅ 使用 function 声明而非 React.FC\n- ✅ Props 精简，只保留核心功能\n- ✅ Named export\n\n#### 3.4 导出文件\n\n```typescript\n// index.tsx\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { MyComponent } from './my-component';\nexport type { MyComponentProps } from './my-component';\n```\n\n### Step 4: 在 form-materials 主入口导出\n\n编辑 `packages/materials/form-materials/src/components/index.ts`：\n\n```typescript\nexport {\n  // ... 其他组件按字母序\n  MyComponent,\n  // ... 继续其他组件\n  type MyComponentProps,\n  // ... 继续其他类型\n} from './components';\n```\n\n然后编辑 `packages/materials/form-materials/src/index.ts`，确保新组件在主导出列表中：\n\n```typescript\nexport {\n  // ... 其他组件按字母序\n  MyComponent,\n  // ...\n  type MyComponentProps,\n  // ...\n} from './components';\n```\n\n### Step 5: 创建 Storybook Story\n\n在 `apps/demo-materials/src/stories/components/` 创建 Story：\n\n```typescript\n// my-component.stories.tsx\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\nimport type { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { MyComponent } from '@flowgram.ai/form-materials';\nimport type { SomeType } from '@flowgram.ai/json-schema';\n\nconst MyComponentDemo: React.FC = () => {\n  const [result, setResult] = useState<SomeType | null>(null);\n\n  return (\n    <div style={{ padding: '20px' }}>\n      <h2>My Component Demo</h2>\n      <MyComponent\n        onSomething={(data) => {\n          console.log('Generated data:', data);\n          setResult(data);\n        }}\n      />\n\n      {result && (\n        <div style={{ marginTop: '20px' }}>\n          <h3>结果:</h3>\n          <pre style={{\n            background: '#f5f5f5',\n            padding: '16px',\n            borderRadius: '4px',\n            overflow: 'auto'\n          }}>\n            {JSON.stringify(result, null, 2)}\n          </pre>\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst meta: Meta<typeof MyComponentDemo> = {\n  title: 'Form Components/MyComponent',\n  component: MyComponentDemo,\n  parameters: {\n    layout: 'centered',\n    docs: {\n      description: {\n        component: '组件功能描述',\n      },\n    },\n  },\n  tags: ['autodocs'],\n};\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n```\n\n### Step 6: 运行类型检查\n\n```bash\ncd packages/materials/form-materials\nyarn ts-check\n```\n\n确保通过所有类型检查。\n\n### Step 7: 启动开发环境测试\n\n开启两个 Terminal：\n\n**Terminal 1 - 监听包编译：**\n```bash\nrush build:watch\n```\n\n**Terminal 2 - 启动 Storybook：**\n```bash\ncd apps/demo-materials\nyarn dev\n```\n\n访问 http://localhost:6006/，找到你的组件进行测试。\n\n## 常见问题\n\n### Q1: React 引用错误\n\n**错误信息**：\n```\nerror TS2686: 'React' refers to a UMD global, but the current file is a module.\n```\n\n**解决方案**：\n在文件顶部添加：\n```typescript\nimport React from 'react';\n```\n\n### Q2: 组件未导出\n\n**错误信息**：\n```\nElement type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined.\n```\n\n**解决方案**：\n检查以下文件的导出：\n1. `components/{组件名}/index.tsx`\n2. `components/index.ts`\n3. `src/index.ts`\n\n### Q3: 类型找不到\n\n**错误信息**：\n```\nCannot find module '@flowgram.ai/json-schema' or its corresponding type declarations.\n```\n\n**解决方案**：\n- 使用 `type IJsonSchema` 而非 `type JSONSchema7`\n- 从 `@flowgram.ai/json-schema` 导入而非 `json-schema`\n\n### Q4: CodeEditor 没有 height 属性\n\n**错误信息**：\n```\nProperty 'height' does not exist on type 'CodeEditorPropsType'.\n```\n\n**解决方案**：\n使用外层 div 设置高度：\n```tsx\n<div style={{ minHeight: 300 }}>\n  <JsonCodeEditor value={value} onChange={onChange} />\n</div>\n```\n\n## 验收标准\n\n- [ ] 组件在 `packages/materials/form-materials/src/components/` 下创建\n- [ ] 使用 named export\n- [ ] 通过 `yarn ts-check` 类型检查\n- [ ] Props 精简，只保留核心功能\n- [ ] 在 Storybook 中可以正常显示和使用\n- [ ] 功能正常，无明显 bug\n- [ ] 代码符合 FlowGram 代码规范\n\n## 最佳实践\n\n### 1. 组件设计\n\n- **单一职责**：一个组件只做一件事\n- **Props 精简**：避免过度配置\n- **命名清晰**：组件名和 Props 名要清晰易懂\n\n### 2. 代码风格\n\n- **使用 TypeScript**：充分利用类型系统\n- **显式导入**：明确导入所需的依赖\n- **注释适度**：关键逻辑添加注释\n\n### 3. UI 一致性\n\n- **使用 Semi UI**：保持 UI 风格一致\n- **响应式设计**：考虑不同屏幕尺寸\n- **错误处理**：友好的错误提示\n\n### 4. 性能优化\n\n- **Named export**：支持 tree shaking\n- **按需加载**：避免不必要的依赖\n- **合理使用 memo**：必要时使用 React.memo\n\n## 示例参考\n\n完整示例请参考：\n- `packages/materials/form-materials/src/components/json-schema-creator/`\n- `apps/demo-materials/src/stories/components/json-schema-creator.stories.tsx`\n\n"
  },
  {
    "path": ".claude/skills/material-component-doc/SKILL.md",
    "content": "---\nname: material-component-doc\ndescription: 用于 FlowGram 物料库组件文档撰写的专用技能，提供组件文档生成、Story 创建、翻译等功能的指导和自动化支持\nmetadata:\n  version: \"1.1.0\"\n  category: \"documentation\"\n  language: \"zh-CN\"\n  framework: \"FlowGram\"\n---\n\n# FlowGram 文档的组织结构\n\n- **英文文档**: `apps/docs/src/en`\n- **中文文档**: `apps/docs/src/zh`\n- **Story 组件**: `apps/docs/components/form-materials/components`\n- **物料源码**: `packages/materials/form-materials/src/components`\n- **文档模板**: `./templates/material.mdx`\n\n# 组件物料文档撰写流程\n\n## 1. 源码定位\n\n在 `packages/materials/form-materials/src/components` 目录下确认物料源代码地址。\n\n**操作**：\n- 使用 Glob 工具搜索物料文件\n- 确认目录结构（是否有 hooks.ts, context.tsx 等）\n- 记录导出名称和文件路径\n\n## 2. 需求收集\n\n向用户询问物料使用实例和具体需求。\n\n**收集信息**：\n- 主要使用场景\n- 典型代码示例（1-2 个）\n- 特殊配置或高级用法\n- 是否需要配图\n\n## 3. 功能分析\n\n深入阅读源代码，理解物料功能。\n\n**分析要点**：\n- Props 接口（类型、默认值、描述）\n- 核心功能和实现方式\n- 依赖关系（FlowGram API、其他物料、第三方库）\n- Hooks 和 Context\n- 特殊逻辑（条件渲染、副作用等）\n\n## 4. Story 创建\n\n在 `apps/docs/components/form-materials/components` 下创建 Story 组件（详见下方 Story 规范）。\n\n## 5. 文档撰写\n\n基于模板 `./templates/material.mdx` 撰写完整文档。\n\n**文档位置**：\n- 中文：`apps/docs/src/zh/materials/components/{物料名称}.mdx`\n- 英文：`apps/docs/src/en/materials/components/{物料名称}.mdx`（翻译后）\n\n## 6. 质量检查\n\n**检查清单**：\n- [ ] Story 组件能正常运行\n- [ ] 代码示例准确无误\n- [ ] API 表格完整\n- [ ] 依赖链接正确可访问\n- [ ] 图片路径正确\n- [ ] Mermaid 流程图语法正确\n- [ ] CLI 命令路径准确\n\n**用户确认中文文档的撰写后，再执行翻译**。\n**用户确认中文文档的撰写后，再执行翻译**。\n**用户确认中文文档的撰写后，再执行翻译**。\n---\n\n# Story 组件规范\n\n> **参考示例**: `apps/docs/components/form-materials/components/variable-selector.tsx`\n\n## 命名规范\n\n**文件命名**: kebab-case，与物料名称一致\n- ✅ `variable-selector.tsx`\n- ✅ `dynamic-value-input.tsx`\n- ❌ `VariableSelector.tsx`\n\n**Story 导出命名**: PascalCase + \"Story\" 后缀\n- `BasicStory` - 基础使用（必需）\n- `WithSchemaStory` - 带 Schema 约束\n- `DisabledStory` - 禁用状态\n- `CustomFilterStory` - 自定义过滤\n- 根据物料特性命名，见名知意\n\n## 代码要求\n\n### 1. 懒加载导入\n\n```tsx\n// ✅ 正确\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\n// ❌ 错误\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n```\n\n### 2. 包装组件\n\n```tsx\n// ✅ 正确\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[]> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\n// ❌ 错误：缺少包装\nexport const BasicStory = () => (\n  <VariableSelector value={[]} onChange={() => {}} />\n);\n```\n\n### 3. 类型标注\n\n```tsx\n// ✅ 正确\n<Field<string[] | undefined> name=\"variable_selector\">\n\n// ❌ 错误\n<Field<any> name=\"variable_selector\">\n```\n\n### 4. 语言规范\n\n代码和注释只使用英文，无中文。\n\n## 完整示例\n\n```tsx\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const FilterSchemaStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                includeSchema={{ type: 'string' }}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n```\n\n---\n\n# 物料文档格式\n\n## 使用模板\n\n**模板文件**: `./templates/material.mdx`\n\n文档必须严格按照模板格式编写，包含以下章节：\n1. Import 语句\n2. 标题和简介（带可选配图）\n3. 案例演示（基本使用 + 高级用法）\n4. API 参考（Props 表格）\n5. 源码导读（目录结构、核心实现、流程图、依赖梳理）\n\n## 参考示例\n\n- [`dynamic-value-input.mdx`](apps/docs/src/zh/materials/components/dynamic-value-input.mdx) - 完整的流程图和依赖说明\n- [`variable-selector.mdx`](apps/docs/src/zh/materials/components/variable-selector.mdx) - 多个 API 表格和警告提示\n\n## 关键注意事项\n\n**API 表格要求**：\n- 必须包含所有公开的 Props\n- 类型使用反引号（如 \\`string\\`）\n- 描述清晰简洁\n- 多个相关类型分开列表\n\n**源码导读要求**：\n- 目录结构：展示文件列表及说明\n- 核心实现：用代码片段说明关键逻辑\n- 整体流程：Mermaid 流程图（推荐）\n- 依赖梳理：分类列出 FlowGram API、其他物料、第三方库\n\n---\n\n# 图片处理指南\n\n## 截图要求\n\n1. **时机**: Story 组件完成后，运行 docs 站点截图\n2. **内容**: 捕获物料的典型使用状态，清晰可见\n3. **格式**: PNG，适当压缩\n\n## 命名和存储\n\n- **命名**: `{物料名称}.png`（kebab-case）\n- **存储**: `apps/docs/src/public/materials/{物料名称}.png`\n- **引用**: `/materials/{物料名称}.png`\n\n## 在文档中使用\n\n```mdx\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/{物料名称}.png\" alt=\"{物料名称} 组件\" style={{ width: '50%' }} />\n</div>\n```\n\n---\n\n# 翻译流程\n\n## 翻译时机\n\n- ✅ 用户明确要求翻译\n- ✅ 中文文档已经用户审核确认\n- ❌ 文档还在修改中\n- ❌ 用户未确认最终版本\n\n## 翻译原则\n\n**术语一致性**：\n- ComponentName → ComponentName（组件名不翻译）\n- Props、Hook、Schema 等术语保持原文\n\n**代码不翻译**：\n- 所有代码块、命令、路径保持原样\n\n**链接处理**：\n- 内部链接：`/zh/` → `/en/`\n- 外部链接和 GitHub 链接：保持不变\n\n**格式保持**：\n- Markdown 格式、缩进、空行完全一致\n\n## 翻译检查清单\n\n- [ ] 标题和描述已翻译\n- [ ] 代码示例未被翻译\n- [ ] 命令和路径保持原样\n- [ ] 内部文档链接已更新\n- [ ] API 表格描述列已翻译\n- [ ] Mermaid 图中文节点已翻译\n- [ ] 术语使用一致\n\n---\n\n# 最佳实践\n\n## Props 提取技巧\n\n1. 查找 `interface` 或 `type` 定义\n2. 检查组件函数参数类型\n3. 查找 `defaultProps` 确认默认值\n4. 阅读 JSDoc 提取描述\n\n## 依赖分析方法\n\n1. 查看 import 语句（直接依赖）\n2. 分析 Hook 调用（FlowGram API）\n3. 查找组件引用（其他物料）\n4. 检查 package.json（第三方库）\n\n## Mermaid 流程图建议\n\n1. 简洁明了，关注核心流程\n2. 使用时序图绘制\n\n## 常见错误避免\n\n❌ 直接导入物料而不使用 `React.lazy`\n❌ API 表格遗漏 Props\n❌ 依赖链接失效\n❌ 中英文混用\n❌ 路径格式错误\n\n✅ 参考优秀示例\n✅ 仔细阅读源码\n✅ 验证所有链接\n✅ 保持语言和格式一致\n✅ 使用项目约定的路径格式\n\n---\n\n# 相关工具和资源\n\n## 开发命令\n\n```bash\n# 启动文档站点\nrush dev:docs\n\n# 查看修改\ngit diff\ngit diff --cached\n```\n\n## 关键目录\n\n| 目录 | 说明 |\n|------|------|\n| `packages/materials/form-materials/src/components` | 物料源码 |\n| `apps/docs/src/zh/materials/components` | 中文文档 |\n| `apps/docs/src/en/materials/components` | 英文文档 |\n| `apps/docs/components/form-materials/components` | Story 组件 |\n| `apps/docs/src/public/materials` | 图片资源 |\n| `./templates` | 文档模板 |\n\n"
  },
  {
    "path": ".claude/skills/material-component-doc/templates/material.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithSchemaStory } from 'components/form-materials/components/{物料名称}';\n\n# ComponentName\n\nComponentName 是一个用于...的组件，它支持...功能。[用 1-2 段文字描述物料的核心功能、使用场景和主要特性]\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/{物料名称}.png\" alt=\"ComponentName 组件\" style={{ width: '50%' }} />\n</div>\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { ComponentName } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<ValueType> name=\"field_name\">\n        {({ field }) => (\n          <ComponentName\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 高级用法示例（根据物料特性添加）\n\n<WithSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { ComponentName } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<ValueType> name=\"field_name\">\n        {({ field }) => (\n          <ComponentName\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'string' }}\n            // 其他高级配置...\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### ComponentName Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `ValueType` | - | 组件的值 |\n| `onChange` | `(value: ValueType) => void` | - | 值变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n\n### RelatedConfigType（如果有相关的配置类型）\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `property1` | `string` | - | 属性说明 |\n| `property2` | `boolean` | `false` | 属性说明 |\n\n### RelatedProviderProps（如果有 Provider 组件）\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `children` | `React.ReactNode` | - | 子组件 |\n| `config` | `ConfigType` | - | 配置对象 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/{物料路径}\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/{物料路径}\n```\n\n### 目录结构讲解\n\n```\n{物料名称}/\n├── index.tsx           # 主组件实现，包含 ComponentName 核心逻辑\n├── hooks.ts            # 自定义 Hooks，处理... [如果有]\n├── context.tsx         # Context Provider，提供... [如果有]\n├── utils.ts            # 工具函数，用于... [如果有]\n└── styles.css          # 样式文件\n```\n\n### 核心实现说明\n\n#### 功能点1\n[用简洁的文字描述实现原理]\n\n```typescript\n// 展示关键代码片段\nconst result = useHookName(props);\n```\n\n#### 功能点2\n[描述另一个关键功能的实现方式]\n\n```typescript\n// 展示关键逻辑\nif (condition) {\n  return <ComponentA />;\n} else {\n  return <ComponentB />;\n}\n```\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[组件初始化] --> B{判断条件}\n    B -->|条件1| C[执行分支A]\n    B -->|条件2| D[执行分支B]\n\n    C --> E[处理用户交互]\n    D --> F[处理数据变化]\n\n    E --> G[触发 onChange 回调]\n    F --> G\n```\n\n### 使用到的 FlowGram API\n\n[**@flowgram.ai/package-name**](https://github.com/bytedance/flowgram.ai/tree/main/packages/path)\n- [`ApiName`](https://flowgram.ai/auto-docs/package/type/ApiName): API 的功能说明\n- [`HookName`](https://flowgram.ai/auto-docs/package/functions/HookName): Hook 的功能说明\n\n[**@flowgram.ai/another-package**](https://github.com/bytedance/flowgram.ai/tree/main/packages/another-path)\n- [`TypeName`](https://flowgram.ai/auto-docs/package/interfaces/TypeName): 类型定义说明\n\n### 依赖的其他物料\n\n[**DependentMaterial**](./dependent-material) 物料的简要说明\n- `ExportedComponent`: 导出组件的用途\n- `ExportedHook`: 导出 Hook 的用途\n\n[**AnotherMaterial**](./another-material) 物料的简要说明\n\n### 使用的第三方库\n\n[**library-name**](https://library-url.com) 库的说明\n- `ImportedComponent`: 组件的用途\n- `importedFunction`: 函数的用途\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Don't allow people to merge changes to these generated files, because the result\n# may be invalid.  You need to run \"rush update\" again.\npnpm-lock.yaml               merge=ours\nshrinkwrap.yaml              merge=binary\nnpm-shrinkwrap.json          merge=binary\nyarn.lock                    merge=binary\n\n# Rush's JSON config files use JavaScript-style code comments.  The rule below prevents pedantic\n# syntax highlighters such as GitHub's from highlighting these comments as errors.  Your text editor\n# may also require a special configuration to allow comments in JSON.\n#\n# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088\n#\n*.json                       linguist-language=JSON-with-Comments\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# 文件路径与代码负责人分配\n# 对整个仓库设置代码负责人\n* @xiamidaxia @luics @dragooncjw  @YuanHeDx @sanmaopep @louisyoungx\n\n# 对特定目录设置代码负责人\n/apps/docs/ @xiamidaxia @dragooncjw @YuanHeDx @sanmaopep @louisyoungx\n/apps/demo-node-form/ @xiamidaxia @dragooncjw @YuanHeDx\n/packages/node-engine/ @xiamidaxia @dragooncjw @YuanHeDx\n/packages/variable-engine/ @xiamidaxia @dragooncjw @sanmaopep\n/packages/plugins/variable-plugin/ @xiamidaxia @dragooncjw @sanmaopep\n/packages/plugins/node-variable-plugin/ @xiamidaxia @dragooncjw @sanmaopep\n/packages/plugins/node-core-plugin/ @xiamidaxia @dragooncjw @YuanHeDx\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug Report\nabout: Report Bug\ntitle: \"[Bug] \"\nlabels: [bug]\n---\n\n## 🙋 SDK Version\nPlease input version of SDK.\n\n## 📌 Layout\nFree layout or Fixed layout?\n\n## 💻 Environment\n- Operation System: (e.g. Windows 11 / macOs 14.3)\n- Node.js:\n- Other:\n\n## 📝 Question Description\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "content": "---\nname: Question Report\nabout: Report Question\ntitle: \"[Question] \"\nlabels: [question]\n---\n\n## 🙋 SDK Version\nPlease input version of SDK.\n\n## 📌 Layout\nFree layout or Fixed layout?\n\n## 💻 Environment\n- Operation System: (e.g. Windows 11 / macOs 14.3)\n- Node.js:\n- Other:\n\n## 📝 Question Description\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non:\n  push:\n    branches: [\"main\"]\n  pull_request:\n    branches: [\"main\"]\n  merge_group:\n    branches: [\"main\"]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      - name: For Debug\n        run: |\n          echo \"Listing files in the root directory:\"\n          ls -alh\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n      # - name: Verify Change Logs\n      #   run: node common/scripts/install-run-rush.js change --verify\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      - name: Check Lint\n        run: node common/scripts/install-run-rush.js lint --verbose\n      - name: Check TS\n        run: node common/scripts/install-run-rush.js ts-check\n      - name: Test (coverage)\n        run: node common/scripts/install-run-rush.js test:cov\n"
  },
  {
    "path": ".github/workflows/common-pr-checks.yml",
    "content": "name: PR Common Checks\non:\n  pull_request:\n    types: [opened, edited, synchronize, reopened]\n\njobs:\n  common-checks:\n    name: PR Common Checks\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 1\n\n      - name: Config Git User\n        run: |\n          git config --local user.name \"tecvan\"\n          git config --local user.email \"fanwenjie.fe@bytedance.com\"\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Install Dependencies\n        run: node common/scripts/install-run-rush.js install\n\n      # PR Title Format Check\n      - name: Check PR Title Format\n        if: ${{ !contains(github.event.pull_request.title, 'WIP') && !contains(github.event.pull_request.title, 'wip') }}\n        env:\n          PR_TITLE: ${{ github.event.pull_request.title }}\n        run: |\n          node common/scripts/install-run-rush.js update-autoinstaller --name rush-commitlint && \\\n          pushd common/autoinstallers/rush-commitlint && \\\n          echo \"$PR_TITLE\" | npx commitlint --config commitlint.config.js && \\\n          popd\n\n      # Add more common checks here\n      # For example: file size checks, specific file format validations, etc.\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy With Actions\non: workflow_dispatch\n\nconcurrency:\n  group: \"main-deploy-branch-workflow\"\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pages: write\n      id-token: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n          persist-credentials: true\n\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install -t @flowgram.ai/docs\n\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build -t @flowgram.ai/docs\n\n      - name: Generate docs\n        run: |\n          cd apps/docs\n          npm run docs\n\n      - name: Copy auto-docs to en\n        run: cp -r apps/docs/src/zh/auto-docs apps/docs/src/en/auto-docs\n\n      - name: Build Doc site\n        run: |\n          cd apps/docs\n          npm run build\n\n      - name: Replace docs\n        run: |\n          rm -rf docs\n          mv apps/docs/doc_build docs\n\n      # 🔥 新增步骤：在 docs 目录下生成 vercel.json\n      - name: Create vercel.json\n        run: |\n          cat > docs/vercel.json <<EOF\n          {\n            \"version\": 2,\n            \"buildCommand\": \"\",\n            \"outputDirectory\": \".\",\n            \"cleanUrls\": true,\n            \"trailingSlash\": false\n          }\n          EOF\n\n      # 🔥 推送到 gh-pages 分支\n      - name: Push to gh-pages branch\n        uses: peaceiris/actions-gh-pages@v3\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./docs\n          publish_branch: gh-pages\n          force: true\n"
  },
  {
    "path": ".github/workflows/e2e.yml",
    "content": "name: E2E Tests\n\non:\n  pull_request:\n    branches: [\"main\"]\n  merge_group:\n    branches: [\"main\"]\n\njobs:\n  e2e:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n\n      # 缓存 Playwright 浏览器\n      - name: Cache Playwright browsers\n        uses: actions/cache@v3\n        with:\n          path: ~/.cache/ms-playwright\n          key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/fixed-layout/package.json') }}\n          restore-keys: |\n            ${{ runner.os }}-playwright-\n\n      - name: Install Playwright Browsers\n        run: pushd e2e/fixed-layout && npx playwright install --with-deps --only-shell chromium && popd\n\n      - name: Run E2E tests\n        run: node common/scripts/install-run-rush.js e2e:test --verbose\n"
  },
  {
    "path": ".github/workflows/publish-alpha.yml",
    "content": "name: Publish Alpha Version\non: workflow_dispatch\n\nconcurrency:\n  group: \"main-branch-alpha-publish-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      - name: Get alpha latest npm version\n        id: get_version\n        run: |\n          LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=alpha latest 2>/dev/null || true)\n          if [ -n \"$LATEST_VERSION\" ]; then\n            echo \"Using existing version: $LATEST_VERSION\"\n          else\n            LATEST_VERSION=\"0.1.0-alpha.1\"\n            echo \"Version not found, using default: $LATEST_VERSION\"\n          fi\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          echo \"The package version is : $LATEST_VERSION\"\n          echo \"The package output version is ${{ steps.get_version.outputs.version }}\"\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"publishPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\",\n              \\\"nextBump\\\": \\\"prerelease\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: replace with alpha version\n        run: node common/scripts/install-run-rush.js version --ensure-version-policy --override-version=$LATEST_VERSION --version-policy=publishPolicy\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --bump --version-policy publishPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag alpha\n      - name: Get new Version\n        id: get_new_version\n        run: |\n          NEW_VERSION=$(npm view @flowgram.ai/core version --tag=alpha latest)\n          echo \"NEW_VERSION=$NEW_VERSION\" >> $GITHUB_ENV\n      - name: Create tag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git tag \"v$NEW_VERSION\"\n          git push origin \"v$NEW_VERSION\"\n"
  },
  {
    "path": ".github/workflows/publish-app-to-version.yml",
    "content": "name: Publish App To Version\non:\n  workflow_dispatch:\n    inputs:\n      sdk-version:\n        description: \"要升级到的 SDK 版本（e.g. 1.0.0）\"\n        required: true\n        default: \"\"\n\nconcurrency:\n  group: \"main-branch-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          LATEST_VERSION=${{ github.event.inputs.sdk-version }}\n          echo \"The package input version is ${{ github.event.inputs.sdk-version }}\"\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"appPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --ensure-version-policy --override-version=$LATEST_VERSION --version-policy appPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest\n      - name: Get new Version\n        id: get_new_version\n        run: |\n          NEW_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"NEW_VERSION=$NEW_VERSION\" >> $GITHUB_ENV\n      - name: Create tag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git tag \"v$NEW_VERSION\"\n          git push origin \"v$NEW_VERSION\"\n"
  },
  {
    "path": ".github/workflows/publish-app.yml",
    "content": "name: Publish-Apps\non: workflow_dispatch\n\nconcurrency:\n  group: \"main-branch-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      - name: Get latest npm version\n        id: get_version\n        run: |\n          LATEST_VERSION=$(npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest)\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          echo \"The package version is : $LATEST_VERSION\"\n          echo \"The package output version is ${{ steps.get_version.outputs.version }}\"\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"appPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\",\n              \\\"nextBump\\\": \\\"patch\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --bump --version-policy appPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest\n"
  },
  {
    "path": ".github/workflows/publish-minor.yml",
    "content": "name: Publish-Minor\non: workflow_dispatch\n\nconcurrency:\n  group: \"main-branch-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      - name: Get latest npm version\n        id: get_version\n        run: |\n          LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          echo \"The package version is : $LATEST_VERSION\"\n          echo \"The package output version is ${{ steps.get_version.outputs.version }}\"\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"publishPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\",\n              \\\"nextBump\\\": \\\"minor\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --bump --version-policy publishPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest\n      - name: Get new Version\n        id: get_new_version\n        run: |\n          NEW_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"NEW_VERSION=$NEW_VERSION\" >> $GITHUB_ENV\n      - name: Create tag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git tag \"v$NEW_VERSION\"\n          git push origin \"v$NEW_VERSION\"\n"
  },
  {
    "path": ".github/workflows/publish-to-version.yml",
    "content": "name: Publish To Version\non:\n  workflow_dispatch:\n    inputs:\n      sdk-version:\n        description: \"要升级到的 SDK 版本（e.g. 1.0.0）\"\n        required: true\n        default: \"\"\n\nconcurrency:\n  group: \"main-branch-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          LATEST_VERSION=${{ github.event.inputs.sdk-version }}\n          echo \"The package input version is ${{ github.event.inputs.sdk-version }}\"\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"publishPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --ensure-version-policy --override-version=$LATEST_VERSION --version-policy publishPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest\n      - name: Get new Version\n        id: get_new_version\n        run: |\n          NEW_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"NEW_VERSION=$NEW_VERSION\" >> $GITHUB_ENV\n      - name: Create tag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git tag \"v$NEW_VERSION\"\n          git push origin \"v$NEW_VERSION\"\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\non: workflow_dispatch\n\nconcurrency:\n  group: \"main-branch-workflow\" # 唯一标识符，确保只运行一个实例\n  cancel-in-progress: false # 不取消正在运行的实例，后续触发需要等待当前实例完成\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          fetch-depth: 2\n      - name: Set up npm token\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: echo \"//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\" > ~/.npmrc\n      - name: Debug Auth\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: |\n          npm whoami\n      - name: Config Git User\n        run: |\n          git config --local user.name \"dragooncjw\"\n          git config --local user.email \"289056872@qq.com\"\n      - name: Get latest npm version\n        id: get_version\n        run: |\n          LATEST_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"LATEST_VERSION=$LATEST_VERSION\" >> $GITHUB_ENV\n      # https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/\n      - name: Echo version\n        run: |\n          echo \"The package version is : $LATEST_VERSION\"\n          echo \"The package output version is ${{ steps.get_version.outputs.version }}\"\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: \"https://registry.npmjs.org/\"\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n      # version bump 之前保证是远端最新的，这样无需 commit package.json version\n      - name: Sync versions\n        run: |\n          echo \"[\n            {\n              \\\"policyName\\\": \\\"publishPolicy\\\",\n              \\\"definitionName\\\": \\\"lockStepVersion\\\",\n              \\\"version\\\": \\\"$LATEST_VERSION\\\",\n              \\\"nextBump\\\": \\\"patch\\\"\n            }\n          ]\" > common/config/rush/version-policies.json\n      - name: Version Bump\n        run: node common/scripts/install-run-rush.js version --bump --version-policy publishPolicy\n      - name: Publish\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}\n        run: node common/scripts/install-run-rush.js publish --include-all -p --tag latest\n      - name: Get new Version\n        id: get_new_version\n        run: |\n          NEW_VERSION=$(npm view @flowgram.ai/core version --tag=latest latest)\n          echo \"NEW_VERSION=$NEW_VERSION\" >> $GITHUB_ENV\n      - name: Create tag\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git tag \"v$NEW_VERSION\"\n          git push origin \"v$NEW_VERSION\"\n"
  },
  {
    "path": ".github/workflows/sync-screenshot.yml",
    "content": "name: Sync Screenshot\non: workflow_dispatch\n\nconcurrency:\n  group: \"manual-sync-screenshot\"\n  cancel-in-progress: false\n\njobs:\n  e2e:\n    # can not update screenshot run on main.\n    if: github.ref_name != 'main'\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v3\n\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n\n      - name: Set Git user.name and user.email from trigger actor\n        run: |\n          git config --global user.name \"${{ github.actor }}\"\n          git config --global user.email \"${{ github.actor }}@users.noreply.github.com\"\n\n      - name: Rush Install\n        run: node common/scripts/install-run-rush.js install\n\n      - name: Rush build\n        run: node common/scripts/install-run-rush.js build\n\n      - name: Install Playwright Browsers\n        run: npx playwright install --with-deps\n\n      - name: Run E2E tests\n        run: node common/scripts/install-run-rush.js e2e:update-screenshot --verbose\n\n      - name: Commit and push changes\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: |\n          git add .\n          if git diff-index --quiet HEAD; then\n            echo \"No changes to commit\"\n          else\n            git commit -m \"chore: sync screenshot\"\n            git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} HEAD:${{ github.ref_name }}\n          fi\n"
  },
  {
    "path": ".gitignore",
    "content": "# e2e results\ntest-results/\ndoc_build/\n\n# Logs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov/\n\n# Coverage directory used by tools like istanbul\ncoverage/\n\n# nyc test coverage\n.nyc_output/\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt/\n\n# Bower dependency directory (https://bower.io/)\nbower_components/\n\n# node-waf configuration\n.lock-wscript/\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release/\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm/\n\n# Optional eslint cache\n.eslintcache/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# next.js build output\n.next/\n\n# Docusaurus cache and generated files\n.docusaurus/\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# yarn v2\n.yarn/cache/\n.yarn/unplugged/\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# OS X temporary files\n.DS_Store\n\n# IntelliJ IDEA project files; if you want to commit IntelliJ settings, this recipe may be helpful:\n# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n.idea/\n*.iml\n\n# Visual Studio Code\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n\n# Rush temporary files\ncommon/deploy/\ncommon/temp/\ncommon/autoinstallers/*/.npmrc\n**/.rush/temp/\n*.lock\n\n# Common toolchain intermediate files\ntemp/\nlib/\nlib-amd/\nlib-es6/\nlib-esnext/\nlib-commonjs/\nlib-shim/\ndist/\ndist-storybook/\n*.tsbuildinfo\n\n# Heft temporary files\n.cache/\n.heft/\n\n# rush standard files\n.eslintcache\n\n# eslint cache for v9\n.lintcache/\n"
  },
  {
    "path": ".vscode/extentions.json",
    "content": "{\n  \"recommendations\": [\n    \"styled-components.vscode-styled-components\",\n    \"editorconfig.editorconfig\",\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"streetsidesoftware.code-spell-checker\",\n    \"codezombiech.gitignore\",\n    \"aaron-bond.better-comments\"\n  ],\n  \"unwantedRecommendations\": [\n    \"nucllear.vscode-extension-auto-import\",\n    \"steoates.autoimport\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"eslint.nodePath\": \"config/eslint-config/node_modules/eslint\",\n  \"prettier.prettierPath\": \"config/eslint-config/node_modules/prettier\",\n  \"editor.tabSize\": 2,\n  \"editor.insertSpaces\": true,\n  \"editor.formatOnSave\": true,\n  \"editor.formatOnType\": false,\n  \"editor.formatOnPaste\": false,\n  \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\",\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll\": \"explicit\",\n    \"source.fixAll.eslint\": \"explicit\"\n  },\n  \"search.followSymlinks\": false,\n  \"search.exclude\": {\n    \"**/node_modules\": true,\n    \"**/.nyc_output\": true,\n    \"**/.rush\": true,\n    \"**/pnpm-lock.yaml\": true,\n    \"**/CHANGELOG.json\": true,\n    \"**/CHANGELOG.md\": true,\n    \"common/changes\": true,\n    \"**/output\": true,\n    \"**/lib\": true,\n    \"**/dist\": true,\n    \"**/coverage\": true,\n    \"common/temp\": true\n  },\n  \"eslint.workingDirectories\": [\n    {\n      \"mode\": \"auto\"\n    }\n  ],\n  \"files.defaultLanguage\": \"plaintext\",\n  \"files.associations\": {\n    \".code-workspace\": \"jsonc\",\n    \".babelrc\": \"json\",\n    \".eslintrc\": \"jsonc\",\n    \".eslintrc*.json\": \"jsonc\",\n    \".stylelintrc\": \"jsonc\",\n    \"stylelintrc\": \"jsonc\",\n    \"*.json\": \"jsonc\",\n    \"package.json\": \"json\",\n    \".htmlhintrc\": \"jsonc\",\n    \"htmlhintrc\": \"jsonc\",\n    \"Procfile*\": \"shellscript\",\n    \"README\": \"markdown\",\n    \"**/coverage/**/*.*\": \"plaintext\",\n    \"OWNERS\": \"yaml\",\n    \"**/pnpm-lock.yaml\": \"plaintext\",\n    \"**/dist/**\": \"plaintext\",\n    \"**/dist_*/**\": \"plaintext\",\n    \"*.map\": \"plaintext\",\n    \"*.log\": \"plaintext\"\n  },\n  \"files.exclude\": {\n    \"**/.git\": true,\n    \"**/.svn\": true,\n    \"**/.hg\": true,\n    \"**/CVS\": true,\n    \"**/.DS_Store\": true,\n    \"**/Thumbs.db\": true,\n    \"**/.rush\": true\n  },\n  \"files.watcherExclude\": {\n    \"**/.git/objects/**\": true,\n    \"**/.git/subtree-cache/**\": true,\n    \"**/node_modules/*/**\": true\n  },\n  \"search.useIgnoreFiles\": true,\n  //\n  \"editor.rulers\": [\n    80,\n    120\n  ],\n  \"files.eol\": \"\\n\",\n  \"files.trimTrailingWhitespace\": true,\n  \"files.insertFinalNewline\": true,\n  \"cSpell.diagnosticLevel\": \"Warning\",\n  \"eslint.probe\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\"\n  ],\n  \"eslint.format.enable\": true,\n  \"eslint.lintTask.enable\": true,\n  \"javascript.validate.enable\": false,\n  \"typescript.validate.enable\": true,\n  \"typescript.tsdk\": \"config/ts-config/node_modules/typescript/lib\",\n  \"typescript.tsserver.maxTsServerMemory\": 8192,\n  // \"typescript.tsserver.experimental.enableProjectDiagnostics\": true,\n  \"typescript.tsserver.watchOptions\": {\n    \"fallbackPolling\": \"dynamicPriorityPolling\",\n    \"synchronousWatchDirectory\": false,\n    \"watchDirectory\": \"dynamicPriorityPolling\",\n    \"watchFile\": \"useFsEventsOnParentDirectory\"\n  },\n  \"css.validate\": false,\n  \"scss.validate\": false,\n  \"less.validate\": false,\n  \"emmet.triggerExpansionOnTab\": true,\n  \"[yaml]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[css]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[html]\": {\n    \"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n  },\n  \"[json]\": {\n    \"editor.defaultFormatter\": \"vscode.json-language-features\"\n  },\n  \"[jsonc]\": {\n    \"editor.defaultFormatter\": \"vscode.json-language-features\"\n  },\n  \"[less]\": {\n    \"editor.defaultFormatter\": \"vscode.css-language-features\"\n  },\n  \"[typescript]\": {\n    \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\"\n  },\n  \"[javascriptreact]\": {\n    \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\"\n  },\n  \"[typescriptreact]\": {\n    \"editor.defaultFormatter\": \"dbaeumer.vscode-eslint\"\n  },\n  \"[ignore]\": {\n    \"editor.defaultFormatter\": \"foxundermoon.shell-format\"\n  },\n  \"[shellscript]\": {\n    \"editor.defaultFormatter\": \"foxundermoon.shell-format\"\n  },\n  \"[dotenv]\": {\n    \"editor.defaultFormatter\": \"foxundermoon.shell-format\"\n  },\n  \"[svg]\": {\n    \"editor.defaultFormatter\": \"jock.svg\"\n  },\n  \"[mdx]\": {\n    \"editor.defaultFormatter\": \"unifiedjs.vscode-mdx\"\n  },\n  \"cSpell.words\": [\n    \"Twoway\"\n  ]\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Repository Guidelines\n\n## Project Structure & Module Organization\nFlowGram is a Rush-managed monorepo. Production-ready libraries live under `packages/*` (canvas engine, node engine, runtime, plugins). Demo UIs and docs sit inside `apps/*` (for example `apps/demo-free-layout`, `apps/docs`). Shared tooling, config, and scripts live under `common/` and `config/`. End-to-end Playwright suites are isolated in `e2e/<scenario>` so they can be installed and run independently. New code should land in the closest existing package; create additional Rush projects only when a module needs its own version and publish cycle.\n\n## Build, Test, and Development Commands\nUse Node.js 18 LTS with pnpm 10.6.5 (Rush enforces versions). Install dependencies via `rush install`. Run `rush build` to compile every registered project. Use `rush dev:docs` or `rush dev:demo-free-layout` for hot-reload docs and demos. `rush lint`, `rush lint:fix`, and `rush ts-check` keep lint/TS diagnostics consistent. `rush test` aggregates unit tests; `rush e2e:test` runs Playwright suites, while `rush e2e:update-screenshot` refreshes snapshots.\n\n## Coding Style & Naming Conventions\nWe write TypeScript with React, sharing configs from `config/eslint-config`. ESLint enforces 2-space indentation, semicolons, and import order; run `rush lint:fix` before committing. Use `PascalCase` for React components and classes, `camelCase` for variables/functions, and `SCREAMING_SNAKE_CASE` only for constants exported from config files. File names follow kebab-case (e.g., `flow-node-form.tsx`). Keep public API surfaces documented via barrel files such as `packages/canvas-engine/core/src/index.ts`.\n\n## Testing Guidelines\nUnit tests use Vitest and live beside source in `__tests__` folders or `*.test.ts` files; prefer descriptive names like `node-service.test.ts`. Ensure new logic is covered by `rush test`, and include data fixtures where possible. Playwright specs under `e2e/*/tests` cover critical workflows—coordinate UI changes with updated snapshots and run `rush e2e:test --to <package>` to scope failures.\n\n## Commit & Pull Request Guidelines\nFollow conventional commits (`type(scope): subject`) as seen in history (`fix(auto-layout): ...`). Keep subjects imperative and ≤72 characters, with optional bodies for context. PRs must describe the change, link GitHub issues, and attach before/after screenshots for UI updates. Confirm CI status, note any follow-ups, and request reviewers from the owning package tags (see `rush.json`).\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- Add `flowing` field to `LineColor` interface for configuring flowing line colors\n  - Added `flowing: string` field to `LineColor` interface\n  - Added `LineColors.FLOWING` enum value with default color\n  - Updated `WorkflowLinesManager.getLineColor()` to support flowing state\n  - Updated demo configurations to include flowing color examples\n  - Added comprehensive test coverage for flowing line functionality\n\n### Features\n\n- Lines can now be colored differently when in flowing state (e.g., during workflow execution)\n- Priority order: hidden > error > highlight > drawing > hovered > selected > flowing > default\n- Backward compatible with existing line color configurations\n\n### Demo Updates\n\n- Updated `apps/demo-free-layout` to include flowing color configuration\n- Added CSS variable support: `var(--g-workflow-line-color-flowing,#4d53e8)`\n\n### Tests\n\n- Added test cases for flowing line color functionality\n- Verified priority ordering with other line states\n- Ensured backward compatibility\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Repository Overview\n\nFlowGram is a composable, visual workflow development framework built as a Rush-managed monorepo. It provides tools for building AI workflow platforms, including a flow canvas, node configuration forms, variable scope chains, and pre-built materials (LLM, Condition, Code Editor, etc.).\n\n## Build System & Package Management\n\nThis monorepo uses **Rush 5.150.0** with **pnpm 10.6.5** as the package manager. Node.js version must be >=18.20.3 <19.0.0 || >=20.14.0 <23.0.0.\n\n### Essential Commands\n\n```bash\n# Install dependencies (required first step)\nrush install\n\n# Build all packages\nrush build\n\n# Build specific package and its dependencies\nrush build --to @flowgram.ai/core\n\n# Lint all packages\nrush lint\n\n# Fix lint issues\nrush lint:fix\n\n# TypeScript type checking\nrush ts-check\n\n# Run unit tests\nrush test\n\n# Run tests with coverage\nrush test:cov\n\n# Build packages in watch mode (for development)\nrush build:watch\n\n# E2E tests with Playwright\nrush e2e:test\n\n# Update E2E screenshots\nrush e2e:update-screenshot\n```\n\n### Development Commands for Demos\n\n```bash\n# Run docs site with hot reload\nrush dev:docs\n\n# Run specific demo apps with hot reload\nrush dev:demo-free-layout\nrush dev:demo-fixed-layout\nrush dev:demo-fixed-layout-simple\nrush dev:demo-free-layout-simple\nrush dev:demo-nextjs\nrush dev:demo-nextjs-antd\n```\n\nThese commands use `concurrently` to run `rush build:watch` for dependencies alongside the demo's dev server.\n\n### Single Package Development\n\nTo work on a single package in isolation:\n```bash\ncd packages/canvas-engine/core\nrushx build        # Runs the build script for this package only\nrushx test         # Runs tests for this package only\nrushx ts-check     # Type checks this package only\n```\n\n## Monorepo Structure\n\n### Core Organization\n\n- **`packages/`** - Production libraries organized by functional area:\n  - `canvas-engine/` - Canvas rendering and layout systems (core, document, renderer, fixed-layout-core, free-layout-core)\n  - `node-engine/` - Node data and form management (node, form, form-core)\n  - `variable-engine/` - Variable scoping and type inference (variable-core, variable-layout, json-schema)\n  - `runtime/` - Workflow execution engines (interface, js-core, nodejs)\n  - `plugins/` - Extensibility modules (23+ plugins for features like history, drag, snap, minimap, etc.)\n  - `client/` - High-level React components (editor, fixed-layout-editor, free-layout-editor, playground-react)\n  - `materials/` - Pre-built node materials (form-materials, form-antd-materials, fixed-semi-materials, coze-editor, type-editor)\n  - `common/` - Shared utilities (utils, reactive, command, history, history-storage, i18n)\n\n- **`apps/`** - Demos and documentation:\n  - `docs/` - Main documentation site\n  - `demo-*/` - Example applications (free-layout, fixed-layout, nextjs, vite, playground, etc.)\n  - `create-app/`, `cli/` - CLI tools for scaffolding\n\n- **`e2e/`** - End-to-end test suites (fixed-layout, free-layout)\n- **`config/`** - Shared configuration (eslint-config, ts-config)\n- **`common/`** - Rush tooling and scripts\n\n### Architectural Layers\n\nFlowGram is architected in distinct layers:\n\n1. **Canvas Engine Layer** (`@flowgram.ai/core`, `@flowgram.ai/document`, `@flowgram.ai/renderer`)\n   - Core abstractions for canvas rendering, document model, and viewport management\n   - Supports two layout modes: free-layout (drag-anywhere) and fixed-layout (structured positioning)\n   - Plugin-based architecture using dependency injection (inversify)\n\n2. **Node Engine Layer** (`@flowgram.ai/node`, `@flowgram.ai/form`, `@flowgram.ai/form-core`)\n   - Manages node data structures and lifecycle\n   - Form engine with validation, side effects, linkage, and error capture\n   - Uses `FormModelV2` (exported as `FormModel` from `@flowgram.ai/editor`)\n\n3. **Variable Engine Layer** (`@flowgram.ai/variable-core`, `@flowgram.ai/json-schema`)\n   - Provides variable scoping, structure inspection, and type inference\n   - Manages data flow constraints across workflow nodes\n   - Scope chain mechanism for variable resolution\n\n4. **Runtime Layer** (`@flowgram.ai/runtime-js`, `@flowgram.ai/runtime-nodejs`, `@flowgram.ai/runtime-interface`)\n   - Executes workflows in JavaScript/Node.js environments\n   - Interface package defines runtime contracts\n   - Separate implementations for browser and server\n\n5. **Client/Editor Layer** (`@flowgram.ai/editor`, `@flowgram.ai/fixed-layout-editor`, `@flowgram.ai/free-layout-editor`)\n   - High-level React components that integrate all subsystems\n   - `@flowgram.ai/editor` is the main barrel export for fixed-layout workflows\n   - Re-exports from core, form, variable, and plugin packages\n\n6. **Plugin Ecosystem**\n   - 20+ plugins providing features like drag-and-drop, history/undo, snap-to-grid, minimap, auto-layout, etc.\n   - Plugins are registered via dependency injection containers\n   - Naming convention: `free-*-plugin` for free-layout, `fixed-*-plugin` for fixed-layout, or generic plugins\n\n## Key Design Patterns\n\n### Dependency Injection\nThe codebase heavily uses **inversify** for dependency injection. Services are decorated with `@injectable()` and injected via `@inject()`. Container modules organize related services.\n\n### Reactive State Management\nUses a custom reactive system (`@flowgram.ai/reactive`) with React hooks:\n- `ReactiveState` and `ReactiveBaseState` for observable state\n- `useReactiveState`, `useReadonlyReactiveState`, `useObserve` hooks\n- `Tracker` for dependency tracking\n\n### Command Pattern\n`@flowgram.ai/command` provides a command/command registry system for undo/redo operations. Re-exported by `@flowgram.ai/core`.\n\n### Plugin Architecture\nPlugins extend functionality via:\n- `Plugin` interface from `@flowgram.ai/core`\n- Registration through container modules\n- Lifecycle hooks (`onInit`, `onDestroy`, etc.)\n\n## Testing\n\n- **Unit tests**: Use Vitest, located in `__tests__/` folders or `*.test.ts` files\n- **E2E tests**: Use Playwright, located in `e2e/*/tests/` directories\n- Run all tests: `rush test`\n- Run E2E tests for specific package: `rush e2e:test --to @flowgram.ai/e2e-free-layout`\n- Update Playwright snapshots: `rush e2e:update-screenshot`\n\n## Code Quality\n\n### Linting & Type Checking\n- ESLint configuration in `config/eslint-config` enforces 2-space indentation, semicolons, and import order\n- TypeScript config in `config/ts-config`\n- Always run `rush lint:fix` before committing\n- Run `rush ts-check` to validate TypeScript across all packages\n\n### Naming Conventions\n- **React components/classes**: PascalCase\n- **Variables/functions**: camelCase\n- **Constants**: SCREAMING_SNAKE_CASE (only for exported config)\n- **File names**: kebab-case (e.g., `flow-node-form.tsx`)\n\n### Pre-commit Hooks\n- `rush lint-staged` - Runs linting on staged files\n- `rush commitlint` - Validates commit message format (conventional commits)\n\n### Dependency Checks\n- `rush check-circular-dependency` - Detects circular dependencies\n- `rush dep-check` - Validates dependency consistency\n\n## Commit & Pull Request Standards\n\nFollow **conventional commits** format: `type(scope): subject`\n\nExamples from recent history:\n- `fix(auto-layout): rankdir top to bottom`\n- `feat(landing): hover logo node glowing`\n- `docs(variable): optimize variable docs by codex`\n\nKeep commit subjects imperative, ≤72 characters. Reference GitHub issues in PR descriptions.\n\n## Working with Packages\n\n### Adding a New Package\n1. Create folder under appropriate category (`packages/<category>/<package-name>`)\n2. Add entry to `rush.json` \"projects\" array with:\n   - `packageName`: `@flowgram.ai/<package-name>`\n   - `projectFolder`: relative path\n   - `versionPolicyName`: typically \"publishPolicy\" for libraries, \"appPolicy\" for apps\n   - `tags`: for categorization\n3. Run `rush update` to link the package\n\n### Publishing Workflow\nPackages with `versionPolicyName: \"publishPolicy\"` are publishable to npm. Apps use `\"appPolicy\"`.\n\n### Inter-package Dependencies\nUse `workspace:^x.x.x` protocol in package.json for internal dependencies. Rush will link them locally during development.\n\n## Common Issues\n\n### Build Failures\n- Ensure `rush install` was run after pulling changes\n- Check Node.js version matches `nodeSupportedVersionRange` in rush.json\n- Clear incremental build cache: delete `common/temp` and rebuild\n\n### Type Errors in Editor Packages\nThe main editor packages (`@flowgram.ai/editor`, `@flowgram.ai/fixed-layout-editor`, `@flowgram.ai/free-layout-editor`) re-export many types. Look for type definitions in their upstream dependencies (@flowgram.ai/form, @flowgram.ai/core, @flowgram.ai/node).\n\n### Plugin Registration\nPlugins must be registered in a dependency injection container. Check demo apps for examples of container module setup.\n\n### Rush Command Not Found\nEnsure Rush is installed globally: `npm install -g @microsoft/rush`\nOr use the install-run script: `node common/scripts/install-run-rush.js <command>`\n\n## Additional Resources\n\n- Documentation: https://flowgram.ai\n- Issues: https://github.com/bytedance/flowgram.ai/issues\n- Contributing: See CONTRIBUTING.md\n"
  },
  {
    "path": "CNAME",
    "content": "flowgram.ai"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to flowgram.ai\n\n## Quick Start\n\n### Prerequisites\n\n- Node.js 18+ (LTS/Hydrogen recommended)\n- pnpm 10.6.5\n- Rush 5.150.0\n\n### Installation\n\n1. **Install Node.js 18+**\n\n``` bash\nnvm install lts/hydrogen\nnvm alias default lts/hydrogen # set default node version\nnvm use lts/hydrogen\n```\n\n2. **Clone the repository**\n\n``` bash\ngit clone git@github.com:bytedance/flowgram.ai.git\n```\n\n3. **Install required global dependencies**\n\n``` bash\nnpm i -g pnpm@10.6.5 @microsoft/rush@5.150.0\n```\n\n4. **Install project dependencies**\n\n``` bash\nrush install\n```\n\n5. **Build the project**\n\n``` bash\nrush build\n```\n\n6. **Run docs or demo**\n\n``` bash\nrush dev:docs # docs\nrush dev:demo-fixed-layout\nrush dev:demo-free-layout\n```\n\nAfter that, you can start to develop projects inside this repository.\n\n\n## Submitting Changes\n\n1. Create a new branch from `main` using the format:\n    - `feat/description` for features\n    - `fix/description` for bug fixes\n    - `docs/description` for documentation\n    - `chore/description` for maintenance\n\n2. Write code and tests\n    - Follow our coding standards\n    - Add/update tests for changes\n    - Update documentation if needed\n\n3. Ensure quality\n    - Run `cd path/to/packageName && npm test` for all tests\n    - Run `rush lint` for code style\n    - Run `rush build` to verify build\n\n4. Create Pull Request\n    - Use the PR template\n    - Link related issues\n    - Provide clear description of changes\n\n5. Review Process\n    - Maintainers will review your PR\n    - Address review feedback if any\n    - Changes must pass CI checks\n\n6. Commit Message Format\n   ```\n   type(scope): subject\n   body\n   ```\n   Types: feat, fix, docs, style, refactor, test, chore\n\n## Reporting Bugs\n\nReport bugs via [GitHub Issues](https://github.com/bytedance/flowgram.ai/issues/new/choose). Please include:\n\n- Issue description\n- Steps to reproduce\n- Expected behavior\n- Actual behavior\n- Code examples (if applicable)\n\n## Documentation\n\n- Update API documentation for interface changes\n- Update README.md if usage is affected\n\n## License\n\nThis project is under the [MIT License](http://choosealicense.com/licenses/mit/). By submitting code, you agree to these terms.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Bytedance Ltd. and/or its affiliates\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![License](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram｜Workflow development framework\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram is a composable, visual, easy-to-integrate, and extensible workflow development framework & toolkit.\nOur goal is to help developers build AI workflow platforms **faster** and **simpler**.\nFlowGram comes with a suite of built-in tools for workflow development: flow canvas, node configuration form, variable scope chain, and ready-to-use materials (LLM, Condition, Code Editor etc). It’s not a ready-made workflow platform; it’s the framework and toolkit to build yours.\n\nLearn more at [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 Demo\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\nOpen in [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) or [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n\nIn this demo, we iterate through a list of cities, fetch real-time weather via HTTP, parse temperatures with a Code node, generate outfit suggestions with an LLM, gate by a Condition, aggregate results across the loop, and finally use an Advisor LLM to pick the most comfortable city before sending the result to the End node.\n\n## 🚀 Quick Start\n\n1. Create a new FlowGram project:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> We recommend choosing the `Free Layout Demo ⭐️` template.\n\n2. Start the project:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. Open [http://localhost:3000](http://localhost:3000) in your browser.\n\n## ✨ Features\n\n| Feature                                                                                      | Description                                                                                                                                                                                               | Demo                                                                                         |\n| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html)    | Free layout canvas where nodes can be placed anywhere and connected using free-form lines.                                                                                                                | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif)                 |\n| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Fixed layout canvas where nodes can be dragged to specified positions, with support for compound nodes like branches and loops.                                                                           | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)              |\n| [Form](https://flowgram.ai/examples/node-form/basic.html)                                    | The form engine manages the CRUD operations of node data and provides rendering, validation, side effects, linkage, and error-capturing capabilities, simplifying the development of node configurations. | ![Form](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78)     |\n| [Variable](https://flowgram.ai/guide/variable/basic.html)                                    | The variable engine supports scope constraints, variable structure inspection, and type inference, making it easy to manage data flow within the workflow.                                                | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |\n\n\n## 📖 Documentation\n\nYou can find the FlowGram documentation [on the website](https://flowgram.ai).\n\nThe documentation is divided into several sections:\n\n- [Quick Start](https://flowgram.ai/guide/getting-started/introduction.html)\n- [Canvas](https://flowgram.ai/guide/free-layout/load.html)\n- [Form](https://flowgram.ai/guide/form/form.html)\n- [Variable](https://flowgram.ai/guide/variable/basic.html)\n- [Material](https://flowgram.ai/materials/introduction.html)\n- [Runtime](https://flowgram.ai/guide/runtime/introduction.html)\n- [Advanced Guides](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [API Reference](https://flowgram.ai/api/index.html)\n- [Where to get Support](https://flowgram.ai/guide/contact-us.html)\n- [Contributing Guide](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 Contributors\n\n[![FlowGram.AI Contributors](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 Adoption\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) is an all-in-one AI agent development tool. Providing the latest large models and tools, various development modes and frameworks, Coze Studio offers the most convenient AI agent development environment, from development to deployment.\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) is a workflow-based multi-platform ai deployment tool.\n- [Certimate](https://github.com/certimate-go/certimate)  is an open-source SSL certificate management tool that helps you automatically apply for and deploy SSL certificates with a visual workflow. It is one of the ACME client options listed in the official documentation of Let's Encrypt.\n\n## 📬 Contact us\n\n- Issues: [Issues](https://github.com/bytedance/flowgram.ai/issues)\n- Lark: Scan the QR code below with [Register Feishu](https://www.feishu.cn/en/) to join our FlowGram user group.\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_DE.md",
    "content": "![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![Lizenz](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram | Workflow-Entwicklungs-Framework\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram ist ein zusammensetzbares, visuelles, einfach zu integrierendes und erweiterbares Workflow-Entwicklungs-Framework & Toolkit.\nUnser Ziel ist es, Entwicklern zu helfen, KI-Workflow-Plattformen **schneller** und **einfacher** zu erstellen.\nFlowGram wird mit einer Reihe von integrierten Werkzeugen für die Workflow-Entwicklung geliefert: eine visuelle Flow-Canvas, Node-Konfigurationsformulare, eine Variablen-Scope-Chain und sofort einsatzbereite Materialien (LLM, Bedingung, Code-Editor usw.). Es ist keine fertige Workflow-Plattform; es ist das Framework und Toolkit, um Ihre zu erstellen.\n\nErfahren Sie mehr unter [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 Demo\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\nÖffnen Sie in [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) oder [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n\nIn dieser Demo durchlaufen wir eine Liste von Städten, rufen das Echtzeit-Wetter über HTTP ab, parsen die Temperaturen mit einem Code-Knoten, generieren Outfit-Vorschläge mit einem LLM, steuern durch eine Bedingung, aggregieren die Ergebnisse über die Schleife und verwenden schließlich einen Berater-LLM, um die komfortabelste Stadt auszuwählen, bevor das Ergebnis an den Endknoten gesendet wird.\n\n## 🚀 Schnellstart\n\n1. Erstellen Sie ein neues FlowGram-Projekt:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> Wir empfehlen, die Vorlage `Free Layout Demo ⭐️` zu wählen.\n\n2. Starten Sie das Projekt:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. Öffnen Sie [http://localhost:3000](http://localhost:3000) in Ihrem Browser.\n\n## ✨ Funktionen\n\n| Funktion                                                                                     | Beschreibung                                                                                                                                                                  | Demo                                                                                         |\n| -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html)    | Freie Layout-Canvas, auf der Knoten beliebig platziert und mit Freiformlinien verbunden werden können.                                                                        | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif)                 |\n| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Feste Layout-Canvas, auf der Knoten an bestimmte Positionen gezogen werden können, mit Unterstützung für zusammengesetzte Knoten wie Verzweigungen und Schleifen.             | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)              |\n| [Formular](https://flowgram.ai/examples/node-form/basic.html)                                | Die Formular-Engine verwaltet CRUD-Operationen von Knotendaten und bietet Rendering-, Validierungs-, Nebeneffekt-, Verknüpfungs- und Fehlererfassungsfunktionen, wodurch die Entwicklung von Knotenkonfigurationen vereinfacht wird. | ![Formular](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |\n| [Variable](https://flowgram.ai/guide/variable/basic.html)                                    | Die Variablen-Engine unterstützt Bereichsbeschränkungen, Variablenstrukturinspektion und Typinferenz, wodurch der Datenfluss im Workflow einfach verwaltet werden kann. | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |\n\n\n## 📖 Dokumentation\n\nSie finden die FlowGram-Dokumentation [auf der Website](https://flowgram.ai).\n\nDie Dokumentation ist in mehrere Abschnitte unterteilt:\n\n- [Schnellstart](https://flowgram.ai/guide/getting-started/introduction.html)\n- [Canvas](https://flowgram.ai/guide/free-layout/load.html)\n- [Formular](https://flowgram.ai/guide/form/form.html)\n- [Variable](https://flowgram.ai/guide/variable/basic.html)\n- [Material](https://flowgram.ai/materials/introduction.html)\n- [Laufzeit](https://flowgram.ai/guide/runtime/introduction.html)\n- [Erweiterte Anleitungen](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [API-Referenz](https://flowgram.ai/api/index.html)\n- [Wo Sie Unterstützung erhalten](https://flowgram.ai/guide/contact-us.html)\n- [Leitfaden für Beiträge](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 Mitwirkende\n\n[![FlowGram.AI-Mitwirkende](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 Einführung\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) ist ein All-in-One-KI-Agenten-Entwicklungstool. Coze Studio bietet die neuesten großen Modelle und Werkzeuge, verschiedene Entwicklungsmodi und Frameworks und bietet die bequemste KI-Agenten-Entwicklungsumgebung, von der Entwicklung bis zur Bereitstellung.\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) ist ein Workflow-basiertes Multi-Plattform-KI-Bereitstellungstool.\n- [Certimate](https://github.com/certimate-go/certimate) ist ein Open-Source-SSL-Zertifikatsverwaltungstool, mit dem Sie SSL-Zertifikate automatisch mit einem visuellen Workflow beantragen und bereitstellen können. Es ist eine der ACME-Client-Optionen, die in der offiziellen Dokumentation von Let's Encrypt aufgeführt sind.\n\n## 📬 Kontaktieren Sie uns\n\n- Probleme: [Probleme](https://github.com/bytedance/flowgram.ai/issues)\n- Lark: Scannen Sie den QR-Code unten mit [Register Feishu](https://www.feishu.cn/en/), um unserer FlowGram-Benutzergruppe beizutreten.\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_ES.md",
    "content": "![Imagen](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![Licencia](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram | Marco de desarrollo de flujos de trabajo\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram es un marco y conjunto de herramientas de desarrollo de flujos de trabajo componible, visual, fácil de integrar y extensible.\nNuestro objetivo es ayudar a los desarrolladores a crear plataformas de flujo de trabajo de IA de forma **más rápida** y **sencilla**.\nFlowGram viene con un conjunto de herramientas integradas para el desarrollo de flujos de trabajo: un lienzo de flujo visual, formularios de configuración de nodos, una cadena de alcance de variables y materiales listos para usar (LLM, Condición, Editor de código, etc.). No es una plataforma de flujo de trabajo ya hecha; es el marco y el conjunto de herramientas para crear la suya.\n\nObtenga más información en [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 Demostración\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\nAbra en [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) o [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n\nEn esta demostración, iteramos a través de una lista de ciudades, obtenemos el clima en tiempo real a través de HTTP, analizamos las temperaturas con un nodo de código, generamos sugerencias de atuendos con un LLM, controlamos mediante una condición, agregamos los resultados a lo largo del bucle y, finalmente, usamos un LLM asesor para elegir la ciudad más cómoda antes de enviar el resultado al nodo final.\n\n## 🚀 Inicio rápido\n\n1. Cree un nuevo proyecto de FlowGram:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> Le recomendamos que elija la plantilla `Free Layout Demo ⭐️`.\n\n2. Inicie el proyecto:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. Abra [http://localhost:3000](http://localhost:3000) en su navegador.\n\n## ✨ Características\n\n| Característica                                                                                 | Descripción                                                                                                                                                                                            | Demostración                                                                                   |\n| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |\n| [Lienzo de diseño libre](https://flowgram.ai/examples/free-layout/free-feature-overview.html)  | Lienzo de diseño libre donde los nodos se pueden colocar en cualquier lugar y conectar mediante líneas de forma libre.                                                                                 | ![Demostración de diseño libre](./apps/docs/src/public/free-layout/free-layout-demo.gif)       |\n| [Lienzo de diseño fijo](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Lienzo de diseño fijo donde los nodos se pueden arrastrar a posiciones específicas, con soporte para nodos compuestos como ramas y bucles.                                                             | ![Demostración de diseño fijo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)      |\n| [Formulario](https://flowgram.ai/examples/node-form/basic.html)                                | El motor de formularios gestiona las operaciones CRUD de datos de nodos y proporciona capacidades de renderizado, validación, efectos secundarios, vinculación y captura de errores, simplificando el desarrollo de configuraciones de nodos. | ![Formulario](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |\n| [Variable](https://flowgram.ai/guide/variable/basic.html)                                      | El motor de variables admite restricciones de ámbito, inspección de estructura de variables e inferencia de tipos, facilitando la gestión del flujo de datos dentro del flujo de trabajo.                                     | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5)   |\n\n\n## 📖 Documentación\n\nPuede encontrar la documentación de FlowGram [en el sitio web](https://flowgram.ai).\n\nLa documentación se divide en varias secciones:\n\n- [Inicio rápido](https://flowgram.ai/guide/getting-started/introduction.html)\n- [Lienzo](https://flowgram.ai/guide/free-layout/load.html)\n- [Formulario](https://flowgram.ai/guide/form/form.html)\n- [Variable](https://flowgram.ai/guide/variable/basic.html)\n- [Material](https://flowgram.ai/materials/introduction.html)\n- [Tiempo de ejecución](https://flowgram.ai/guide/runtime/introduction.html)\n- [Guías avanzadas](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [Referencia de la API](https://flowgram.ai/api/index.html)\n- [Dónde obtener soporte](https://flowgram.ai/guide/contact-us.html)\n- [Guía de contribución](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 Colaboradores\n\n[![Colaboradores de FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 Adopción\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) es una herramienta de desarrollo de agentes de IA todo en uno. Coze Studio, que proporciona los últimos modelos y herramientas grandes, varios modos y marcos de desarrollo, ofrece el entorno de desarrollo de agentes de IA más conveniente, desde el desarrollo hasta la implementación.\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) es una herramienta de implementación de IA multiplataforma basada en flujos de trabajo.\n- [Certimate](https://github.com/certimate-go/certimate) es una herramienta de gestión de certificados SSL de código abierto que le ayuda a solicitar e implementar automáticamente certificados SSL con un flujo de trabajo visual. Es una de las opciones de cliente ACME que se enumeran en la documentación oficial de Let's Encrypt.\n\n## 📬 Contáctenos\n\n- Problemas: [Problemas](https://github.com/bytedance/flowgram.ai/issues)\n- Lark: Escanee el código QR a continuación con [Registrar Feishu](https://www.feishu.cn/en/) para unirse a nuestro grupo de usuarios de FlowGram.\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_JA.md",
    "content": "![画像](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![ライセンス](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![DeepWikiに聞く](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram｜ワークフロー開発フレームワーク\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGramは、構成可能で、視覚的で、統合しやすく、拡張可能なワークフロー開発フレームワーク＆ツールキットです。\n私たちの目標は、開発者がAIワークフロープラットフォームを**より速く**、**よりシンプルに**構築できるよう支援することです。\nFlowGramには、ワークフロー開発用の組み込みツール一式が付属しています。視覚的なフローキャンバス、ノード構成フォーム、変数スコープチェーン、すぐに使えるマテリアル（LLM、条件、コードエディターなど）です。これは既製のワークフロープラットフォームではありません。あなたのワークフロープラットフォームを構築するためのフレームワークとツールキットです。\n\n詳細は[FlowGram.AI 🌐](https://flowgram.ai)をご覧ください。\n\n## 🎬 デモ\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\n[CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main)または[StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)で開く\n\nこのデモでは、都市のリストを反復処理し、HTTP経由でリアルタイムの天気を取得し、コードノードで気温を解析し、LLMで服装の提案を生成し、条件でゲートし、ループ全体で結果を集計し、最後にアドバイザーLLMを使用して最も快適な都市を選択してから、結果を終了ノードに送信します。\n\n## 🚀 クイックスタート\n\n1. 新しいFlowGramプロジェクトを作成します:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> `Free Layout Demo ⭐️` テンプレートを選択することをお勧めします。\n\n2. プロジェクトを開始します:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. ブラウザで[http://localhost:3000](http://localhost:3000)を開きます。\n\n## ✨ 機能\n\n| 機能                                                                                              | 説明                                                                                                                                        | デモ                                                                                         |\n| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| [フリーレイアウトキャンバス](https://flowgram.ai/examples/free-layout/free-feature-overview.html) | ノードをどこにでも配置し、自由形式の線で接続できるフリーレイアウトキャンバス。                                                              | ![フリーレイアウトデモ](./apps/docs/src/public/free-layout/free-layout-demo.gif)             |\n| [固定レイアウトキャンバス](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | 分岐やループなどの複合ノードをサポートし、ノードを指定した位置にドラッグできる固定レイアウトキャンバス。                                    | ![固定レイアウトデモ](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)             |\n| [フォーム](https://flowgram.ai/examples/node-form/basic.html)                                     | フォームエンジンはノードデータのCRUD操作を管理し、レンダリング、検証、副作用、連動、エラー処理機能を提供し、ノード設定の開発を簡素化します。 | ![フォーム](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |\n| [変数](https://flowgram.ai/guide/variable/basic.html)                                             | 変数エンジンはスコープ制約、変数構造検査、型推論をサポートし、ワークフロー内のデータフローの管理を容易にします。            | ![変数](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5)     |\n\n\n## 📖 ドキュメント\n\nFlowGramのドキュメントは[ウェブサイト](https://flowgram.ai)でご覧いただけます。\n\nドキュメントはいくつかのセクションに分かれています。\n\n- [クイックスタート](https://flowgram.ai/guide/getting-started/introduction.html)\n- [キャンバス](https://flowgram.ai/guide/free-layout/load.html)\n- [フォーム](https://flowgram.ai/guide/form/form.html)\n- [変数](https://flowgram.ai/guide/variable/basic.html)\n- [マテリアル](https://flowgram.ai/materials/introduction.html)\n- [ランタイム](https://flowgram.ai/guide/runtime/introduction.html)\n- [高度なガイド](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [APIリファレンス](https://flowgram.ai/api/index.html)\n- [サポートの入手先](https://flowgram.ai/guide/contact-us.html)\n- [貢献ガイド](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 貢献者\n\n[![FlowGram.AI貢献者](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 採用\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio)は、オールインワンのAIエージェント開発ツールです。最新の主要モデルとツール、さまざまな開発モードとフレームワークを提供するCoze Studioは、開発から展開まで、最も便利なAIエージェント開発環境を提供します。\n- [NNDeploy](https://github.com/NNDeploy/nndeploy)は、ワークフローベースのマルチプラットフォームAI展開ツールです。\n- [Certimate](https://github.com/certimate-go/certimate)は、視覚的なワークフローでSSL証明書を自動的に申請および展開するのに役立つオープンソースのSSL証明書管理ツールです。これは、Let's Encryptの公式ドキュメントに記載されているACMEクライアントオプションの1つです。\n\n## 📬 お問い合わせ\n\n- 問題：[問題](https://github.com/bytedance/flowgram.ai/issues)\n- Lark：[Register Feishu](https://www.feishu.cn/en/)で以下のQRコードをスキャンして、FlowGramユーザーグループに参加してください。\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_PT.md",
    "content": "![Imagem](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![Licença](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Pergunte ao DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram | Estrutura de desenvolvimento de fluxo de trabalho\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram é uma estrutura e kit de ferramentas de desenvolvimento de fluxo de trabalho componível, visual, fácil de integrar e extensível.\nNosso objetivo é ajudar os desenvolvedores a criar plataformas de fluxo de trabalho de IA de forma **mais rápida** e **simples**.\nO FlowGram vem com um conjunto de ferramentas integradas para o desenvolvimento de fluxo de trabalho: uma tela de fluxo visual, formulários de configuração de nós, uma cadeia de escopo de variáveis e materiais prontos para uso (LLM, Condição, Editor de Código, etc.). Não é uma plataforma de fluxo de trabalho pronta; é a estrutura e o kit de ferramentas para construir a sua.\n\nSaiba mais em [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 Demonstração\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\nAbra no [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) ou [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n\nNesta demonstração, iteramos por uma lista de cidades, buscamos o clima em tempo real via HTTP, analisamos as temperaturas com um nó de Código, geramos sugestões de roupas com um LLM, controlamos por uma Condição, agregamos os resultados ao longo do loop e, finalmente, usamos um LLM Conselheiro para escolher a cidade mais confortável antes de enviar o resultado para o nó Final.\n\n## 🚀 Início rápido\n\n1. Crie um novo projeto FlowGram:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> Recomendamos escolher o template `Free Layout Demo ⭐️`.\n\n2. Inicie o projeto:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. Abra [http://localhost:3000](http://localhost:3000) no seu navegador.\n\n## ✨ Recursos\n\n| Recurso                                                                                      | Descrição                                                                                                                                                                            | Demonstração                                                                                   |\n| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |\n| [Tela de Layout Livre](https://flowgram.ai/examples/free-layout/free-feature-overview.html)  | Tela de layout livre onde os nós podem ser colocados em qualquer lugar e conectados usando linhas de forma livre.                                                                    | ![Demonstração de Layout Livre](./apps/docs/src/public/free-layout/free-layout-demo.gif)       |\n| [Tela de Layout Fixo](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Tela de layout fixo onde os nós podem ser arrastados para posições especificadas, com suporte para nós compostos como ramificações e loops.                                          | ![Demonstração de Layout Fixo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)      |\n| [Formulário](https://flowgram.ai/examples/node-form/basic.html)                              | O mecanismo de formulário gerencia as operações CRUD de dados do nó e fornece recursos de renderização, validação, efeitos colaterais, vinculação e captura de erros, simplificando o desenvolvimento de configurações de nó. | ![Formulário](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78) |\n| [Variável](https://flowgram.ai/guide/variable/basic.html)                                    | O mecanismo de variáveis suporta restrições de escopo, inspeção de estrutura de variáveis e inferência de tipos, facilitando o gerenciamento do fluxo de dados dentro do fluxo de trabalho.                           | ![Variável](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5)   |\n\n\n## 📖 Documentação\n\nVocê pode encontrar a documentação do FlowGram [no site](https://flowgram.ai).\n\nA documentação está dividida em várias seções:\n\n- [Início Rápido](https://flowgram.ai/guide/getting-started/introduction.html)\n- [Tela](https://flowgram.ai/guide/free-layout/load.html)\n- [Formulário](https://flowgram.ai/guide/form/form.html)\n- [Variável](https://flowgram.ai/guide/variable/basic.html)\n- [Material](https://flowgram.ai/materials/introduction.html)\n- [Tempo de Execução](https://flowgram.ai/guide/runtime/introduction.html)\n- [Guias Avançados](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [Referência da API](https://flowgram.ai/api/index.html)\n- [Onde obter suporte](https://flowgram.ai/guide/contact-us.html)\n- [Guia de contribuição](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 Colaboradores\n\n[![Colaboradores do FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 Adoção\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) é uma ferramenta de desenvolvimento de agente de IA tudo-em-um. Fornecendo os modelos e ferramentas mais recentes, vários modos e estruturas de desenvolvimento, o Coze Studio oferece o ambiente de desenvolvimento de agente de IA mais conveniente, do desenvolvimento à implantação.\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) é uma ferramenta de implantação de IA multiplataforma baseada em fluxo de trabalho.\n- [Certimate](https://github.com/certimate-go/certimate) é uma ferramenta de gerenciamento de certificados SSL de código aberto que ajuda você a solicitar e implantar certificados SSL automaticamente com um fluxo de trabalho visual. É uma das opções de cliente ACME listadas na documentação oficial do Let's Encrypt.\n\n## 📬 Contate-nos\n\n- Problemas: [Problemas](https://github.com/bytedance/flowgram.ai/issues)\n- Lark: Digitalize o código QR abaixo com [Registrar Feishu](https://www.feishu.cn/en/) para se juntar ao nosso grupo de usuários FlowGram.\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_RU.md",
    "content": "![Изображение](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![Лицензия](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Спросить DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram | Фреймворк для разработки рабочих процессов\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram — это компонуемый, визуальный, легко интегрируемый и расширяемый фреймворк и набор инструментов для разработки рабочих процессов.\nНаша цель — помочь разработчикам создавать платформы для рабочих процессов ИИ **быстрее** и **проще**.\nFlowGram поставляется со встроенным набором инструментов для разработки рабочих процессов: визуальным холстом потока, формами конфигурации узлов, цепочкой области видимости переменных и готовыми к использованию материалами (LLM, Условие, Редактор кода и т. д.). Это не готовая платформа для рабочих процессов; это фреймворк и набор инструментов для создания вашей собственной.\n\nУзнайте больше на [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 Демо\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\nОткройте в [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) или [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n\nВ этом демо мы перебираем список городов, получаем погоду в реальном времени по HTTP, анализируем температуру с помощью узла «Код», генерируем предложения по одежде с помощью LLM, управляем с помощью «Условия», агрегируем результаты по циклу и, наконец, используем LLM-советника, чтобы выбрать самый комфортный город, прежде чем отправить результат в конечный узел.\n\n## 🚀 Быстрый старт\n\n1. Создайте новый проект FlowGram:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> Мы рекомендуем выбрать шаблон `Free Layout Demo ⭐️`.\n\n2. Запустите проект:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. Откройте [http://localhost:3000](http://localhost:3000) в вашем браузере.\n\n## ✨ Особенности\n\n| Особенность                                                                                                | Описание                                                                                                                                                                          | Демо                                                                                           |\n| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |\n| [Холст со свободной компоновкой](https://flowgram.ai/examples/free-layout/free-feature-overview.html)      | Холст со свободной компоновкой, где узлы можно размещать где угодно и соединять линиями произвольной формы.                                                                       | ![Демо со свободной компоновкой](./apps/docs/src/public/free-layout/free-layout-demo.gif)      |\n| [Холст с фиксированной компоновкой](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | Холст с фиксированной компоновкой, где узлы можно перетаскивать в указанные позиции, с поддержкой составных узлов, таких как ветви и циклы.                                       | ![Демо с фиксированной компоновкой](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif) |\n| [Форма](https://flowgram.ai/examples/node-form/basic.html)                                                 | Движок форм управляет операциями CRUD данных узлов и предоставляет возможности рендеринга, валидации, побочных эффектов, связывания и обработки ошибок, упрощая разработку конфигураций узлов. | ![Форма](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78)      |\n| [Переменная](https://flowgram.ai/guide/variable/basic.html)                                                | Движок переменных поддерживает ограничения области видимости, инспекцию структуры переменных и вывод типов, облегчая управление потоком данных в рабочем процессе.                                  | ![Переменная](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |\n\n\n## 📖 Документация\n\nВы можете найти документацию FlowGram [на веб-сайте](https://flowgram.ai).\n\nДокументация разделена на несколько разделов:\n\n- [Быстрый старт](https://flowgram.ai/guide/getting-started/introduction.html)\n- [Холст](https://flowgram.ai/guide/free-layout/load.html)\n- [Форма](https://flowgram.ai/guide/form/form.html)\n- [Переменная](https://flowgram.ai/guide/variable/basic.html)\n- [Материал](https://flowgram.ai/materials/introduction.html)\n- [Среда выполнения](https://flowgram.ai/guide/runtime/introduction.html)\n- [Расширенные руководства](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [Справочник по API](https://flowgram.ai/api/index.html)\n- [Где получить поддержку](https://flowgram.ai/guide/contact-us.html)\n- [Руководство по вкладу](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 Участники\n\n[![Участники FlowGram.AI](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 Внедрение\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) — это универсальный инструмент для разработки агентов ИИ. Предоставляя новейшие большие модели и инструменты, различные режимы и фреймворки разработки, Coze Studio предлагает наиболее удобную среду разработки агентов ИИ, от разработки до развертывания.\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) — это мультиплатформенный инструмент развертывания ИИ на основе рабочих процессов.\n- [Certimate](https://github.com/certimate-go/certimate) — это инструмент управления SSL-сертификатами с открытым исходным кодом, который помогает автоматически подавать заявки и развертывать SSL-сертификаты с помощью визуального рабочего процесса. Это один из вариантов клиента ACME, перечисленных в официальной документации Let's Encrypt.\n\n## 📬 Свяжитесь с нами\n\n- Проблемы: [Проблемы](https://github.com/bytedance/flowgram.ai/issues)\n- Lark: отсканируйте QR-код ниже с помощью [Register Feishu](https://www.feishu.cn/en/), чтобы присоединиться к нашей группе пользователей FlowGram.\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "README_ZH.md",
    "content": "![Image](https://github.com/user-attachments/assets/4f9dfa0e-e600-4d4e-9e73-c919184f7573)\n\n<div align=\"center\">\n\n[![License](https://img.shields.io/github/license/bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/blob/main/LICENSE) [![@flowgram.ai/editor](https://img.shields.io/npm/dm/%40flowgram.ai%2Fcore)](https://www.npmjs.com/package/@flowgram.ai/editor) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bytedance/flowgram.ai) [![juejin](https://img.shields.io/badge/juejin-FFFFFF?logo=juejin&logoColor=%23007FFF)](https://juejin.cn/column/7479814468601315362)\n\n[![](https://trendshift.io/api/badge/repositories/13877)](https://trendshift.io/repositories/13877)\n\n</div>\n\n# FlowGram.AI｜工作流开发框架\n\n[English](README.md) | [中文](README_ZH.md) | [Español](README_ES.md) | [Русский](README_RU.md) | [Português](README_PT.md) | [Deutsch](README_DE.md) | [日本語](README_JA.md)\n\nFlowGram 是一个可组合、可视化、易于集成且可扩展的工作流开发框架与工具集。\n我们的目标是帮助开发者以更快、更简单的方式搭建 AI 工作流平台。\nFlowGram 内置开箱开箱即用的工作流开发能力：可视化流程画布、节点配置表单、变量作用域链，以及开箱即用的物料（LLM、条件、代码编辑器等）。这并非一个现成的工作流平台，而是帮助你构建平台的框架与工具。\n\n了解更多 [FlowGram.AI 🌐](https://flowgram.ai)\n\n## 🎬 演示\n\n<https://github.com/user-attachments/assets/fee87890-ceec-4c07-b659-08afc4dedc26>\n\n在 [CodeSandbox 🌐](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) 或 [StackBlitz 🌐](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo) 中打开\n\n在该演示中，我们遍历一组城市，通过 HTTP 获取实时天气，用 Code 节点解析温度，借助 LLM 生成穿搭建议，经由 Condition 进行筛选，在循环中汇总结果，最后使用 Advisor LLM 选出最舒适的城市，并将结果发送至 End 节点。\n\n## 🚀 快速上手\n\n1. 创建一个新的 FlowGram 项目:\n\n```sh\nnpx @flowgram.ai/create-app@latest\n```\n\n> 我们推荐选择 `Free Layout Demo ⭐️` 模板。\n\n2. 启动项目:\n\n```sh\ncd demo-free-layout\nnpm install\nnpm start\n```\n\n3. 在浏览器中打开 [http://localhost:3000](http://localhost:3000)。\n\n## ✨ 特性\n\n| 特性                                                                                         | 说明                                                                              | 演示                                                                                         |\n| -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| [Free Layout Canvas](https://flowgram.ai/examples/free-layout/free-feature-overview.html)    | 自由布局画布，节点可任意摆放，可在节点间创建边进行链接。                          | ![Free Layout Demo](./apps/docs/src/public/free-layout/free-layout-demo.gif)                 |\n| [Fixed Layout Canvas](https://flowgram.ai/examples/fixed-layout/fixed-feature-overview.html) | 固定布局画布，节点可拖拽至指定位置，支持复合节点（如分支与循环）。                | ![Fixed Layout Demo](./apps/docs/src/public/fixed-layout/fixed-layout-demo.gif)              |\n| [Form](https://flowgram.ai/examples/node-form/basic.html)                                    | 表单引擎管理节点数据的增删改查操作，并提供渲染、验证、副作用、联动和错误捕获等功能，简化节点配置的开发。 | ![Form](https://github.com/user-attachments/assets/13e9b4cd-e993-4d21-901c-fb6cf106de78)     |\n| [Variable](https://flowgram.ai/guide/variable/basic.html)                                    | 变量引擎支持作用域约束、变量结构检查和类型推断等功能，便于管理工作流中的数据流。  | ![Variable](https://github.com/user-attachments/assets/442006db-25e3-4fb5-972c-7a0545638ff5) |\n\n## 📖 文档\n\n你可以在官网查阅完整文档：[FlowGram 文档](https://flowgram.ai)。\n\n文档分为以下章节：\n\n- [快速入门](https://flowgram.ai/guide/getting-started/introduction.html)\n- [自由画布](https://flowgram.ai/guide/free-layout/load.html)\n- [固定画布](https://flowgram.ai/guide/fixed-layout/load.html)\n- [表单](https://flowgram.ai/guide/form/form.html)\n- [变量](https://flowgram.ai/guide/variable/basic.html)\n- [素材](https://flowgram.ai/materials/introduction.html)\n- [运行时](https://flowgram.ai/guide/runtime/introduction.html)\n- [进阶指南](https://flowgram.ai/guide/advanced/zoom-scroll.html)\n- [API 参考](https://flowgram.ai/api/index.html)\n- [获取支持](https://flowgram.ai/guide/contact-us.html)\n- [贡献指南](https://flowgram.ai/guide/contributing.html)\n\n## 🙌 贡献者\n\n[![FlowGram.AI Contributors](https://contrib.rocks/image?repo=bytedance/flowgram.ai)](https://github.com/bytedance/flowgram.ai/graphs/contributors)\n\n## 🌍 被这些项目采用\n\n- [Coze Studio](https://github.com/coze-dev/coze-studio) 是一体化的 AI Agent 开发工具，提供最新的大模型与工具、多样的开发模式与框架，并在从开发到部署的全流程中，提供最便捷的 Agent 开发体验。\n- [NNDeploy](https://github.com/NNDeploy/nndeploy) 是一个基于工作流的多平台 AI 部署工具。\n- [Certimate](https://github.com/certimate-go/certimate) 是开源的 SSL 证书管理工具，借助可视化工作流帮助你自动申请与部署证书；它也是官方文档列出的 Let's Encrypt ACME 客户端选项之一。\n\n## 📬 联系我们\n\n- 问题反馈： [Issues](https://github.com/bytedance/flowgram.ai/issues)\n- 飞书：使用 [Register Feishu](https://www.feishu.cn/en/) 扫码下方二维码，加入 FlowGram 用户群。\n\n<img src=\"./apps/docs/src/public/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "apps/cli/.gitignore",
    "content": ".download\n"
  },
  {
    "path": "apps/cli/bin/index.js",
    "content": "#!/usr/bin/env node\nimport '../dist/index.js';\n"
  },
  {
    "path": "apps/cli/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineFlatConfig } from '@flowgram.ai/eslint-config';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n  },\n  settings: {\n    react: {\n      version: '18',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/cli/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/cli\",\n  \"version\": \"0.1.8\",\n  \"description\": \"A CLI tool to create demo projects or sync materials\",\n  \"bin\": {\n    \"flowgram-cli\": \"./bin/index.js\"\n  },\n  \"type\": \"module\",\n  \"files\": [\n    \"bin\",\n    \"src\",\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup src/index.ts --format esm,cjs --dts --out-dir dist\",\n    \"watch\": \"tsup src/index.ts --format esm,cjs --out-dir dist --watch\",\n    \"start\": \"node bin/index.js\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\"\n  },\n  \"dependencies\": {\n    \"fs-extra\": \"^9.1.0\",\n    \"commander\": \"^11.0.0\",\n    \"chalk\": \"^5.3.0\",\n    \"tar\": \"7.4.3\",\n    \"inquirer\": \"^12.9.4\",\n    \"ignore\": \"~7.0.5\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/download\": \"8.0.5\",\n    \"@types/fs-extra\": \"11.0.4\",\n    \"@types/node\": \"^18\",\n    \"@types/inquirer\": \"^9.0.9\",\n    \"tsup\": \"^8.0.1\",\n    \"eslint\": \"^9.0.0\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/create-app/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport https from 'https';\nimport { execSync } from 'child_process';\n\nimport * as tar from 'tar';\nimport inquirer from 'inquirer';\nimport fs from 'fs-extra';\nimport chalk from 'chalk';\n\nconst updateFlowGramVersions = (dependencies: any[], latestVersion: string) => {\n  for (const packageName in dependencies) {\n    if (packageName.startsWith('@flowgram.ai')) {\n      dependencies[packageName] = latestVersion;\n    }\n  }\n};\n\n// 使用 https 下载文件\nfunction downloadFile(url: string, dest: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const file = fs.createWriteStream(dest);\n\n    https\n      .get(url, (response) => {\n        if (response.statusCode !== 200) {\n          reject(new Error(`Download failed: ${response.statusCode}`));\n          return;\n        }\n\n        response.pipe(file);\n\n        file.on('finish', () => {\n          file.close();\n          resolve();\n        });\n      })\n      .on('error', (err) => {\n        fs.unlink(dest, () => reject(err));\n      });\n\n    file.on('error', (err) => {\n      fs.unlink(dest, () => reject(err));\n    });\n  });\n}\n\nexport const createApp = async (projectName?: string) => {\n  console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!'));\n  const latest = execSync('npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest')\n    .toString()\n    .trim();\n\n  let folderName = '';\n\n  if (!projectName) {\n    // 询问用户选择 demo 项目\n    const { repo } = await inquirer.prompt([\n      {\n        type: 'list',\n        name: 'repo',\n        message: 'Select a demo to create:',\n        choices: [\n          { name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },\n          { name: 'Free Layout Demo', value: 'demo-free-layout' },\n          { name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },\n          { name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' },\n          { name: 'Free Layout Nextjs Demo', value: 'demo-nextjs' },\n          { name: 'Free Layout Vite Demo Simple', value: 'demo-vite' },\n          { name: 'Demo Playground for infinite canvas', value: 'demo-playground' },\n        ],\n      },\n    ]);\n\n    folderName = repo;\n  } else {\n    if (\n      [\n        'fixed-layout',\n        'free-layout',\n        'fixed-layout-simple',\n        'free-layout-simple',\n        'playground',\n        'nextjs',\n      ].includes(projectName)\n    ) {\n      folderName = `demo-${projectName}`;\n    } else {\n      console.error('Invalid projectName. Please run \"npx create-app\" to choose demo.');\n      return;\n    }\n  }\n\n  try {\n    const targetDir = path.join(process.cwd());\n\n    // 下载 npm 包的 tarball\n    const downloadPackage = async () => {\n      try {\n        const url = `https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`;\n        const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);\n\n        console.log(chalk.blue(`Downloading ${url} ...`));\n        await downloadFile(url, tempTarballPath);\n\n        fs.ensureDirSync(targetDir);\n\n        await tar.x({\n          file: tempTarballPath,\n          C: targetDir,\n        });\n\n        fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName));\n\n        fs.unlinkSync(tempTarballPath);\n        return true;\n      } catch (error) {\n        console.error(`Error downloading or extracting package`);\n        console.error(error);\n        return false;\n      }\n    };\n\n    const res = await downloadPackage();\n\n    // 替换 package.json 内部的 @flowgram.ai 包版本为 latest\n    const pkgJsonPath = path.join(targetDir, folderName, 'package.json');\n    const data = fs.readFileSync(pkgJsonPath, 'utf-8');\n\n    const packageLatestVersion = execSync('npm view @flowgram.ai/core version --tag=latest latest')\n      .toString()\n      .trim();\n\n    const jsonData = JSON.parse(data);\n    if (jsonData.dependencies) {\n      updateFlowGramVersions(jsonData.dependencies, packageLatestVersion);\n    }\n\n    if (jsonData.devDependencies) {\n      updateFlowGramVersions(jsonData.devDependencies, packageLatestVersion);\n    }\n\n    fs.writeFileSync(pkgJsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');\n\n    if (res) {\n      console.log(chalk.green(`${folderName} Demo project created successfully!`));\n      console.log(chalk.yellow('Run the following commands to start:'));\n      console.log(chalk.cyan(`  cd ${folderName}`));\n      console.log(chalk.cyan('  npm install'));\n      console.log(chalk.cyan('  npm start'));\n    } else {\n      console.log(chalk.red('Download failed'));\n    }\n  } catch (error) {\n    console.error('Error downloading repo:', error);\n    return;\n  }\n};\n"
  },
  {
    "path": "apps/cli/src/find-materials/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport chalk from 'chalk';\n\nimport { traverseRecursiveTsFiles } from '../utils/ts-file';\nimport { Project } from '../utils/project';\nimport { loadNpm } from '../utils/npm';\nimport { Material } from '../materials/material';\n\nexport async function findUsedMaterials() {\n  // materialName can be undefined\n  console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));\n\n  const project = await Project.getSingleton();\n  project.printInfo();\n\n  const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');\n  const materials: Material[] = Material.listAll(formMaterialPkg);\n\n  const allUsedMaterials = new Set<Material>();\n\n  const exportName2Material = new Map<string, Material>();\n\n  for (const material of materials) {\n    if (!material.indexFile) {\n      console.warn(`Material ${material.name} not found`);\n      return;\n    }\n\n    console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);\n\n    material.allExportNames.forEach((exportName) => {\n      exportName2Material.set(exportName, material);\n    });\n  }\n\n  for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {\n    const fileMaterials = new Set<Material>();\n\n    let fileImportPrinted = false;\n    for (const importDeclaration of tsFile.imports) {\n      if (\n        !importDeclaration.source.startsWith('@flowgram.ai/form-materials') ||\n        !importDeclaration.namedImports?.length\n      ) {\n        continue;\n      }\n\n      if (!fileImportPrinted) {\n        fileImportPrinted = true;\n        console.log(chalk.bold(`\\n👀 Searching ${tsFile.path}`));\n      }\n\n      console.log(`🔍 ${importDeclaration.statement}`);\n\n      if (importDeclaration.namedImports) {\n        importDeclaration.namedImports.forEach((namedImport) => {\n          const material = exportName2Material.get(namedImport.imported);\n\n          if (material) {\n            fileMaterials.add(material);\n            allUsedMaterials.add(material);\n            console.log(`import ${chalk.bold(material.fullName)} by ${namedImport.imported}`);\n          }\n        });\n      }\n    }\n  }\n\n  console.log(chalk.bold('\\n📦 All used materials:'));\n  console.log([...allUsedMaterials].map((_material) => _material.fullName).join(','));\n}\n"
  },
  {
    "path": "apps/cli/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport { Command } from 'commander';\n\nimport { updateFlowgramVersion } from './update-version';\nimport { syncMaterial } from './materials';\nimport { findUsedMaterials } from './find-materials';\nimport { createApp } from './create-app';\n\nconst program = new Command();\n\nprogram.name('flowgram-cli').version('1.0.0').description('Flowgram CLI');\n\nprogram\n  .command('create-app')\n  .description('Create a new flowgram project')\n  .argument('[string]', 'Project name')\n  .action(async (projectName) => {\n    await createApp(projectName);\n  });\n\nprogram\n  .command('materials')\n  .description('Sync materials to the project')\n  .argument(\n    '[string]',\n    'Material name or names\\nExample 1: components/variable-selector \\nExample2: components/variable-selector,effect/provideJsonSchemaOutputs'\n  )\n  .option('--refresh-project-imports', 'Refresh project imports to copied materials', false)\n  .option('--target-material-root-dir <string>', 'Target directory to copy materials')\n  .option('--select-multiple', 'Select multiple materials', false)\n  .action(async (materialName, options) => {\n    await syncMaterial({\n      materialName,\n      refreshProjectImports: options.refreshProjectImports,\n      targetMaterialRootDir: options.targetMaterialRootDir\n        ? path.join(process.cwd(), options.targetMaterialRootDir)\n        : undefined,\n      selectMultiple: options.selectMultiple,\n    });\n  });\n\nprogram\n  .command('find-used-materials')\n  .description('Find used materials in the project')\n  .action(async () => {\n    await findUsedMaterials();\n  });\n\nprogram\n  .command('update-version')\n  .description('Update flowgram version in the project')\n  .argument('[string]', 'Flowgram version')\n  .action(async (version) => {\n    await updateFlowgramVersion(version);\n  });\n\nprogram.parse(process.argv);\n"
  },
  {
    "path": "apps/cli/src/materials/copy.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport fs from 'fs';\n\nimport { traverseRecursiveTsFiles } from '../utils/ts-file';\nimport { SyncMaterialContext } from './types';\n\ninterface CopyMaterialReturn {\n  packagesToInstall: string[];\n}\n\nexport const copyMaterials = (ctx: SyncMaterialContext): CopyMaterialReturn => {\n  const { selectedMaterials, project, formMaterialPkg, targetFormMaterialRoot } = ctx;\n  const formMaterialDependencies = formMaterialPkg.dependencies;\n  const packagesToInstall: Set<string> = new Set();\n\n  for (const material of selectedMaterials) {\n    const sourceDir: string = material.sourceDir;\n    const targetDir: string = path.join(targetFormMaterialRoot, material.type, material.name);\n\n    fs.cpSync(sourceDir, targetDir, { recursive: true });\n\n    for (const file of traverseRecursiveTsFiles(targetDir)) {\n      for (const importDeclaration of file.imports) {\n        const { source } = importDeclaration;\n\n        if (source.startsWith('@/')) {\n          // is inner import\n          console.log(`Replace Import from ${source} to @flowgram.ai/form-materials`);\n          file.replaceImport(\n            [importDeclaration],\n            [{ ...importDeclaration, source: '@flowgram.ai/form-materials' }]\n          );\n          packagesToInstall.add(`@flowgram.ai/form-materials@${project.flowgramVersion}`);\n        } else if (!source.startsWith('.') && !source.startsWith('react')) {\n          // check if is in form material dependencies\n          const [dep, version] =\n            Object.entries(formMaterialDependencies).find(([_key]) => source.startsWith(_key)) ||\n            [];\n          if (!dep) {\n            continue;\n          }\n          if (dep.startsWith('@flowgram.ai/')) {\n            packagesToInstall.add(`${dep}@${project.flowgramVersion}`);\n          } else {\n            packagesToInstall.add(`${dep}@${version}`);\n          }\n        }\n      }\n    }\n  }\n\n  return {\n    packagesToInstall: [...packagesToInstall],\n  };\n};\n"
  },
  {
    "path": "apps/cli/src/materials/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport chalk from 'chalk';\n\nimport { Project } from '../utils/project';\nimport { loadNpm } from '../utils/npm';\nimport { MaterialCliOptions, SyncMaterialContext } from './types';\nimport { getSelectedMaterials } from './select';\nimport { executeRefreshProjectImport } from './refresh-project-import';\nimport { Material } from './material';\nimport { copyMaterials } from './copy';\n\nexport async function syncMaterial(cliOpts: MaterialCliOptions) {\n  const { refreshProjectImports, targetMaterialRootDir } = cliOpts;\n\n  // materialName can be undefined\n  console.log(chalk.bold('🚀 Welcome to @flowgram.ai form-materials CLI!'));\n\n  const project = await Project.getSingleton();\n  project.printInfo();\n\n  // where to place all material in target project\n  const targetFormMaterialRoot =\n    targetMaterialRootDir || path.join(project.projectPath, 'src', 'form-materials');\n  console.log(chalk.black(`  - Target material root: ${targetFormMaterialRoot}`));\n\n  if (!project.flowgramVersion) {\n    throw new Error(\n      chalk.red(\n        '❌ Please install @flowgram.ai/fixed-layout-editor or @flowgram.ai/free-layout-editor'\n      )\n    );\n  }\n\n  const formMaterialPkg = await loadNpm('@flowgram.ai/form-materials');\n\n  let selectedMaterials: Material[] = await getSelectedMaterials(cliOpts, formMaterialPkg);\n\n  // Ensure material is defined before proceeding\n  if (!selectedMaterials.length) {\n    console.error(chalk.red('No material selected. Exiting.'));\n    process.exit(1);\n  }\n\n  const context: SyncMaterialContext = {\n    selectedMaterials: selectedMaterials,\n    project,\n    formMaterialPkg,\n    cliOpts,\n    targetFormMaterialRoot,\n  };\n\n  // Copy the materials to the project\n  console.log(chalk.bold('🚀 The following materials will be added to your project'));\n  console.log(selectedMaterials.map((material) => `📦 ${material.fullName}`).join('\\n'));\n  console.log('\\n');\n\n  let { packagesToInstall } = copyMaterials(context);\n\n  // Refresh project imports\n  if (refreshProjectImports) {\n    console.log(chalk.bold('🚀 Refresh imports in your project'));\n    executeRefreshProjectImport(context);\n  }\n\n  // Install the dependencies\n  await project.addDependencies(packagesToInstall);\n  console.log(chalk.bold('\\n✅ These npm dependencies is added to your package.json'));\n  packagesToInstall.forEach((_package) => {\n    console.log(`- ${_package}`);\n  });\n  console.log(chalk.bold(chalk.bold('\\n➡️ Please run npm install to install dependencies\\n')));\n}\n"
  },
  {
    "path": "apps/cli/src/materials/material.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport { readdirSync } from 'fs';\n\nimport { getIndexTsFile } from '../utils/ts-file';\nimport { LoadedNpmPkg } from '../utils/npm';\n\nexport class Material {\n  protected static _all_materials_cache: Material[] = [];\n\n  static ALL_TYPES = [\n    'components',\n    'effects',\n    'plugins',\n    'shared',\n    'validate',\n    'form-plugins',\n    'hooks',\n  ];\n\n  constructor(public type: string, public name: string, public formMaterialPkg: LoadedNpmPkg) {}\n\n  get fullName() {\n    return `${this.type}/${this.name}`;\n  }\n\n  get sourceDir() {\n    return path.join(this.formMaterialPkg.srcPath, this.type, this.name);\n  }\n\n  get indexFile() {\n    return getIndexTsFile(this.sourceDir);\n  }\n\n  get allExportNames() {\n    return this.indexFile?.allExportNames || [];\n  }\n\n  static listAll(formMaterialPkg: LoadedNpmPkg): Material[] {\n    if (!this._all_materials_cache.length) {\n      this._all_materials_cache = Material.ALL_TYPES.map((type) => {\n        const materialsPath: string = path.join(formMaterialPkg.srcPath, type);\n        return readdirSync(materialsPath)\n          .map((_path: string) => {\n            if (_path === 'index.ts') {\n              return null;\n            }\n\n            return new Material(type, _path, formMaterialPkg);\n          })\n          .filter((material): material is Material => material !== null);\n      }).flat();\n    }\n\n    return this._all_materials_cache;\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/materials/refresh-project-import.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport chalk from 'chalk';\n\nimport { traverseRecursiveTsFiles } from '../utils/ts-file';\nimport { ImportDeclaration, NamedImport } from '../utils/import';\nimport { SyncMaterialContext } from './types';\nimport { Material } from './material';\n\nexport function executeRefreshProjectImport(context: SyncMaterialContext) {\n  const { selectedMaterials, project, targetFormMaterialRoot } = context;\n\n  const exportName2Material = new Map<string, Material>();\n\n  const targetModule = `@/${path.relative(project.srcPath, targetFormMaterialRoot)}`;\n\n  for (const material of selectedMaterials) {\n    if (!material.indexFile) {\n      console.warn(`Material ${material.name} not found`);\n      return;\n    }\n\n    console.log(`👀 The exports of ${material.name} is ${material.allExportNames.join(',')}`);\n\n    material.allExportNames.forEach((exportName) => {\n      exportName2Material.set(exportName, material);\n    });\n  }\n\n  for (const tsFile of traverseRecursiveTsFiles(project.srcPath)) {\n    for (const importDeclaration of tsFile.imports) {\n      if (importDeclaration.source.startsWith('@flowgram.ai/form-materials')) {\n        // Import Module and its related named Imported\n        const restImports: NamedImport[] = [];\n        const importMap: Record<string, NamedImport[]> = {};\n\n        if (!importDeclaration.namedImports) {\n          continue;\n        }\n\n        for (const nameImport of importDeclaration.namedImports) {\n          const material = exportName2Material.get(nameImport.imported);\n          if (material) {\n            const importModule = `${targetModule}/${material.fullName}`;\n            importMap[importModule] = importMap[importModule] || [];\n            importMap[importModule].push(nameImport);\n          } else {\n            restImports.push(nameImport);\n          }\n        }\n\n        if (Object.keys(importMap).length === 0) {\n          continue;\n        }\n\n        const nextImports: ImportDeclaration[] = Object.entries(importMap).map(\n          ([importModule, namedImports]) => ({\n            ...importDeclaration,\n            namedImports,\n            source: importModule,\n          })\n        );\n\n        if (restImports?.length) {\n          nextImports.unshift({\n            ...importDeclaration,\n            namedImports: restImports,\n          });\n        }\n\n        tsFile.replaceImport([importDeclaration], nextImports);\n        console.log(chalk.green(`🔄 Refresh Imports In: ${tsFile.path}`));\n\n        console.log(\n          `From:\\n${importDeclaration.statement}\\nTo:\\n${nextImports\n            .map((item) => item.statement)\n            .join('\\n')}`\n        );\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/materials/select.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport inquirer from 'inquirer';\nimport chalk from 'chalk';\n\nimport { LoadedNpmPkg } from '../utils/npm';\nimport { MaterialCliOptions } from './types';\nimport { Material } from './material';\n\nexport const getSelectedMaterials = async (\n  cliOpts: MaterialCliOptions,\n  formMaterialPkg: LoadedNpmPkg\n) => {\n  const { materialName, selectMultiple } = cliOpts;\n\n  const materials: Material[] = Material.listAll(formMaterialPkg);\n\n  let selectedMaterials: Material[] = [];\n\n  // 1. Check if materialName is provided and exists in materials\n  if (materialName) {\n    selectedMaterials = materialName\n      .split(',')\n      .map((_name) => materials.find((_m) => _m.fullName === _name.trim())!)\n      .filter(Boolean);\n  }\n\n  // 2. If material not found or materialName not provided, prompt user to select\n  if (!selectedMaterials.length) {\n    console.log(chalk.yellow(`Material \"${materialName}\" not found. Please select from the list:`));\n\n    const choices = materials.map((_material) => ({\n      name: _material.fullName,\n      value: _material,\n    }));\n    if (selectMultiple) {\n      // User select one component\n      const result = await inquirer.prompt<{\n        material: Material[]; // Specify type for prompt result\n      }>([\n        {\n          type: 'checkbox',\n          name: 'material',\n          message: 'Select multiple materials to add:',\n          choices: choices,\n        },\n      ]);\n      selectedMaterials = result.material;\n    } else {\n      // User select one component\n      const result = await inquirer.prompt<{\n        material: Material; // Specify type for prompt result\n      }>([\n        {\n          type: 'list',\n          name: 'material',\n          message: 'Select one material to add:',\n          choices: choices,\n        },\n      ]);\n      selectedMaterials = [result.material];\n    }\n  }\n\n  return selectedMaterials;\n};\n"
  },
  {
    "path": "apps/cli/src/materials/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Project } from '../utils/project';\nimport { LoadedNpmPkg } from '../utils/npm';\nimport { Material } from './material';\n\nexport interface MaterialCliOptions {\n  materialName?: string;\n  refreshProjectImports?: boolean;\n  targetMaterialRootDir?: string;\n  selectMultiple?: boolean;\n}\n\nexport interface SyncMaterialContext {\n  selectedMaterials: Material[];\n  project: Project;\n  formMaterialPkg: LoadedNpmPkg;\n  cliOpts: MaterialCliOptions;\n  targetFormMaterialRoot: string;\n}\n"
  },
  {
    "path": "apps/cli/src/update-version/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport chalk from 'chalk';\n\nimport { getLatestVersion } from '../utils/npm';\nimport { traverseRecursiveFiles } from '../utils/file';\n\nexport async function updateFlowgramVersion(inputVersion?: string) {\n  console.log(chalk.bold('🚀 Welcome to @flowgram.ai update-version helper'));\n\n  // Get latest version\n  const latestVersion = await getLatestVersion('@flowgram.ai/editor');\n  const currentPath = process.cwd();\n  console.log('- Latest flowgram version: ', latestVersion);\n  console.log('- Current Path: ', currentPath);\n\n  // User Input flowgram version, default is latestVersion\n  const flowgramVersion: string = inputVersion || latestVersion;\n\n  for (const file of traverseRecursiveFiles(currentPath)) {\n    if (file.path.endsWith('package.json')) {\n      console.log('👀 Find package.json: ', file.path);\n      let updated = false;\n      const json = JSON.parse(file.content);\n      if (json.dependencies) {\n        for (const key in json.dependencies) {\n          if (key.startsWith('@flowgram.ai/')) {\n            updated = true;\n            json.dependencies[key] = flowgramVersion;\n            console.log(`- Update ${key} to ${flowgramVersion}`);\n          }\n        }\n      }\n      if (json.devDependencies) {\n        for (const key in json.devDependencies) {\n          if (key.startsWith('@flowgram.ai/')) {\n            updated = true;\n            json.devDependencies[key] = flowgramVersion;\n            console.log(`- Update ${key} to ${flowgramVersion}`);\n          }\n        }\n      }\n\n      if (updated) {\n        file.write(JSON.stringify(json, null, 2));\n        console.log(`✅ ${file.path} Updated`);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/utils/export.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function extractNamedExports(content: string) {\n  const valueExports = [];\n  const typeExports = [];\n\n  // Collect all type definition names\n  const typeDefinitions = new Set();\n  const typePatterns = [/\\b(?:type|interface)\\s+(\\w+)/g, /\\bexport\\s+(?:type|interface)\\s+(\\w+)/g];\n\n  let match;\n  for (const pattern of typePatterns) {\n    while ((match = pattern.exec(content)) !== null) {\n      typeDefinitions.add(match[1]);\n    }\n  }\n\n  // Match various export patterns\n  const exportPatterns = [\n    // export const/var/let/function/class/type/interface\n    /\\bexport\\s+(const|var|let|function|class|type|interface)\\s+(\\w+)/g,\n    // export { name1, name2 }\n    /\\bexport\\s*\\{([^}]+)\\}/g,\n    // export { name as alias }\n    /\\bexport\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+(\\w+)[^}]*\\}/g,\n    // export default function name()\n    /\\bexport\\s+default\\s+(?:function|class)\\s+(\\w+)/g,\n    // export type { Type1, Type2 }\n    /\\bexport\\s+type\\s*\\{([^}]+)\\}/g,\n    // export type { Original as Alias }\n    /\\bexport\\s+type\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+(\\w+)[^}]*\\}/g,\n  ];\n\n  // Handle first pattern: export const/var/let/function/class/type/interface\n  exportPatterns[0].lastIndex = 0;\n  while ((match = exportPatterns[0].exec(content)) !== null) {\n    const [, kind, name] = match;\n    if (kind === 'type' || kind === 'interface' || typeDefinitions.has(name)) {\n      typeExports.push(name);\n    } else {\n      valueExports.push(name);\n    }\n  }\n\n  // Handle second pattern: export { name1, name2 }\n  exportPatterns[1].lastIndex = 0;\n  while ((match = exportPatterns[1].exec(content)) !== null) {\n    const exportsList = match[1]\n      .split(',')\n      .map((item) => item.trim())\n      .filter((item) => item && !item.includes(' as '));\n\n    for (const name of exportsList) {\n      if (name.startsWith('type ')) {\n        typeExports.push(name.replace('type ', '').trim());\n      } else if (typeDefinitions.has(name)) {\n        typeExports.push(name);\n      } else {\n        valueExports.push(name);\n      }\n    }\n  }\n\n  // Handle third pattern: export { name as alias }\n  exportPatterns[2].lastIndex = 0;\n  while ((match = exportPatterns[2].exec(content)) !== null) {\n    const [, original, alias] = match;\n    if (typeDefinitions.has(original)) {\n      typeExports.push(alias);\n    } else {\n      valueExports.push(alias);\n    }\n  }\n\n  // Handle fourth pattern: export default function name()\n  exportPatterns[3].lastIndex = 0;\n  while ((match = exportPatterns[3].exec(content)) !== null) {\n    const name = match[1];\n    if (typeDefinitions.has(name)) {\n      typeExports.push(name);\n    } else {\n      valueExports.push(name);\n    }\n  }\n\n  // Handle fifth pattern: export type { Type1, Type2 }\n  exportPatterns[4].lastIndex = 0;\n  while ((match = exportPatterns[4].exec(content)) !== null) {\n    const exportsList = match[1]\n      .split(',')\n      .map((item) => item.trim())\n      .filter((item) => item && !item.includes(' as '));\n\n    for (const name of exportsList) {\n      typeExports.push(name);\n    }\n  }\n\n  // Handle sixth pattern: export type { Original as Alias }\n  exportPatterns[5].lastIndex = 0;\n  while ((match = exportPatterns[5].exec(content)) !== null) {\n    const [, original, alias] = match;\n    typeExports.push(alias);\n  }\n\n  // Deduplicate and sort\n  return {\n    values: [...new Set(valueExports)].sort(),\n    types: [...new Set(typeExports)].sort(),\n  };\n}\n"
  },
  {
    "path": "apps/cli/src/utils/file.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport fs from 'fs';\n\nimport ignore, { Ignore } from 'ignore';\n\nexport class File {\n  content: string;\n\n  isUtf8: boolean;\n\n  relativePath: string;\n\n  path: string;\n\n  suffix: string;\n\n  constructor(filePath: string, public root: string = '/') {\n    this.path = filePath;\n    this.relativePath = path.relative(this.root, this.path);\n    this.suffix = path.extname(this.path);\n\n    // Check if exists\n    if (!fs.existsSync(this.path)) {\n      throw Error(`File ${path} Not Exists`);\n    }\n\n    // If no utf-8, skip\n    try {\n      this.content = fs.readFileSync(this.path, 'utf-8');\n      this.isUtf8 = true;\n    } catch (e) {\n      this.isUtf8 = false;\n      return;\n    }\n  }\n\n  replace(updater: (content: string) => string) {\n    if (!this.isUtf8) {\n      console.warn('Not UTF-8 file skipped: ', this.path);\n      return;\n    }\n    this.content = updater(this.content);\n    fs.writeFileSync(this.path, this.content, 'utf-8');\n  }\n\n  write(nextContent: string) {\n    this.content = nextContent;\n    fs.writeFileSync(this.path, this.content, 'utf-8');\n  }\n}\n\nexport function* traverseRecursiveFilePaths(\n  folder: string,\n  ig: Ignore = ignore().add('.git'),\n  root: string = folder\n): Generator<string> {\n  const files = fs.readdirSync(folder);\n\n  // add .gitignore to ignore if exists\n  if (fs.existsSync(path.join(folder, '.gitignore'))) {\n    ig.add(fs.readFileSync(path.join(folder, '.gitignore'), 'utf-8'));\n  }\n\n  for (const file of files) {\n    const filePath = path.join(folder, file);\n\n    if (ig.ignores(path.relative(root, filePath))) {\n      continue;\n    }\n\n    if (fs.statSync(filePath).isDirectory()) {\n      yield* traverseRecursiveFilePaths(filePath, ig, root);\n    } else {\n      yield filePath;\n    }\n  }\n}\n\nexport function* traverseRecursiveFiles(folder: string): Generator<File> {\n  for (const filePath of traverseRecursiveFilePaths(folder)) {\n    yield new File(filePath, folder);\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/utils/import.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface NamedImport {\n  local?: string;\n  imported: string;\n  typeOnly?: boolean;\n}\n\n/**\n * Cases\n * import { A, B } from 'module';\n * import A from 'module';\n * import * as C from 'module';\n * import D, { type E, F } from 'module';\n * import A, { B as B1 } from 'module';\n */\nexport interface ImportDeclaration {\n  // origin statement\n  statement: string;\n\n  // import { A, B } from 'module';\n  namedImports?: NamedImport[];\n\n  // import A from 'module';\n  defaultImport?: string;\n\n  // import * as C from 'module';\n  namespaceImport?: string;\n\n  source: string;\n}\n\nexport function assembleImport(declaration: ImportDeclaration): string {\n  const { namedImports, defaultImport, namespaceImport, source } = declaration;\n  const importClauses = [];\n  if (namedImports) {\n    importClauses.push(\n      `{ ${namedImports\n        .map(\n          ({ local, imported, typeOnly }) =>\n            `${typeOnly ? 'type ' : ''}${imported}${local ? ` as ${local}` : ''}`\n        )\n        .join(', ')} }`\n    );\n  }\n  if (defaultImport) {\n    importClauses.push(defaultImport);\n  }\n  if (namespaceImport) {\n    importClauses.push(`* as ${namespaceImport}`);\n  }\n  return `import ${importClauses.join(', ')} from '${source}';`;\n}\n\nexport function replaceImport(\n  fileContent: string,\n  origin: ImportDeclaration,\n  replaceTo: ImportDeclaration[]\n): string {\n  const replaceImportStatements = replaceTo.map(assembleImport);\n  // replace origin statement\n  return fileContent.replace(origin.statement, replaceImportStatements.join('\\n'));\n}\n\nexport function* traverseFileImports(fileContent: string): Generator<ImportDeclaration> {\n  // 匹配所有 import 语句的正则表达式\n  const importRegex =\n    /import\\s+([^{}*,]*?)?(?:\\s*\\*\\s*as\\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*,?)?(?:\\s*\\{([^}]*)\\}\\s*,?)?(?:\\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\\s*,?)?\\s*from\\s*['\"`]([^'\"`]+)['\"`]\\;?/g;\n\n  let match;\n  while ((match = importRegex.exec(fileContent)) !== null) {\n    const [fullMatch, defaultPart, namespacePart, namedPart, defaultPart2, source] = match;\n\n    const declaration: ImportDeclaration = {\n      statement: fullMatch,\n      source: source,\n    };\n\n    // 处理默认导入\n    const defaultImport = defaultPart?.trim() || defaultPart2?.trim();\n    if (defaultImport && !namespacePart && !namedPart) {\n      declaration.defaultImport = defaultImport;\n    } else if (defaultImport && (namespacePart || namedPart)) {\n      declaration.defaultImport = defaultImport;\n    }\n\n    // 处理命名空间导入 (* as)\n    if (namespacePart) {\n      declaration.namespaceImport = namespacePart.trim();\n    }\n\n    // 处理命名导入\n    if (namedPart) {\n      const namedImports = [];\n      const namedItems = namedPart\n        .split(',')\n        .map((item) => item.trim())\n        .filter(Boolean);\n\n      for (const item of namedItems) {\n        const typeOnly = item.startsWith('type ');\n        const cleanItem = typeOnly ? item.slice(5).trim() : item;\n\n        if (cleanItem.includes(' as ')) {\n          const [imported, local] = cleanItem.split(' as ').map((s) => s.trim());\n          namedImports.push({\n            imported,\n            local,\n            typeOnly,\n          });\n        } else {\n          namedImports.push({\n            imported: cleanItem,\n            typeOnly,\n          });\n        }\n      }\n\n      if (namedImports.length > 0) {\n        declaration.namedImports = namedImports;\n      }\n    }\n\n    yield declaration;\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/utils/npm.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport https from 'https';\nimport { existsSync, readFileSync } from 'fs';\nimport { execSync } from 'child_process';\n\nimport * as tar from 'tar';\nimport fs from 'fs-extra';\n\nexport class LoadedNpmPkg {\n  constructor(public name: string, public version: string, public path: string) {}\n\n  get srcPath() {\n    return path.join(this.path, 'src');\n  }\n\n  get distPath() {\n    return path.join(this.path, 'dist');\n  }\n\n  protected _packageJson: Record<string, any>;\n\n  get packageJson() {\n    if (!this._packageJson) {\n      this._packageJson = JSON.parse(readFileSync(path.join(this.path, 'package.json'), 'utf8'));\n    }\n    return this._packageJson;\n  }\n\n  get dependencies(): Record<string, string> {\n    return this.packageJson.dependencies;\n  }\n}\n\n// 使用 https 下载文件\nfunction downloadFile(url: string, dest: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const file = fs.createWriteStream(dest);\n\n    https\n      .get(url, (response) => {\n        if (response.statusCode !== 200) {\n          reject(new Error(`Download failed: ${response.statusCode}`));\n          return;\n        }\n\n        response.pipe(file);\n\n        file.on('finish', () => {\n          file.close();\n          resolve();\n        });\n      })\n      .on('error', (err) => {\n        fs.unlink(dest, () => reject(err));\n      });\n\n    file.on('error', (err) => {\n      fs.unlink(dest, () => reject(err));\n    });\n  });\n}\n\nexport async function getLatestVersion(packageName: string): Promise<string> {\n  return execSync(`npm view ${packageName} version --tag=latest`).toString().trim();\n}\n\nexport async function loadNpm(packageName: string): Promise<LoadedNpmPkg> {\n  const packageLatestVersion = await getLatestVersion(packageName);\n\n  const packagePath = path.join(__dirname, `./.download/${packageName}-${packageLatestVersion}`);\n\n  if (existsSync(packagePath)) {\n    return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);\n  }\n\n  try {\n    // 获取 tarball 地址\n    const tarballUrl = execSync(`npm view ${packageName}@${packageLatestVersion} dist.tarball`)\n      .toString()\n      .trim();\n\n    // 临时 tarball 路径\n    const tempTarballPath = path.join(\n      __dirname,\n      `./.download/${packageName}-${packageLatestVersion}.tgz`\n    );\n\n    // 确保目录存在\n    fs.ensureDirSync(path.dirname(tempTarballPath));\n\n    // 下载 tarball\n    await downloadFile(tarballUrl, tempTarballPath);\n\n    fs.ensureDirSync(packagePath);\n\n    // 解压到目标目录\n    await tar.x({\n      file: tempTarballPath,\n      cwd: packagePath,\n      strip: 1,\n    });\n\n    // 删除临时文件\n    fs.unlinkSync(tempTarballPath);\n\n    return new LoadedNpmPkg(packageName, packageLatestVersion, packagePath);\n  } catch (error) {\n    console.error(`Error downloading or extracting package`);\n    console.error(error);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/utils/project.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport { existsSync, readFileSync, writeFileSync } from 'fs';\n\nimport chalk from 'chalk';\n\nimport { getLatestVersion } from './npm';\n\ninterface PackageJson {\n  dependencies: { [key: string]: string };\n  devDependencies?: { [key: string]: string };\n  peerDependencies?: { [key: string]: string };\n  [key: string]: any;\n}\n\nexport class Project {\n  flowgramVersion?: string;\n\n  projectPath: string;\n\n  packageJsonPath: string;\n\n  packageJson: PackageJson;\n\n  srcPath: string;\n\n  protected constructor() {}\n\n  async init() {\n    // get nearest package.json\n    let projectPath: string = process.cwd();\n\n    while (projectPath !== '/' && !existsSync(path.join(projectPath, 'package.json'))) {\n      projectPath = path.join(projectPath, '..');\n    }\n    if (projectPath === '/') {\n      throw new Error('Please run this command in a valid project');\n    }\n\n    this.projectPath = projectPath;\n\n    this.srcPath = path.join(projectPath, 'src');\n    this.packageJsonPath = path.join(projectPath, 'package.json');\n    this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf8'));\n\n    this.flowgramVersion =\n      this.packageJson.dependencies['@flowgram.ai/fixed-layout-editor'] ||\n      this.packageJson.dependencies['@flowgram.ai/free-layout-editor'] ||\n      this.packageJson.dependencies['@flowgram.ai/editor'];\n  }\n\n  async addDependency(dependency: string) {\n    let name: string;\n    let version: string;\n\n    // 处理作用域包（如 @types/react@1.0.0）\n    const lastAtIndex = dependency.lastIndexOf('@');\n\n    if (lastAtIndex <= 0) {\n      // 没有@符号 或者@在开头（如 @types/react）\n      name = dependency;\n      version = await getLatestVersion(name);\n    } else {\n      // 正常分割包名和版本\n      name = dependency.substring(0, lastAtIndex);\n      version = dependency.substring(lastAtIndex + 1);\n\n      // 如果版本部分为空，获取最新版本\n      if (!version.trim()) {\n        version = await getLatestVersion(name);\n      }\n    }\n\n    this.packageJson.dependencies[name] = version;\n    writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));\n  }\n\n  async addDependencies(dependencies: string[]) {\n    for (const dependency of dependencies) {\n      await this.addDependency(dependency);\n    }\n  }\n\n  writeToPackageJsonFile() {\n    writeFileSync(this.packageJsonPath, JSON.stringify(this.packageJson, null, 2));\n  }\n\n  printInfo() {\n    console.log(chalk.bold('Project Info:'));\n    console.log(chalk.black(`  - Flowgram Version: ${this.flowgramVersion}`));\n    console.log(chalk.black(`  - Project Path: ${this.projectPath}`));\n  }\n\n  static async getSingleton() {\n    const info = new Project();\n    await info.init();\n    return info;\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/utils/ts-file.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path, { join } from 'path';\nimport fs from 'fs';\n\nimport { assembleImport, ImportDeclaration, traverseFileImports } from './import';\nimport { File, traverseRecursiveFilePaths } from './file';\nimport { extractNamedExports } from './export';\n\nclass TsFile extends File {\n  exports: {\n    values: string[];\n    types: string[];\n  } = {\n    values: [],\n    types: [],\n  };\n\n  imports: ImportDeclaration[] = [];\n\n  get allExportNames() {\n    return [...this.exports.values, ...this.exports.types];\n  }\n\n  constructor(filePath: string, root?: string) {\n    super(filePath, root);\n\n    this.exports = extractNamedExports(fs.readFileSync(filePath, 'utf-8'));\n    this.imports = Array.from(traverseFileImports(fs.readFileSync(filePath, 'utf-8')));\n  }\n\n  addImport(importDeclarations: ImportDeclaration[]) {\n    importDeclarations.forEach((importDeclaration) => {\n      importDeclaration.statement = assembleImport(importDeclaration);\n    });\n    // add in last import statement\n    this.replace((content) => {\n      const lastImportStatement = this.imports[this.imports.length - 1];\n      return content.replace(\n        lastImportStatement.statement,\n        `${lastImportStatement?.statement}\\n${importDeclarations.map((item) => item.statement)}\\n`\n      );\n    });\n    this.imports.push(...importDeclarations);\n  }\n\n  removeImport(importDeclarations: ImportDeclaration[]) {\n    this.replace((content) =>\n      importDeclarations.reduce((prev, cur) => prev.replace(cur.statement, ''), content)\n    );\n    this.imports = this.imports.filter((item) => !importDeclarations.includes(item));\n  }\n\n  replaceImport(oldImports: ImportDeclaration[], newImports: ImportDeclaration[]) {\n    newImports.forEach((importDeclaration) => {\n      importDeclaration.statement = assembleImport(importDeclaration);\n    });\n    this.replace((content) => {\n      oldImports.forEach((oldImport, idx) => {\n        const replaceTo = newImports[idx];\n        if (replaceTo) {\n          content = content.replace(oldImport.statement, replaceTo.statement);\n          this.imports.map((_import) => {\n            if (_import.statement === oldImport.statement) {\n              _import = replaceTo;\n            }\n          });\n        } else {\n          content = content.replace(oldImport.statement, '');\n          this.imports = this.imports.filter(\n            (_import) => _import.statement !== oldImport.statement\n          );\n        }\n      });\n\n      const restNewImports = newImports.slice(oldImports.length);\n      if (restNewImports.length > 0) {\n        const lastImportStatement = newImports[oldImports.length - 1].statement;\n        content = content.replace(\n          lastImportStatement,\n          `${lastImportStatement}\n${restNewImports.map((item) => item.statement).join('\\n')}\n`\n        );\n      }\n      this.imports.push(...restNewImports);\n\n      return content;\n    });\n  }\n}\n\nexport function* traverseRecursiveTsFiles(folder: string): Generator<TsFile> {\n  for (const filePath of traverseRecursiveFilePaths(folder)) {\n    if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {\n      yield new TsFile(filePath, folder);\n    }\n  }\n}\n\nexport function getIndexTsFile(folder: string): TsFile | undefined {\n  // ts or tsx\n  const files = fs.readdirSync(folder);\n  for (const file of files) {\n    if (file === 'index.ts' || file === 'index.tsx') {\n      return new TsFile(path.join(folder, file), folder);\n    }\n  }\n  return undefined;\n}\n"
  },
  {
    "path": "apps/cli/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/cli/tsup.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  shims: true,\n})\n"
  },
  {
    "path": "apps/create-app/bin/index.js",
    "content": "#!/usr/bin/env node\nimport '../dist/index.js';\n"
  },
  {
    "path": "apps/create-app/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineFlatConfig } from '@flowgram.ai/eslint-config';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineFlatConfig({\n  preset: 'node',\n  packageRoot: __dirname,\n  ignore: [\n    '**/*.d.ts',\n    '**/__mocks__',\n    '**/node_modules',\n    '**/build',\n    '**/dist',\n    '**/es',\n    '**/lib',\n    '**/.codebase',\n    '**/.changeset',\n    '**/config',\n    '**/common/scripts',\n    '**/output',\n    'error-log-str.js',\n    '*.bundle.js',\n    '*.min.js',\n    '*.js.map',\n    '**/*.log',\n    '**/tsconfig.tsbuildinfo',\n    '**/vitest.config.ts',\n    'package.json',\n    '*.json',\n  ],\n  rules: {\n    'no-console': 'off',\n    'import/prefer-default-export': 'off',\n    'lines-between-class-members': 'warn',\n    'no-unused-vars': 'off',\n    'no-redeclare': 'off',\n    'no-empty-function': 'off',\n    'prefer-destructuring': 'off',\n    'no-underscore-dangle': 'off',\n    'no-multi-assign': 'off',\n    'arrow-body-style': 'warn',\n    'no-useless-constructor': 'off',\n    'no-param-reassign': 'off',\n    'max-classes-per-file': 'off',\n    'grouped-accessor-pairs': 'off',\n    'no-plusplus': 'off',\n    'no-restricted-syntax': 'off',\n    'import/extensions': 'off',\n    'consistent-return': 'off',\n    'no-use-before-define': 'off',\n    'no-bitwise': 'off',\n    'no-case-declarations': 'off',\n    'no-dupe-class-members': 'off',\n    'class-methods-use-this': 'off',\n    'default-param-last': 'off',\n  },\n});\n"
  },
  {
    "path": "apps/create-app/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/create-app\",\n  \"version\": \"0.1.8\",\n  \"description\": \"A CLI tool to create demo projects\",\n  \"bin\": {\n    \"create-app\": \"./bin/index.js\"\n  },\n  \"type\": \"module\",\n  \"files\": [\n    \"bin\",\n    \"src\",\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsup src/index.ts --format esm,cjs --dts --out-dir dist\",\n    \"start\": \"node bin/index.js\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\"\n  },\n  \"dependencies\": {\n    \"fs-extra\": \"^9.1.0\",\n    \"commander\": \"^11.0.0\",\n    \"chalk\": \"^5.3.0\",\n    \"tar\": \"7.4.3\",\n    \"inquirer\": \"^12.9.4\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/fs-extra\": \"11.0.4\",\n    \"@types/node\": \"^18\",\n    \"@types/inquirer\": \"^9.0.9\",\n    \"tsup\": \"^8.0.1\",\n    \"eslint\": \"^9.0.0\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/create-app/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport https from 'https';\nimport http from 'http';\nimport { execSync } from 'child_process';\n\nimport * as tar from 'tar';\nimport inquirer from 'inquirer';\nimport fs from 'fs-extra';\nimport { Command } from 'commander';\nimport chalk from 'chalk';\n\nconst program = new Command();\nconst args = process.argv.slice(2);\n\nconst updateFlowGramVersions = (dependencies: any[], latestVersion: string) => {\n  for (const packageName in dependencies) {\n    if (packageName.startsWith('@flowgram.ai')) {\n      dependencies[packageName] = latestVersion;\n    }\n  }\n};\n\n// 使用 http/https 下载文件\nfunction downloadFile(url: string, dest: string): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const lib = url.startsWith('https') ? https : http;\n\n    const file = fs.createWriteStream(dest);\n\n    const request = lib.get(url, (response) => {\n      if (response.statusCode !== 200) {\n        reject(new Error(`Download failed: ${response.statusCode}`));\n        return;\n      }\n\n      response.pipe(file);\n\n      file.on('finish', () => {\n        file.close();\n        resolve();\n      });\n    });\n\n    request.on('error', (err) => {\n      fs.unlink(dest, () => reject(err));\n    });\n\n    file.on('error', (err) => {\n      fs.unlink(dest, () => reject(err));\n    });\n  });\n}\n\nconst main = async () => {\n  console.log(chalk.green('Welcome to @flowgram.ai/create-app CLI!123123'));\n  const latest = execSync('npm view @flowgram.ai/demo-fixed-layout version --tag=latest latest')\n    .toString()\n    .trim();\n\n  let folderName = '';\n\n  if (!args?.length) {\n    const { repo } = await inquirer.prompt([\n      {\n        type: 'list',\n        name: 'repo',\n        message: 'Select a demo to create:',\n        choices: [\n          { name: 'Fixed Layout Demo', value: 'demo-fixed-layout' },\n          { name: 'Free Layout Demo', value: 'demo-free-layout' },\n          { name: 'Fixed Layout Demo Simple', value: 'demo-fixed-layout-simple' },\n          { name: 'Free Layout Demo Simple', value: 'demo-free-layout-simple' },\n          { name: 'Free Layout Nextjs Demo', value: 'demo-nextjs' },\n          { name: 'Free Layout Vite Demo Simple', value: 'demo-vite' },\n          { name: 'Demo Playground for infinite canvas', value: 'demo-playground' },\n        ],\n      },\n    ]);\n\n    folderName = repo;\n  } else {\n    if (\n      [\n        'fixed-layout',\n        'free-layout',\n        'fixed-layout-simple',\n        'free-layout-simple',\n        'playground',\n        'nextjs',\n      ].includes(args[0])\n    ) {\n      folderName = `demo-${args[0]}`;\n    } else {\n      console.error('Invalid argument. Please run \"npx create-app\" to choose demo.');\n      return;\n    }\n  }\n\n  try {\n    const targetDir = path.join(process.cwd());\n\n    const downloadPackage = async () => {\n      try {\n        const tempTarballPath = path.join(process.cwd(), `${folderName}.tgz`);\n        const url = `https://registry.npmjs.org/@flowgram.ai/${folderName}/-/${folderName}-${latest}.tgz`;\n\n        console.log(chalk.blue(`Downloading ${url} ...`));\n        await downloadFile(url, tempTarballPath);\n\n        fs.ensureDirSync(targetDir);\n\n        await tar.x({\n          file: tempTarballPath,\n          C: targetDir,\n        });\n\n        fs.renameSync(path.join(targetDir, 'package'), path.join(targetDir, folderName));\n        fs.unlinkSync(tempTarballPath);\n\n        return true;\n      } catch (error) {\n        console.error(`Error downloading or extracting package`);\n        console.error(error);\n        return false;\n      }\n    };\n\n    const res = await downloadPackage();\n\n    const pkgJsonPath = path.join(targetDir, folderName, 'package.json');\n    const data = fs.readFileSync(pkgJsonPath, 'utf-8');\n\n    const packageLatestVersion = execSync('npm view @flowgram.ai/core version --tag=latest latest')\n      .toString()\n      .trim();\n\n    const jsonData = JSON.parse(data);\n    if (jsonData.dependencies) {\n      updateFlowGramVersions(jsonData.dependencies, packageLatestVersion);\n    }\n\n    if (jsonData.devDependencies) {\n      updateFlowGramVersions(jsonData.devDependencies, packageLatestVersion);\n    }\n\n    fs.writeFileSync(pkgJsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');\n\n    if (res) {\n      console.log(chalk.green(`${folderName} Demo project created successfully!`));\n      console.log(chalk.yellow('Run the following commands to start:'));\n      console.log(chalk.cyan(`  cd ${folderName}`));\n      console.log(chalk.cyan('  npm install'));\n      console.log(chalk.cyan('  npm start'));\n    } else {\n      console.log(chalk.red('Download failed'));\n    }\n  } catch (error) {\n    console.error('Error downloading repo:', error);\n    return;\n  }\n};\n\nprogram.version('1.0.0').description('Create a demo project');\nprogram.parse(process.argv);\n\nmain();\n"
  },
  {
    "path": "apps/create-app/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/README.md",
    "content": "# FlowGram.AI - Demo Fixed Layout\n\nBest practices demo for fixed layout\n\n## Installation\n\n```shell\nnpx @flowgram.ai/create-app@latest fixed-layout\n```\n\n## Project Overview\n\n### Core Tech Stack\n- Frontend Framework: React 18 + TypeScript\n- Build Tool: Rsbuild (a modern build tool based on Rspack)\n- Styling: Less + Styled Components + CSS Variables\n- UI Component Library: Semi Design (@douyinfe/semi-ui)\n- State Management: Editor framework developed in-house by Flowgram\n- Dependency Injection: Inversify\n\n\n### Core Dependencies\n\n- @flowgram.ai/fixed-layout-editor: Core dependency for the fixed-layout editor\n- @flowgram.ai/fixed-semi-materials: Semi Design materials library\n- @flowgram.ai/form-materials: Form materials library\n- @flowgram.ai/group-plugin: Group plugin\n- @flowgram.ai/minimap-plugin: Minimap plugin\n- @flowgram.ai/export-plugin: Download/export plugin\n\n## Code Overview\n\n```\nsrc/\n├── app.tsx                    # Application entry component\n├── editor.tsx                 # Main editor component\n├── index.ts                   # Module export entry\n├── initial-data.ts            # Initial data configuration\n├── type.d.ts                  # Global type declarations\n│\n├── assets/                    # Static assets\n│   ├── icon-mouse.tsx         # Mouse icon component\n│   └── icon-pad.tsx           # Trackpad icon component\n│\n├── components/                # Common components library\n│   ├── index.ts               # Components export entry\n│   ├── node-list.tsx          # Node list component\n│   │\n│   ├── agent-adder/           # Agent adder component\n│   │   └── index.tsx\n│   ├── agent-label/           # Agent label component\n│   │   └── index.tsx\n│   ├── base-node/             # Base node component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── branch-adder/          # Branch adder component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── drag-node/             # Draggable node component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── node-adder/            # Node adder component\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   └── utils.ts\n│   ├── selector-box-popover/  # Selection box popover component\n│   │   └── index.tsx\n│   ├── sidebar/               # Sidebar components\n│   │   ├── index.tsx\n│   │   ├── sidebar-node-renderer.tsx\n│   │   ├── sidebar-provider.tsx\n│   │   └── sidebar-renderer.tsx\n│   └── tools/                 # Toolbar components\n│       ├── index.tsx\n│       ├── styles.tsx\n│       ├── fit-view.tsx       # Fit view tool\n│       ├── minimap-switch.tsx # Minimap toggle\n│       ├── minimap.tsx        # Minimap component\n│       ├── readonly.tsx       # Readonly mode toggle\n│       ├── run.tsx            # Run tool\n│       ├── save.tsx           # Save tool\n│       ├── switch-vertical.tsx # Vertical layout toggle\n│       └── zoom-select.tsx    # Zoom selector\n│\n├── context/                   # React Context state management\n│   ├── index.ts               # Context export entry\n│   ├── node-render-context.ts # Node render context\n│   └── sidebar-context.ts     # Sidebar context\n│\n├── form-components/           # Form components library\n│   ├── index.ts               # Export entry for form components\n│   ├── feedback.tsx           # Feedback component\n│   │\n│   ├── form-content/          # Form content components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-header/           # Form header components\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   ├── title-input.tsx\n│   │   └── utils.tsx\n│   ├── form-inputs/           # Form input components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-item/             # Form item component\n│   │   ├── index.css\n│   │   └── index.tsx\n│   ├── form-outputs/          # Form output components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   └── properties-edit/       # Property editing components\n│       ├── index.tsx\n│       ├── property-edit.tsx\n│       └── styles.tsx\n│\n├── hooks/                     # Custom React hooks\n│   ├── index.ts               # Hooks export entry\n│   ├── use-editor-props.ts    # Hook for editor properties\n│   ├── use-is-sidebar.ts      # Hook for sidebar state\n│   └── use-node-render-context.ts # Hook for node render context\n│\n├── nodes/                     # Flow node definitions\n│   ├── index.ts               # Node registry\n│   ├── default-form-meta.tsx  # Default form metadata\n│   │\n│   ├── agent/                 # Agent node type\n│   │   ├── index.ts\n│   │   ├── agent.ts\n│   │   ├── agent-llm.ts\n│   │   ├── agent-memory.ts\n│   │   ├── agent-tools.ts\n│   │   ├── memory.ts\n│   │   └── tool.ts\n│   ├── break-loop/            # Break loop node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case/                  # Case branch node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case-default/          # Default case node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── catch-block/           # Exception catch block node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── end/                   # End node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── if/                    # Conditional node\n│   │   └── index.ts\n│   ├── if-block/              # Conditional block node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── llm/                   # LLM node\n│   │   └── index.ts\n│   ├── loop/                  # Loop node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── start/                 # Start node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── switch/                # Switch branch node\n│   │   └── index.ts\n│   └── trycatch/              # Try-Catch node\n│       ├── index.ts\n│       └── form-meta.tsx\n│\n├── plugins/                   # Plugin system\n│   ├── index.ts               # Plugins export entry\n│   │\n│   ├── clipboard-plugin/      # Clipboard plugin\n│   │   └── create-clipboard-plugin.ts\n│   ├── group-plugin/          # Group plugin\n│   │   ├── index.ts\n│   │   ├── group-box-header.tsx\n│   │   ├── group-node.tsx\n│   │   ├── group-note.tsx\n│   │   ├── group-tools.tsx\n│   │   ├── icons/\n│   │   │   └── index.tsx\n│   │   └── multilang-textarea-editor/ # Multi-language textarea editor\n│   │       ├── index.css\n│   │       ├── index.tsx\n│   │       └── base-textarea.tsx\n│   └── variable-panel-plugin/ # Variable panel plugin\n│       ├── index.ts\n│       ├── variable-panel-layer.tsx\n│       ├── variable-panel-plugin.ts\n│       └── components/\n│           ├── full-variable-list.tsx\n│           ├── global-variable-editor.tsx\n│           └── variable-panel.tsx\n│\n├── services/                  # Services layer\n│   ├── index.ts\n│   └── custom-service.ts      # Custom service\n│\n├── shortcuts/                 # Shortcuts system\n│   ├── index.ts\n│   ├── constants.ts           # Shortcut constants\n│   └── utils.ts               # Shortcut utilities\n│\n└── typings/                   # Type definitions\n    ├── index.ts               # Types export entry\n    ├── json-schema.ts         # JSON Schema types\n    └── node.ts                # Node type definitions\n```\n\n## Architecture Design Analysis\n\n### Overall Architecture Pattern\n\nThis project adopts a layered architecture combined with modular design:\n\n1. Presentation Layer\n    - Component layer: responsible for UI rendering and user interactions\n    - Tools layer: provides editor tool features\n\n2. Business Logic Layer\n    - Node system: defines the behavior and properties of various flow nodes\n    - Plugin system: provides extensible functional modules\n    - Services layer: handles business logic and data operations\n\n3. Data Layer\n    - Context state management: manages global application state\n    - Type system: ensures consistency of data structures\n\n### Key Design Patterns\n\n#### 1. Provider Pattern\n```typescript\n// The main editor component uses multiple nested Providers\n<FixedLayoutEditorProvider {...editorProps}>\n  <SidebarProvider>\n    <EditorRenderer />\n    <DemoTools />\n    <SidebarRenderer />\n  </SidebarProvider>\n  </FixedLayoutEditorProvider>\n```\n\nUse cases:\n- `FixedLayoutEditorProvider`: provides core editor features and state\n- `SidebarProvider`: manages sidebar visibility and the selected node\n\n#### 2. Registry Pattern\n```typescript\nexport const FlowNodeRegistries: FlowNodeRegistry[] = [\n  StartNodeRegistry,\n  EndNodeRegistry,\n  SwitchNodeRegistry,\n  LLMNodeRegistry,\n  // ... more node types\n];\n```\n\nAdvantages:\n- Supports dynamic registration of node types\n- Easy to extend with new node types\n- Decouples node type definitions\n\n#### 3. Plugin Pattern\n```typescript\nplugins: () => [\n  createMinimapPlugin({...}),\n  createGroupPlugin({...}),\n  createClipboardPlugin(),\n  createVariablePanelPlugin({}),\n]\n```\n\nPlugin system highlights:\n- Minimap plugin: provides a canvas minimap\n- Group plugin: supports node grouping and management\n- Clipboard plugin: enables copy and paste\n- Variable panel plugin: provides a UI for variable management\n\n#### 4. Factory Pattern\nWidely used in node creation and configuration:\n```typescript\ngetNodeDefaultRegistry(type) {\n  return {\n    type,\n    meta: {\n      defaultExpanded: true,\n    },\n  };\n}\n```\n\n#### 5. Observer Pattern\nImplemented via the history system:\n```typescript\nhistory: {\n  enable: true,\n  enableChangeNode: true,\n  onApply: debounce((ctx, opt) => {\n    console.log('auto save: ', ctx.document.toJSON());\n  }, 100),\n}\n```\n\n#### 6. Strategy Pattern\nReflected in the materials system:\n```typescript\nmaterials: {\n  components: {\n    ...defaultFixedSemiMaterials,\n    [FlowRendererKey.ADDER]: NodeAdder,\n    [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n    // Different render strategies can be swapped by key\n  }\n}\n```\n\n### State Management Architecture\n\n#### Context System Design\nThe project uses multiple dedicated Contexts to manage different domains of state:\n\n1. SidebarContext: manages sidebar state\n```typescript\nexport const SidebarContext = React.createContext<{\n  visible: boolean;\n  nodeId?: string;\n  setNodeId: (node: string | undefined) => void;\n}>({ visible: false, setNodeId: () => {} });\n```\n\n2. NodeRenderContext: manages state related to node rendering\n3. IsSidebarContext: simple boolean state\n\n#### Custom Hooks\n- `useEditorProps`: centralizes all editor configuration props\n- `useIsSidebar`: determines whether the current environment is the sidebar\n- `useNodeRenderContext`: gets the node render context\n\n### Component Architecture\n\n#### Component Layering\n1. Base components\n    - `BaseNode`: base rendering component for all nodes\n    - `DragNode`: node component in drag state\n\n2. Functional components\n    - Adders: `NodeAdder`, `BranchAdder`, `AgentAdder`\n    - Tools: zoom, save, run, and other utilities\n\n3. Container components\n    - `Sidebar`: sidebar container and its subcomponents\n    - `Tools`: toolbar container\n\n### Data Flow Architecture\n\n#### Initial Data Structure\nThe project defines a complete initial flow dataset, including examples of multiple node types:\n- Start node: entry point of the flow, defines output parameters\n- Agent node: contains LLM, Memory, and Tools subcomponents\n- LLM node: large language model processing node\n- Switch node: conditional branch node\n- Loop node: loop processing node\n- TryCatch node: exception handling node\n- End node: end of the flow\n\n#### Data Transformation Mechanism\n```typescript\nfromNodeJSON(node, json) {\n  return json; // Transform logic on data import\n},\ntoNodeJSON(node, json) {\n  return json; // Transform logic on data export\n}\n```\n"
  },
  {
    "path": "apps/demo-fixed-layout/README.zh_CN.md",
    "content": "# FlowGram.AI - Demo Fixed Layout\n\n固定布局最佳实践 demo\n\n## 安装\n\n```shell\nnpx @flowgram.ai/create-app@latest fixed-layout\n```\n\n## 项目概览\n\n### 核心技术栈\n- **前端框架**: React 18 + TypeScript\n- **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)\n- **样式方案**: Less + Styled Components + CSS Variables\n- **UI 组件库**: Semi Design (@douyinfe/semi-ui)\n- **状态管理**: 基于 Flowgram 自研的编辑器框架\n- **依赖注入**: Inversify\n\n\n### 核心依赖包\n\n- **@flowgram.ai/fixed-layout-editor**: 固定布局编辑器核心依赖\n- **@flowgram.ai/fixed-semi-materials**: Semi Design 物料库\n- **@flowgram.ai/form-materials**: 表单物料库\n- **@flowgram.ai/group-plugin**: 分组插件\n- **@flowgram.ai/minimap-plugin**: 缩略图插件\n- **@flowgram.ai/export-plugin**: 下载/导出插件\n\n## 代码说明\n\n```\nsrc/\n├── app.tsx                    # 应用入口组件\n├── editor.tsx                 # 主编辑器组件\n├── index.ts                   # 模块导出入口\n├── initial-data.ts            # 初始化数据配置\n├── type.d.ts                  # 全局类型声明\n│\n├── assets/                    # 静态资源\n│   ├── icon-mouse.tsx         # 鼠标图标组件\n│   └── icon-pad.tsx           # 触控板图标组件\n│\n├── components/                # 通用组件库\n│   ├── index.ts               # 组件导出入口\n│   ├── node-list.tsx          # 节点列表组件\n│   │\n│   ├── agent-adder/           # Agent 添加器组件\n│   │   └── index.tsx\n│   ├── agent-label/           # Agent 标签组件\n│   │   └── index.tsx\n│   ├── base-node/             # 基础节点组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── branch-adder/          # 分支添加器组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── drag-node/             # 拖拽节点组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── node-adder/            # 节点添加器组件\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   └── utils.ts\n│   ├── selector-box-popover/  # 选择框弹出层组件\n│   │   └── index.tsx\n│   ├── sidebar/               # 侧边栏组件\n│   │   ├── index.tsx\n│   │   ├── sidebar-node-renderer.tsx\n│   │   ├── sidebar-provider.tsx\n│   │   └── sidebar-renderer.tsx\n│   └── tools/                 # 工具栏组件群\n│       ├── index.tsx\n│       ├── styles.tsx\n│       ├── fit-view.tsx       # 适应视图工具\n│       ├── minimap-switch.tsx # 缩略图开关\n│       ├── minimap.tsx        # 缩略图组件\n│       ├── readonly.tsx       # 只读模式切换\n│       ├── run.tsx            # 运行工具\n│       ├── save.tsx           # 保存工具\n│       ├── switch-vertical.tsx # 垂直布局切换\n│       └── zoom-select.tsx    # 缩放选择器\n│\n├── context/                   # React Context 状态管理\n│   ├── index.ts               # Context 导出入口\n│   ├── node-render-context.ts # 节点渲染上下文\n│   └── sidebar-context.ts     # 侧边栏上下文\n│\n├── form-components/           # 表单组件库\n│   ├── index.ts               # 表单组件导出入口\n│   ├── feedback.tsx           # 反馈组件\n│   │\n│   ├── form-content/          # 表单内容组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-header/           # 表单头部组件\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   ├── title-input.tsx\n│   │   └── utils.tsx\n│   ├── form-inputs/           # 表单输入组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-item/             # 表单项组件\n│   │   ├── index.css\n│   │   └── index.tsx\n│   ├── form-outputs/          # 表单输出组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   └── properties-edit/       # 属性编辑组件\n│       ├── index.tsx\n│       ├── property-edit.tsx\n│       └── styles.tsx\n│\n├── hooks/                     # 自定义 React Hooks\n│   ├── index.ts               # Hooks 导出入口\n│   ├── use-editor-props.ts    # 编辑器属性 Hook\n│   ├── use-is-sidebar.ts      # 侧边栏状态 Hook\n│   └── use-node-render-context.ts # 节点渲染上下文 Hook\n│\n├── nodes/                     # 流程节点定义\n│   ├── index.ts               # 节点注册表\n│   ├── default-form-meta.tsx  # 默认表单元数据\n│   │\n│   ├── agent/                 # Agent 节点类型\n│   │   ├── index.ts\n│   │   ├── agent.ts\n│   │   ├── agent-llm.ts\n│   │   ├── agent-memory.ts\n│   │   ├── agent-tools.ts\n│   │   ├── memory.ts\n│   │   └── tool.ts\n│   ├── break-loop/            # 跳出循环节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case/                  # Case 分支节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case-default/          # 默认 Case 节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── catch-block/           # 异常捕获块节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── end/                   # 结束节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── if/                    # 条件判断节点\n│   │   └── index.ts\n│   ├── if-block/              # 条件块节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── llm/                   # LLM 节点\n│   │   └── index.ts\n│   ├── loop/                  # 循环节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── start/                 # 开始节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── switch/                # Switch 分支节点\n│   │   └── index.ts\n│   └── trycatch/              # Try-Catch 节点\n│       ├── index.ts\n│       └── form-meta.tsx\n│\n├── plugins/                   # 插件系统\n│   ├── index.ts               # 插件导出入口\n│   │\n│   ├── clipboard-plugin/      # 剪贴板插件\n│   │   └── create-clipboard-plugin.ts\n│   ├── group-plugin/          # 分组插件\n│   │   ├── index.ts\n│   │   ├── group-box-header.tsx\n│   │   ├── group-node.tsx\n│   │   ├── group-note.tsx\n│   │   ├── group-tools.tsx\n│   │   ├── icons/\n│   │   │   └── index.tsx\n│   │   └── multilang-textarea-editor/\n│   │       ├── index.css\n│   │       ├── index.tsx\n│   │       └── base-textarea.tsx\n│   └── variable-panel-plugin/ # 变量面板插件\n│       ├── index.ts\n│       ├── variable-panel-layer.tsx\n│       ├── variable-panel-plugin.ts\n│       └── components/\n│           ├── full-variable-list.tsx\n│           ├── global-variable-editor.tsx\n│           └── variable-panel.tsx\n│\n├── services/                  # 服务层\n│   ├── index.ts\n│   └── custom-service.ts      # 自定义服务\n│\n├── shortcuts/                 # 快捷键系统\n│   ├── index.ts\n│   ├── constants.ts           # 快捷键常量\n│   └── utils.ts               # 快捷键工具函数\n│\n└── typings/                   # 类型定义\n    ├── index.ts               # 类型导出入口\n    ├── json-schema.ts         # JSON Schema 类型\n    └── node.ts                # 节点类型定义\n```\n\n## 架构设计分析\n\n### 整体架构模式\n\n该项目采用了**分层架构**和**模块化设计**相结合的架构模式：\n\n1. **表现层 (Presentation Layer)**\n   - 组件层：负责 UI 渲染和用户交互\n   - 工具层：提供编辑器工具功能\n\n2. **业务逻辑层 (Business Logic Layer)**\n   - 节点系统：定义各种流程节点的行为和属性\n   - 插件系统：提供可扩展的功能模块\n   - 服务层：处理业务逻辑和数据操作\n\n3. **数据层 (Data Layer)**\n   - Context 状态管理：管理应用全局状态\n   - 类型系统：确保数据结构的一致性\n\n### 核心设计模式\n\n#### 1. 提供者模式 (Provider Pattern)\n```typescript\n// 主编辑器组件使用多层 Provider 嵌套\n<FixedLayoutEditorProvider {...editorProps}>\n  <SidebarProvider>\n    <EditorRenderer />\n    <DemoTools />\n    <SidebarRenderer />\n  </SidebarProvider>\n</FixedLayoutEditorProvider>\n```\n\n**应用场景**:\n- `FixedLayoutEditorProvider`: 提供编辑器核心功能和状态\n- `SidebarProvider`: 管理侧边栏的显示状态和选中节点\n\n#### 2. 注册表模式 (Registry Pattern)\n```typescript\nexport const FlowNodeRegistries: FlowNodeRegistry[] = [\n  StartNodeRegistry,\n  EndNodeRegistry,\n  SwitchNodeRegistry,\n  LLMNodeRegistry,\n  // ... 更多节点类型\n];\n```\n\n**设计优势**:\n- 支持动态节点类型注册\n- 易于扩展新的节点类型\n- 实现了节点类型的解耦\n\n#### 3. 插件模式 (Plugin Pattern)\n```typescript\nplugins: () => [\n  createMinimapPlugin({...}),\n  createGroupPlugin({...}),\n  createClipboardPlugin(),\n  createVariablePanelPlugin({}),\n]\n```\n\n**插件系统特点**:\n- **缩略图插件**: 提供画布缩略图功能\n- **分组插件**: 支持节点分组管理\n- **剪贴板插件**: 实现复制粘贴功能\n- **变量面板插件**: 提供变量管理界面\n\n#### 4. 工厂模式 (Factory Pattern)\n在节点创建和配置中广泛使用：\n```typescript\ngetNodeDefaultRegistry(type) {\n  return {\n    type,\n    meta: {\n      defaultExpanded: true,\n    },\n  };\n}\n```\n\n#### 5. 观察者模式 (Observer Pattern)\n通过历史记录系统实现：\n```typescript\nhistory: {\n  enable: true,\n  enableChangeNode: true,\n  onApply: debounce((ctx, opt) => {\n    console.log('auto save: ', ctx.document.toJSON());\n  }, 100),\n}\n```\n\n#### 6. 策略模式 (Strategy Pattern)\n在材料系统中体现：\n```typescript\nmaterials: {\n  components: {\n    ...defaultFixedSemiMaterials,\n    [FlowRendererKey.ADDER]: NodeAdder,\n    [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n    // 可根据 key 替换不同的渲染策略\n  }\n}\n```\n\n### 状态管理架构\n\n#### Context 系统设计\n项目采用了多个专用的 Context 来管理不同领域的状态：\n\n1. **SidebarContext**: 管理侧边栏状态\n```typescript\nexport const SidebarContext = React.createContext<{\n  visible: boolean;\n  nodeId?: string;\n  setNodeId: (node: string | undefined) => void;\n}>({ visible: false, setNodeId: () => {} });\n```\n\n2. **NodeRenderContext**: 管理节点渲染相关状态\n3. **IsSidebarContext**: 简单的布尔状态管理\n\n#### 自定义 Hooks 设计\n- `useEditorProps`: 集中管理编辑器的所有配置属性\n- `useIsSidebar`: 判断当前是否在侧边栏环境中\n- `useNodeRenderContext`: 获取节点渲染上下文\n\n### 组件架构设计\n\n#### 组件分层结构\n1. **基础组件层**\n   - `BaseNode`: 所有节点的基础渲染组件\n   - `DragNode`: 拖拽状态下的节点组件\n\n2. **功能组件层**\n   - 添加器组件: `NodeAdder`, `BranchAdder`, `AgentAdder`\n   - 工具组件: 缩放、保存、运行等功能组件\n\n3. **容器组件层**\n   - `Sidebar`: 侧边栏容器及其子组件\n   - `Tools`: 工具栏容器\n\n### 数据流架构\n\n#### 初始数据结构\n项目定义了完整的初始流程数据，包含多种节点类型的示例：\n- **Start 节点**: 流程起始点，定义输出参数\n- **Agent 节点**: 包含 LLM、Memory、Tools 子组件\n- **LLM 节点**: 大语言模型处理节点\n- **Switch 节点**: 条件分支节点\n- **Loop 节点**: 循环处理节点\n- **TryCatch 节点**: 异常处理节点\n- **End 节点**: 流程结束点\n\n#### 数据转换机制\n```typescript\nfromNodeJSON(node, json) {\n  return json; // 数据导入时的转换逻辑\n},\ntoNodeJSON(node, json) {\n  return json; // 数据导出时的转换逻辑\n}\n```\n"
  },
  {
    "path": "apps/demo-fixed-layout/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FixedLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-fixed-layout/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-fixed-layout\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.ts\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\",\n    \"README.md\",\n    \"README.zh_CN.md\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-semi-materials\": \"workspace:*\",\n    \"@flowgram.ai/form-materials\": \"workspace:*\",\n    \"@flowgram.ai/group-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/export-plugin\": \"workspace:*\",\n    \"@flowgram.ai/panel-manager-plugin\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@rsbuild/plugin-less\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\",\n    \"less\": \"^4.1.2\",\n    \"less-loader\": \"^6\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { pluginLess } from '@rsbuild/plugin-less';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n    /**\n     * support inversify @injectable() and @inject decorators\n     */\n    decorators: {\n      version: 'legacy',\n    },\n  },\n  html: {\n    title: 'demo-fixed-layout',\n  },\n  tools: {\n    rspack: {\n      /**\n       * ignore warnings from @coze-editor/editor/language-typescript\n       */\n      ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { Editor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/assets/icon-mouse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconMouse(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 34}\n      height={height || 52}\n      viewBox=\"0 0 34 52\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.8\"\n      />\n    </svg>\n  );\n}\n\nexport const IconMouseTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/assets/icon-pad.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconPad(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 48}\n      height={height || 38}\n      viewBox=\"0 0 48 38\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect\n        x=\"1.83317\"\n        y=\"1.49998\"\n        width=\"44.3333\"\n        height=\"35\"\n        rx=\"3.5\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n      />\n      <path\n        d=\"M14.6665 30.6667H33.3332\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport const IconPadTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z\"\n    ></path>\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/agent-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { ToolNodeRegistry } from '../../nodes/agent/tool';\n\ninterface PropsType {\n  node: FlowNodeEntity;\n}\n\nexport function AgentAdder(props: PropsType) {\n  const { node } = props;\n\n  const nodeData = node.firstChild?.getData<FlowNodeRenderData>(FlowNodeRenderData);\n  const ctx = useClientContext();\n\n  async function addPort() {\n    ctx.operation.addNode(ToolNodeRegistry.onAdd!(ctx, node), {\n      parent: node,\n    });\n  }\n\n  /**\n   * 1. Tools can always be added\n   * 2. LLM/Memory can only be added when there is no block\n   */\n  const canAdd = node.flowNodeType === 'agentTools' || node.blocks.length === 0;\n\n  if (!canAdd) {\n    return null;\n  }\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        color: '#fff',\n        background: 'rgb(187, 191, 196)',\n        width: 20,\n        height: 20,\n        borderRadius: 10,\n        overflow: 'hidden',\n      }}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <div\n        style={{\n          width: 20,\n          height: 20,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          cursor: 'pointer',\n        }}\n        onClick={() => addPort()}\n      >\n        <IconPlus size=\"small\" />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/agent-label/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\nimport { Typography } from '@douyinfe/semi-ui';\n\ninterface PropsType {\n  node: FlowNodeEntity;\n}\n\nconst Text = Typography.Text;\n\nexport function AgentLabel(props: PropsType) {\n  const { node } = props;\n\n  let label = 'Default';\n\n  switch (node.flowNodeType) {\n    case 'agentMemory':\n      label = 'Memory';\n      break;\n    case 'agentLLM':\n      label = 'LLM';\n      break;\n    case 'agentTools':\n      label = 'Tools';\n  }\n\n  return (\n    <Text\n      ellipsis={{ showTooltip: true }}\n      style={{\n        maxWidth: 65,\n        fontSize: 12,\n        textAlign: 'center',\n        padding: '2px',\n        backgroundColor: 'var(--g-editor-background)',\n        color: '#8F959E',\n      }}\n    >\n      {label}\n    </Text>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/base-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { usePanelManager } from '@flowgram.ai/panel-manager-plugin';\nimport {\n  FlowNodeEntity,\n  useNodeRender,\n  PlaygroundEntityContext,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { ConfigProvider } from '@douyinfe/semi-ui';\n\nimport { NodeRenderContext } from '../../context';\nimport { BaseNodeStyle, ErrorIcon } from './styles';\nimport { nodeFormPanelFactory } from '../sidebar';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  /**\n   * Provides methods related to node rendering\n   * 提供节点渲染相关的方法\n   */\n  const nodeRender = useNodeRender();\n  /**\n   * It can only be used when nodeEngine is enabled\n   * 只有在节点引擎开启时候才能使用表单\n   */\n  const form = nodeRender.form;\n\n  /**\n   * Used to make the Tooltip scale with the node, which can be implemented by itself depending on the UI library\n   * 用于让 Tooltip 跟随节点缩放, 这个可以根据不同的 ui 库自己实现\n   */\n  const getPopupContainer = useCallback(() => node.renderData.node || document.body, []);\n\n  const panelManager = usePanelManager();\n\n  return (\n    <ConfigProvider getPopupContainer={getPopupContainer}>\n      {form?.state.invalid && <ErrorIcon />}\n      <BaseNodeStyle\n        /*\n         * onMouseEnter is added to a fixed layout node primarily to listen for hover highlighting of branch lines\n         * onMouseEnter 加到固定布局节点主要是为了监听 分支线条的 hover 高亮\n         **/\n        onMouseEnter={nodeRender.onMouseEnter}\n        onMouseLeave={nodeRender.onMouseLeave}\n        className={nodeRender.activated ? 'activated' : ''}\n        onClick={() => {\n          if (nodeRender.dragging) {\n            return;\n          }\n          panelManager.open(nodeFormPanelFactory.key, 'right', {\n            props: {\n              nodeId: nodeRender.node.id,\n            },\n          });\n        }}\n        style={{\n          /**\n           * Lets you precisely control the style of branch nodes\n           * 用于精确控制分支节点的样式\n           * isBlockIcon: 整个 condition 分支的 头部节点\n           * isBlockOrderIcon: 分支的第一个节点\n           */\n          ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? {} : {}),\n          ...nodeRender.node.getNodeRegistry().meta.style,\n          opacity: nodeRender.dragging ? 0.3 : 1,\n          outline: form?.state.invalid ? '1px solid red' : 'none',\n        }}\n      >\n        {/**\n         * PlaygroundEntityContext is used to allow forms and variables to correctly identify which node they currently belong to\n         * PlaygroundEntityContext 用于让表单和变量能正确识别当前属于哪个节点\n         */}\n        <PlaygroundEntityContext.Provider value={nodeRender.node}>\n          <NodeRenderContext.Provider value={nodeRender}>\n            {form?.render()}\n          </NodeRenderContext.Provider>\n        </PlaygroundEntityContext.Provider>\n      </BaseNodeStyle>\n    </ConfigProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/base-node/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { IconInfoCircle } from '@douyinfe/semi-icons';\n\nexport const BaseNodeStyle = styled.div`\n  align-items: flex-start;\n  background-color: #fff;\n  border: 1px solid rgba(6, 7, 9, 0.15);\n  border-radius: 8px;\n  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  width: 360px;\n  cursor: default;\n  &.activated {\n    border: 1px solid #82a7fc;\n  }\n`;\n\nexport const ErrorIcon = () => (\n  <IconInfoCircle\n    style={{\n      position: 'absolute',\n      color: 'red',\n      left: -6,\n      top: -6,\n      zIndex: 1,\n      background: 'white',\n      borderRadius: 8,\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/branch-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { CatchBlockNodeRegistry } from '../../nodes/catch-block';\nimport { CaseNodeRegistry } from '../../nodes/case';\nimport { Container } from './styles';\n\ninterface PropsType {\n  activated?: boolean;\n  node: FlowNodeEntity;\n}\n\nexport default function BranchAdder(props: PropsType) {\n  const { activated, node } = props;\n  const nodeData = node.firstChild!.renderData;\n  const ctx = useClientContext();\n  const { operation, playground } = ctx;\n  const { isVertical } = node;\n\n  function addBranch() {\n    const block = operation.addBlock(\n      node,\n      node.flowNodeType === 'switch'\n        ? CaseNodeRegistry.onAdd!(ctx, node)\n        : CatchBlockNodeRegistry.onAdd!(ctx, node),\n      {\n        index: 0,\n      }\n    );\n\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n  }\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <Container\n      isVertical={isVertical}\n      activated={activated}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <div\n        onClick={() => {\n          addBranch();\n        }}\n        aria-hidden=\"true\"\n        style={{ flexGrow: 1, textAlign: 'center' }}\n      >\n        <IconPlus />\n      </div>\n    </Container>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/branch-adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Container = styled.div<{ activated?: boolean; isVertical: boolean }>`\n  width: 28px;\n  height: 18px;\n  background: ${(props) => (props.activated ? '#82A7FC' : 'rgb(187, 191, 196)')};\n  display: flex;\n  border-radius: 9px;\n  justify-content: space-evenly;\n  align-items: center;\n  color: #fff;\n  font-size: 10px;\n  font-weight: bold;\n  transform: ${(props) => (props.isVertical ? '' : 'rotate(90deg)')};\n  div {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    svg {\n      width: 12px;\n      height: 12px;\n    }\n  }\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/drag-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeEntity, FlowNodeJSON, Xor } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistries } from '../../nodes';\nimport { Icon } from '../../form-components/form-header/styles';\nimport { UIDragNodeContainer, UIDragCounts } from './styles';\n\nexport type PropsType = Xor<\n  {\n    dragStart: FlowNodeEntity;\n  },\n  {\n    dragJSON: FlowNodeJSON;\n  }\n> & {\n  dragNodes: FlowNodeEntity[];\n};\n\nexport function DragNode(props: PropsType): JSX.Element {\n  const { dragStart, dragNodes, dragJSON } = props;\n\n  const icon = FlowNodeRegistries.find(\n    (registry) => registry.type === dragStart?.flowNodeType || dragJSON?.type\n  )?.info?.icon;\n\n  const dragLength = (dragNodes || [])\n    .map((_node) =>\n      _node.allCollapsedChildren.length\n        ? _node.allCollapsedChildren.filter((_n) => !_n.hidden).length\n        : 1\n    )\n    .reduce((acm, curr) => acm + curr, 0);\n\n  return (\n    <UIDragNodeContainer>\n      <Icon src={icon} />\n      {dragStart?.id || dragJSON?.id}\n      {dragLength > 1 && (\n        <>\n          <UIDragCounts>{dragLength}</UIDragCounts>\n          <UIDragNodeContainer\n            style={{\n              position: 'absolute',\n              top: 5,\n              right: -5,\n              left: 5,\n              bottom: -5,\n              opacity: 0.5,\n            }}\n          />\n        </>\n      )}\n    </UIDragNodeContainer>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/drag-node/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nconst primary = 'hsl(252 62% 54.9%)';\nconst primaryOpacity09 = 'hsl(252deg 62% 55% / 9%)';\n\nexport const UIDragNodeContainer = styled.div`\n  position: relative;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  column-gap: 8px;\n  cursor: pointer;\n  font-size: 19px;\n  border: 1px solid ${primary};\n  padding: 0 15px;\n  &:hover: {\n    background-color: ${primaryOpacity09};\n    color: ${primary};\n  }\n`;\n\nexport const UIDragCounts = styled.div`\n  position: absolute;\n  top: -8px;\n  right: -8px;\n  text-align: center;\n  line-height: 16px;\n  width: 16px;\n  height: 16px;\n  border-radius: 8px;\n  font-size: 12px;\n  color: #fff;\n  background-color: ${primary};\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { DemoTools } from './tools';\nexport { DragNode } from './drag-node';\nexport { AgentAdder } from './agent-adder';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/node-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useMemo, useState } from 'react';\n\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor';\nimport { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\nimport { Popover, Toast, Typography } from '@douyinfe/semi-ui';\nimport { IconCopyAdd, IconPlusCircle } from '@douyinfe/semi-icons';\n\nimport { NodeList } from '../node-list';\nimport { readData } from '../../shortcuts/utils';\nimport { generateNodeId } from './utils';\nimport { PasteIcon, Wrap } from './styles';\n\nconst generateNewIdForChildren = (n: FlowNodeEntity): FlowNodeEntity => {\n  if (n.blocks) {\n    return {\n      ...n,\n      id: generateNodeId(n),\n      blocks: n.blocks.map((b) => generateNewIdForChildren(b)),\n    } as FlowNodeEntity;\n  } else {\n    return {\n      ...n,\n      id: generateNodeId(n),\n    } as FlowNodeEntity;\n  }\n};\n\nexport default function Adder(props: {\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}) {\n  const { from } = props;\n  const isVertical = from.isVertical;\n  const [visible, setVisible] = useState(false);\n  const { playground, operation, clipboard } = useClientContext();\n\n  const [pasteIconVisible, setPasteIconVisible] = useState(false);\n\n  const activated = useMemo(\n    () => props.hoverActivated && !playground.config.readonly,\n    [props.hoverActivated, playground.config.readonly]\n  );\n\n  const add = (addProps: any) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const block = operation.addFromNode(from, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    setVisible(false);\n  };\n\n  const handlePaste = useCallback(async (e: any) => {\n    try {\n      e.stopPropagation();\n      const nodes = await readData(clipboard);\n\n      if (!nodes) {\n        Toast.error({\n          content: 'The clipboard content has been updated, please copy the node again.',\n        });\n        return;\n      }\n\n      nodes.reverse().forEach((n: FlowNodeEntity) => {\n        const newNodeData = generateNewIdForChildren(n);\n        operation.addFromNode(from, newNodeData);\n      });\n\n      Toast.success({\n        content: 'Paste successfully!',\n      });\n    } catch (error) {\n      console.error(error);\n      Toast.error({\n        content: (\n          <Typography.Text>\n            Paste failed, please check if you have permission to read the clipboard,\n          </Typography.Text>\n        ),\n      });\n    }\n  }, []);\n  if (playground.config.readonly) return null;\n\n  return (\n    <Popover\n      visible={visible}\n      onVisibleChange={setVisible}\n      content={<NodeList onSelect={add} from={from} />}\n      placement=\"right\"\n      trigger=\"click\"\n      popupAlign={{ offset: [30, 0] }}\n      overlayStyle={{\n        padding: 0,\n      }}\n    >\n      <Wrap\n        style={\n          props.hoverActivated\n            ? {\n                width: 15,\n                height: 15,\n              }\n            : {}\n        }\n        onMouseDown={(e) => e.stopPropagation()}\n      >\n        {props.hoverActivated ? (\n          <IconPlusCircle\n            onClick={() => {\n              setVisible(true);\n            }}\n            onMouseEnter={() => {\n              const data = clipboard.readText();\n              setPasteIconVisible(!!data);\n            }}\n            style={{\n              backgroundColor: '#fff',\n              color: '#3370ff',\n              borderRadius: 15,\n            }}\n          />\n        ) : (\n          ''\n        )}\n        {activated && pasteIconVisible && (\n          <Popover position=\"top\" showArrow content=\"Paste\">\n            <PasteIcon\n              onClick={handlePaste}\n              style={\n                isVertical\n                  ? {\n                      right: -25,\n                      top: 0,\n                    }\n                  : {\n                      right: 0,\n                      top: -20,\n                    }\n              }\n            >\n              <IconCopyAdd\n                style={{\n                  backgroundColor: 'var(--semi-color-bg-0)',\n                  borderRadius: 15,\n                }}\n              />\n            </PasteIcon>\n          </Popover>\n        )}\n      </Wrap>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/node-adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const PasteIcon = styled.div`\n  position: absolute;\n  width: 15px;\n  height: 15px;\n  color: #3370ff;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n`;\n\nexport const Wrap = styled.div`\n  position: relative;\n  width: 6px;\n  height: 6px;\n  background-color: rgb(143, 149, 158);\n  color: #fff;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/node-adder/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\n\nexport const generateNodeId = (n: FlowNodeEntity) => `${n.type || n.flowNodeType}_${nanoid()}`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/node-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport {\n  FlowNodeEntity,\n  FlowNodeRegistry,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistries } from '../nodes';\n\nconst NodeWrap = styled.div`\n  width: 100%;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  font-size: 19px;\n  padding: 0 15px;\n  &:hover {\n    background-color: hsl(252deg 62% 55% / 9%);\n    color: hsl(252 62% 54.9%);\n  },\n`;\n\nconst NodeLabel = styled.div`\n  font-size: 12px;\n  margin-left: 10px;\n`;\n\nfunction Node(props: { label: string; icon: JSX.Element; onClick: () => void; disabled: boolean }) {\n  return (\n    <NodeWrap\n      onClick={props.disabled ? undefined : props.onClick}\n      style={props.disabled ? { opacity: 0.3 } : {}}\n    >\n      <div style={{ fontSize: 14 }}>{props.icon}</div>\n      <NodeLabel>{props.label}</NodeLabel>\n    </NodeWrap>\n  );\n}\n\nconst NodesWrap = styled.div`\n  max-height: 500px;\n  overflow: auto;\n  &::-webkit-scrollbar {\n    display: none;\n  }\n`;\n\nexport function NodeList(props: { onSelect: (meta: any) => void; from: FlowNodeEntity }) {\n  const context = useClientContext();\n  const handleClick = (registry: FlowNodeRegistry) => {\n    const addProps = registry.onAdd(context, props.from);\n    props.onSelect?.(addProps);\n  };\n  return (\n    <NodesWrap style={{ width: 80 * 2 + 20 }}>\n      {FlowNodeRegistries.filter((registry) => !registry.meta?.addDisable).map((registry) => (\n        <Node\n          key={registry.type}\n          disabled={!(registry.canAdd?.(context, props.from) ?? true)}\n          icon={<img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info.icon} />}\n          label={registry.type as string}\n          onClick={() => handleClick(registry)}\n        />\n      ))}\n    </NodesWrap>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/selector-box-popover/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FunctionComponent, useMemo } from 'react';\n\nimport {\n  useStartDragNode,\n  FlowNodeRenderData,\n  FlowNodeBaseType,\n  FlowGroupService,\n  type FlowNodeEntity,\n  SelectorBoxPopoverProps,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Button, ButtonGroup, Tooltip } from '@douyinfe/semi-ui';\nimport {\n  IconCopy,\n  IconDeleteStroked,\n  IconExpand,\n  IconHandle,\n  IconShrink,\n} from '@douyinfe/semi-icons';\n\nimport { FlowCommandId } from '../../shortcuts/constants';\nimport { IconGroupOutlined } from '../../plugins/group-plugin/icons';\n\nconst BUTTON_HEIGHT = 24;\n\nexport const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({\n  bounds,\n  children,\n  flowSelectConfig,\n  commandRegistry,\n}) => {\n  const selectNodes = flowSelectConfig.selectedNodes;\n\n  const { startDrag } = useStartDragNode();\n\n  const draggable = selectNodes[0]?.getData(FlowNodeRenderData)?.draggable;\n\n  // Does the selected component have a group node? (High-cost computation must use memo)\n  const hasGroup: boolean = useMemo(() => {\n    if (!selectNodes || selectNodes.length === 0) {\n      return false;\n    }\n    const findGroupInNodes = (nodes: FlowNodeEntity[]): boolean =>\n      nodes.some((node) => {\n        if (node.flowNodeType === FlowNodeBaseType.GROUP) {\n          return true;\n        }\n        if (node.blocks && node.blocks.length) {\n          return findGroupInNodes(node.blocks);\n        }\n        return false;\n      });\n    return findGroupInNodes(selectNodes);\n  }, [selectNodes]);\n\n  const canGroup = !hasGroup && FlowGroupService.validate(selectNodes);\n\n  return (\n    <>\n      <div\n        style={{\n          position: 'absolute',\n          left: bounds.right,\n          top: bounds.top,\n          transform: 'translate(-100%, -100%)',\n        }}\n        onMouseDown={(e) => {\n          e.stopPropagation();\n        }}\n      >\n        <ButtonGroup\n          size=\"small\"\n          style={{ display: 'flex', flexWrap: 'nowrap', height: BUTTON_HEIGHT }}\n        >\n          {draggable && (\n            <Tooltip content=\"Drag\">\n              <Button\n                style={{ cursor: 'grab', height: BUTTON_HEIGHT }}\n                icon={<IconHandle />}\n                type=\"primary\"\n                theme=\"solid\"\n                onMouseDown={(e) => {\n                  e.stopPropagation();\n                  startDrag(e, {\n                    dragStartEntity: selectNodes[0],\n                    dragEntities: selectNodes,\n                  });\n                }}\n              />\n            </Tooltip>\n          )}\n\n          <Tooltip content={'Collapse'}>\n            <Button\n              icon={<IconShrink />}\n              style={{ height: BUTTON_HEIGHT }}\n              type=\"primary\"\n              theme=\"solid\"\n              onMouseDown={(e) => {\n                commandRegistry.executeCommand(FlowCommandId.COLLAPSE);\n              }}\n            />\n          </Tooltip>\n\n          <Tooltip content={'Expand'}>\n            <Button\n              icon={<IconExpand />}\n              style={{ height: BUTTON_HEIGHT }}\n              type=\"primary\"\n              theme=\"solid\"\n              onMouseDown={(e) => {\n                commandRegistry.executeCommand(FlowCommandId.EXPAND);\n              }}\n            />\n          </Tooltip>\n\n          <Tooltip content={'Group'}>\n            <Button\n              icon={<IconGroupOutlined />}\n              type=\"primary\"\n              theme=\"solid\"\n              style={{\n                display: canGroup ? 'inherit' : 'none',\n                height: BUTTON_HEIGHT,\n              }}\n              onClick={() => {\n                commandRegistry.executeCommand(FlowCommandId.GROUP);\n              }}\n            />\n          </Tooltip>\n\n          <Tooltip content={'Copy'}>\n            <Button\n              icon={<IconCopy />}\n              style={{ height: BUTTON_HEIGHT }}\n              type=\"primary\"\n              theme=\"solid\"\n              onClick={() => {\n                commandRegistry.executeCommand(FlowCommandId.COPY);\n              }}\n            />\n          </Tooltip>\n\n          <Tooltip content={'Delete'}>\n            <Button\n              type=\"primary\"\n              theme=\"solid\"\n              icon={<IconDeleteStroked />}\n              style={{ height: BUTTON_HEIGHT }}\n              onClick={() => {\n                commandRegistry.executeCommand(FlowCommandId.DELETE);\n              }}\n            />\n          </Tooltip>\n        </ButtonGroup>\n      </div>\n      <div\n        style={{ cursor: draggable ? 'grab' : 'auto' }}\n        onMouseDown={(e) => {\n          e.stopPropagation();\n          startDrag(e, {\n            dragStartEntity: selectNodes[0],\n            dragEntities: selectNodes,\n          });\n        }}\n      >\n        {children}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/sidebar/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { nodeFormPanelFactory } from './sidebar-renderer';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/sidebar/sidebar-node-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  useNodeRender,\n  FlowNodeEntity,\n  PlaygroundEntityContext,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { NodeRenderContext } from '../../context';\n\nexport function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {\n  const { node } = props;\n  const nodeRender = useNodeRender(node);\n\n  return (\n    <PlaygroundEntityContext.Provider value={nodeRender.node}>\n      <NodeRenderContext.Provider value={nodeRender}>\n        <div\n          style={{\n            background: 'rgb(251, 251, 251)',\n            height: '100%',\n            borderRadius: 8,\n            border: '1px solid rgba(82,100,154, 0.13)',\n            boxSizing: 'border-box',\n          }}\n        >\n          {nodeRender.form?.render()}\n        </div>\n      </NodeRenderContext.Provider>\n    </PlaygroundEntityContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/sidebar/sidebar-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, startTransition } from 'react';\n\nimport { type PanelFactory, usePanelManager } from '@flowgram.ai/panel-manager-plugin';\nimport {\n  PlaygroundEntityContext,\n  useRefresh,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeMeta } from '../../typings';\nimport { IsSidebarContext } from '../../context';\nimport { SidebarNodeRenderer } from './sidebar-node-renderer';\n\nexport interface NodeFormPanelProps {\n  nodeId: string;\n}\n\nexport const SidebarRenderer: React.FC<NodeFormPanelProps> = ({ nodeId }) => {\n  const panelManager = usePanelManager();\n  const { selection, playground, document } = useClientContext();\n  const refresh = useRefresh();\n  const handleClose = useCallback(() => {\n    // Sidebar delayed closing\n    startTransition(() => {\n      panelManager.close(nodeFormPanelFactory.key);\n    });\n  }, []);\n  const node = nodeId ? document.getNode(nodeId) : undefined;\n  /**\n   * Listen readonly\n   */\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => {\n      handleClose();\n      refresh();\n    });\n    return () => disposable.dispose();\n  }, [playground]);\n  /**\n   * Listen selection\n   */\n  useEffect(() => {\n    const toDispose = selection.onSelectionChanged(() => {\n      /**\n       * 如果没有选中任何节点，则自动关闭侧边栏\n       * If no node is selected, the sidebar is automatically closed\n       */\n      if (selection.selection.length === 0) {\n        handleClose();\n      } else if (selection.selection.length === 1 && selection.selection[0] !== node) {\n        handleClose();\n      }\n    });\n    return () => toDispose.dispose();\n  }, [selection, handleClose, node]);\n  /**\n   * Close when node disposed\n   */\n  useEffect(() => {\n    if (node) {\n      const toDispose = node.onDispose(() => {\n        panelManager.close(nodeFormPanelFactory.key);\n      });\n      return () => toDispose.dispose();\n    }\n    return () => {};\n  }, [node]);\n\n  if (!node || node.getNodeMeta<FlowNodeMeta>().sidebarDisabled === true) {\n    return null;\n  }\n\n  if (playground.config.readonly) {\n    return null;\n  }\n\n  return (\n    <IsSidebarContext.Provider value={true}>\n      <PlaygroundEntityContext.Provider key={node.id} value={node}>\n        <SidebarNodeRenderer node={node} />\n      </PlaygroundEntityContext.Provider>\n    </IsSidebarContext.Provider>\n  );\n};\n\nexport const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {\n  key: 'node-form-panel',\n  defaultSize: 400,\n  render: (props: NodeFormPanelProps) => <SidebarRenderer {...props} />,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/download.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState, type FC } from 'react';\n\nimport { usePlayground, useService } from '@flowgram.ai/fixed-layout-editor';\nimport { FlowDownloadFormat, FlowDownloadService } from '@flowgram.ai/export-plugin';\nimport { IconButton, Toast, Dropdown, Tooltip } from '@douyinfe/semi-ui';\nimport { IconFilledArrowDown } from '@douyinfe/semi-icons';\n\nconst formatOptions = [\n  {\n    label: 'PNG',\n    value: FlowDownloadFormat.PNG,\n  },\n  {\n    label: 'JPEG',\n    value: FlowDownloadFormat.JPEG,\n  },\n  {\n    label: 'SVG',\n    value: FlowDownloadFormat.SVG,\n  },\n  {\n    label: 'JSON',\n    value: FlowDownloadFormat.JSON,\n  },\n  {\n    label: 'YAML',\n    value: FlowDownloadFormat.YAML,\n  },\n];\n\nexport const DownloadTool: FC = () => {\n  const [downloading, setDownloading] = useState<boolean>(false);\n  const [visible, setVisible] = useState(false);\n  const playground = usePlayground();\n  const { readonly } = playground.config;\n  const downloadService = useService(FlowDownloadService);\n\n  useEffect(() => {\n    const subscription = downloadService.onDownloadingChange((v) => {\n      setDownloading(v);\n    });\n\n    return () => {\n      subscription.dispose();\n    };\n  }, [downloadService]);\n\n  const handleDownload = async (format: FlowDownloadFormat) => {\n    setVisible(false);\n    await downloadService.download({\n      format,\n    });\n    const formatOption = formatOptions.find((option) => option.value === format);\n    Toast.success(`Download ${formatOption?.label} successfully`);\n  };\n\n  const button = (\n    <IconButton\n      type=\"tertiary\"\n      theme=\"borderless\"\n      className={visible ? '!coz-mg-secondary-pressed' : undefined}\n      icon={<IconFilledArrowDown />}\n      loading={downloading}\n      onClick={() => setVisible(true)}\n    />\n  );\n\n  return (\n    <Dropdown\n      trigger=\"custom\"\n      visible={visible}\n      position=\"topLeft\"\n      onClickOutSide={() => setVisible(false)}\n      render={\n        <Dropdown.Menu className=\"min-w-[120px]\">\n          {formatOptions.map((item) => (\n            <Dropdown.Item\n              disabled={downloading || readonly}\n              key={item.value}\n              onClick={() => handleDownload(item.value)}\n            >\n              {item.label}\n            </Dropdown.Item>\n          ))}\n        </Dropdown.Menu>\n      }\n    >\n      {visible ? (\n        button\n      ) : (\n        <div>\n          <Tooltip content=\"Download\">{button}</Tooltip>\n        </div>\n      )}\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/fit-view.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\nimport { IconExpand } from '@douyinfe/semi-icons';\n\nexport const FitView = (props: { fitView: () => void }) => (\n  <Tooltip content=\"FitView\">\n    <IconButton\n      icon={<IconExpand />}\n      type=\"tertiary\"\n      theme=\"borderless\"\n      onClick={() => props.fitView()}\n    />\n  </Tooltip>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nimport { usePlayground, usePlaygroundTools, useRefresh } from '@flowgram.ai/fixed-layout-editor';\nimport { Tooltip, IconButton } from '@douyinfe/semi-ui';\nimport { IconUndo, IconRedo } from '@douyinfe/semi-icons';\n\nimport { ZoomSelect } from './zoom-select';\nimport { SwitchVertical } from './switch-vertical';\nimport { ToolContainer, ToolSection } from './styles';\nimport { Save } from './save';\nimport { Run } from './run';\nimport { Readonly } from './readonly';\nimport { MinimapSwitch } from './minimap-switch';\nimport { Minimap } from './minimap';\nimport { Interactive } from './interactive';\nimport { FitView } from './fit-view';\nimport { DownloadTool } from './download';\n\nexport const DemoTools = () => {\n  const tools = usePlaygroundTools();\n  const [minimapVisible, setMinimapVisible] = useState(false);\n  const playground = usePlayground();\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());\n    return () => disposable.dispose();\n  }, [playground]);\n\n  return (\n    <ToolContainer className=\"fixed-demo-tools\">\n      <ToolSection>\n        <Interactive />\n        <SwitchVertical />\n        <ZoomSelect />\n        <FitView fitView={tools.fitView} />\n        <MinimapSwitch minimapVisible={minimapVisible} setMinimapVisible={setMinimapVisible} />\n        <Minimap visible={minimapVisible} />\n        <Readonly />\n        <Tooltip content=\"Undo\">\n          <IconButton\n            theme=\"borderless\"\n            icon={<IconUndo />}\n            disabled={!tools.canUndo || playground.config.readonly}\n            onClick={() => tools.undo()}\n          />\n        </Tooltip>\n        <Tooltip content=\"Redo\">\n          <IconButton\n            theme=\"borderless\"\n            icon={<IconRedo />}\n            disabled={!tools.canRedo || playground.config.readonly}\n            onClick={() => tools.redo()}\n          />\n        </Tooltip>\n        <DownloadTool />\n        <Save disabled={playground.config.readonly} />\n        <Run />\n      </ToolSection>\n    </ToolContainer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/interactive.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, PlaygroundInteractiveType } from '@flowgram.ai/fixed-layout-editor';\nimport { Tooltip, Popover } from '@douyinfe/semi-ui';\n\nimport { MousePadSelector } from './mouse-pad-selector';\n\nexport const CACHE_KEY = 'workflow_prefer_interactive_type';\nexport const IS_MAC_OS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);\n\nexport const getPreferInteractiveType = () => {\n  const data = localStorage.getItem(CACHE_KEY) as string;\n  if (data && [InteractiveType.Mouse, InteractiveType.Pad].includes(data as InteractiveType)) {\n    return data;\n  }\n  return IS_MAC_OS ? InteractiveType.Pad : InteractiveType.Mouse;\n};\n\nexport const setPreferInteractiveType = (type: InteractiveType) => {\n  localStorage.setItem(CACHE_KEY, type);\n};\n\nexport enum InteractiveType {\n  Mouse = 'MOUSE',\n  Pad = 'PAD',\n}\n\nexport const Interactive = () => {\n  const tools = usePlaygroundTools();\n  const [visible, setVisible] = useState(false);\n\n  const [interactiveType, setInteractiveType] = useState<InteractiveType>(\n    () => getPreferInteractiveType() as InteractiveType\n  );\n\n  const [showInteractivePanel, setShowInteractivePanel] = useState(false);\n\n  const mousePadTooltip =\n    interactiveType === InteractiveType.Mouse ? 'Mouse-Friendly' : 'Touchpad-Friendly';\n\n  useEffect(() => {\n    // read from localStorage\n    const preferInteractiveType = getPreferInteractiveType();\n    tools.setInteractiveType(preferInteractiveType as PlaygroundInteractiveType);\n  }, []);\n\n  const handleClose = () => {\n    setVisible(false);\n  };\n\n  return (\n    <Popover trigger=\"custom\" position=\"top\" visible={visible} onClickOutSide={handleClose}>\n      <Tooltip\n        content={mousePadTooltip}\n        style={{ display: showInteractivePanel ? 'none' : 'block' }}\n      >\n        <div className=\"workflow-toolbar-interactive\">\n          <MousePadSelector\n            value={interactiveType}\n            onChange={(value) => {\n              setInteractiveType(value);\n              setPreferInteractiveType(value);\n              tools.setInteractiveType(value);\n            }}\n            onPopupVisibleChange={setShowInteractivePanel}\n            containerStyle={{\n              border: 'none',\n              height: '32px',\n              width: '32px',\n              justifyContent: 'center',\n              alignItems: 'center',\n              gap: '2px',\n              padding: '4px',\n              borderRadius: 'var(--small, 6px)',\n            }}\n            iconStyle={{\n              margin: '0',\n              width: '16px',\n              height: '16px',\n            }}\n            arrowStyle={{\n              width: '12px',\n              height: '12px',\n            }}\n          />\n        </div>\n      </Tooltip>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/minimap-switch.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Tooltip, IconButton } from '@douyinfe/semi-ui';\nimport { IconGridRectangle } from '@douyinfe/semi-icons';\n\nexport const MinimapSwitch = (props: {\n  minimapVisible: boolean;\n  setMinimapVisible: (visible: boolean) => void;\n}) => {\n  const { minimapVisible, setMinimapVisible } = props;\n\n  return (\n    <Tooltip content=\"Minimap\">\n      <IconButton\n        theme=\"borderless\"\n        icon={\n          <IconGridRectangle\n            style={{\n              color: minimapVisible ? undefined : '#060709cc',\n            }}\n          />\n        }\n        onClick={() => {\n          setMinimapVisible(Boolean(!minimapVisible));\n        }}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nimport { MinimapContainer } from './styles';\n\nexport const Minimap = ({ visible }: { visible?: boolean }) => {\n  if (!visible) {\n    return <></>;\n  }\n  return (\n    <MinimapContainer>\n      <MinimapRender\n        panelStyles={{}}\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </MinimapContainer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/mouse-pad-selector.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* stylelint-disable no-descending-specificity */\n/* stylelint-disable selector-class-pattern */\n.ui-mouse-pad-selector {\n  position: relative;\n\n  display: flex;\n  align-items: center;\n\n  box-sizing: border-box;\n  width: 68px;\n  height: 32px;\n  padding: 8px 12px;\n\n  border: 1px solid rgba(29, 28, 35, 8%);\n  border-radius: 8px;\n\n  &-icon {\n    height: 20px;\n    margin-right: 12px;\n  }\n\n  &-arrow {\n    height: 16px;\n    font-size: 12px;\n  }\n\n  &-popover {\n    padding: 16px;\n\n    &-options {\n      display: flex;\n      gap: 12px;\n      margin-top: 12px;\n    }\n\n    .mouse-pad-option {\n      box-sizing: border-box;\n      width: 220px;\n      padding-bottom: 20px;\n\n      text-align: center;\n\n      background: var(--coz-mg-card, #FFF);\n      border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));\n      border-radius: var(--default, 8px);\n\n      &-icon {\n        padding-top: 26px;\n      }\n\n      &-title {\n        padding-top: 8px;\n      }\n\n      &-subTitle {\n        padding: 4px 12px 0;\n      }\n\n      &-icon-selected {\n        color: rgb(19 0 221);\n      }\n\n      &-title-selected {\n        color: var(--coz-fg-hglt, #4E40E5);\n      }\n\n      &-subTitle-selected {\n        color: var(--coz-fg-hglt, #4E40E5);\n      }\n\n      &-selected {\n        cursor: pointer;\n        background-color: var(--coz-mg-hglt, rgba(186, 192, 255, 20%));\n        border: 1px solid var(--coz-stroke-hglt, #4E40E5);\n        border-radius: var(--default, 8px);\n      }\n\n      &:hover:not(&-selected) {\n        cursor: pointer;\n\n        background-color: var(--coz-mg-card-hovered, #FFF);\n        border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));\n        border-radius: var(--default, 8px);\n        box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 16%), 0 16px 48px 0 rgba(0, 0, 0, 8%);\n      }\n\n      &:active:not(&-selected) {\n        background-color: rgba(46, 46, 56, 12%);\n      }\n\n      &:last-of-type {\n        padding-top: 13px;\n      }\n    }\n  }\n\n  &:hover {\n    cursor: pointer;\n    background-color: rgba(46, 46, 56, 8%);\n    border-color: rgba(77, 83, 232, 100%);\n  }\n\n  &:active,\n  &:focus {\n    background-color: rgba(46, 46, 56, 12%);\n    border-color: rgba(77, 83, 232, 100%);\n  }\n\n  &-active {\n    border-color: rgba(77, 83, 232, 100%);\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/mouse-pad-selector.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { type CSSProperties, useState } from 'react';\n\nimport { Popover, Typography } from '@douyinfe/semi-ui';\n\nimport { IconPad, IconPadTool } from '../../assets/icon-pad';\nimport { IconMouse, IconMouseTool } from '../../assets/icon-mouse';\n\nimport './mouse-pad-selector.less';\n\nconst { Title, Paragraph } = Typography;\n\nexport enum InteractiveType {\n  Mouse = 'MOUSE',\n  Pad = 'PAD',\n}\n\nexport interface MousePadSelectorProps {\n  value: InteractiveType;\n  onChange: (value: InteractiveType) => void;\n  onPopupVisibleChange?: (visible: boolean) => void;\n  containerStyle?: CSSProperties;\n  iconStyle?: CSSProperties;\n  arrowStyle?: CSSProperties;\n}\n\nconst InteractiveItem: React.FC<{\n  title: string;\n  subTitle: string;\n  icon: React.ReactNode;\n  value: InteractiveType;\n  selected: boolean;\n  onChange: (value: InteractiveType) => void;\n}> = ({ title, subTitle, icon, onChange, value, selected }) => (\n  <div\n    className={`mouse-pad-option ${selected ? 'mouse-pad-option-selected' : ''}`}\n    onClick={() => onChange(value)}\n  >\n    <div className={`mouse-pad-option-icon ${selected ? 'mouse-pad-option-icon-selected' : ''}`}>\n      {icon}\n    </div>\n    <Title\n      heading={6}\n      className={`mouse-pad-option-title ${selected ? 'mouse-pad-option-title-selected' : ''}`}\n    >\n      {title}\n    </Title>\n    <Paragraph\n      type=\"tertiary\"\n      className={`mouse-pad-option-subTitle ${\n        selected ? 'mouse-pad-option-subTitle-selected' : ''\n      }`}\n    >\n      {subTitle}\n    </Paragraph>\n  </div>\n);\n\nexport const MousePadSelector: React.FC<\n  MousePadSelectorProps & React.RefAttributes<HTMLDivElement>\n> = ({ value, onChange, onPopupVisibleChange, containerStyle, iconStyle, arrowStyle }) => {\n  const isMouse = value === InteractiveType.Mouse;\n  const [visible, setVisible] = useState(false);\n\n  return (\n    <Popover\n      trigger=\"custom\"\n      position=\"topLeft\"\n      closeOnEsc\n      visible={visible}\n      onVisibleChange={(v) => {\n        onPopupVisibleChange?.(v);\n      }}\n      onClickOutSide={() => {\n        setVisible(false);\n      }}\n      spacing={20}\n      content={\n        <div className={'ui-mouse-pad-selector-popover'}>\n          <Typography.Title heading={4}>{'Interaction mode'}</Typography.Title>\n          <div className={'ui-mouse-pad-selector-popover-options'}>\n            <InteractiveItem\n              title={'Mouse-Friendly'}\n              subTitle={'Drag the canvas with the left mouse button, zoom with the scroll wheel.'}\n              value={InteractiveType.Mouse}\n              selected={value === InteractiveType.Mouse}\n              icon={<IconMouse />}\n              onChange={onChange}\n            />\n\n            <InteractiveItem\n              title={'Touchpad-Friendly'}\n              subTitle={\n                'Drag with two fingers moving in the same direction, zoom by pinching or spreading two fingers.'\n              }\n              value={InteractiveType.Pad}\n              selected={value === InteractiveType.Pad}\n              icon={<IconPad />}\n              onChange={onChange}\n            />\n          </div>\n        </div>\n      }\n    >\n      <div\n        className={`ui-mouse-pad-selector ${visible ? 'ui-mouse-pad-selector-active' : ''}`}\n        onClick={() => {\n          setVisible(!visible);\n        }}\n        style={containerStyle}\n      >\n        <div className={'ui-mouse-pad-selector-icon'} style={iconStyle}>\n          {isMouse ? <IconMouseTool /> : <IconPadTool />}\n        </div>\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/readonly.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/fixed-layout-editor';\nimport { IconButton } from '@douyinfe/semi-ui';\nimport { IconUnlock, IconLock } from '@douyinfe/semi-icons';\n\nexport const Readonly = () => {\n  const playground = usePlayground();\n  const toggleReadonly = useCallback(() => {\n    playground.config.readonly = !playground.config.readonly;\n  }, [playground]);\n\n  return playground.config.readonly ? (\n    <IconButton theme=\"borderless\" type=\"tertiary\" icon={<IconLock />} onClick={toggleReadonly} />\n  ) : (\n    <IconButton theme=\"borderless\" type=\"tertiary\" icon={<IconUnlock />} onClick={toggleReadonly} />\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/run.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport {\n  usePlayground,\n  FlowNodeEntity,\n  FixedLayoutPluginContext,\n  useClientContext,\n  delay,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Button } from '@douyinfe/semi-ui';\n\nconst styleElement = document.createElement('style');\nconst RUNNING_COLOR = 'rgb(78, 64, 229)';\nconst RUNNING_INTERVAL = 1000;\n\nfunction getRunningNodes(targetNode?: FlowNodeEntity | undefined, addChildren?: boolean): string[] {\n  const result: string[] = [];\n  if (targetNode) {\n    result.push(targetNode.id);\n    if (addChildren) {\n      result.push(...targetNode.allChildren.map((n) => n.id));\n    }\n    if (targetNode.parent) {\n      result.push(targetNode.parent.id);\n    }\n    if (targetNode.pre) {\n      result.push(...getRunningNodes(targetNode.pre, true));\n    }\n    if (targetNode.parent) {\n      if (targetNode.parent.pre) {\n        result.push(...getRunningNodes(targetNode.parent.pre, true));\n      }\n    }\n  }\n  return result;\n}\n\nfunction clear() {\n  styleElement.innerText = '';\n}\n\nfunction runningNode(ctx: FixedLayoutPluginContext, nodeId: string) {\n  const nodes = getRunningNodes(ctx.document.getNode(nodeId), true);\n  if (nodes.length === 0) {\n    styleElement.innerText = '';\n  } else {\n    const content = nodes\n      .map(\n        (n) => `\n      path[data-line-id$=\"${n}\"] {\n        animation: flowingDash 0.5s linear infinite;\n        stroke-dasharray: 8, 5;\n        stroke: ${RUNNING_COLOR} !important;\n      }\n      marker[data-line-id$=\"${n}\"] path {\n        fill: ${RUNNING_COLOR} !important;\n      }\n      [data-node-id$=\"${n}\"] {\n        border: 1px dashed ${RUNNING_COLOR} !important;\n        border-radius: 8px;\n      }\n      [data-label-id$=\"${n}\"] {\n        color: ${RUNNING_COLOR} !important;\n      }\n    `\n      )\n      .join('\\n');\n    styleElement.innerText = `\n   @keyframes flowingDash {\n    to {\n      stroke-dashoffset: -13;\n    }\n  }\n  ${content}\n  `;\n  }\n  if (!styleElement.parentNode) {\n    document.body.appendChild(styleElement);\n  }\n}\n\n/**\n * Run the simulation and highlight the lines\n */\nexport function Run() {\n  const [isRunning, setRunning] = useState(false);\n  const ctx = useClientContext();\n  const playground = usePlayground();\n  const onRun = async () => {\n    setRunning(true);\n    playground.config.readonly = true;\n    const nodes = ctx.document.root.blocks.slice();\n    while (nodes.length > 0) {\n      const currentNode = nodes.shift();\n      runningNode(ctx, currentNode!.id);\n      await delay(RUNNING_INTERVAL);\n    }\n\n    playground.config.readonly = false;\n    clear();\n    setRunning(false);\n  };\n  return (\n    <Button onClick={onRun} loading={isRunning}>\n      Run\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/save.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect, useCallback } from 'react';\n\nimport { useClientContext, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\nimport { Button, Badge } from '@douyinfe/semi-ui';\n\nexport function Save(props: { disabled: boolean }) {\n  const [errorCount, setErrorCount] = useState(0);\n  const clientContext = useClientContext();\n\n  const updateValidateData = useCallback(() => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    const count = allForms.filter((form) => form?.state.invalid).length;\n    setErrorCount(count);\n  }, [clientContext]);\n\n  /**\n   * Validate all node and Save\n   */\n  const onSave = useCallback(async () => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    await Promise.all(allForms.map(async (form) => form?.validate()));\n    console.log('>>>>> save data: ', clientContext.document.toJSON());\n  }, [clientContext]);\n\n  useEffect(() => {\n    /**\n     * Listen single node validate\n     */\n    const listenSingleNodeValidate = (node: FlowNodeEntity) => {\n      const form = node.form;\n      if (form) {\n        const formValidateDispose = form.onValidate(() => updateValidateData());\n        node.onDispose(() => formValidateDispose.dispose());\n      }\n    };\n    clientContext.document.getAllNodes().map((node) => listenSingleNodeValidate(node));\n    const dispose = clientContext.document.onNodeCreate(({ node }) =>\n      listenSingleNodeValidate(node)\n    );\n    return () => dispose.dispose();\n  }, [clientContext]);\n  if (errorCount === 0) {\n    return (\n      <Button disabled={props.disabled} onClick={onSave}>\n        Save\n      </Button>\n    );\n  }\n  return (\n    <Badge count={errorCount} position=\"rightTop\" type=\"danger\">\n      <Button type=\"danger\" disabled={props.disabled} onClick={onSave}>\n        Save\n      </Button>\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const ToolContainer = styled.div`\n  position: absolute;\n  bottom: 16px;\n  display: flex;\n  justify-content: left;\n  min-width: 360px;\n  pointer-events: none;\n  gap: 8px;\n\n  z-index: 20;\n`;\n\nexport const ToolSection = styled.div`\n  display: flex;\n  align-items: center;\n  background-color: #fff;\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  border-radius: 10px;\n  box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 6px 0px, rgba(0, 0, 0, 0.02) 0px 4px 12px 0px;\n  column-gap: 2px;\n  height: 40px;\n  padding: 0 4px;\n  pointer-events: auto;\n`;\n\nexport const SelectZoom = styled.span`\n  padding: 2px;\n  border-radius: 8px;\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  font-size: 12px;\n  width: 40px;\n`;\n\nexport const MinimapContainer = styled.div`\n  position: absolute;\n  bottom: 60px;\n  width: 198px;\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/switch-vertical.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';\nimport { Button, Tooltip } from '@douyinfe/semi-ui';\nimport { IconServer } from '@douyinfe/semi-icons';\n\nexport const SwitchVertical = () => {\n  const tools = usePlaygroundTools();\n  return (\n    <Tooltip content={!tools.isVertical ? 'Vertical Layout' : 'Horizontal Layout'}>\n      <Button\n        theme=\"borderless\"\n        size=\"small\"\n        onClick={() => tools.changeLayout()}\n        icon={\n          <IconServer\n            style={{\n              transform: !tools.isVertical ? '' : 'rotate(90deg)',\n              transition: 'transform .3s ease',\n            }}\n          />\n        }\n        type=\"tertiary\"\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/components/tools/zoom-select.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';\nimport { Divider, Dropdown } from '@douyinfe/semi-ui';\n\nimport { SelectZoom } from './styles';\n\nexport const ZoomSelect = () => {\n  const tools = usePlaygroundTools({ maxZoom: 2, minZoom: 0.25 });\n  const [dropDownVisible, openDropDown] = useState(false);\n  return (\n    <Dropdown\n      position=\"top\"\n      trigger=\"custom\"\n      visible={dropDownVisible}\n      onClickOutSide={() => openDropDown(false)}\n      render={\n        <Dropdown.Menu>\n          <Dropdown.Item onClick={() => tools.zoomin()}>Zoomin</Dropdown.Item>\n          <Dropdown.Item onClick={() => tools.zoomout()}>Zoomout</Dropdown.Item>\n          <Divider layout=\"horizontal\" />\n          <Dropdown.Item onClick={() => tools.updateZoom(0.5)}>50%</Dropdown.Item>\n          <Dropdown.Item onClick={() => tools.updateZoom(1)}>100%</Dropdown.Item>\n          <Dropdown.Item onClick={() => tools.updateZoom(1.5)}>150%</Dropdown.Item>\n          <Dropdown.Item onClick={() => tools.updateZoom(2.0)}>200%</Dropdown.Item>\n        </Dropdown.Menu>\n      }\n    >\n      <SelectZoom onClick={() => openDropDown(true)}>{Math.floor(tools.zoom * 100)}%</SelectZoom>\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeRenderContext } from './node-render-context';\nexport { IsSidebarContext } from './sidebar-context';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/context/node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { type NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRenderContext = React.createContext<NodeRenderReturnType>({} as any);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/context/sidebar-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const IsSidebarContext = React.createContext<boolean>(false);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EditorRenderer, FixedLayoutEditorProvider } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistries } from './nodes';\nimport { initialData } from './initial-data';\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { DemoTools } from './components';\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nexport const Editor = () => {\n  /**\n   * Editor Config\n   */\n  const editorProps = useEditorProps(initialData, FlowNodeRegistries);\n\n  return (\n    <div className=\"doc-feature-overview\">\n      <FixedLayoutEditorProvider {...editorProps}>\n        <EditorRenderer />\n        <DemoTools />\n      </FixedLayoutEditorProvider>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/feedback.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { FieldError, FieldState, FieldWarning } from '@flowgram.ai/fixed-layout-editor';\n\ninterface StatePanelProps {\n  errors?: FieldState['errors'];\n  warnings?: FieldState['warnings'];\n}\n\nconst Error = styled.span`\n  font-size: 12px;\n  color: red;\n`;\n\nconst Warning = styled.span`\n  font-size: 12px;\n  color: orange;\n`;\n\nexport const Feedback = ({ errors, warnings }: StatePanelProps) => {\n  const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {\n    if (!fs) return null;\n    return fs.map((f) => <span key={f.name}>{f.message}</span>);\n  };\n  return (\n    <div>\n      <div>\n        <Error>{renderFeedbacks(errors)}</Error>\n      </div>\n      <div>\n        <Warning>{renderFeedbacks(warnings)}</Warning>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-content/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../hooks';\nimport { FormTitleDescription, FormWrapper } from './styles';\n\n/**\n * @param props\n * @constructor\n */\nexport function FormContent(props: { children?: React.ReactNode }) {\n  const { node, expanded } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n  const registry = node.getNodeRegistry<FlowNodeRegistry>();\n  return (\n    <FormWrapper>\n      <>\n        {isSidebar && <FormTitleDescription>{registry.info?.description}</FormTitleDescription>}\n        {(expanded || isSidebar) && props.children}\n      </>\n    </FormWrapper>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-content/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FormWrapper = styled.div`\n  box-sizing: border-box;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  background-color: rgb(251, 251, 251);\n  border-radius: 0 0 8px 8px;\n  padding: 0 12px 12px;\n`;\n\nexport const FormTitleDescription = styled.div`\n  color: var(--semi-color-text-2);\n  font-size: 12px;\n  line-height: 20px;\n  padding: 0px 4px;\n  word-break: break-all;\n  white-space: break-spaces;\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-header/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext, useCallback, useMemo, useState } from 'react';\n\nimport { usePanelManager } from '@flowgram.ai/panel-manager-plugin';\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor';\nimport { IconButton, Dropdown, Button } from '@douyinfe/semi-ui';\nimport { IconClose, IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';\nimport { IconMore } from '@douyinfe/semi-icons';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport { FlowCommandId } from '../../shortcuts/constants';\nimport { useIsSidebar } from '../../hooks';\nimport { NodeRenderContext } from '../../context';\nimport { nodeFormPanelFactory } from '../../components/sidebar';\nimport { getIcon } from './utils';\nimport { TitleInput } from './title-input';\nimport { Header, Operators } from './styles';\n\nfunction DropdownContent(props: { updateTitleEdit: (editing: boolean) => void }) {\n  const { updateTitleEdit } = props;\n  const { node, deleteNode } = useContext(NodeRenderContext);\n  const clientContext = useClientContext();\n  const registry = node.getNodeRegistry<FlowNodeRegistry>();\n\n  const handleCopy = useCallback(\n    (e: React.MouseEvent) => {\n      clientContext.playground.commandService.executeCommand(FlowCommandId.COPY, node);\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n\n  const handleDelete = useCallback(\n    (e: React.MouseEvent) => {\n      deleteNode();\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n\n  const handleEditTitle = useCallback(() => {\n    updateTitleEdit(true);\n  }, [updateTitleEdit]);\n\n  const deleteDisabled = useMemo(() => {\n    if (registry.canDelete) {\n      return !registry.canDelete(clientContext, node);\n    }\n    return registry.meta!.deleteDisable;\n  }, [registry, node]);\n\n  return (\n    <Dropdown.Menu>\n      <Dropdown.Item onClick={handleEditTitle}>Edit Title</Dropdown.Item>\n      <Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>\n        Copy\n      </Dropdown.Item>\n      <Dropdown.Item onClick={handleDelete} disabled={deleteDisabled}>\n        Delete\n      </Dropdown.Item>\n    </Dropdown.Menu>\n  );\n}\n\nexport function FormHeader() {\n  const { node, expanded, startDrag, toggleExpand, readonly } = useContext(NodeRenderContext);\n  const [titleEdit, updateTitleEdit] = useState<boolean>(false);\n  const panelManager = usePanelManager();\n  const isSidebar = useIsSidebar();\n  const handleExpand = (e: React.MouseEvent) => {\n    toggleExpand();\n    e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n  };\n  const handleClose = () => {\n    panelManager.close(nodeFormPanelFactory.key);\n  };\n\n  return (\n    <Header\n      onMouseDown={(e) => {\n        // trigger drag node\n        startDrag(e);\n        e.stopPropagation();\n      }}\n    >\n      {getIcon(node)}\n      <TitleInput readonly={readonly} titleEdit={titleEdit} updateTitleEdit={updateTitleEdit} />\n      {node.renderData.expandable && !isSidebar && (\n        <Button\n          type=\"primary\"\n          icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}\n          size=\"small\"\n          theme=\"borderless\"\n          onClick={handleExpand}\n        />\n      )}\n      {readonly ? undefined : (\n        <Operators>\n          <Dropdown\n            trigger=\"hover\"\n            position=\"bottomRight\"\n            render={<DropdownContent updateTitleEdit={updateTitleEdit} />}\n          >\n            <IconButton\n              color=\"secondary\"\n              size=\"small\"\n              theme=\"borderless\"\n              icon={<IconMore />}\n              onClick={(e) => e.stopPropagation()}\n            />\n          </Dropdown>\n        </Operators>\n      )}\n      {isSidebar && (\n        <Button\n          type=\"primary\"\n          icon={<IconClose />}\n          size=\"small\"\n          theme=\"borderless\"\n          onClick={handleClose}\n        />\n      )}\n    </Header>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-header/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Header = styled.div`\n  box-sizing: border-box;\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  width: 100%;\n  column-gap: 8px;\n  border-radius: 8px 8px 0 0;\n\n  background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);\n  overflow: hidden;\n  padding: 8px;\n  cursor: move;\n`;\n\nexport const Title = styled.div`\n  font-size: 20px;\n  flex: 1;\n  width: 0;\n`;\n\nexport const Icon = styled.img`\n  width: 24px;\n  height: 24px;\n  scale: 0.8;\n  border-radius: 4px;\n`;\n\nexport const Operators = styled.div`\n  display: flex;\n  align-items: center;\n  column-gap: 4px;\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-header/title-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef, useEffect } from 'react';\n\nimport { Field, FieldRenderProps } from '@flowgram.ai/fixed-layout-editor';\nimport { Typography, Input } from '@douyinfe/semi-ui';\n\nimport { Title } from './styles';\nimport { Feedback } from '../feedback';\nconst { Text } = Typography;\n\nexport function TitleInput(props: {\n  readonly: boolean;\n  titleEdit: boolean;\n  updateTitleEdit: (setEdit: boolean) => void;\n}): JSX.Element {\n  const { readonly, titleEdit, updateTitleEdit } = props;\n  const ref = useRef<any>();\n  const titleEditing = titleEdit && !readonly;\n  useEffect(() => {\n    if (titleEditing) {\n      ref.current?.focus();\n    }\n  }, [titleEditing]);\n\n  return (\n    <Title>\n      <Field name=\"title\">\n        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n          <div style={{ height: 24 }}>\n            {titleEditing ? (\n              <Input\n                value={value}\n                onChange={onChange}\n                ref={ref}\n                onBlur={() => updateTitleEdit(false)}\n              />\n            ) : (\n              <Text ellipsis={{ showTooltip: true }}>{value}</Text>\n            )}\n            <Feedback errors={fieldState?.errors} />\n          </div>\n        )}\n      </Field>\n    </Title>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-header/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport { Icon } from './styles';\n\nexport const getIcon = (node: FlowNodeEntity) => {\n  const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;\n  if (!icon) return null;\n  return <Icon src={icon} />;\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FormItem } from '../form-item';\nimport { Feedback } from '../feedback';\nimport { JsonSchema } from '../../typings';\nimport { useNodeRenderContext } from '../../hooks';\n\nexport function FormInputs() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <Field<JsonSchema> name=\"inputs\">\n      {({ field: inputsField }) => {\n        const required = inputsField.value?.required || [];\n        const properties = inputsField.value?.properties;\n        if (!properties) {\n          return <></>;\n        }\n        const content = Object.keys(properties).map((key) => {\n          const property = properties[key];\n\n          const formComponent = property.extra?.formComponent;\n\n          const vertical = ['prompt-editor'].includes(formComponent || '');\n\n          return (\n            <Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>\n              {({ field, fieldState }) => (\n                <FormItem\n                  name={key}\n                  vertical={vertical}\n                  type={property.type as string}\n                  required={required.includes(key)}\n                >\n                  {formComponent === 'prompt-editor' && (\n                    <PromptEditorWithVariables\n                      value={field.value}\n                      onChange={field.onChange}\n                      readonly={readonly}\n                      hasError={Object.keys(fieldState?.errors || {}).length > 0}\n                    />\n                  )}\n                  {!formComponent && (\n                    <DynamicValueInput\n                      value={field.value}\n                      onChange={field.onChange}\n                      readonly={readonly}\n                      hasError={Object.keys(fieldState?.errors || {}).length > 0}\n                      schema={property}\n                    />\n                  )}\n                  <Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />\n                </FormItem>\n              )}\n            </Field>\n          );\n        });\n        return <>{content}</>;\n      }}\n    </Field>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import styled from 'styled-components';\n\n// TODO\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-item/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.form-item-type-tag {\n  color: inherit;\n  padding: 0 2px;\n  height: 18px;\n  width: 18px;\n  vertical-align: middle;\n  flex-shrink: 0;\n  flex-grow: 0;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-item/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { DisplaySchemaTag } from '@flowgram.ai/form-materials';\nimport { Typography, Tooltip } from '@douyinfe/semi-ui';\n\nimport './index.css';\n\nconst { Text } = Typography;\n\ninterface FormItemProps {\n  children: React.ReactNode;\n  name: string;\n  type: string;\n  required?: boolean;\n  description?: string;\n  labelWidth?: number;\n  vertical?: boolean;\n}\nexport function FormItem({\n  children,\n  name,\n  required,\n  description,\n  type,\n  labelWidth,\n  vertical,\n}: FormItemProps): JSX.Element {\n  const renderTitle = useCallback(\n    (showTooltip?: boolean) => (\n      <div style={{ width: '0', display: 'flex', flex: '1' }}>\n        <Text style={{ width: '100%' }} ellipsis={{ showTooltip: !!showTooltip }}>\n          {name}\n        </Text>\n        {required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}\n      </div>\n    ),\n    []\n  );\n  return (\n    <div\n      style={{\n        fontSize: 12,\n        marginBottom: 6,\n        width: '100%',\n        position: 'relative',\n        display: 'flex',\n        gap: 8,\n        ...(vertical\n          ? { flexDirection: 'column' }\n          : {\n              justifyContent: 'center',\n              alignItems: 'center',\n            }),\n      }}\n    >\n      <div\n        style={{\n          justifyContent: 'center',\n          alignItems: 'center',\n          color: 'var(--semi-color-text-0)',\n          width: labelWidth || 118,\n          position: 'relative',\n          display: 'flex',\n          columnGap: 4,\n          flexShrink: 0,\n        }}\n      >\n        <DisplaySchemaTag value={{ type }} />\n        {description ? <Tooltip content={description}>{renderTitle()}</Tooltip> : renderTitle(true)}\n      </div>\n\n      <div\n        style={{\n          flexGrow: 1,\n          minWidth: 0,\n        }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-outputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DisplayOutputs } from '@flowgram.ai/form-materials';\n\nimport { useIsSidebar } from '../../hooks';\n\nexport function FormOutputs() {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return null;\n  }\n  return <DisplayOutputs displayFromScope />;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/form-outputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FormOutputsContainer = styled.div`\n  display: flex;\n  gap: 6px;\n  flex-wrap: wrap;\n  border-top: 1px solid var(--semi-color-border);\n  padding: 8px 0 0;\n  width: 100%;\n\n  :global(.semi-tag .semi-tag-content) {\n    font-size: 10px;\n  }\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './feedback';\nexport * from './form-content';\nexport * from './form-outputs';\nexport * from './form-inputs';\nexport * from './form-header';\nexport * from './form-item';\nexport * from './properties-edit';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/properties-edit/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useContext, useState } from 'react';\n\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { JsonSchema } from '../../typings';\nimport { NodeRenderContext } from '../../context';\nimport { PropertyEdit } from './property-edit';\n\nexport interface PropertiesEditProps {\n  value?: Record<string, JsonSchema>;\n  onChange: (value: Record<string, JsonSchema>) => void;\n  useFx?: boolean;\n}\n\nexport const PropertiesEdit: React.FC<PropertiesEditProps> = (props) => {\n  const value = (props.value || {}) as Record<string, JsonSchema>;\n  const { readonly } = useContext(NodeRenderContext);\n  const [newProperty, updateNewPropertyFromCache] = useState<{ key: string; value: JsonSchema }>({\n    key: '',\n    value: { type: 'string' },\n  });\n  const [newPropertyVisible, setNewPropertyVisible] = useState<boolean>();\n  const clearCache = () => {\n    updateNewPropertyFromCache({ key: '', value: { type: 'string' } });\n    setNewPropertyVisible(false);\n  };\n\n  // 替换对象的key时，保持顺序\n  const replaceKeyAtPosition = (\n    obj: Record<string, any>,\n    oldKey: string,\n    newKey: string,\n    newValue: any\n  ) => {\n    const keys = Object.keys(obj);\n    const index = keys.indexOf(oldKey);\n\n    if (index === -1) {\n      // 如果 oldKey 不存在，直接添加到末尾\n      return { ...obj, [newKey]: newValue };\n    }\n\n    // 在原位置替换\n    const newKeys = [...keys.slice(0, index), newKey, ...keys.slice(index + 1)];\n\n    return newKeys.reduce((acc, key) => {\n      if (key === newKey) {\n        acc[key] = newValue;\n      } else {\n        acc[key] = obj[key];\n      }\n      return acc;\n    }, {} as Record<string, any>);\n  };\n\n  const updateProperty = (\n    propertyValue: JsonSchema,\n    propertyKey: string,\n    newPropertyKey?: string\n  ) => {\n    if (newPropertyKey) {\n      const orderedValue = replaceKeyAtPosition(value, propertyKey, newPropertyKey, propertyValue);\n      props.onChange(orderedValue);\n    } else {\n      const newValue = { ...value };\n      newValue[propertyKey] = propertyValue;\n      props.onChange(newValue);\n    }\n  };\n  const updateNewProperty = (\n    propertyValue: JsonSchema,\n    propertyKey: string,\n    newPropertyKey?: string\n  ) => {\n    // const newValue = { ...value }\n    if (newPropertyKey) {\n      if (!(newPropertyKey in value)) {\n        updateProperty(propertyValue, propertyKey, newPropertyKey);\n      }\n      clearCache();\n    } else {\n      updateNewPropertyFromCache({\n        key: newPropertyKey || propertyKey,\n        value: propertyValue,\n      });\n    }\n  };\n  return (\n    <>\n      {Object.keys(props.value || {}).map((key) => {\n        const property = (value[key] || {}) as JsonSchema;\n        return (\n          <PropertyEdit\n            key={key}\n            propertyKey={key}\n            useFx={props.useFx}\n            value={property}\n            disabled={readonly}\n            onChange={updateProperty}\n            onDelete={() => {\n              const newValue = { ...value };\n              delete newValue[key];\n              props.onChange(newValue);\n            }}\n          />\n        );\n      })}\n      {newPropertyVisible && (\n        <PropertyEdit\n          propertyKey={newProperty.key}\n          value={newProperty.value}\n          useFx={props.useFx}\n          onChange={updateNewProperty}\n          onDelete={() => {\n            const key = newProperty.key;\n            // after onblur\n            setTimeout(() => {\n              const newValue = { ...value };\n              delete newValue[key];\n              props.onChange(newValue);\n              clearCache();\n            }, 10);\n          }}\n        />\n      )}\n      {!readonly && (\n        <div>\n          <Button\n            theme=\"borderless\"\n            icon={<IconPlus />}\n            onClick={() => setNewPropertyVisible(true)}\n          >\n            Add\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/properties-edit/property-edit.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useLayoutEffect } from 'react';\n\nimport { TypeSelector, DynamicValueInput } from '@flowgram.ai/form-materials';\nimport { Input, Button } from '@douyinfe/semi-ui';\nimport { IconCrossCircleStroked } from '@douyinfe/semi-icons';\n\nimport { JsonSchema } from '../../typings';\nimport { LeftColumn, Row } from './styles';\n\nexport interface PropertyEditProps {\n  propertyKey: string;\n  value: JsonSchema;\n  useFx?: boolean;\n  disabled?: boolean;\n  onChange: (value: JsonSchema, propertyKey: string, newPropertyKey?: string) => void;\n  onDelete?: () => void;\n}\n\nexport const PropertyEdit: React.FC<PropertyEditProps> = (props) => {\n  const { value, disabled } = props;\n  const [inputKey, updateKey] = useState(props.propertyKey);\n  const updateProperty = (key: keyof JsonSchema, val: any) => {\n    value[key] = val;\n    props.onChange(value, props.propertyKey);\n  };\n\n  const partialUpdateProperty = (val?: Partial<JsonSchema>) => {\n    props.onChange({ ...value, ...val }, props.propertyKey);\n  };\n\n  useLayoutEffect(() => {\n    updateKey(props.propertyKey);\n  }, [props.propertyKey]);\n  return (\n    <Row>\n      <LeftColumn>\n        <TypeSelector\n          value={value}\n          disabled={disabled}\n          style={{ position: 'absolute', top: 2, left: 4, zIndex: 1, padding: '0 5px', height: 20 }}\n          onChange={(val) => partialUpdateProperty(val)}\n        />\n        <Input\n          value={inputKey}\n          disabled={disabled}\n          size=\"small\"\n          onChange={(v) => updateKey(v.trim())}\n          onBlur={() => {\n            if (inputKey !== '') {\n              props.onChange(value, props.propertyKey, inputKey);\n            } else {\n              updateKey(props.propertyKey);\n            }\n          }}\n          style={{ paddingLeft: 26 }}\n        />\n      </LeftColumn>\n      {\n        <DynamicValueInput\n          value={value.default}\n          onChange={(val) => updateProperty('default', val)}\n          schema={value}\n          style={{ flexGrow: 1 }}\n        />\n      }\n      {props.onDelete && !disabled && (\n        <Button\n          style={{ marginLeft: 5, position: 'relative', top: 2 }}\n          size=\"small\"\n          theme=\"borderless\"\n          icon={<IconCrossCircleStroked />}\n          onClick={props.onDelete}\n        />\n      )}\n    </Row>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/form-components/properties-edit/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Row = styled.div`\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  font-size: 12px;\n  margin-bottom: 6px;\n`;\n\nexport const LeftColumn = styled.div`\n  width: 120px;\n  margin-right: 5px;\n  position: relative;\n`;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useEditorProps } from './use-editor-props';\nexport { useNodeRenderContext } from './use-node-render-context';\nexport { useIsSidebar } from './use-is-sidebar';\nexport { useFormValue } from './use-form-value';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/hooks/use-editor-props.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { debounce } from 'lodash-es';\nimport { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createGroupPlugin } from '@flowgram.ai/group-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport {\n  FixedLayoutProps,\n  FlowDocumentJSON,\n  FlowLayoutDefault,\n  FlowRendererKey,\n  ShortcutsRegistry,\n  ConstantKeys,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { createDownloadPlugin } from '@flowgram.ai/export-plugin';\n\nimport { type FlowNodeRegistry } from '../typings';\nimport { shortcutGetter } from '../shortcuts';\nimport { CustomService } from '../services';\nimport { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';\nimport { createClipboardPlugin, createVariablePanelPlugin } from '../plugins';\nimport { nodeFormPanelFactory } from '../components/sidebar';\nimport { SelectorBoxPopover } from '../components/selector-box-popover';\nimport NodeAdder from '../components/node-adder';\nimport BranchAdder from '../components/branch-adder';\nimport { BaseNode } from '../components/base-node';\nimport { AgentLabel } from '../components/agent-label';\nimport { DragNode, AgentAdder } from '../components';\n\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * 画布相关配置\n       * Canvas-related configurations\n       */\n      playground: {\n        ineractiveType: 'MOUSE',\n        /**\n         * Prevent Mac browser gestures from turning pages\n         * 阻止 mac 浏览器手势翻页\n         */\n        preventGlobalGesture: true,\n      },\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            /**\n             * Default expanded\n             * 默认展开所有节点\n             */\n            defaultExpanded: true,\n          },\n        };\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.fromJSON 调用\n       * Node data transformation, called by ctx.document.fromJSON\n       * @param node\n       * @param json\n       */\n      fromNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.toJSON 调用\n       * Node data transformation, called by ctx.document.toJSON\n       * @param node\n       * @param json\n       */\n      toNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * Set default layout\n       */\n      defaultLayout: FlowLayoutDefault.VERTICAL_FIXED_LAYOUT, // or FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT\n      /**\n       * Style config\n       */\n      constants: {\n        // [ConstantKeys.NODE_SPACING]: 24,\n        // [ConstantKeys.BRANCH_SPACING]: 20,\n        // [ConstantKeys.INLINE_SPACING_BOTTOM]: 24,\n        // [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_BOTTOM]: 13,\n        // [ConstantKeys.ROUNDED_LINE_RADIUS]: 32,\n        // [ConstantKeys.ROUNDED_LINE_X_RADIUS]: 8,\n        // [ConstantKeys.ROUNDED_LINE_Y_RADIUS]: 10,\n        // [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_TOP]: 23,\n        // [ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM]: 30,\n        // [ConstantKeys.COLLAPSED_SPACING]: 10,\n        [ConstantKeys.BASE_COLOR]: '#B8BCC1',\n        [ConstantKeys.BASE_ACTIVATED_COLOR]: '#82A7FC',\n      },\n      /**\n       * SelectBox config\n       */\n      selectBox: {\n        SelectorBoxPopover,\n      },\n\n      // Config shortcuts\n      shortcuts: (registry: ShortcutsRegistry, ctx) => {\n        registry.addHandlers(...shortcutGetter.map((getter) => getter(ctx)));\n      },\n      /**\n       * Drag/Drop config\n       */\n      dragdrop: {\n        /**\n         * Callback when drag drop\n         */\n        onDrop: (ctx, dropData) => {\n          // console.log(\n          //   '>>> onDrop: ',\n          //   dropData.dropNode.id,\n          //   dropData.dragNodes.map(n => n.id),\n          // );\n        },\n        canDrop: (ctx, dropData) =>\n          // console.log(\n          //   '>>> canDrop: ',\n          //   dropData.isBranch,\n          //   dropData.dropNode.id,\n          //   dropData.dragNodes.map(n => n.id),\n          // );\n          true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n        onApply: debounce((ctx, opt) => {\n          if (ctx.document.disposed) return;\n          // Listen change to trigger auto save\n          console.log('auto save: ', ctx.document.toJSON());\n        }, 100),\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Variable engine enable\n       */\n      variableEngine: {\n        enable: true,\n      },\n      /**\n       * Materials, components can be customized based on the key\n       * @see https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx\n       * 可以通过 key 自定义 UI 组件\n       */\n      materials: {\n        components: {\n          ...defaultFixedSemiMaterials,\n          [FlowRendererKey.ADDER]: NodeAdder, // Node Add Button\n          [FlowRendererKey.BRANCH_ADDER]: BranchAdder, // Branch Add Button\n          [FlowRendererKey.DRAG_NODE]: DragNode, // Component in node dragging\n          [FlowRendererKey.SLOT_ADDER]: AgentAdder, // Agent adder\n          [FlowRendererKey.SLOT_LABEL]: AgentLabel, // Agent label\n        },\n        renderDefaultNode: BaseNode, // node render\n        renderTexts: {\n          'loop-end-text': 'Loop End',\n          'loop-traverse-text': 'Loop',\n          'try-start-text': 'Try Start',\n          'try-end-text': 'Try End',\n          'catch-text': 'Catch Error',\n        },\n      },\n      /**\n       * Bind custom service\n       */\n      onBind: ({ bind }) => {\n        bind(CustomService).toSelf().inSingletonScope();\n      },\n      scroll: {\n        /**\n         * 限制滚动，防止节点都看不到\n         * Limit scrolling so that none of the nodes can see it\n         */\n        enableScrollLimit: true,\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {\n        /**\n         * Data can also be dynamically loaded via fromJSON\n         * 也可以通过 fromJSON 动态加载数据\n         */\n        // ctx.document.fromJSON(initialData)\n        console.log('---- Playground Init ----');\n      },\n      /**\n       * Playground render\n       */\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          // fitView all nodes\n          ctx.tools.fitView();\n        }, 10);\n        console.log(ctx.document.toString(true)); // Get the document tree\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose: () => {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          enableDisplayAllNodes: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Download plugin\n         * 下载插件\n         */\n        createDownloadPlugin({}),\n        /**\n         * Group plugin\n         * 分组插件\n         */\n        createGroupPlugin({\n          components: {\n            GroupBoxHeader,\n            GroupNode,\n          },\n        }),\n        /**\n         * Clipboard plugin\n         * 剪切板插件\n         */\n        createClipboardPlugin(),\n\n        /**\n         * Variable panel plugin\n         * 变量面板插件\n         */\n        createVariablePanelPlugin({}),\n        createPanelManagerPlugin({\n          factories: [nodeFormPanelFactory],\n        }),\n      ],\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/hooks/use-form-value.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { FlowNodeEntity, FlowNodeFormData, FormModelV2 } from '@flowgram.ai/fixed-layout-editor';\n\nexport const useFormValue = <T = unknown>(params: {\n  node?: FlowNodeEntity;\n  fieldName: string;\n  defaultValue?: T;\n}): [T, (v: T) => void] => {\n  const { node, fieldName, defaultValue } = params;\n  const formModel = node?.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n\n  const [innerValue, setInnerValue] = useState<T | undefined>(() =>\n    formModel?.getValueIn<T>(fieldName)\n  );\n\n  // 初始化表单值\n  useEffect(() => {\n    if (!formModel) {\n      return;\n    }\n    const initValue = formModel.getValueIn<{ width: number; height: number }>(fieldName);\n    if (!initValue) {\n      formModel.setValueIn(fieldName, defaultValue);\n    }\n  }, [defaultValue, formModel, fieldName]);\n\n  // 同步表单外部值变化：初始化/undo/redo/协同\n  useEffect(() => {\n    if (!formModel) {\n      return;\n    }\n    const disposer = formModel.onFormValueChangeIn(fieldName, () => {\n      const newValue = formModel.getValueIn<T>(fieldName);\n      if (!newValue) {\n        return;\n      }\n      setInnerValue(newValue);\n    });\n    return () => disposer.dispose();\n  }, [formModel, fieldName]);\n\n  const setValue = (newValue: T) => {\n    if (!formModel) {\n      return;\n    }\n    formModel.setValueIn(fieldName, newValue);\n    setInnerValue(newValue);\n  };\n\n  const value = (innerValue ?? defaultValue) as T;\n  return [value, setValue];\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/hooks/use-is-sidebar.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { IsSidebarContext } from '../context';\n\nexport function useIsSidebar() {\n  return useContext(IsSidebarContext);\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/hooks/use-node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { NodeRenderContext } from '../context';\n\nexport function useNodeRenderContext() {\n  return useContext(NodeRenderContext);\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFixedLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from './typings';\n\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            query: {\n              type: 'string',\n              default: 'Hello Flow.',\n            },\n            enable: {\n              type: 'boolean',\n              default: true,\n            },\n            array_obj: {\n              type: 'array',\n              items: {\n                type: 'object',\n                properties: {\n                  int: {\n                    type: 'number',\n                  },\n                  str: {\n                    type: 'string',\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'agent_0',\n      type: 'agent',\n      data: {\n        title: 'Agent',\n      },\n      blocks: [\n        {\n          id: 'agentLLM_0',\n          type: 'agentLLM',\n          blocks: [\n            {\n              id: 'llm_5',\n              type: 'llm',\n              meta: {\n                defaultExpanded: false,\n              },\n              data: {\n                title: 'LLM',\n                inputsValues: {\n                  modelType: {\n                    type: 'constant',\n                    content: 'gpt-3.5-turbo',\n                  },\n                  temperature: {\n                    type: 'constant',\n                    content: 0.5,\n                  },\n                  systemPrompt: {\n                    type: 'template',\n                    content: '# Role\\nYou are an AI assistant.\\n',\n                  },\n                  prompt: {\n                    type: 'template',\n                    content: '',\n                  },\n                },\n                inputs: {\n                  type: 'object',\n                  required: ['modelType', 'temperature', 'prompt'],\n                  properties: {\n                    modelType: {\n                      type: 'string',\n                    },\n                    temperature: {\n                      type: 'number',\n                    },\n                    systemPrompt: {\n                      type: 'string',\n                      extra: { formComponent: 'prompt-editor' },\n                    },\n                    prompt: {\n                      type: 'string',\n                      extra: { formComponent: 'prompt-editor' },\n                    },\n                  },\n                },\n                outputs: {\n                  type: 'object',\n                  properties: {\n                    result: { type: 'string' },\n                  },\n                },\n              },\n            },\n          ],\n        },\n        {\n          id: 'agentMemory_0',\n          type: 'agentMemory',\n          blocks: [\n            {\n              id: 'memory_0',\n              type: 'memory',\n              meta: {\n                defaultExpanded: false,\n              },\n              data: {\n                title: 'Memory',\n              },\n            },\n          ],\n        },\n        {\n          id: 'agentTools_0',\n          type: 'agentTools',\n          blocks: [\n            {\n              id: 'tool_0',\n              type: 'tool',\n              data: {\n                title: 'Tool0',\n              },\n            },\n            {\n              id: 'tool_1',\n              type: 'tool',\n              data: {\n                title: 'Tool1',\n              },\n            },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'llm_0',\n      type: 'llm',\n      blocks: [],\n      data: {\n        title: 'LLM',\n        inputsValues: {\n          modelType: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: '# Role\\nYou are an AI assistant.\\n',\n          },\n          prompt: {\n            type: 'template',\n            content: '',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelType', 'temperature', 'prompt'],\n          properties: {\n            modelType: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: { formComponent: 'prompt-editor' },\n            },\n            prompt: {\n              type: 'string',\n              extra: { formComponent: 'prompt-editor' },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    },\n    {\n      id: 'switch_0',\n      type: 'switch',\n      data: {\n        title: 'Switch',\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n      blocks: [\n        {\n          id: 'case_0',\n          type: 'case',\n          data: {\n            title: 'Case_0',\n            inputsValues: {\n              condition: { type: 'constant', content: true },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n          blocks: [],\n        },\n        {\n          id: 'case_1',\n          type: 'case',\n          data: {\n            title: 'Case_1',\n            inputsValues: {\n              condition: { type: 'constant', content: true },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'case_default_1',\n          type: 'caseDefault',\n          data: {\n            title: 'Default',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      data: {\n        title: 'Loop',\n        loopFor: {\n          type: 'ref',\n          content: ['start_0', 'array_obj'],\n        },\n      },\n      blocks: [\n        {\n          id: 'if_0',\n          type: 'if',\n          data: {\n            title: 'If',\n            inputsValues: {\n              condition: { type: 'constant', content: true },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n          blocks: [\n            {\n              id: 'if_true',\n              type: 'ifBlock',\n              data: {\n                title: 'true',\n              },\n              blocks: [],\n            },\n            {\n              id: 'if_false',\n              type: 'ifBlock',\n              data: {\n                title: 'false',\n              },\n              blocks: [\n                {\n                  id: 'break_0',\n                  type: 'breakLoop',\n                  data: {\n                    title: 'BreakLoop',\n                  },\n                },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'tryCatch_0',\n      type: 'tryCatch',\n      data: {\n        title: 'TryCatch',\n      },\n      blocks: [\n        {\n          id: 'tryBlock_0',\n          type: 'tryBlock',\n          blocks: [],\n        },\n        {\n          id: 'catchBlock_0',\n          type: 'catchBlock',\n          data: {\n            title: 'Catch Block 1',\n            inputsValues: {\n              condition: { type: 'constant', content: true },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n          blocks: [],\n        },\n        {\n          id: 'catchBlock_1',\n          type: 'catchBlock',\n          data: {\n            title: 'Catch Block 2',\n            inputsValues: {\n              condition: { type: 'constant', content: true },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n          blocks: [],\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n      data: {\n        title: 'End',\n        inputsValues: {\n          success: { type: 'constant', content: true, schema: { type: 'boolean' } },\n        },\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/agent-llm.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\n\nexport const AgentLLMNodeRegistry: FlowNodeRegistry = {\n  type: 'agentLLM',\n  extend: FlowNodeBaseType.SLOT_BLOCK,\n  meta: {\n    addDisable: true,\n    sidebarDisable: true,\n    draggable: false,\n  },\n  info: {\n    icon: '',\n    description: 'Agent LLM.',\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/agent-memory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\n\nexport const AgentMemoryNodeRegistry: FlowNodeRegistry = {\n  type: 'agentMemory',\n  extend: FlowNodeBaseType.SLOT_BLOCK,\n  meta: {\n    addDisable: true,\n    sidebarDisable: true,\n  },\n  info: {\n    icon: '',\n    description: 'Agent Memory.',\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/agent-tools.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\n\nlet index = 0;\nexport const AgentToolsNodeRegistry: FlowNodeRegistry = {\n  type: 'agentTools',\n  extend: FlowNodeBaseType.SLOT_BLOCK,\n  info: {\n    icon: '',\n    description: 'Agent Tools.',\n  },\n  meta: {\n    addDisable: true,\n    sidebarDisable: true,\n  },\n  onAdd() {\n    return {\n      id: `tool_${nanoid(5)}`,\n      type: 'agentTool',\n      data: {\n        title: `Tool_${++index}`,\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/agent.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { LLMNodeRegistry } from '../llm';\nimport { defaultFormMeta } from '../default-form-meta';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconRobot from '../../assets/icon-robot.svg';\nimport { ToolNodeRegistry } from './tool';\nimport { MemoryNodeRegistry } from './memory';\n\nlet index = 0;\nexport const AgentNodeRegistry: FlowNodeRegistry = {\n  type: 'agent',\n  extend: FlowNodeBaseType.SLOT,\n  info: {\n    icon: iconRobot,\n    description: 'AI Agent.',\n  },\n  formMeta: defaultFormMeta,\n  onAdd(ctx, from) {\n    return {\n      id: `agent_${nanoid(5)}`,\n      type: 'agent',\n      blocks: [\n        {\n          id: `agentLLM_${nanoid(5)}`,\n          type: 'agentLLM',\n          blocks: [LLMNodeRegistry.onAdd!(ctx, from)],\n        },\n        {\n          id: `agentMemory_${nanoid(5)}`,\n          type: 'agentMemory',\n          blocks: [MemoryNodeRegistry.onAdd!(ctx, from)],\n        },\n        {\n          id: `agentTools_${nanoid(5)}`,\n          type: 'agentTools',\n          blocks: [ToolNodeRegistry.onAdd!(ctx, from)],\n        },\n      ],\n      data: {\n        title: `Agent_${++index}`,\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ToolNodeRegistry } from './tool';\nimport { MemoryNodeRegistry } from './memory';\nimport { AgentToolsNodeRegistry } from './agent-tools';\nimport { AgentMemoryNodeRegistry } from './agent-memory';\nimport { AgentLLMNodeRegistry } from './agent-llm';\nimport { AgentNodeRegistry } from './agent';\n\nexport const AgentNodeRegistries = [\n  AgentNodeRegistry,\n  AgentMemoryNodeRegistry,\n  AgentToolsNodeRegistry,\n  AgentLLMNodeRegistry,\n  MemoryNodeRegistry,\n  ToolNodeRegistry,\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/memory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconMemory from '../../assets/icon-memory.svg';\n\nlet index = 0;\nexport const MemoryNodeRegistry: FlowNodeRegistry = {\n  type: 'memory',\n  info: {\n    icon: iconMemory,\n    description: 'Memory.',\n  },\n  meta: {\n    addDisable: true,\n    // deleteDisable: true, // memory 不能单独删除，只能通过 agent\n    copyDisable: true,\n    draggable: false,\n    selectable: false,\n  },\n  formMeta: defaultFormMeta,\n  onAdd() {\n    return {\n      id: `memory_${nanoid(5)}`,\n      type: 'memory',\n      data: {\n        title: `Memory_${++index}`,\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/agent/tool.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconTool from '../../assets/icon-tool.svg';\n\nlet index = 0;\nexport const ToolNodeRegistry: FlowNodeRegistry = {\n  type: 'tool',\n  info: {\n    icon: iconTool,\n    description: 'Tool.',\n  },\n  meta: {\n    // addDisable: true,\n    copyDisable: true,\n    draggable: false,\n    selectable: false,\n  },\n  formMeta: defaultFormMeta,\n  onAdd() {\n    return {\n      id: `tool${nanoid(5)}`,\n      type: 'tool',\n      data: {\n        title: `Tool_${++index}`,\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/break-loop/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FormHeader } from '../../form-components';\n\nexport const renderForm = () => (\n  <>\n    <FormHeader />\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/break-loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconBreak from '../../assets/icon-break.svg';\nimport { formMeta } from './form-meta';\n\n/**\n * Break 节点用于在 loop 中根据条件终止并跳出\n */\nexport const BreakLoopNodeRegistry: FlowNodeRegistry = {\n  type: 'breakLoop',\n  extend: 'end',\n  info: {\n    icon: iconBreak,\n    description: 'Break in current Loop.',\n  },\n  meta: {\n    style: {\n      width: 240,\n    },\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  canAdd(ctx, from) {\n    while (from.parent) {\n      if (from.parent.flowNodeType === 'loop') return true;\n      from = from.parent;\n    }\n    return false;\n  },\n  onAdd(ctx, from) {\n    return {\n      id: `break_${nanoid()}`,\n      type: 'breakLoop',\n      data: {\n        title: 'BreakLoop',\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/case/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropetyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n      if (\n        required.includes(valuePropetyKey) &&\n        (value === '' || value === undefined || value?.content === '')\n      ) {\n        return `${valuePropetyKey} is required`;\n      }\n      return undefined;\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/case/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCase from '../../assets/icon-case.png';\nimport { formMeta } from './form-meta';\n\nlet id = 2;\nexport const CaseNodeRegistry: FlowNodeRegistry = {\n  type: 'case',\n  /**\n   * 分支节点需要继承自 block\n   * Branch nodes need to inherit from 'block'\n   */\n  extend: 'block',\n  meta: {\n    copyDisable: true,\n    addDisable: true,\n  },\n  info: {\n    icon: iconCase,\n    description: 'Execute the branch when the condition is met.',\n  },\n  canDelete: (ctx, node) => node.parent!.blocks.length >= 3,\n  onAdd(ctx, from) {\n    return {\n      id: `Case_${nanoid(5)}`,\n      type: 'case',\n      data: {\n        title: `Case_${id++}`,\n        inputs: {\n          type: 'object',\n          required: ['condition'],\n          inputsValues: {\n            condition: '',\n          },\n          properties: {\n            condition: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    };\n  },\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/case-default/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropetyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n      if (\n        required.includes(valuePropetyKey) &&\n        (value === '' || value === undefined || value?.content === '')\n      ) {\n        return `${valuePropetyKey} is required`;\n      }\n      return undefined;\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/case-default/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCase from '../../assets/icon-case.png';\nimport { formMeta } from './form-meta';\n\nexport const CaseDefaultNodeRegistry: FlowNodeRegistry = {\n  type: 'caseDefault',\n  /**\n   * 分支节点需要继承自 block\n   * Branch nodes need to inherit from 'block'\n   */\n  extend: 'case',\n  meta: {\n    copyDisable: true,\n    addDisable: true,\n    /**\n     * caseDefault 永远在最后一个分支，所以不允许拖拽排序\n     * \"caseDefault\" is always in the last branch, so dragging and sorting is not allowed.\n     */\n    draggable: false,\n    deleteDisable: true,\n    style: {\n      width: 240,\n    },\n  },\n  info: {\n    icon: iconCase,\n    description: 'Switch default branch',\n  },\n  canDelete: (ctx, node) => false,\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/catch-block/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropetyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n      if (\n        required.includes(valuePropetyKey) &&\n        (value === '' || value === undefined || value?.content === '')\n      ) {\n        return `${valuePropetyKey} is required`;\n      }\n      return undefined;\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/catch-block/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCase from '../../assets/icon-case.png';\nimport { formMeta } from './form-meta';\n\nlet id = 3;\nexport const CatchBlockNodeRegistry: FlowNodeRegistry = {\n  type: 'catchBlock',\n  meta: {\n    copyDisable: true,\n    addDisable: true,\n  },\n  info: {\n    icon: iconCase,\n    description: 'Execute the catch branch when the condition is met.',\n  },\n  canAdd: () => false,\n  canDelete: (ctx, node) => node.parent!.blocks.length >= 2,\n  onAdd(ctx, from) {\n    return {\n      id: `Catch_${nanoid(5)}`,\n      type: 'catchBlock',\n      data: {\n        title: `Catch Block ${id++}`,\n        inputs: {\n          type: 'object',\n          required: ['condition'],\n          inputsValues: {\n            condition: '',\n          },\n          properties: {\n            condition: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    };\n  },\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/default-form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  autoRenameRefEffect,\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n} from '@flowgram.ai/form-materials';\nimport {\n  FormRenderProps,\n  FormMeta,\n  ValidateTrigger,\n  FeedbackLevel,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../typings';\nimport { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const defaultFormMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  /**\n   * Initialize (fromJSON) data transformation\n   * 初始化(fromJSON) 数据转换\n   * @param value\n   * @param ctx\n   */\n  formatOnInit: (value, ctx) => value,\n  /**\n   * Save (toJSON) data transformation\n   * 保存(toJSON) 数据转换\n   * @param value\n   * @param ctx\n   */\n  formatOnSubmit: (value, ctx) => value,\n  /**\n   * Supported writing as:\n   * 1: validate as options: { title: () => {} , ... }\n   * 2: validate as dynamic function: (values,  ctx) => ({ title: () => {}, ... })\n   */\n  validate: {\n    title: ({ value }) => (value ? undefined : 'Title is required'),\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropetyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n      if (\n        required.includes(valuePropetyKey) &&\n        (value === '' || value === undefined || value?.content === '')\n      ) {\n        return {\n          message: `${valuePropetyKey} is required`,\n          level: FeedbackLevel.Error, // Error || Warning\n        };\n      }\n      return undefined;\n    },\n  },\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n    inputsValues: autoRenameRefEffect,\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/end/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  createInferInputsPlugin,\n  DisplayInputsValues,\n  IFlowValue,\n  InputsValues,\n} from '@flowgram.ai/form-materials';\nimport { Field, FormMeta } from '@flowgram.ai/fixed-layout-editor';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent } from '../../form-components';\n\nexport const renderForm = () => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n            {({ field: { value, onChange } }) => (\n              <>\n                <InputsValues value={value} onChange={(_v) => onChange(_v)} />\n              </>\n            )}\n          </Field>\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n          {({ field: { value } }) => (\n            <>\n              <DisplayInputsValues value={value} />\n            </>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: renderForm,\n  plugins: [\n    createInferInputsPlugin({\n      sourceKey: 'inputsValues',\n      targetKey: 'inputs',\n    }),\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconEnd from '../../assets/icon-end.jpg';\nimport { formMeta } from './form-meta';\n\nexport const EndNodeRegistry: FlowNodeRegistry = {\n  type: 'end',\n  meta: {\n    isNodeEnd: true, // Mark as end\n    selectable: false, // End node cannot select\n    copyDisable: true, // End node canot copy\n    expandable: false, // disable expanded\n  },\n  info: {\n    icon: iconEnd,\n    description:\n      'The final node of the workflow, used to return the result information after the workflow is run.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  canAdd(ctx, from) {\n    // You can only add to the last node of the branch\n    if (!from.isLast) return false;\n    /**\n     * condition\n     *  blockIcon\n     *  inlineBlocks\n     *    block1\n     *      blockOrderIcon\n     *      <---- [add end]\n     *    block2\n     *      blockOrderIcon\n     *      end\n     */\n    // originParent can determine whether it is condition , and then determine whether it is the last one\n    // https://github.com/bytedance/flowgram.ai/pull/146\n    if (\n      from.parent &&\n      from.parent.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS &&\n      from.parent.originParent &&\n      !from.parent.originParent.isLast\n    ) {\n      const allBranches = from.parent.parent!.blocks;\n      // Determine whether the last node of all branch is end, All branches are not allowed to be end\n      const branchEndCount = allBranches.filter(\n        (block) => block.blocks[block.blocks.length - 1]?.getNodeMeta().isNodeEnd\n      ).length;\n      return branchEndCount < allBranches.length - 1;\n    }\n    return true;\n  },\n  canDelete(ctx, node) {\n    return node.parent !== ctx.document.root;\n  },\n  onAdd(ctx, from) {\n    return {\n      id: `end_${nanoid()}`,\n      type: 'end',\n      data: {\n        title: 'End',\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/if/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeSplitType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconIf from '../../assets/icon-if.png';\n\nexport const IFNodeRegistry: FlowNodeRegistry = {\n  extend: FlowNodeSplitType.STATIC_SPLIT,\n  type: 'if',\n  info: {\n    icon: iconIf,\n    description: 'Only the corresponding branch will be executed if the set conditions are met.',\n  },\n  meta: {\n    expandable: false, // disable expanded\n  },\n  formMeta: defaultFormMeta,\n  onAdd() {\n    return {\n      id: `if_${nanoid(5)}`,\n      type: 'if',\n      data: {\n        title: 'If',\n        inputsValues: {\n          condition: { type: 'constant', content: true },\n        },\n        inputs: {\n          type: 'object',\n          required: ['condition'],\n          properties: {\n            condition: {\n              type: 'boolean',\n            },\n          },\n        },\n      },\n      blocks: [\n        {\n          id: nanoid(5),\n          type: 'ifBlock',\n          data: {\n            title: 'true',\n          },\n          blocks: [],\n        },\n        {\n          id: nanoid(5),\n          type: 'ifBlock',\n          data: {\n            title: 'false',\n          },\n        },\n      ],\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/if-block/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { useNodeRenderContext } from '../../hooks';\n\nexport const renderForm = (props: FormRenderProps<FlowNodeJSON['data']>) => {\n  const { node } = useNodeRenderContext();\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        backgroundColor: node.index === 0 ? 'green' : 'red',\n        color: 'white',\n        display: 'flex',\n        pointerEvents: 'none',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n    >\n      <Field name=\"title\">{({ field }) => <>{field.value}</>}</Field>\n    </div>\n  );\n};\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/if-block/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconIf from '../../assets/icon-if.png';\nimport { formMeta } from './form-meta';\n\nexport const IFBlockNodeRegistry: FlowNodeRegistry = {\n  type: 'ifBlock',\n  /**\n   * 分支节点需要继承自 block\n   * Branch nodes need to inherit from 'block'\n   */\n  extend: 'block',\n  meta: {\n    copyDisable: true,\n    addDisable: true,\n    sidebarDisable: true,\n    defaultExpanded: false,\n    style: {\n      width: 66,\n      height: 20,\n      borderRadius: 4,\n    },\n  },\n  info: {\n    icon: iconIf,\n    description: '',\n  },\n  canAdd: () => false,\n  canDelete: (ctx, node) => false,\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeRegistry } from '../typings';\nimport { TryCatchNodeRegistry } from './trycatch';\nimport { SwitchNodeRegistry } from './switch';\nimport { StartNodeRegistry } from './start';\nimport { LoopNodeRegistry } from './loop';\nimport { LLMNodeRegistry } from './llm';\nimport { IFBlockNodeRegistry } from './if-block';\nimport { IFNodeRegistry } from './if';\nimport { EndNodeRegistry } from './end';\nimport { CatchBlockNodeRegistry } from './catch-block';\nimport { CaseDefaultNodeRegistry } from './case-default';\nimport { CaseNodeRegistry } from './case';\nimport { BreakLoopNodeRegistry } from './break-loop';\nimport { AgentNodeRegistries } from './agent';\n\nexport const FlowNodeRegistries: FlowNodeRegistry[] = [\n  StartNodeRegistry,\n  EndNodeRegistry,\n  SwitchNodeRegistry,\n  LLMNodeRegistry,\n  LoopNodeRegistry,\n  CaseNodeRegistry,\n  TryCatchNodeRegistry,\n  CatchBlockNodeRegistry,\n  IFNodeRegistry,\n  IFBlockNodeRegistry,\n  BreakLoopNodeRegistry,\n  CaseDefaultNodeRegistry,\n  ...AgentNodeRegistries,\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/llm/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { AgentLLMNodeRegistry } from '../agent/agent-llm';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconLLM from '../../assets/icon-llm.jpg';\n\nlet index = 0;\nexport const LLMNodeRegistry: FlowNodeRegistry = {\n  type: 'llm',\n  info: {\n    icon: iconLLM,\n    description:\n      'Call the large language model and use variables and prompt words to generate responses.',\n  },\n  formMeta: defaultFormMeta,\n  meta: {\n    draggable: (node) => node.parent?.flowNodeType !== AgentLLMNodeRegistry.type,\n  },\n  canDelete(ctx, node) {\n    return node.parent?.flowNodeType !== AgentLLMNodeRegistry.type;\n  },\n  onAdd() {\n    return {\n      id: `llm_${nanoid(5)}`,\n      type: 'llm',\n      data: {\n        title: `LLM_${++index}`,\n        inputsValues: {\n          modelType: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: '# Role\\nYou are an AI assistant.\\n',\n          },\n          prompt: {\n            type: 'template',\n            content: '',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelType', 'temperature', 'prompt'],\n          properties: {\n            modelType: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: { formComponent: 'prompt-editor' },\n            },\n            prompt: {\n              type: 'string',\n              extra: { formComponent: 'prompt-editor' },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/loop/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  BatchVariableSelector,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../hooks';\nimport { FormHeader, FormContent, FormOutputs, FormItem, Feedback } from '../../form-components';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n  const { readonly } = useNodeRenderContext();\n\n  const loopFor = (\n    <Field<IFlowRefValue> name={`loopFor`}>\n      {({ field, fieldState }) => (\n        <FormItem name={'loopFor'} type={'array'} required>\n          <BatchVariableSelector\n            style={{ width: '100%' }}\n            value={field.value?.content}\n            onChange={(val) => field.onChange({ type: 'ref', content: val })}\n            readonly={readonly}\n            hasError={Object.keys(fieldState?.errors || {}).length > 0}\n          />\n          <Feedback errors={fieldState?.errors} />\n        </FormItem>\n      )}\n    </Field>\n  );\n\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          {loopFor}\n          <FormOutputs />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        {loopFor}\n        <FormOutputs />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta<LoopNodeJSON['data']> = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconLoop from '../../assets/icon-loop.svg';\nimport { formMeta } from './form-meta';\n\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: 'loop',\n  info: {\n    icon: iconLoop,\n    description:\n      'Used to repeatedly execute a series of tasks by setting the number of iterations and logic',\n  },\n  meta: {\n    expandable: false, // disable expanded\n  },\n  formMeta,\n  onAdd() {\n    return {\n      id: `loop_${nanoid(5)}`,\n      type: 'loop',\n      data: {\n        title: 'Loop',\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/start/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  JsonSchemaEditor,\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n} from '@flowgram.ai/form-materials';\nimport {\n  Field,\n  FieldRenderProps,\n  FormRenderProps,\n  FormMeta,\n  ValidateTrigger,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON, JsonSchema } from '../../typings';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent, FormOutputs } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field\n            name=\"outputs\"\n            render={({ field: { value, onChange } }: FieldRenderProps<JsonSchema>) => (\n              <>\n                <JsonSchemaEditor\n                  value={value}\n                  onChange={(value) => onChange(value as JsonSchema)}\n                />\n              </>\n            )}\n          />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <FormOutputs />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n  },\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconStart from '../../assets/icon-start.jpg';\nimport { formMeta } from './form-meta';\n\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: 'start',\n  meta: {\n    isStart: true, // Mark as start\n    deleteDisable: true, // Start node cannot delete\n    selectable: false, // Start node cannot select\n    copyDisable: true, // Start node cannot copy\n    expandable: false, // disable expanded\n    addDisable: true, // Start Node cannot be added\n  },\n  info: {\n    icon: iconStart,\n    description:\n      'The starting node of the workflow, used to set the information needed to initiate the workflow.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/switch/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeSplitType } from '@flowgram.ai/fixed-layout-editor';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCondition from '../../assets/icon-condition.svg';\n\nexport const SwitchNodeRegistry: FlowNodeRegistry = {\n  extend: FlowNodeSplitType.DYNAMIC_SPLIT,\n  type: 'switch',\n  info: {\n    icon: iconCondition,\n    description:\n      'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',\n  },\n  meta: {\n    expandable: false, // disable expanded\n  },\n  formMeta: defaultFormMeta,\n  onAdd() {\n    return {\n      id: `switch_${nanoid(5)}`,\n      type: 'switch',\n      data: {\n        title: 'Switch',\n      },\n      blocks: [\n        {\n          id: nanoid(5),\n          type: 'case',\n          data: {\n            title: 'Case_0',\n            inputsValues: {\n              condition: { type: 'constant', content: '' },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n          blocks: [],\n        },\n        {\n          id: nanoid(5),\n          type: 'case',\n          data: {\n            title: 'Case_1',\n            inputsValues: {\n              condition: { type: 'constant', content: '' },\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: nanoid(5),\n          type: 'caseDefault',\n          data: {\n            title: 'Default',\n          },\n          blocks: [],\n        },\n      ],\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/trycatch/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent, FormOutputs } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON['data']> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/nodes/trycatch/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconTryCatch from '../../assets/icon-trycatch.svg';\nimport { formMeta } from './form-meta';\n\nexport const TryCatchNodeRegistry: FlowNodeRegistry = {\n  type: 'tryCatch',\n  info: {\n    icon: iconTryCatch,\n    description: 'try catch.',\n  },\n  meta: {\n    expandable: false, // disable expanded\n  },\n  formMeta,\n  onAdd() {\n    return {\n      id: `tryCatch${nanoid(5)}`,\n      type: 'tryCatch',\n      data: {\n        title: 'TryCatch',\n      },\n      blocks: [\n        {\n          id: `tryBlock${nanoid(5)}`,\n          type: 'tryBlock',\n          blocks: [],\n        },\n        {\n          id: `catchBlock${nanoid(5)}`,\n          type: 'catchBlock',\n          blocks: [],\n          data: {\n            title: 'Catch Block 1',\n            inputsValues: {\n              condition: '',\n            },\n            inputs: {\n              type: 'object',\n              required: ['condition'],\n              properties: {\n                condition: {\n                  type: 'boolean',\n                },\n              },\n            },\n          },\n        },\n      ],\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/clipboard-plugin/create-clipboard-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  definePluginCreator,\n  FixedLayoutPluginContext,\n  PluginCreator,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { readData } from '../../shortcuts/utils';\n\nexport const createClipboardPlugin: PluginCreator<void> = definePluginCreator<\n  void,\n  FixedLayoutPluginContext\n>({\n  async onInit(ctx) {\n    const clipboard = ctx.clipboard;\n    clipboard.writeText(await readData(clipboard));\n    const clipboardListener = (e: any) => {\n      clipboard.writeText(e.value);\n    };\n    navigator.clipboard.addEventListener('onchange', clipboardListener);\n    ctx.playground.toDispose.onDispose(() => {\n      navigator.clipboard.removeEventListener('onchange', clipboardListener);\n    });\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/group-box-header.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type CSSProperties } from 'react';\n\nimport { IGroupBoxHeader } from '@flowgram.ai/group-plugin';\n\nimport { GroupTools } from './group-tools';\nimport { GroupNote } from './group-note';\n\nexport const GroupBoxHeader: IGroupBoxHeader = (props: any) => {\n  const { groupNode, groupController } = props;\n\n  if (!groupController || groupController.collapsed) {\n    return <></>;\n  }\n\n  const basicStyle: CSSProperties = {\n    display: 'flex',\n    alignItems: 'flex-start',\n    justifyContent: 'space-between',\n    padding: 10,\n    zIndex: 10,\n  };\n\n  return (\n    <div className=\"gedit-group-container\" style={basicStyle}>\n      <GroupNote\n        containerStyle={{\n          width: '48%',\n          transform: 'translateY(-6px)',\n        }}\n        textStyle={{\n          wordBreak: 'break-all',\n          whiteSpace: 'nowrap',\n          overflow: 'hidden',\n          textOverflow: 'ellipsis',\n          height: groupController.positionConfig.headerHeight,\n        }}\n        groupNode={groupNode}\n        groupController={groupController}\n      />\n      <GroupTools\n        groupNode={groupNode}\n        groupController={groupController}\n        visible={groupController.hovered}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/group-node.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IGroupNode } from '@flowgram.ai/group-plugin';\n\nimport { GroupTools } from './group-tools';\nimport { GroupNote } from './group-note';\n\nexport const GroupNode: IGroupNode = (props: any) => {\n  const { groupNode, groupController } = props;\n\n  if (!groupController || !groupController.collapsed) {\n    return <></>;\n  }\n\n  return (\n    <div\n      style={{\n        border: '1px solid rgb(97, 69, 211)',\n        backgroundColor: 'rgb(236 233 247)',\n        borderRadius: 10,\n        width: 200,\n        height: 'auto',\n        padding: 10,\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'space-around',\n        alignItems: 'flex-start',\n      }}\n    >\n      <div\n        style={{\n          display: 'flex',\n          flexDirection: 'row',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n          width: '100%',\n        }}\n      >\n        <div\n          style={{\n            width: '2rem',\n            height: '2rem',\n            borderRadius: '1rem',\n            background: 'rgb(198 188 241)',\n            color: 'rgb(97, 69, 211)',\n            display: 'flex',\n            justifyContent: 'center',\n            alignItems: 'center',\n          }}\n        >\n          <p style={{ margin: 0 }}>{groupController.nodes.length}</p>\n        </div>\n        <GroupTools groupNode={groupNode} groupController={groupController} visible={true} />\n      </div>\n      <GroupNote\n        groupNode={groupNode}\n        groupController={groupController}\n        containerStyle={{\n          paddingTop: 10,\n          width: '100%',\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/group-note.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef, useState, type CSSProperties, type FC } from 'react';\n\nimport {\n  type FlowGroupController,\n  type FlowNodeEntity,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\nimport type { AutosizeRow } from '@douyinfe/semi-ui/lib/es/input/textarea';\nimport { Tooltip } from '@douyinfe/semi-ui';\n\nimport { useFormValue } from '../../hooks';\nimport MultiLineEditor from './multilang-textarea-editor';\n\ninterface GroupNoteProps {\n  groupNode: FlowNodeEntity;\n  groupController: FlowGroupController;\n  autoSize?: AutosizeRow | boolean;\n  textStyle?: CSSProperties;\n  containerStyle?: CSSProperties;\n  enableTooltip?: boolean;\n}\n\nexport const GroupNote: FC<GroupNoteProps> = (props) => {\n  const {\n    groupNode,\n    groupController,\n    containerStyle = {},\n    textStyle = {},\n    autoSize = true,\n    enableTooltip = false,\n  } = props;\n\n  const [editingValue, setEditingValue] = useFormValue<string>({\n    node: groupNode,\n    fieldName: 'note',\n    defaultValue: '',\n  });\n\n  const ref = useRef<HTMLDivElement>(null);\n  const [tooltipVisible, setTooltipVisible] = useState<boolean>(false);\n  const { playground } = useClientContext();\n  const [editing, setEditing] = useState<boolean>(false);\n\n  if (!groupController) {\n    return <></>;\n  }\n\n  return (\n    <div\n      className=\"gedit-group-note\"\n      ref={ref}\n      style={containerStyle}\n      onMouseEnter={() => {\n        if (!editingValue || !enableTooltip || editing) {\n          if (tooltipVisible) {\n            setTooltipVisible(false);\n          }\n          return;\n        }\n        setTooltipVisible(true);\n      }}\n      onMouseLeave={() => {\n        setTooltipVisible(false);\n      }}\n    >\n      <Tooltip\n        className=\"gedit-group-note-tooltip\"\n        trigger=\"custom\"\n        visible={tooltipVisible}\n        content={editingValue}\n      >\n        <MultiLineEditor\n          value={editingValue}\n          onChange={(note) => {\n            setEditingValue(note || '');\n          }}\n          readonly={playground.config.readonly}\n          placeholder=\"Please enter note\"\n          style={textStyle}\n          autoSize={autoSize}\n          onEditingChange={(editingState) => {\n            if (editingState) {\n              setTooltipVisible(false);\n            }\n            setEditing(editingState);\n          }}\n        />\n      </Tooltip>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/group-tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type CSSProperties, type FC } from 'react';\n\nimport {\n  useService,\n  useStartDragNode,\n  FlowGroupService,\n  type FlowNodeEntity,\n  type FlowGroupController,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Button, ButtonGroup, Toast, Tooltip } from '@douyinfe/semi-ui';\nimport {\n  IconCopy,\n  IconDeleteStroked,\n  IconExpand,\n  IconHandle,\n  IconShrink,\n} from '@douyinfe/semi-icons';\n\nimport { writeData } from '../../shortcuts/utils';\nimport { IconUngroupOutlined } from './icons';\n\ninterface GroupToolsProps {\n  groupNode: FlowNodeEntity;\n  groupController: FlowGroupController;\n  visible: boolean;\n  style?: CSSProperties;\n}\n\nconst BUTTON_HEIGHT = 24;\n\nexport const GroupTools: FC<GroupToolsProps> = (props) => {\n  const { groupNode, groupController, visible, style = {} } = props;\n\n  const groupService = useService<FlowGroupService>(FlowGroupService);\n  const { operation, playground, clipboard } = useClientContext();\n\n  const { startDrag } = useStartDragNode();\n\n  const buttonStyle = {\n    cursor: 'pointer',\n    height: BUTTON_HEIGHT,\n  };\n  if (playground.config.readonly) return null;\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        opacity: visible ? 1 : 0,\n        gap: 5,\n        paddingBottom: 5,\n        color: 'rgb(97, 69, 211)',\n        ...style,\n      }}\n      onMouseDown={(e) => {\n        e.stopPropagation();\n      }}\n    >\n      <ButtonGroup size=\"small\" theme=\"borderless\" style={{ display: 'flex', flexWrap: 'nowrap' }}>\n        <Tooltip content=\"Drag\">\n          <Button\n            style={{ ...buttonStyle, cursor: 'grab' }}\n            icon={<IconHandle />}\n            type=\"primary\"\n            theme=\"borderless\"\n            onMouseDown={(e) => {\n              e.stopPropagation();\n              startDrag(e, {\n                dragStartEntity: groupNode,\n                dragEntities: [groupNode],\n              });\n            }}\n          />\n        </Tooltip>\n\n        <Tooltip content={groupController?.collapsed ? 'Expand' : 'Collapse'}>\n          <Button\n            style={buttonStyle}\n            icon={groupController?.collapsed ? <IconExpand /> : <IconShrink />}\n            type=\"primary\"\n            theme=\"borderless\"\n            onClick={(e) => {\n              if (!groupController) {\n                return;\n              }\n              e.stopPropagation();\n              if (groupController.collapsed) {\n                groupController.expand();\n              } else {\n                groupController.collapse();\n              }\n            }}\n          />\n        </Tooltip>\n        <Tooltip content=\"Ungroup\">\n          <Button\n            style={buttonStyle}\n            icon={<IconUngroupOutlined />}\n            type=\"primary\"\n            theme=\"borderless\"\n            onClick={() => {\n              groupService.ungroup(groupNode);\n            }}\n          />\n        </Tooltip>\n        <Tooltip content=\"Copy\">\n          <Button\n            icon={<IconCopy />}\n            style={buttonStyle}\n            type=\"primary\"\n            theme=\"borderless\"\n            onClick={() => {\n              const nodeJSON = groupNode.toJSON();\n\n              writeData([nodeJSON], clipboard);\n              Toast.success({\n                content: 'Copied. You can move to any [+] to paste.',\n              });\n            }}\n          />\n        </Tooltip>\n        <Tooltip content=\"Delete\">\n          <Button\n            style={buttonStyle}\n            type=\"primary\"\n            theme=\"borderless\"\n            icon={<IconDeleteStroked />}\n            onClick={() => {\n              operation.deleteNode(groupNode);\n            }}\n          />\n        </Tooltip>\n      </ButtonGroup>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/icons/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type ReactNode, type Ref, forwardRef } from 'react';\n\nimport Icon, { type IconProps } from '@douyinfe/semi-icons';\n\nconst SvgGroup = () => (\n  <svg\n    className=\"icon\"\n    width=\"16px\"\n    height=\"16px\"\n    viewBox=\"0 0 1024 1024\"\n    version=\"1.1\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M341.333333 341.333333v170.666667h213.333334V341.333333H341.333333M42.666667 42.666667h170.666666v42.666666h597.333334V42.666667h170.666666v170.666666h-42.666666v597.333334h42.666666v170.666666h-170.666666v-42.666666H213.333333v42.666666H42.666667v-170.666666h42.666666V213.333333H42.666667V42.666667m170.666666 768v42.666666h597.333334v-42.666666h42.666666V213.333333h-42.666666V170.666667H213.333333v42.666666H170.666667v597.333334h42.666666M256 256h384v170.666667h128v341.333333H341.333333v-170.666667H256V256m384 341.333333h-213.333333v85.333334h256v-170.666667h-42.666667v85.333333z\" />\n  </svg>\n);\n\nconst SvgUngroup = () => (\n  <svg\n    className=\"icon\"\n    width=\"16px\"\n    height=\"16px\"\n    viewBox=\"0 0 1024 1024\"\n    version=\"1.1\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M85.333333 85.333333 256 85.333333 256 128 554.666667 128 554.666667 85.333333 725.333333 85.333333 725.333333 256 682.666667 256 682.666667 384 768 384 768 341.333333 938.666667 341.333333 938.666667 512 896 512 896 768 938.666667 768 938.666667 938.666667 768 938.666667 768 896 512 896 512 938.666667 341.333333 938.666667 341.333333 768 384 768 384 682.666667 256 682.666667 256 725.333333 85.333333 725.333333 85.333333 554.666667 128 554.666667 128 256 85.333333 256 85.333333 85.333333M768 512 768 469.333333 682.666667 469.333333 682.666667 554.666667 725.333333 554.666667 725.333333 725.333333 554.666667 725.333333 554.666667 682.666667 469.333333 682.666667 469.333333 768 512 768 512 810.666667 768 810.666667 768 768 810.666667 768 810.666667 512 768 512M554.666667 256 554.666667 213.333333 256 213.333333 256 256 213.333333 256 213.333333 554.666667 256 554.666667 256 597.333333 384 597.333333 384 512 341.333333 512 341.333333 341.333333 512 341.333333 512 384 597.333333 384 597.333333 256 554.666667 256M512 512 469.333333 512 469.333333 597.333333 554.666667 597.333333 554.666667 554.666667 597.333333 554.666667 597.333333 469.333333 512 469.333333 512 512Z\" />\n  </svg>\n);\n\nconst IconFactory = (svg: ReactNode) =>\n  // eslint-disable-next-line react/display-name\n  forwardRef((props: Omit<IconProps, 'svg' | 'ref'>, ref: Ref<HTMLSpanElement>) => (\n    <Icon svg={svg} {...props} ref={ref} />\n  ));\n\nexport const IconGroupOutlined: any = IconFactory(<SvgGroup />);\nexport const IconUngroupOutlined: any = IconFactory(<SvgUngroup />);\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { GroupBoxHeader } from './group-box-header';\nexport { GroupNode } from './group-node';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/multilang-textarea-editor/base-textarea.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useRef, useEffect, useCallback } from 'react';\n\nimport type { AutosizeRow } from '@douyinfe/semi-ui/lib/es/input/textarea';\nimport { TextArea } from '@douyinfe/semi-ui';\n\ninterface Props {\n  value: string | undefined;\n  onChange: (data: string | undefined) => void;\n  onBlur: () => void;\n  onFocus?: () => void;\n  onSubmit?: () => void;\n  editing?: boolean;\n  autoSize?: AutosizeRow | boolean;\n  // eslint-disable-next-line\n  [key: string]: any;\n}\n\nconst BaseTextarea: React.FC<Props> = (props) => {\n  const { value, onChange, onBlur, editing, onFocus, autoSize = true, ...rest } = props;\n\n  const [data, setData] = useState(value);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n\n  const onSubmit = useCallback(() => {\n    onChange(data);\n    onBlur?.();\n  }, [data, onChange]);\n\n  const handleBlur = () => {\n    onBlur?.();\n    onSubmit?.();\n  };\n\n  useEffect(() => {\n    setData(value);\n  }, [value]);\n\n  useEffect(() => {\n    if (textareaRef.current && editing) {\n      textareaRef.current?.focus();\n    }\n  }, [editing]);\n\n  return (\n    <TextArea\n      {...rest}\n      ref={textareaRef}\n      value={data}\n      onChange={(v) => {\n        setData(v);\n      }}\n      onEnterPress={onSubmit}\n      onBlur={handleBlur}\n      onFocus={onFocus}\n      autosize={autoSize}\n      rows={1}\n      className={'base-textarea'}\n    />\n  );\n};\n\nexport default BaseTextarea;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/multilang-textarea-editor/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-description {\n  color: var(--semi-color-text-2);\n  font-size: 12px;\n  line-height: 20px;\n  padding: 2px 9px;\n  word-break: break-all;\n  white-space: break-spaces;\n}\n\n.base-textarea {\n  word-break: break-all;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/group-plugin/multilang-textarea-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useCallback, useRef, type CSSProperties } from 'react';\n\nimport styled from 'styled-components';\nimport type { AutosizeRow } from '@douyinfe/semi-ui/lib/es/input/textarea';\n\nimport BaseTextarea from './base-textarea';\n\nimport './index.css';\n\nconst OverlayWrap = styled.div`\n  width: 100%;\n`;\n\ninterface Props {\n  value?: string;\n  readonly?: boolean;\n  placeholder?: string;\n  autoSize?: AutosizeRow | boolean;\n  style?: CSSProperties;\n  onChange: (data?: string) => void;\n  onEditingChange?: (editing: boolean) => void;\n}\n\nconst MultiLineEditor: React.FC<Props> = (props) => {\n  const {\n    value,\n    onChange,\n    onEditingChange,\n    readonly,\n    autoSize,\n    placeholder = '',\n    style = {},\n  } = props;\n\n  const [editing, setEditing] = useState<boolean>(false);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const textRef = useRef(null);\n\n  const handleEdit = useCallback(() => {\n    if (readonly) {\n      return;\n    }\n    setEditing(true);\n    onEditingChange?.(true);\n    setTimeout(() => {\n      textareaRef.current?.focus();\n    }, 0);\n  }, [readonly]);\n\n  const handleEditEnd = useCallback(() => {\n    setEditing(false);\n    onEditingChange?.(false);\n  }, []);\n\n  return (\n    <OverlayWrap className=\"multilang-textarea-editor\">\n      {editing && !readonly ? (\n        <BaseTextarea\n          ref={textareaRef}\n          value={value}\n          onChange={onChange}\n          editing={editing}\n          onBlur={handleEditEnd}\n          onSubmit={handleEditEnd}\n          placeholder={placeholder}\n          autoSize={autoSize}\n        />\n      ) : (\n        <div\n          ref={textRef}\n          className={'node-description'}\n          onClick={handleEdit}\n          style={readonly ? { paddingLeft: 0, ...style } : style}\n        >\n          {value || placeholder}\n        </div>\n      )}\n    </OverlayWrap>\n  );\n};\n\nexport default MultiLineEditor;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createClipboardPlugin } from './clipboard-plugin/create-clipboard-plugin';\nexport { createVariablePanelPlugin } from './variable-panel-plugin';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useVariableTree } from '@flowgram.ai/form-materials';\nimport { Tree } from '@douyinfe/semi-ui';\n\nexport function FullVariableList() {\n  const treeData = useVariableTree({});\n\n  return <Tree treeData={treeData} />;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { JsonSchemaEditor, JsonSchemaUtils } from '@flowgram.ai/form-materials';\nimport {\n  BaseVariableField,\n  GlobalScope,\n  useRefresh,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport function GlobalVariableEditor() {\n  const globalScope = useService(GlobalScope);\n\n  const refresh = useRefresh();\n\n  const globalVar = globalScope.getVar() as BaseVariableField;\n\n  useEffect(() => {\n    const disposable = globalScope.output.onVariableListChange(() => {\n      refresh();\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  if (!globalVar) {\n    return;\n  }\n\n  const value = globalVar.type ? JsonSchemaUtils.astToSchema(globalVar.type) : { type: 'object' };\n\n  return (\n    <JsonSchemaEditor\n      value={value}\n      onChange={(_schema) => globalVar.updateType(JsonSchemaUtils.schemaToAST(_schema))}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.panel-wrapper {\n  position: relative;\n  z-index: 9999;\n}\n\n.variable-panel-button {\n  position: absolute;\n  top: 0;\n  right: 0;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  z-index: 1;\n\n  &.close {\n    width: 30px;\n    height: 30px;\n    top: 10px;\n    right: 10px;\n  }\n}\n\n.panel-container {\n  width: 500px;\n  border-radius: 5px;\n  background-color: #fff;\n  overflow: hidden;\n  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);\n  z-index: 30;\n\n  :global(.semi-tabs-bar) {\n    padding-left: 20px;\n  }\n\n  :global(.semi-tabs-content) {\n    padding: 20px;\n    height: 500px;\n    overflow: auto;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { Button, Collapsible, Tabs, Tooltip } from '@douyinfe/semi-ui';\nimport { IconMinus } from '@douyinfe/semi-icons';\n\nimport iconVariable from '../../../assets/icon-variable.png';\nimport { GlobalVariableEditor } from './global-variable-editor';\nimport { FullVariableList } from './full-variable-list';\n\nimport styles from './index.module.less';\n\nexport function VariablePanel() {\n  const [isOpen, setOpen] = useState<boolean>(false);\n\n  return (\n    <div className={styles['panel-wrapper']}>\n      <Tooltip content=\"Toggle Variable Panel\">\n        <Button\n          className={`${styles['variable-panel-button']} ${isOpen ? styles.close : ''}`}\n          theme={isOpen ? 'borderless' : 'light'}\n          onClick={() => setOpen((_open) => !_open)}\n        >\n          {isOpen ? <IconMinus /> : <img src={iconVariable} width={20} height={20} />}\n        </Button>\n      </Tooltip>\n      <Collapsible isOpen={isOpen}>\n        <div className={styles['panel-container']}>\n          <Tabs>\n            <Tabs.TabPane itemKey=\"variables\" tab=\"Variable List\">\n              <FullVariableList />\n            </Tabs.TabPane>\n            <Tabs.TabPane itemKey=\"global\" tab=\"Global Editor\">\n              <GlobalVariableEditor />\n            </Tabs.TabPane>\n          </Tabs>\n        </div>\n      </Collapsible>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createVariablePanelPlugin } from './variable-panel-plugin';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { domUtils, injectable, Layer } from '@flowgram.ai/fixed-layout-editor';\n\nimport { VariablePanel } from './components/variable-panel';\n\n@injectable()\nexport class VariablePanelLayer extends Layer {\n  onReady(): void {\n    // Fix variable panel in the right of canvas\n    this.config.onDataChange(() => {\n      const { scrollX, scrollY } = this.config.config;\n      domUtils.setStyle(this.node, {\n        position: 'absolute',\n        right: 25 - scrollX,\n        top: scrollY + 25,\n      });\n    });\n  }\n\n  render(): JSX.Element {\n    return <VariablePanel />;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { JsonSchemaUtils } from '@flowgram.ai/form-materials';\nimport { ASTFactory, definePluginCreator, GlobalScope } from '@flowgram.ai/fixed-layout-editor';\n\nimport iconVariable from '../../assets/icon-variable.png';\nimport { VariablePanelLayer } from './variable-panel-layer';\n\nconst fetchMockVariableFromRemote = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return {\n    type: 'object',\n    properties: {\n      userId: { type: 'string' },\n    },\n  };\n};\n\nexport const createVariablePanelPlugin = definePluginCreator({\n  onInit(ctx) {\n    ctx.playground.registerLayer(VariablePanelLayer);\n\n    // Fetch Global Variable\n    fetchMockVariableFromRemote().then((v) => {\n      ctx.get(GlobalScope).setVar(\n        ASTFactory.createVariableDeclaration({\n          key: 'global',\n          meta: {\n            title: 'Global',\n            icon: iconVariable,\n          },\n          type: JsonSchemaUtils.schemaToAST(v),\n        })\n      );\n    });\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/services/custom-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from '@flowgram.ai/fixed-layout-editor';\nimport {\n  FixedLayoutPluginContext,\n  SelectionService,\n  Playground,\n  FlowDocument,\n} from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Docs: https://inversify.io/docs/introduction/getting-started/\n * Warning: Use decorator legacy\n *   // rsbuild.config.ts\n *   {\n *     source: {\n *       decorators: {\n *         version: 'legacy'\n *       }\n *     }\n *   }\n * Usage:\n *  1.\n *    const myService = useService(CustomService)\n *    myService.save()\n *  2.\n *    const myService = useClientContext().get(CustomService)\n *  3.\n *    const myService = node.getService(CustomService)\n */\n@injectable()\nexport class CustomService {\n  @inject(FixedLayoutPluginContext) ctx: FixedLayoutPluginContext;\n\n  @inject(SelectionService) selectionService: SelectionService;\n\n  @inject(Playground) playground: Playground;\n\n  @inject(FlowDocument) document: FlowDocument;\n\n  save() {\n    console.log(this.document.toJSON());\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { CustomService } from './custom-service';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/shortcuts/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum FlowCommandId {\n  COPY = 'COPY',\n  PASTE = 'PASTE',\n  CUT = 'CUT',\n  GROUP = 'GROUP',\n  UNGROUP = 'UNGROUP',\n  COLLAPSE = 'COLLPASE',\n  EXPAND = 'EXPAND',\n  DELETE = 'DELETE',\n  ZOOM_IN = 'ZOOM_IN',\n  ZOOM_OUT = 'ZOOM_OUT',\n  RESET_ZOOM = 'RESET_ZOOM',\n  SELECT_ALL = 'SELECT_ALL',\n  CANCEL_SELECT = 'CANCEL_SELECT',\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/shortcuts/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport {\n  CommandRegistry,\n  type FixedLayoutPluginContext,\n  FlowGroupService,\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeRenderData,\n  type ShortcutsRegistry,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport { writeData } from './utils';\nimport { FlowCommandId } from './constants';\n\ntype ShortcutGetter = (\n  ctx: FixedLayoutPluginContext\n) => Parameters<ShortcutsRegistry['addHandlers']>[0];\n\nconst copy: ShortcutGetter = (ctx) => {\n  const selection = ctx.selection;\n  const clipboard = ctx.clipboard;\n\n  return {\n    commandId: FlowCommandId.COPY,\n    shortcuts: ['meta c', 'ctrl c'],\n    isEnabled: (node) =>\n      (selection?.selection.length > 0 || node instanceof FlowNodeEntity) &&\n      !ctx.playground.config.readonlyOrDisabled,\n    execute: (node) => {\n      const nodes =\n        node instanceof FlowNodeEntity\n          ? [node]\n          : (selection.selection.filter(\n              (_entity) => _entity instanceof FlowNodeEntity\n            ) as FlowNodeEntity[]);\n      const originNodes = nodes.map((n) => ({\n        ...n.toJSON(),\n        id: `${n.flowNodeType}_${nanoid()}`,\n      }));\n\n      writeData(originNodes, clipboard);\n      Toast.success({\n        content: 'Copied. You can move to any [+] to paste.',\n      });\n    },\n  };\n};\n\nconst cut: ShortcutGetter = (ctx) => {\n  const selection = ctx.selection;\n\n  const commandRegistry = ctx.get<CommandRegistry>(CommandRegistry);\n\n  return {\n    commandId: FlowCommandId.CUT,\n    commandDetail: {\n      label: 'Cut',\n    },\n    shortcuts: ['meta x', 'ctrl x'],\n    isEnabled: () => selection.selection.length > 0 && !ctx.playground.config.readonlyOrDisabled,\n    execute: () => {\n      // nodeService.copyNodes(\n      //   selection.selection.filter(\n      //     _entity => _entity instanceof FlowNodeEntity,\n      //   ) as FlowNodeEntity[],\n      // );\n\n      Toast.success({\n        content: 'Cut. You can move to any [+] to paste.',\n      });\n\n      commandRegistry.executeCommand(FlowCommandId.DELETE);\n    },\n  };\n};\n\nconst zoomIn: ShortcutGetter = (ctx) => {\n  const config = ctx.playground.config;\n\n  return {\n    commandId: FlowCommandId.ZOOM_IN,\n    shortcuts: ['meta =', 'ctrl ='],\n    execute: () => {\n      config.zoomin();\n    },\n  };\n};\n\nconst zoomOut: ShortcutGetter = (ctx) => {\n  const config = ctx.playground.config;\n\n  return {\n    commandId: FlowCommandId.ZOOM_OUT,\n    shortcuts: ['meta -', 'ctrl -'],\n    execute: () => {\n      config.zoomout();\n    },\n  };\n};\n\nconst resetZoom: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.RESET_ZOOM,\n  commandDetail: {\n    label: 'Reset Zoom',\n  },\n  shortcuts: ['meta 0', 'ctrl 0'],\n  execute: () => {\n    ctx.playground.config.updateZoom(1);\n  },\n});\n\nconst group: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.GROUP,\n  commandDetail: {\n    label: 'Create Group',\n  },\n  shortcuts: ['meta g', 'ctrl g'],\n  isEnabled: () => !ctx.playground.config.readonlyOrDisabled,\n\n  execute: () => {\n    const groupService = ctx.get<FlowGroupService>(FlowGroupService);\n    const selection = ctx.playground.selectionService;\n\n    groupService.createGroup(\n      selection.selection.filter((_entity) => _entity instanceof FlowNodeEntity) as FlowNodeEntity[]\n    );\n\n    ctx.playground.selectionService.selection = [];\n  },\n});\n\nconst selectAll: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.SELECT_ALL,\n  commandDetail: {\n    label: 'Select All',\n  },\n  shortcuts: ['meta a', 'ctrl a'],\n  isEnabled: () => !ctx.playground.config.readonlyOrDisabled,\n  execute: () => {\n    const allNodes = (ctx.document.root.children || []).filter(\n      (node) => node.flowNodeType !== 'start' && node.flowNodeType !== 'end'\n    );\n\n    ctx.playground.selectionService.selection = allNodes;\n  },\n});\n\nconst cancelSelect: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.CANCEL_SELECT,\n  commandDetail: {\n    label: 'Cancel Select',\n  },\n  shortcuts: ['esc'],\n  execute: () => {\n    ctx.playground.selectionService.selection = [];\n  },\n});\n\nconst collapse: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.COLLAPSE,\n  commandDetail: {\n    label: 'Collapse',\n  },\n  shortcuts: ['meta alt openbracket', 'ctrl alt openbracket'],\n  isEnabled: () => !ctx.playground.config.readonlyOrDisabled,\n  execute: () => {\n    const selection = ctx.selection;\n\n    const selectNodes = selection.selection.filter(\n      (_entity) => _entity instanceof FlowNodeEntity\n    ) as FlowNodeEntity[];\n\n    selectNodes\n      .map((_node) => [_node, ..._node.allCollapsedChildren])\n      .flat()\n      .forEach((node) => {\n        const renderData = node.getData(FlowNodeRenderData);\n\n        if (\n          node.firstChild &&\n          [FlowNodeBaseType.BLOCK_ICON, FlowNodeBaseType.BLOCK_ORDER_ICON].includes(\n            node.firstChild.flowNodeType as FlowNodeBaseType\n          )\n        ) {\n          node.collapsed = true;\n        }\n\n        renderData.expanded = false;\n      });\n  },\n});\n\nconst expand: ShortcutGetter = (ctx) => ({\n  commandId: FlowCommandId.EXPAND,\n  commandDetail: {\n    label: 'Expand',\n  },\n  shortcuts: ['meta alt closebracket', 'ctrol alt openbracket'],\n  isEnabled: () => !ctx.playground.config.readonlyOrDisabled,\n  execute: () => {\n    const selection = ctx.selection;\n\n    const selectNodes = selection.selection.filter(\n      (_entity) => _entity instanceof FlowNodeEntity\n    ) as FlowNodeEntity[];\n\n    selectNodes\n      .map((_node) => [_node, ..._node.allCollapsedChildren])\n      .flat()\n      .forEach((node) => {\n        const renderData = node.getData(FlowNodeRenderData);\n\n        if (\n          node.firstChild &&\n          [FlowNodeBaseType.BLOCK_ICON, FlowNodeBaseType.BLOCK_ORDER_ICON].includes(\n            node.firstChild.flowNodeType as FlowNodeBaseType\n          )\n        ) {\n          node.collapsed = false;\n        }\n\n        renderData.expanded = true;\n      });\n  },\n});\n\nexport const shortcutGetter: ShortcutGetter[] = [\n  copy,\n  cut,\n  selectAll,\n  cancelSelect,\n  zoomIn,\n  zoomOut,\n  resetZoom,\n  group,\n  collapse,\n  expand,\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/shortcuts/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ClipboardService } from '@flowgram.ai/fixed-layout-editor';\n\nexport const readData = async (clipboard: ClipboardService) => {\n  let str: string = '';\n  str = (await clipboard.readText()) || '';\n\n  try {\n    const data = JSON.parse(str);\n    return data;\n  } catch (error) {\n    return '';\n  }\n};\n\nexport const writeData = async (newData: any, clipboard: ClipboardService) => {\n  const data: any = newData;\n\n  const newStrData = JSON.stringify(data);\n\n  const oldSaveData = await navigator.clipboard.readText();\n\n  if (oldSaveData !== newStrData) {\n    if (navigator.clipboard && window.isSecureContext) {\n      await navigator.clipboard.writeText(newStrData);\n      const event = new Event('onchange');\n      (event as unknown as { value: string }).value = newStrData;\n      navigator.clipboard.dispatchEvent(event);\n    } else {\n      const textarea = document.createElement('textarea');\n      textarea.value = newStrData;\n\n      textarea.style.display = 'absolute';\n      textarea.style.left = '-99999999px';\n\n      document.body.prepend(textarea);\n\n      // highlight the content of the textarea element\n      textarea.select();\n\n      try {\n        document.execCommand('copy');\n      } catch (err) {\n        console.log(err);\n      } finally {\n        textarea.remove();\n      }\n    }\n\n    clipboard.writeText(newStrData);\n  }\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/type.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ndeclare module '*.svg'\ndeclare module '*.png'\ndeclare module '*.jpg'\ndeclare module '*.module.less'\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node';\nexport * from './json-schema';\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/typings/json-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IJsonSchema, JsonSchemaBasicType } from '@flowgram.ai/form-materials';\n\nexport type BasicType = JsonSchemaBasicType;\nexport type JsonSchema = IJsonSchema;\n"
  },
  {
    "path": "apps/demo-fixed-layout/src/typings/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowValue } from '@flowgram.ai/form-materials';\nimport {\n  FlowNodeJSON as FlowNodeJSONDefault,\n  FlowNodeRegistry as FlowNodeRegistryDefault,\n  FixedLayoutPluginContext,\n  FlowNodeEntity,\n  FlowNodeMeta as FlowNodeMetaDefault,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { type JsonSchema } from './json-schema';\n\n/**\n * You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node\n * 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出\n */\nexport interface FlowNodeJSON extends FlowNodeJSONDefault {\n  data: {\n    /**\n     * Node title\n     */\n    title?: string;\n    /**\n     * Inputs data values\n     */\n    inputsValues?: Record<string, IFlowValue>;\n    /**\n     * Define the inputs data of the node by JsonSchema\n     */\n    inputs?: JsonSchema;\n    /**\n     * Define the outputs data of the node by JsonSchema\n     */\n    outputs?: JsonSchema;\n    /**\n     * Rest properties\n     */\n    [key: string]: any;\n  };\n}\n\n/**\n * You can customize your own node meta\n * 你可以自定义节点的meta\n */\nexport interface FlowNodeMeta extends FlowNodeMetaDefault {\n  sidebarDisable?: boolean;\n  style?: React.CSSProperties;\n}\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport interface FlowNodeRegistry extends FlowNodeRegistryDefault {\n  meta?: FlowNodeMeta;\n  info: {\n    icon: string;\n    description: string;\n  };\n  canAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  canDelete?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  onAdd?: (ctx: FixedLayoutPluginContext, from: FlowNodeEntity) => FlowNodeJSON;\n}\n\nexport type FlowDocumentJSON = {\n  nodes: FlowNodeJSON[];\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ]\n  },\n  \"include\": [\n    \"./src\",\n  ],\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FixedLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-fixed-layout-animation\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/app.ts\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-semi-materials\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"classnames\": \"^2.5.1\",\n    \"styled-components\": \"^5\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@rsbuild/plugin-less\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\",\n    \"less\": \"^4.1.2\",\n    \"less-loader\": \"^6\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { pluginLess } from '@rsbuild/plugin-less';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n    /**\n     * support inversify @injectable() and @inject decorators\n     */\n    decorators: {\n      version: 'legacy',\n    },\n  },\n  html: {\n    title: 'demo-fixed-layout-animation',\n  },\n  tools: {\n    rspack: {\n      /**\n       * ignore warnings from @coze-editor/editor/language-typescript\n       */\n      ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { createRoot } from 'react-dom/client';\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useEditorProps } from '@/hooks/use-editor-props';\nimport { UpdateSchema } from '@/components/update-schema';\nimport { Tools } from '@/components/tools';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n      <Tools />\n      <UpdateSchema />\n    </FixedLayoutEditorProvider>\n  );\n};\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<FlowGramApp />);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/form-render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TitleField } from '@/fields/title-field';\nimport { ContentField } from '@/fields/content-field';\n\nexport const FormRender = () => (\n  <>\n    <TitleField />\n    <ContentField />\n  </>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/loading-dots/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.loading-dots {\n  display: flex;\n  gap: 4px;\n\n  .dot {\n    width: 6px;\n    height: 6px;\n    background: #3b82f6;\n    border-radius: 50%;\n    animation: bounce 1.4s ease-in-out infinite both;\n\n    &:nth-child(1) {\n      animation-delay: -0.32s;\n    }\n\n    &:nth-child(2) {\n      animation-delay: -0.16s;\n    }\n\n    &:nth-child(3) {\n      animation-delay: 0s;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/loading-dots/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.less';\n\nexport const LoadingDots = () => (\n  <div className=\"loading-dots\">\n    <span className=\"dot\"></span>\n    <span className=\"dot\"></span>\n    <span className=\"dot\"></span>\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/node-render/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-render {\n  background: #fff;\n  border: 1px solid rgba(6, 7, 9, 0.15);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  cursor: pointer;\n  padding: 16px;\n  background-color: #ffffff;\n  border-radius: 8px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n  width: auto;\n  min-width: 200px;\n\n  // Activated state - when node is selected/focused\n  &-activated {\n    border-color: #82a7fc;\n  }\n\n  // Dragging state - when node is being dragged\n  &-dragging {\n    opacity: 0.3;\n  }\n\n  // Block icon states - when showing order or regular block icons\n  &-block-icon,\n  &-block-order-icon {\n    width: 260px;\n  }\n\n  // Hover effects for better UX\n  &:hover {\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n  }\n\n  // Focus state for accessibility\n  &:focus-within {\n    outline: 2px solid #82a7fc;\n    outline-offset: 2px;\n  }\n\n  .node-form {\n    transition: opacity 1s ease-in-out;\n  }\n}\n\n.node-render-before-render {\n  max-height: 1px;\n\n  .node-form {\n    opacity: 0;\n  }\n}\n\n.node-render-rendered {\n  max-height: 1px;\n  animation: node-rendered-transition 1s ease-out forwards;\n\n  .node-form {\n    opacity: 1;\n  }\n}\n\n@keyframes node-rendered-transition {\n  0% {\n    max-height: 1px;\n  }\n\n  100% {\n    max-height: 500px;\n  }\n}\n\n.node-render-removed {\n  max-height: 500px;\n  animation: node-removed-transition 0.3s ease-out forwards;\n  overflow: hidden;\n  padding: 0 16px;\n  transition: opacity 0.3s ease-out;\n  opacity: 0;\n}\n\n@keyframes node-removed-transition {\n  0% {\n    max-height: 500px;\n    padding: 16px;\n  }\n\n  100% {\n    max-height: 1px;\n    padding: 0 16px;\n  }\n}\n\n.node-render-border-transition {\n  outline: 2px solid transparent;\n  animation: node-border-appear-hide 0.8s ease-in-out forwards;\n}\n\n@keyframes node-border-appear-hide {\n  0% {\n    outline: 2px solid transparent;\n  }\n\n  50% {\n    outline: 2px solid #82a7fc;\n  }\n\n  100% {\n    outline: 2px solid transparent;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/node-render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\nimport './index.less';\n\nimport classNames from 'classnames';\nimport { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useNodeStatus } from '@/hooks/use-node-loading';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const { onMouseEnter, onMouseLeave, form, dragging, isBlockOrderIcon, isBlockIcon, activated } =\n    useNodeRender();\n\n  const { className } = useNodeStatus();\n\n  return (\n    <div\n      className={classNames('node-render', className, {\n        'node-render-activated': activated,\n        'node-render-dragging': dragging,\n        'node-render-block-order-icon': isBlockOrderIcon,\n        'node-render-block-icon': isBlockIcon,\n      })}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n    >\n      <div className=\"node-form\">{form?.render()}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/thinking-node/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.thinking-node {\n  background: #fff;\n  border: 1px solid oklch(80.9% .105 251.813);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  cursor: pointer;\n  padding: 16px;\n  background-color: oklch(97% .014 254.604);\n  border-radius: 8px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n  width: auto;\n  min-width: 200px;\n\n  .node-form {\n    transition: opacity 1s ease-in-out;\n  }\n}\n\n.thinking-node-loading {\n  &::before {\n    content: '';\n    position: absolute;\n    top: -3px;\n    left: -3px;\n    right: -3px;\n    bottom: -3px;\n    background: linear-gradient(90deg,\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        rgba(59, 130, 246, 0.6),\n        rgba(96, 165, 250, 0.6),\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        transparent);\n    background-size: 400% 100%;\n    border-radius: 8px;\n    z-index: -1;\n    animation: flowing-border 5s linear infinite;\n    pointer-events: none;\n  }\n\n  &::after {\n    content: '';\n    position: absolute;\n    top: -3px;\n    left: -3px;\n    right: -3px;\n    bottom: -3px;\n    background: linear-gradient(90deg,\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        rgba(59, 130, 246, 0.08),\n        rgba(96, 165, 250, 0.08),\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        transparent,\n        transparent);\n    background-size: 400% 100%;\n    border-radius: 8px;\n    z-index: 1;\n    animation: flowing-border 5s linear infinite;\n    pointer-events: none;\n  }\n}\n\n@keyframes flowing-border {\n  0% {\n    background-position: 0% 0;\n  }\n\n  100% {\n    background-position: 400% 0;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/thinking-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport classNames from 'classnames';\nimport { useNodeRender } from '@flowgram.ai/fixed-layout-editor';\nimport './index.less';\n\nimport { useNodeStatus } from '@/hooks/use-node-loading';\n\nexport const ThinkingNode = () => {\n  const { form } = useNodeRender();\n  const { className } = useNodeStatus();\n  return (\n    <div className={classNames('thinking-node', 'thinking-node-loading', className)}>\n      <div className=\"node-form\">{form?.render()}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/tools/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nimport { Minimap } from './minimap';\n\nexport const Tools = () => {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  const buttonStyle: CSSProperties = {\n    border: '1px solid #e0e0e0',\n    borderRadius: '4px',\n    cursor: 'pointer',\n    padding: '4px',\n    color: '#141414',\n    background: '#e1e3e4',\n  };\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <>\n      <Minimap />\n      <div\n        style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}\n      >\n        <button style={buttonStyle} onClick={() => tools.zoomin()}>\n          ZoomIn\n        </button>\n        <button style={buttonStyle} onClick={() => tools.zoomout()}>\n          ZoomOut\n        </button>\n        <span\n          style={{\n            ...buttonStyle,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            cursor: 'default',\n            width: 40,\n          }}\n        >\n          {Math.floor(tools.zoom * 100)}%\n        </span>\n        <button style={buttonStyle} onClick={() => tools.fitView()}>\n          FitView\n        </button>\n        <button style={buttonStyle} onClick={() => tools.changeLayout()}>\n          ChangeLayout\n        </button>\n        <button\n          style={{\n            ...buttonStyle,\n            cursor: canUndo ? 'pointer' : 'not-allowed',\n            color: canUndo ? '#141414' : '#b1b1b1',\n          }}\n          onClick={() => history.undo()}\n          disabled={!canUndo}\n        >\n          Undo\n        </button>\n        <button\n          style={{\n            ...buttonStyle,\n            cursor: canRedo ? 'pointer' : 'not-allowed',\n            color: canRedo ? '#141414' : '#b1b1b1',\n          }}\n          onClick={() => history.redo()}\n          disabled={!canRedo}\n        >\n          Redo\n        </button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/tools/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 16,\n      bottom: 58,\n      zIndex: 100,\n      width: 218,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/update-schema/example-schemas.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nconst initSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: '开始',\n      },\n    },\n  ],\n};\n\nconst processStartSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: '开始',\n        content: '天气穿衣建议工作流',\n      },\n    },\n    {\n      id: 'thinking_0',\n      type: 'thinking',\n      data: {\n        text: '正在生成天气穿衣建议工作流...业务流程：1.进行输入处理 2.获取天气数据 3.生成穿衣建议 4.整理输出。我需要根据这些步骤来生成天气穿衣建议工作流核心节点...',\n      },\n    },\n  ],\n};\n\nconst addCoreNodesSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: '开始',\n        content: '天气穿衣建议工作流',\n      },\n    },\n    {\n      id: 'validate_input_0',\n      type: 'custom',\n      data: {\n        title: '输入处理节点',\n        content: '验证并清理城市名称输入 - validate_city_input()',\n      },\n    },\n    {\n      id: 'thinking_1',\n      type: 'thinking',\n      data: {\n        text: '正在生成错误检查节点与天气检查节点...',\n      },\n    },\n    {\n      id: 'fetch_weather_0',\n      type: 'custom',\n      data: {\n        title: '天气数据获取',\n        content: '调用wttr.in API获取天气信息 - fetch_weather_data()',\n      },\n    },\n    {\n      id: 'generate_suggestion_0',\n      type: 'custom',\n      data: {\n        title: '穿衣建议生成',\n        content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',\n      },\n    },\n    {\n      id: 'format_response_0',\n      type: 'custom',\n      data: {\n        title: '输出整理节点',\n        content: '格式化最终回答 - format_final_response()',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: '结束',\n        content: '返回格式化的穿衣建议',\n      },\n    },\n  ],\n};\n\nconst completeWorkflowLoadingSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: '开始',\n        content: '天气穿衣建议工作流',\n      },\n    },\n    {\n      id: 'validate_input_0',\n      type: 'custom',\n      data: {\n        title: '输入处理节点',\n        content: '验证并清理城市名称输入 - validate_city_input()',\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: '输入验证',\n        content: '检查输入验证是否有错误',\n      },\n      blocks: [\n        {\n          id: 'thinking_2',\n          type: 'thinking',\n          data: {\n            text: '天气数据获取节点生成中',\n          },\n        },\n        {\n          id: 'thinking_3',\n          type: 'thinking',\n          data: {\n            text: '格式化错误节点生成中',\n          },\n        },\n      ],\n    },\n    {\n      id: 'condition_1',\n      type: 'condition',\n      data: {\n        title: '天气数据检查',\n        content: '检查天气数据获取是否成功',\n      },\n      blocks: [\n        {\n          id: 'thinking_4',\n          type: 'thinking',\n          data: {\n            text: '穿衣建议生成节点生成中',\n          },\n        },\n        {\n          id: 'thinking_5',\n          type: 'thinking',\n          data: {\n            text: '格式化错误节点生成中',\n          },\n        },\n      ],\n    },\n    {\n      id: 'format_response_0',\n      type: 'custom',\n      data: {\n        title: '输出整理节点',\n        content: '格式化最终回答 - format_final_response()',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: '结束',\n        content: '返回格式化的穿衣建议',\n      },\n    },\n  ],\n};\n\nconst completeWorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: '开始',\n        content: '天气穿衣建议工作流',\n      },\n    },\n    {\n      id: 'validate_input_0',\n      type: 'custom',\n      data: {\n        title: '输入处理节点',\n        content: '验证并清理城市名称输入 - validate_city_input()',\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: '输入验证',\n        content: '检查输入验证是否有错误',\n      },\n      blocks: [\n        {\n          id: 'block_0',\n          type: 'block',\n          blocks: [\n            {\n              id: 'fetch_weather_0',\n              type: 'custom',\n              data: {\n                title: '天气数据获取',\n                content: '调用wttr.in API获取天气信息 - fetch_weather_data()',\n              },\n            },\n            {\n              id: 'format_data_0',\n              type: 'custom',\n              data: {\n                title: '格式化数据',\n                content: '天气数据提取并进行处理格式化',\n              },\n            },\n          ],\n        },\n        {\n          id: 'format_error_0',\n          type: 'custom',\n          data: {\n            title: '格式化错误',\n            content: '直接跳转到输出格式化',\n          },\n        },\n      ],\n    },\n    {\n      id: 'condition_1',\n      type: 'condition',\n      data: {\n        title: '天气数据检查',\n        content: '检查天气数据获取是否成功',\n      },\n      blocks: [\n        {\n          id: 'generate_suggestion_0',\n          type: 'custom',\n          data: {\n            title: '穿衣建议生成',\n            content: '基于天气数据生成穿衣建议 - generate_clothing_suggestion()',\n          },\n        },\n        {\n          id: 'format_error_1',\n          type: 'custom',\n          data: {\n            title: '格式化错误',\n            content: '跳转到输出格式化',\n          },\n        },\n      ],\n    },\n    {\n      id: 'format_response_0',\n      type: 'custom',\n      data: {\n        title: '输出整理节点',\n        content: '格式化最终回答 - format_final_response()',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: '结束',\n        content: '返回格式化的穿衣建议',\n      },\n    },\n  ],\n};\n\nexport const exampleSchemas: FlowDocumentJSON[] = [\n  initSchema,\n  processStartSchema,\n  addCoreNodesSchema,\n  completeWorkflowLoadingSchema,\n  completeWorkflowSchema,\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/update-schema/example.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nWeather-based Clothing Advisor using LangGraph\nA workflow that fetches weather data and provides clothing recommendations.\n\"\"\"\n\nimport json\nimport re\nimport requests\nfrom typing import Dict, Any, Optional, TypedDict\nfrom dataclasses import dataclass\nfrom langgraph.graph import Graph, StateGraph, END\nfrom langchain_core.messages import HumanMessage, SystemMessage\nfrom langchain_openai import ChatOpenAI\nimport os\n\n\n# State definition for the workflow\nclass WorkflowState(TypedDict):\n    \"\"\"State structure for the weather clothing advisor workflow\"\"\"\n    city_name: str\n    validated_city: str\n    weather_data: Dict[str, Any]\n    temperature: float\n    weather_condition: str\n    clothing_suggestion: str\n    final_response: str\n    error_message: Optional[str]\n\n\n@dataclass\nclass WeatherInfo:\n    \"\"\"Weather information data structure\"\"\"\n    temperature: float\n    condition: str\n    humidity: int\n    wind_speed: float\n    description: str\n\n\nclass WeatherClothingAdvisor:\n    \"\"\"Main class for the weather-based clothing advisor workflow\"\"\"\n\n    def __init__(self, openai_api_key: Optional[str] = None):\n        \"\"\"\n        Initialize the advisor with OpenAI API key\n\n        Args:\n            openai_api_key: OpenAI API key for LLM calls\n        \"\"\"\n        self.openai_api_key = openai_api_key or os.getenv(\"OPENAI_API_KEY\")\n        if self.openai_api_key:\n            self.llm = ChatOpenAI(\n                api_key=self.openai_api_key,\n                model=\"gpt-3.5-turbo\",\n                temperature=0.7\n            )\n        else:\n            self.llm = None\n            print(\"Warning: No OpenAI API key provided. Using rule-based suggestions.\")\n\n    def validate_city_input(self, state: WorkflowState) -> WorkflowState:\n        \"\"\"\n        Node 1: Input processing and validation\n        Validates and cleans the city name input\n\n        Args:\n            state: Current workflow state\n\n        Returns:\n            Updated state with validated city name\n        \"\"\"\n        city_name = state.get(\"city_name\", \"\").strip()\n\n        # Basic validation\n        if not city_name:\n            state[\"error_message\"] = \"城市名称不能为空\"\n            return state\n\n        # Remove special characters and normalize\n        validated_city = re.sub(r'[^\\w\\s-]', '', city_name)\n        validated_city = validated_city.strip()\n\n        if len(validated_city) < 2:\n            state[\"error_message\"] = \"请输入有效的城市名称\"\n            return state\n\n        state[\"validated_city\"] = validated_city\n        state[\"error_message\"] = None\n\n        print(f\"✓ 城市名称验证通过: {validated_city}\")\n        return state\n\n    def fetch_weather_data(self, state: WorkflowState) -> WorkflowState:\n        \"\"\"\n        Node 2: Weather data retrieval\n        Fetches weather information from wttr.in API\n\n        Args:\n            state: Current workflow state\n\n        Returns:\n            Updated state with weather data\n        \"\"\"\n        if state.get(\"error_message\"):\n            return state\n\n        validated_city = state[\"validated_city\"]\n\n        try:\n            # Use wttr.in API for weather data\n            url = f\"http://wttr.in/{validated_city}?format=j1\"\n            headers = {\n                'User-Agent': 'WeatherClothingAdvisor/1.0'\n            }\n\n            print(f\"🌤️  正在获取 {validated_city} 的天气数据...\")\n            response = requests.get(url, headers=headers, timeout=10)\n            response.raise_for_status()\n\n            weather_data = response.json()\n\n            # Extract current weather information\n            current_condition = weather_data[\"current_condition\"][0]\n            temperature_c = float(current_condition[\"temp_C\"])\n            weather_desc = current_condition[\"weatherDesc\"][0][\"value\"]\n            humidity = int(current_condition[\"humidity\"])\n            wind_speed = float(current_condition[\"windspeedKmph\"])\n\n            state[\"weather_data\"] = weather_data\n            state[\"temperature\"] = temperature_c\n            state[\"weather_condition\"] = weather_desc\n\n            weather_info = WeatherInfo(\n                temperature=temperature_c,\n                condition=weather_desc,\n                humidity=humidity,\n                wind_speed=wind_speed,\n                description=f\"{temperature_c}°C, {weather_desc}, 湿度 {humidity}%, 风速 {wind_speed}km/h\"\n            )\n\n            print(f\"✓ 天气数据获取成功: {weather_info.description}\")\n\n        except requests.exceptions.RequestException as e:\n            state[\"error_message\"] = f\"获取天气数据失败: {str(e)}\"\n            print(f\"❌ 天气数据获取失败: {str(e)}\")\n        except (KeyError, ValueError, IndexError) as e:\n            state[\"error_message\"] = f\"天气数据解析失败: {str(e)}\"\n            print(f\"❌ 天气数据解析失败: {str(e)}\")\n\n        return state\n\n    def generate_clothing_suggestion(self, state: WorkflowState) -> WorkflowState:\n        \"\"\"\n        Node 3: Clothing suggestion generation\n        Generates clothing recommendations based on weather data\n\n        Args:\n            state: Current workflow state\n\n        Returns:\n            Updated state with clothing suggestions\n        \"\"\"\n        if state.get(\"error_message\"):\n            return state\n\n        temperature = state[\"temperature\"]\n        weather_condition = state[\"weather_condition\"]\n        city_name = state[\"validated_city\"]\n\n        print(f\"🧥 正在生成穿衣建议...\")\n\n        if self.llm:\n            # Use LLM for intelligent suggestions\n            try:\n                prompt = f\"\"\"\n                作为一个专业的穿衣顾问，请根据以下天气信息为用户提供详细的穿衣建议：\n\n                城市：{city_name}\n                温度：{temperature}°C\n                天气状况：{weather_condition}\n\n                请提供：\n                1. 上身穿着建议\n                2. 下身穿着建议\n                3. 外套建议\n                4. 配饰建议（如帽子、围巾等）\n                5. 鞋子建议\n                6. 特别注意事项\n\n                请用简洁明了的中文回答，语气友好自然。\n                \"\"\"\n\n                messages = [\n                    SystemMessage(content=\"你是一个专业的穿衣顾问，擅长根据天气情况提供实用的穿衣建议。\"),\n                    HumanMessage(content=prompt)\n                ]\n\n                response = self.llm.invoke(messages)\n                state[\"clothing_suggestion\"] = response.content\n\n            except Exception as e:\n                print(f\"⚠️  LLM调用失败，使用规则建议: {str(e)}\")\n                state[\"clothing_suggestion\"] = self._get_rule_based_suggestion(temperature, weather_condition)\n        else:\n            # Use rule-based suggestions\n            state[\"clothing_suggestion\"] = self._get_rule_based_suggestion(temperature, weather_condition)\n\n        print(\"✓ 穿衣建议生成完成\")\n        return state\n\n    def _get_rule_based_suggestion(self, temperature: float, weather_condition: str) -> str:\n        \"\"\"\n        Generate rule-based clothing suggestions\n\n        Args:\n            temperature: Temperature in Celsius\n            weather_condition: Weather condition description\n\n        Returns:\n            Clothing suggestion string\n        \"\"\"\n        suggestions = []\n\n        # Temperature-based suggestions\n        if temperature < 0:\n            suggestions.append(\"🧥 上身：保暖内衣 + 毛衣 + 厚外套\")\n            suggestions.append(\"👖 下身：保暖裤 + 厚裤子\")\n            suggestions.append(\"🧤 配饰：帽子、围巾、手套必备\")\n        elif temperature < 10:\n            suggestions.append(\"🧥 上身：长袖衬衫 + 毛衣 + 外套\")\n            suggestions.append(\"👖 下身：长裤\")\n            suggestions.append(\"🧣 配饰：围巾、帽子\")\n        elif temperature < 20:\n            suggestions.append(\"👔 上身：长袖衬衫 + 薄外套\")\n            suggestions.append(\"👖 下身：长裤或牛仔裤\")\n            suggestions.append(\"🧢 配饰：可选择轻薄围巾\")\n        elif temperature < 25:\n            suggestions.append(\"👕 上身：长袖T恤或薄衬衫\")\n            suggestions.append(\"👖 下身：长裤或休闲裤\")\n        else:\n            suggestions.append(\"👕 上身：短袖T恤或薄衬衫\")\n            suggestions.append(\"🩳 下身：短裤或薄长裤\")\n            suggestions.append(\"🧴 注意：防晒和补水\")\n\n        # Weather condition adjustments\n        weather_lower = weather_condition.lower()\n        if any(word in weather_lower for word in ['rain', 'shower', '雨', '阵雨']):\n            suggestions.append(\"☔ 特别提醒：携带雨伞或穿防水外套\")\n        elif any(word in weather_lower for word in ['snow', '雪']):\n            suggestions.append(\"❄️ 特别提醒：穿防滑鞋，注意保暖\")\n        elif any(word in weather_lower for word in ['wind', '风']):\n            suggestions.append(\"💨 特别提醒：选择防风外套\")\n\n        # Shoe suggestions\n        if temperature < 5:\n            suggestions.append(\"👢 鞋子：保暖靴子或厚底鞋\")\n        elif temperature > 25:\n            suggestions.append(\"👟 鞋子：透气运动鞋或凉鞋\")\n        else:\n            suggestions.append(\"👟 鞋子：舒适的运动鞋或休闲鞋\")\n\n        return \"\\n\".join(suggestions)\n\n    def format_final_response(self, state: WorkflowState) -> WorkflowState:\n        \"\"\"\n        Node 4: Output formatting\n        Formats the final response with weather info and clothing suggestions\n\n        Args:\n            state: Current workflow state\n\n        Returns:\n            Updated state with formatted final response\n        \"\"\"\n        if state.get(\"error_message\"):\n            state[\"final_response\"] = f\"❌ 错误：{state['error_message']}\"\n            return state\n\n        city_name = state[\"validated_city\"]\n        temperature = state[\"temperature\"]\n        weather_condition = state[\"weather_condition\"]\n        clothing_suggestion = state[\"clothing_suggestion\"]\n\n        final_response = f\"\"\"\n🌍 {city_name} 天气穿衣建议\n\n📊 当前天气情况：\n• 温度：{temperature}°C\n• 天气：{weather_condition}\n\n👔 穿衣建议：\n{clothing_suggestion}\n\n💡 温馨提示：\n建议出门前再次确认天气变化，根据个人体感适当调整穿着。\n        \"\"\".strip()\n\n        state[\"final_response\"] = final_response\n        print(\"✓ 最终回答格式化完成\")\n\n        return state\n\n    def create_workflow(self) -> StateGraph:\n        \"\"\"\n        Create and configure the LangGraph workflow\n\n        Returns:\n            Configured StateGraph workflow\n        \"\"\"\n        # Create the graph\n        workflow = StateGraph(WorkflowState)\n\n        # Add nodes\n        workflow.add_node(\"validate_input\", self.validate_city_input)\n        workflow.add_node(\"fetch_weather\", self.fetch_weather_data)\n        workflow.add_node(\"generate_suggestion\", self.generate_clothing_suggestion)\n        workflow.add_node(\"format_response\", self.format_final_response)\n\n        # Define the flow\n        workflow.set_entry_point(\"validate_input\")\n\n        # Add conditional edges\n        workflow.add_conditional_edges(\n            \"validate_input\",\n            lambda state: \"fetch_weather\" if not state.get(\"error_message\") else \"format_response\"\n        )\n\n        workflow.add_conditional_edges(\n            \"fetch_weather\",\n            lambda state: \"generate_suggestion\" if not state.get(\"error_message\") else \"format_response\"\n        )\n\n        workflow.add_conditional_edges(\n            \"generate_suggestion\",\n            lambda state: \"format_response\" if not state.get(\"error_message\") else \"format_response\"\n        )\n\n        workflow.add_edge(\"format_response\", END)\n\n        return workflow.compile()\n\n    def get_clothing_advice(self, city_name: str) -> str:\n        \"\"\"\n        Main method to get clothing advice for a city\n\n        Args:\n            city_name: Name of the city to get weather and clothing advice for\n\n        Returns:\n            Formatted clothing advice string\n        \"\"\"\n        print(f\"🚀 开始为 '{city_name}' 生成穿衣建议...\")\n\n        # Create and run the workflow\n        workflow = self.create_workflow()\n\n        # Initial state\n        initial_state = WorkflowState(\n            city_name=city_name,\n            validated_city=\"\",\n            weather_data={},\n            temperature=0.0,\n            weather_condition=\"\",\n            clothing_suggestion=\"\",\n            final_response=\"\",\n            error_message=None\n        )\n\n        # Execute the workflow\n        result = workflow.invoke(initial_state)\n\n        return result[\"final_response\"]\n\n\ndef main():\n    \"\"\"Main function to demonstrate the weather clothing advisor\"\"\"\n    print(\"🌤️ 天气穿衣建议助手\")\n    print(\"=\" * 50)\n\n    # Initialize the advisor\n    advisor = WeatherClothingAdvisor()\n\n    # Example usage\n    cities = [\"北京\", \"上海\", \"广州\", \"深圳\"]\n\n    for city in cities:\n        print(f\"\\n{'='*20} {city} {'='*20}\")\n        try:\n            advice = advisor.get_clothing_advice(city)\n            print(advice)\n        except Exception as e:\n            print(f\"❌ 处理 {city} 时出错: {str(e)}\")\n        print(\"\\n\" + \"-\" * 60)\n\n    # Interactive mode\n    print(\"\\n🎯 交互模式 (输入 'quit' 退出)\")\n    while True:\n        try:\n            city_input = input(\"\\n请输入城市名称: \").strip()\n            if city_input.lower() in ['quit', 'exit', '退出', 'q']:\n                print(\"👋 再见！\")\n                break\n\n            if city_input:\n                advice = advisor.get_clothing_advice(city_input)\n                print(f\"\\n{advice}\")\n            else:\n                print(\"❌ 请输入有效的城市名称\")\n\n        except KeyboardInterrupt:\n            print(\"\\n👋 再见！\")\n            break\n        except Exception as e:\n            print(f\"❌ 出现错误: {str(e)}\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/update-schema/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.update-schema-button-container {\n  // Position and layout\n  position: absolute;\n  top: 50px;\n  right: 50px;\n  z-index: 100;\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.update-schema-button {\n  // Size and spacing\n  width: auto;\n  min-width: 120px;\n  height: 44px;\n  padding: 12px 24px;\n\n  // Typography\n  font-size: 14px;\n  font-weight: 600;\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n  color: #ffffff;\n\n  // Background and borders\n  background: #667eea;\n  border: none;\n  border-radius: 8px;\n\n  // Shadow and effects\n  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1);\n\n  // Interaction states\n  cursor: pointer;\n  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n\n  // Prevent text selection\n  user-select: none;\n  -webkit-user-select: none;\n\n  // Hover state - subtle enhancement\n  &:hover {\n    background: #5a6fd8;\n    box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5), 0 3px 6px rgba(0, 0, 0, 0.15);\n  }\n\n  // Active/Click state - gentle press effect\n  &:active {\n    background: #4c5bc4;\n    box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3), 0 1px 2px rgba(0, 0, 0, 0.1);\n  }\n\n  // Button content styling\n  .button-content {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/components/update-schema/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { FlowDocumentJSON, useService } from '@flowgram.ai/fixed-layout-editor';\n\nimport './index.less';\nimport { WorkflowLoadSchemaService } from '@/services';\n\nimport { exampleSchemas } from './example-schemas';\n\nexport const UpdateSchema = () => {\n  const loadSchemaService = useService(WorkflowLoadSchemaService);\n  const [currentSchemaIndex, setCurrentSchemaIndex] = useState<number>(0);\n\n  const handleUpdateSchema = (): void => {\n    const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];\n\n    // Update the document with current schema\n    loadSchemaService.load(currentSchema);\n\n    // Move to next schema index, cycle back to 0 when reaching the end\n    setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);\n  };\n\n  const handleForceUpdateSchema = (): void => {\n    const currentSchema: FlowDocumentJSON = exampleSchemas[currentSchemaIndex];\n\n    // Update the document with current schema\n    loadSchemaService.forceLoad(currentSchema);\n\n    // Move to next schema index, cycle back to 0 when reaching the end\n    setCurrentSchemaIndex((currentSchemaIndex + 1) % exampleSchemas.length);\n  };\n\n  return (\n    <div className=\"update-schema-button-container\">\n      <button onClick={handleUpdateSchema} className=\"update-schema-button\">\n        <span className=\"button-content\">{`更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>\n      </button>\n      <button onClick={handleForceUpdateSchema} className=\"update-schema-button\">\n        <span className=\"button-content\">{`强制更新 ${currentSchemaIndex}/${exampleSchemas.length}`}</span>\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/content-field/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.form-render-content {\n  font-size: 14px;\n  line-height: 1.6;\n  color: #666666;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/content-field/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\nimport './index.less';\n\nexport const ContentField = () => (\n  <Field<string> name=\"content\">\n    {({ field }) => <div className=\"form-render-content\">{field.value}</div>}\n  </Field>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.thinking-title {\n  font-size: 14px;\n  font-weight: 500;\n  color: #8d8d8d;\n}\n\n.thinking-text {\n  display: flex;\n  align-items: flex-start;\n  flex-direction: column;\n  gap: 4px;\n  border-radius: 8px;\n  line-height: 1.5;\n  font-size: 14px;\n\n  .thinking-content {\n    flex: 1;\n    word-break: break-word;\n    color: #8d8d8d;\n  }\n\n  .cursor {\n    font-weight: bold;\n    animation: blink 1s infinite;\n  }\n}\n\n@keyframes pulse {\n\n  0%,\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n\n  50% {\n    transform: scale(1.1);\n    opacity: 0.8;\n  }\n}\n\n@keyframes bounce {\n\n  0%,\n  80%,\n  100% {\n    transform: scale(0);\n    opacity: 0.5;\n  }\n\n  40% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n\n@keyframes blink {\n\n  0%,\n  50% {\n    opacity: 1;\n  }\n\n  51%,\n  100% {\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/thinking-text-field/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useEffect } from 'react';\n\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\nimport './index.less';\n\ninterface ThinkingTextProps {\n  thinking?: string;\n}\n\n// ThinkingText component with typewriter effect\nconst ThinkingText: React.FC<ThinkingTextProps> = ({ thinking }) => {\n  const [displayedText, setDisplayedText] = useState<string>('');\n  const [currentIndex, setCurrentIndex] = useState<number>(0);\n\n  // Reset animation when thinking text changes\n  useEffect(() => {\n    setDisplayedText('');\n    setCurrentIndex(0);\n  }, [thinking]);\n\n  // Typewriter effect for thinking text\n  useEffect(() => {\n    if (!thinking || currentIndex >= thinking.length) {\n      return;\n    }\n\n    const timer = setTimeout(() => {\n      setDisplayedText((prev: string) => prev + (thinking?.[currentIndex] || ''));\n      setCurrentIndex((prev: number) => prev + 1);\n    }, 50); // 50ms delay between each character\n\n    return () => clearTimeout(timer);\n  }, [thinking, currentIndex]);\n\n  if (!thinking) {\n    return null;\n  }\n\n  return (\n    <div className=\"thinking-text\">\n      <div className=\"thinking-title\">思考:</div>\n      <div>\n        <span className=\"thinking-content\">{displayedText}</span>\n        <span className=\"cursor\">\n          {currentIndex < (thinking?.length || 0) && <span className=\"cursor\">|</span>}\n        </span>\n      </div>\n    </div>\n  );\n};\n\nexport const ThinkingTextField = () => (\n  <Field<string> name=\"text\">{({ field }) => <ThinkingText thinking={field.value} />}</Field>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/title-field/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.form-render-title {\n  font-size: 18px;\n  font-weight: bold;\n  margin-bottom: 12px;\n  color: #333333;\n  display: flex;\n  gap: 8px;\n  justify-content: flex-start;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/fields/title-field/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useNodeStatus } from '@/hooks/use-node-loading';\nimport { LoadingDots } from '@/components/loading-dots';\nimport './index.less';\n\nexport const TitleField = () => {\n  const { loading } = useNodeStatus();\n  return (\n    <Field<string> name=\"title\">\n      {({ field }) => (\n        <div className=\"form-render-title\">\n          <span>{field.value}</span>\n          {loading && (\n            <span>\n              <LoadingDots />\n            </span>\n          )}\n        </div>\n      )}\n    </Field>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport { FixedLayoutProps, FlowRendererKey } from '@flowgram.ai/fixed-layout-editor';\n\nimport { WorkflowLoadSchemaService } from '@/services';\nimport { nodeRegistries } from '@/nodes';\nimport { ThinkingNode } from '@/components/thinking-node';\nimport { NodeRender } from '@/components/node-render';\nimport { FormRender } from '@/components/form-render';\n\nexport function useEditorProps(): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      plugins: () => [\n        createMinimapPlugin({\n          disableLayer: true,\n          enableDisplayAllNodes: true,\n          canvasStyle: {\n            canvasWidth: 200,\n            canvasHeight: 100,\n            canvasPadding: 50,\n          },\n        }),\n      ],\n      nodeRegistries,\n      initialData: {\n        nodes: [],\n      },\n      materials: {\n        renderDefaultNode: NodeRender,\n        components: {\n          ...defaultFixedSemiMaterials,\n          [FlowRendererKey.DRAG_NODE]: () => <></>,\n          [FlowRendererKey.BRANCH_ADDER]: () => <></>,\n          [FlowRendererKey.ADDER]: () => <></>,\n        },\n        renderNodes: {\n          ThinkingNode,\n        },\n      },\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n        }, 10);\n      },\n      onBind: ({ bind }) => {\n        bind(WorkflowLoadSchemaService).toSelf().inSingletonScope();\n      },\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: FormRender,\n          },\n        };\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n        onApply: (ctx) => {\n          if (ctx.document.disposed) return;\n          // Listen change to trigger auto save\n          // console.log('auto save: ', ctx.document.toJSON());\n        },\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */ nodeEngine: {\n        enable: true,\n      },\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/hooks/use-node-loading.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { FlowNodeFormData, FormModelV2, useNodeRender } from '@flowgram.ai/fixed-layout-editor';\n\ninterface NodeStatus {\n  loading: boolean;\n  className: string;\n}\n\nconst NodeStatusKey = 'status';\n\nexport const useNodeStatus = () => {\n  const { node } = useNodeRender();\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);\n\n  const [loading, setLoading] = useState(formStatus?.loading ?? false);\n  const [className, setClassName] = useState(formStatus?.className ?? '');\n\n  // 初始化表单值\n  useEffect(() => {\n    const initSize = formModel.getValueIn<{ width: number; height: number }>(NodeStatusKey);\n    if (!initSize) {\n      formModel.setValueIn(NodeStatusKey, {\n        loading: false,\n      });\n    }\n  }, [formModel]);\n\n  // 同步表单外部值变化：初始化/undo/redo/协同\n  useEffect(() => {\n    const disposer = formModel.onFormValuesChange(({ name }) => {\n      if (name !== NodeStatusKey && name !== '') {\n        return;\n      }\n      const newStatus = formModel.getValueIn<NodeStatus>(NodeStatusKey);\n      if (!newStatus) {\n        return;\n      }\n      setLoading(newStatus.loading);\n      setClassName(newStatus.className);\n    });\n    return () => disposer.dispose();\n  }, [formModel]);\n\n  return {\n    loading,\n    className,\n  };\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/nodes/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport {\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeJSON,\n  FlowNodeMeta,\n  FlowNodeRegistry,\n  FlowNodeSplitType,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const ConditionNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'condition',\n  extend: FlowNodeSplitType.DYNAMIC_SPLIT,\n  onBlockChildCreate(\n    originParent: FlowNodeEntity,\n    blockData: FlowNodeJSON,\n    addedNodes: FlowNodeEntity[] = [] // 新创建的节点都要存在这里\n  ) {\n    const { document } = originParent;\n    const parent = document.getNode(`$inlineBlocks$${originParent.id}`);\n    const blockNode = document.addNode(\n      {\n        id: `$block$${blockData.id}`,\n        type: FlowNodeBaseType.BLOCK,\n        parent,\n      },\n      addedNodes\n    );\n    const createdNode = document.addNode(\n      {\n        ...blockData,\n        type: blockData.type || FlowNodeBaseType.BLOCK,\n        parent: blockNode,\n      },\n      addedNodes\n    );\n    addedNodes.push(blockNode, createdNode);\n    return createdNode;\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/nodes/custom/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nexport const CustomNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'custom',\n  meta: {},\n  // onAdd() {\n  //   return {\n  //     id: `custom_${nanoid(5)}`,\n  //     type: 'custom',\n  //     data: {\n  //       title: 'Custom',\n  //       content: 'this is custom content',\n  //     },\n  //   };\n  // },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 202 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nimport { ThinkingNodeRegistry } from './thinking';\nimport { CustomNodeRegistry } from './custom';\nimport { ConditionNodeRegistry } from './condition';\n\nexport const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [\n  ConditionNodeRegistry,\n  CustomNodeRegistry,\n  ThinkingNodeRegistry,\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/nodes/thinking/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nimport { ThinkingTextField } from '@/fields/thinking-text-field';\nimport { LoadingDots } from '@/components/loading-dots';\n\nexport const ThinkingNodeRegistry: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'thinking',\n  meta: {\n    renderKey: 'ThinkingNode',\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <div\n          style={{\n            marginBottom: 16,\n          }}\n        >\n          <LoadingDots />\n        </div>\n        <ThinkingTextField />\n      </>\n    ),\n  },\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowLoadSchemaService } from './load-schema-service';\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/services/load-schema-service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  delay,\n  EntityManager,\n  FlowDocument,\n  FlowDocumentJSON,\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeFormData,\n  FlowOperationBaseService,\n  FormModelV2,\n  inject,\n  injectable,\n  Playground,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { WorkflowLoadSchemaUtils } from './utils';\nimport { SchemaPatch, SchemaPatchData } from './type';\n\n@injectable()\nexport class WorkflowLoadSchemaService {\n  @inject(FlowDocument) private document: FlowDocument;\n\n  @inject(EntityManager) private entityManager: EntityManager;\n\n  @inject(FlowOperationBaseService) private operationService: FlowOperationBaseService;\n\n  @inject(Playground) private playground: Playground;\n\n  private currentSchema: FlowDocumentJSON = {\n    nodes: [],\n  };\n\n  // constructor() {\n  //   (window as any).WorkflowLoadSchemaService = this;\n  // }\n\n  public async load(schema: FlowDocumentJSON): Promise<void> {\n    const schemaPatch: SchemaPatch = WorkflowLoadSchemaUtils.createSchemaPatch(\n      this.currentSchema,\n      schema\n    );\n    this.currentSchema = schema;\n    await this.applySchemaPatch(schemaPatch);\n    this.document.fromJSON(schema);\n  }\n\n  public forceLoad(schema: FlowDocumentJSON): void {\n    this.currentSchema = schema;\n    this.document.fromJSON(schema);\n  }\n\n  private async applySchemaPatch(schemaPatch: SchemaPatch): Promise<void> {\n    await this.applyRemovePatch(schemaPatch.remove);\n    await delay(300);\n    await this.applyCreatePatch(schemaPatch.create);\n    await this.playground.config.fitView(this.document.root.bounds.pad(30));\n  }\n\n  private async applyCreatePatch(createSchemaPatchData: SchemaPatchData[]): Promise<void> {\n    const skipNodeIDs: Set<string> = new Set();\n    for (const nodePatchData of createSchemaPatchData) {\n      // 跳过 block 节点\n      if (skipNodeIDs.has(nodePatchData.nodeID)) {\n        continue;\n      }\n      const parentNode = this.getNode(nodePatchData.parentID);\n      // 特殊处理 condition 节点\n      if (parentNode?.flowNodeType === 'condition') {\n        const blocksSchema = createSchemaPatchData\n          .filter((item) => item.parentID === parentNode.id)\n          .map((item) => {\n            skipNodeIDs.add(item.nodeID);\n            return item.schema;\n          });\n        const blocks = this.document.addInlineBlocks(parentNode, blocksSchema);\n        await Promise.all(blocks.map((block) => this.createNodeMotion(block)));\n        continue;\n      }\n      // 更新节点数据\n      const isExist = Boolean(this.getNode(nodePatchData.nodeID));\n      const node = this.createNode(nodePatchData);\n      if (!isExist) {\n        // 新增节点动画\n        await this.createNodeMotion(node);\n      }\n    }\n  }\n\n  private createNode(patchData: SchemaPatchData): FlowNodeEntity {\n    const parent = this.getNode(patchData.parentID) ?? this.document.root;\n    if (parent?.flowNodeType === 'condition') {\n      // 特殊处理 condition 节点\n      const blocks = this.document.addInlineBlocks(parent, [patchData.schema]);\n      return blocks.find((block) => block.flowNodeType === patchData.schema.type) ?? blocks[0];\n    } else if (patchData.fromNodeID) {\n      return this.operationService.addFromNode(patchData.fromNodeID, patchData.schema);\n    } else {\n      return this.document.addNode({\n        ...patchData.schema,\n        parent,\n      });\n    }\n  }\n\n  private getNode(id?: string): FlowNodeEntity | undefined {\n    if (!id) {\n      return undefined;\n    }\n    return this.document.getNode(id);\n  }\n\n  private async createNodeMotion(node: FlowNodeEntity): Promise<void> {\n    // 隐藏节点\n    this.setNodeStatus(node, { loading: true, className: 'node-render-before-render' });\n    this.document.fireRender();\n    await delay(20);\n    // 展示节点动画\n    this.setNodeStatus(node, { loading: true, className: 'node-render-rendered' });\n    await delay(180);\n    // 滚动到节点位置\n    this.playground.scrollToView({\n      bounds: node.bounds,\n      scrollToCenter: true,\n    });\n    // 高亮节点边框\n    this.setNodeStatus(node, { loading: true, className: 'node-render-border-transition' });\n    await delay(800);\n    // 移除节点边框高亮\n    this.setNodeStatus(node, { loading: false, className: '' });\n  }\n\n  private async removeNodeMotion(node: FlowNodeEntity): Promise<void> {\n    // 隐藏节点\n    this.setNodeStatus(node, { loading: false, className: 'node-render-removed' });\n    this.document.fireRender();\n    await delay(300);\n  }\n\n  private async applyRemovePatch(removeNodeIDs: string[]): Promise<void> {\n    await Promise.all(\n      removeNodeIDs.map(async (nodeID) => {\n        const node = this.entityManager.getEntityById<FlowNodeEntity>(nodeID);\n        const parent = node?.parent;\n        if (node) {\n          await this.removeNodeMotion(node);\n          node.dispose();\n        }\n        if (parent?.flowNodeType === FlowNodeBaseType.BLOCK && !parent.blocks.length) {\n          parent.dispose();\n        }\n      })\n    );\n  }\n\n  private setNodeStatus(\n    node: FlowNodeEntity,\n    status: {\n      loading: boolean;\n      className: string;\n    }\n  ): void {\n    const formModel = node.getData(FlowNodeFormData)?.getFormModel<FormModelV2>();\n    formModel?.setValueIn('status', status);\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/services/load-schema-service/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport interface SchemaPatchData {\n  nodeID: string;\n  schema: FlowNodeJSON;\n  parentID?: string;\n  index?: number;\n  fromNodeID?: string;\n}\n\nexport interface SchemaPatch {\n  create: SchemaPatchData[];\n  remove: string[];\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/src/services/load-schema-service/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON, FlowNodeJSON } from '@flowgram.ai/fixed-layout-editor';\n\nimport { SchemaPatch, SchemaPatchData } from './type';\n\nexport namespace WorkflowLoadSchemaUtils {\n  const createSchemaPatchDataMap = (params: {\n    nodeSchemas: FlowNodeJSON[];\n    parentID?: string;\n    schemaPatchDataMap?: Map<string, SchemaPatchData>;\n  }): Map<string, SchemaPatchData> => {\n    const { nodeSchemas, parentID, schemaPatchDataMap = new Map() } = params;\n    nodeSchemas.forEach((nodeSchema: FlowNodeJSON, index: number) => {\n      const prevNodeSchema = nodeSchemas[index - 1];\n      const processedSchema: FlowNodeJSON = {\n        ...nodeSchema,\n        blocks: [],\n      };\n      const schemaPatchData: SchemaPatchData = {\n        nodeID: nodeSchema.id,\n        schema: processedSchema,\n        parentID,\n        index,\n        fromNodeID: prevNodeSchema?.id,\n      };\n      schemaPatchDataMap.set(nodeSchema.id, schemaPatchData);\n      if (nodeSchema.blocks) {\n        createSchemaPatchDataMap({\n          nodeSchemas: nodeSchema.blocks,\n          parentID: nodeSchema.id,\n          schemaPatchDataMap,\n        });\n      }\n    });\n    return schemaPatchDataMap;\n  };\n\n  export const createSchemaPatch = (\n    prevSchema: FlowDocumentJSON,\n    schema: FlowDocumentJSON\n  ): SchemaPatch => {\n    const prevSchemaPatchDataMap = createSchemaPatchDataMap({\n      nodeSchemas: prevSchema.nodes,\n    });\n    const currentSchemaPatchDataMap = createSchemaPatchDataMap({\n      nodeSchemas: schema.nodes,\n    });\n    const prevNodeIDs: string[] = Array.from(prevSchemaPatchDataMap.keys());\n    const currentNodeIDs: string[] = Array.from(currentSchemaPatchDataMap.keys());\n\n    const createNodeIDs: string[] = currentNodeIDs.filter((id) => {\n      if (!prevSchemaPatchDataMap.has(id)) {\n        return true;\n      }\n      const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;\n      const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;\n      return (\n        prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||\n        prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID\n      );\n    });\n\n    const removeNodeIDs: string[] = prevNodeIDs.filter((id) => {\n      if (!currentSchemaPatchDataMap.has(id)) {\n        return true;\n      }\n      const prevSchemaPatchData = prevSchemaPatchDataMap.get(id)!;\n      const currentSchemaPatchData = currentSchemaPatchDataMap.get(id)!;\n      return (\n        prevSchemaPatchData.parentID !== currentSchemaPatchData.parentID ||\n        prevSchemaPatchData.fromNodeID !== currentSchemaPatchData.fromNodeID\n      );\n    });\n\n    const createSchemaPatches: SchemaPatchData[] = createNodeIDs\n      .map((id) => currentSchemaPatchDataMap.get(id)!)\n      .filter(Boolean);\n\n    const schemaPatch: SchemaPatch = {\n      create: createSchemaPatches,\n      remove: removeNodeIDs,\n    };\n\n    console.log('@debug schemaPatch', schemaPatch);\n\n    return schemaPatch;\n  };\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-animation/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ],\n    }\n  },\n  \"include\": [\n    \"./src\"\n  ],\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FixedLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-fixed-layout-simple\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.ts\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-semi-materials\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"nanoid\": \"^5.0.9\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n  },\n  html: {\n    title: 'demo-fixed-layout-simple',\n  },\n});\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { Editor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/base-node.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';\nimport { IconDeleteStroked } from '@douyinfe/semi-icons';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  const ctx = useClientContext();\n  /**\n   * Provides methods related to node rendering\n   * 提供节点渲染相关的方法\n   */\n  const nodeRender = useNodeRender();\n  /**\n   * It can only be used when nodeEngine is enabled\n   * 只有在节点引擎开启时候才能使用表单\n   */\n  const form = nodeRender.form;\n\n  return (\n    <div\n      className=\"demo-fixed-node\"\n      /*\n       * onMouseEnter is added to a fixed layout node primarily to listen for hover highlighting of branch lines\n       * onMouseEnter 加到固定布局节点主要是为了监听 分支线条的 hover 高亮\n       **/\n      onMouseEnter={nodeRender.onMouseEnter}\n      onMouseLeave={nodeRender.onMouseLeave}\n      onMouseDown={(e) => {\n        // trigger drag node\n        nodeRender.startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        /**\n         * Lets you precisely control the style of branch nodes\n         * 用于精确控制分支节点的样式\n         * isBlockIcon: 整个 condition 分支的 头部节点\n         * isBlockOrderIcon: 分支的第一个节点\n         */\n        opacity: nodeRender.dragging ? 0.3 : 1,\n        ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {!nodeRender.readonly && (\n        <IconDeleteStroked\n          style={{ position: 'absolute', right: 4, top: 4 }}\n          onClick={() => ctx.operation.deleteNode(nodeRender.node)}\n        />\n      )}\n      {form?.render()}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/branch-adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\ninterface PropsType {\n  activated?: boolean;\n  node: FlowNodeEntity;\n}\n\nexport function BranchAdder(props: PropsType) {\n  const { activated, node } = props;\n  const nodeData = node.firstChild!.renderData;\n  const ctx = useClientContext();\n  const { operation, playground } = ctx;\n  const { isVertical } = node;\n\n  function addBranch() {\n    let block: FlowNodeEntity;\n    if (node.flowNodeType === 'multiOutputs') {\n      block = operation.addBlock(node, {\n        id: `output_${nanoid(5)}`,\n        type: 'output',\n        data: {\n          title: 'New Ouput',\n          content: '',\n        },\n      });\n    } else if (node.flowNodeType === 'multiInputs') {\n      block = operation.addBlock(node, {\n        id: `input_${nanoid(5)}`,\n        type: 'input',\n        data: {\n          title: 'New Input',\n          content: '',\n        },\n      });\n    } else {\n      block = operation.addBlock(node, {\n        id: `branch_${nanoid(5)}`,\n        type: 'block',\n        data: {\n          title: 'New Branch',\n          content: '',\n        },\n      });\n    }\n\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n  }\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  const className = [\n    'demo-fixed-adder',\n    isVertical ? '' : 'isHorizontal',\n    activated ? 'activated' : '',\n  ].join(' ');\n\n  return (\n    <div\n      className={className}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <div\n        onClick={() => {\n          addBranch();\n        }}\n        aria-hidden=\"true\"\n        style={{ flexGrow: 1, textAlign: 'center' }}\n      >\n        <IconPlus />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/flow-select.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { useClientContext, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FLOW_LIST } from '../data';\n\nconst url = new window.URL(window.location.href);\n\nexport function FlowSelect() {\n  const [demoKey, updateDemoKey] = useState(url.searchParams.get('demo') ?? 'condition');\n  const clientContext = useClientContext();\n  useEffect(() => {\n    const targetDemoJSON = FLOW_LIST[demoKey];\n    if (targetDemoJSON) {\n      clientContext.history.stop(); // Stop redo/undo\n      clientContext.history.clear(); // Clear redo/undo\n      clientContext.document.fromJSON(targetDemoJSON); // Reload Data\n      console.log(clientContext.document.toString(true)); // Print the document tree\n      clientContext.history.start(); // Restart redo/undo\n      clientContext.document.setLayout(\n        targetDemoJSON.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT\n      );\n      // Update URL\n      if (url.searchParams.get('demo')) {\n        url.searchParams.set('demo', demoKey);\n        window.history.pushState({}, '', `/?${url.searchParams.toString()}`);\n      }\n      // Fit View\n      setTimeout(() => {\n        clientContext.playground.config.fitView(clientContext.document.root.bounds, true, 40);\n      }, 20);\n    }\n  }, [demoKey]);\n  return (\n    <div style={{ position: 'absolute', zIndex: 100 }}>\n      <label style={{ marginRight: 12 }}>Select Demo:</label>\n      <select\n        style={{ width: '180px', height: '32px', fontSize: 16 }}\n        onChange={(e) => updateDemoKey(e.target.value)}\n        value={demoKey}\n      >\n        {Object.keys(FLOW_LIST).map((key) => (\n          <option key={key} value={key}>\n            {key}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 16,\n      bottom: 51,\n      zIndex: 100,\n      width: 198,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/node-add-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { useStartDragNode } from '@flowgram.ai/fixed-layout-editor';\n\nimport { nodeRegistries } from '../node-registries';\nimport { useAddNode } from '../hooks/use-add-node';\n\nexport const NodeAddPanel: React.FC = (props) => {\n  const { startDrag } = useStartDragNode();\n  const { handleAdd, handleAddBranch } = useAddNode();\n\n  return (\n    <div className=\"demo-fixed-sidebar\">\n      {nodeRegistries.map((registry) => {\n        const nodeType = registry.type;\n        return (\n          <div\n            key={nodeType}\n            className=\"demo-fixed-card\"\n            onMouseDown={(e) => {\n              e.stopPropagation();\n              const nodeAddData = registry.onAdd();\n              return startDrag(\n                e,\n                {\n                  dragJSON: nodeAddData,\n                  onCreateNode: async (json, dropNode) => handleAdd(json, dropNode),\n                },\n                {\n                  disableDragScroll: true,\n                }\n              );\n            }}\n          >\n            {nodeType}\n          </div>\n        );\n      })}\n      <div\n        key={'branch'}\n        className=\"demo-fixed-card\"\n        onMouseDown={(e) => {\n          e.stopPropagation();\n          return startDrag(\n            e,\n            {\n              dragJSON: {\n                id: `branch_${nanoid(5)}`,\n                type: 'block',\n                data: {\n                  title: 'New Branch',\n                  content: '',\n                },\n              },\n              isBranch: true,\n              onCreateNode: async (json, dropNode) => handleAddBranch(json, dropNode),\n            },\n            {\n              disableDragScroll: true,\n            }\n          );\n        }}\n      >\n        branch\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/node-adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, useClientContext, usePlayground } from '@flowgram.ai/fixed-layout-editor';\nimport { Dropdown } from '@douyinfe/semi-ui';\nimport { IconPlusCircle } from '@douyinfe/semi-icons';\n\nimport { nodeRegistries } from '../node-registries';\nimport { useAddNode } from '../hooks/use-add-node';\n\nexport const NodeAdder = (props: {\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}) => {\n  const { from, hoverActivated } = props;\n  const playground = usePlayground();\n  const context = useClientContext();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <Dropdown\n      render={\n        <Dropdown.Menu>\n          {nodeRegistries.map((registry) => (\n            <Dropdown.Item\n              key={registry.type}\n              onClick={() => {\n                const props = registry?.onAdd(context, from);\n                handleAdd(props, from);\n              }}\n            >\n              {registry.type}\n            </Dropdown.Item>\n          ))}\n        </Dropdown.Menu>\n      }\n    >\n      <div\n        style={{\n          width: hoverActivated ? 15 : 6,\n          height: hoverActivated ? 15 : 6,\n          backgroundColor: 'rgb(143, 149, 158)',\n          color: '#fff',\n          borderRadius: '50%',\n          cursor: 'pointer',\n        }}\n      >\n        {hoverActivated ? (\n          <IconPlusCircle\n            style={{\n              color: '#3370ff',\n              backgroundColor: '#fff',\n              borderRadius: 15,\n            }}\n          />\n        ) : null}\n      </div>\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/slot-adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowDocument,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\ninterface PropsType {\n  node: FlowNodeEntity;\n}\n\nexport function SlotAdder(props: PropsType) {\n  const { node } = props;\n\n  const nodeData = node.firstChild?.getData<FlowNodeRenderData>(FlowNodeRenderData);\n  const document = useService(FlowDocument) as FlowDocument;\n\n  async function addPort() {\n    document.addNode({\n      id: nanoid(5),\n      type: 'custom',\n      parent: node,\n      data: {\n        title: 'Custom',\n        content: 'custom content',\n      },\n    });\n  }\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        background: 'var(--semi-color-bg-0)',\n      }}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <Button\n        onClick={() => {\n          addPort();\n        }}\n        size=\"small\"\n        icon={<IconPlus />}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState, useCallback } from 'react';\n\nimport { usePlaygroundTools, useClientContext, useRefresh } from '@flowgram.ai/fixed-layout-editor';\nimport { IconButton, Space } from '@douyinfe/semi-ui';\nimport { IconUnlock, IconLock } from '@douyinfe/semi-icons';\n\nexport function Tools() {\n  const { history, playground } = useClientContext();\n  const tools = usePlaygroundTools();\n  const refresh = useRefresh();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n  const toggleReadonly = useCallback(() => {\n    playground.config.readonly = !playground.config.readonly;\n  }, [playground]);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());\n    return () => disposable.dispose();\n  }, [playground]);\n\n  return (\n    <Space\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button onClick={() => tools.changeLayout()}>ChangeLayout</button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      {playground.config.readonly ? (\n        <IconButton\n          theme=\"borderless\"\n          type=\"tertiary\"\n          icon={<IconLock />}\n          onClick={toggleReadonly}\n        />\n      ) : (\n        <IconButton\n          theme=\"borderless\"\n          type=\"tertiary\"\n          icon={<IconUnlock />}\n          onClick={toggleReadonly}\n        />\n      )}\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </Space>\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/condition.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const condition: FlowDocumentJSON = {\n  nodes: [\n    // 开始节点\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content',\n      },\n      blocks: [],\n    },\n    // 分支节点\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: 'Condition',\n        content: 'condition content',\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'custom_0',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'break_0',\n              type: 'break',\n              data: {\n                title: 'Break',\n                content: 'Break content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_2',\n          type: 'block',\n          data: {\n            title: 'Branch 2',\n            content: 'branch 2 content',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    // 结束节点\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/dynamicSplit.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const dynamicSplit: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n      },\n      blocks: [],\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      data: {\n        title: 'DynamicSplit',\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n          },\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON, FlowLayoutDefault } from '@flowgram.ai/fixed-layout-editor';\n\nimport { tryCatch } from './tryCatch';\nimport { slot } from './slot';\nimport { multiOutputs } from './multiOutputs';\nimport { multiInputs } from './multiInputs';\nimport { mindmap } from './mindmap';\nimport { loop } from './loop';\nimport { dynamicSplit } from './dynamicSplit';\nimport { condition } from './condition';\n\nexport const FLOW_LIST: Record<string, FlowDocumentJSON & { defaultLayout?: FlowLayoutDefault }> = {\n  condition,\n  mindmap: { ...mindmap, defaultLayout: FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT },\n  tryCatch,\n  dynamicSplit,\n  loop,\n  multiInputs,\n  multiOutputs,\n  slot,\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/loop.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const loop: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n      },\n      blocks: [],\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      data: {\n        title: 'Loop',\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n          },\n          blocks: [\n            {\n              id: 'custom',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n              },\n            },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/mindmap.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const mindmap: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'multiInputs_0',\n      type: 'multiInputs',\n      blocks: [\n        {\n          id: 'input_0',\n          type: 'input',\n          data: {\n            title: 'input_0',\n          },\n        },\n        {\n          id: 'input_1',\n          type: 'input',\n          data: {\n            title: 'input_1',\n          },\n        },\n        {\n          id: 'input_3',\n          type: 'input',\n          data: {\n            title: 'input_3',\n          },\n        },\n      ],\n    },\n    {\n      id: 'multiOutputs_0',\n      type: 'multiOutputs',\n      data: {\n        title: 'mindNode_0',\n      },\n      blocks: [\n        {\n          id: 'output_0',\n          type: 'output',\n          data: {\n            title: 'output_0',\n          },\n        },\n        {\n          id: 'multiOutputs_1',\n          type: 'multiOutputs',\n          data: {\n            title: 'mindNode_1',\n          },\n          blocks: [\n            {\n              id: 'output_1',\n              type: 'output',\n              data: {\n                title: 'output_1',\n              },\n            },\n            {\n              id: 'output_2',\n              type: 'output',\n              data: {\n                title: 'output_2',\n              },\n            },\n            {\n              id: 'output_3',\n              type: 'output',\n              data: {\n                title: 'output_3',\n              },\n            },\n          ],\n        },\n        {\n          id: 'output_4',\n          type: 'output',\n          data: {\n            title: 'output_4',\n          },\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/multiInputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const multiInputs: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'multiInputs_0',\n      type: 'multiInputs',\n      blocks: [\n        {\n          id: 'input_0',\n          type: 'input',\n          data: {\n            title: 'input_0',\n          },\n        },\n        {\n          id: 'input_1',\n          type: 'input',\n          data: {\n            title: 'input_1',\n          },\n        },\n        {\n          id: 'input_3',\n          type: 'input',\n          data: {\n            title: 'input_3',\n          },\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/multiOutputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const multiOutputs: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n      },\n      blocks: [],\n    },\n    {\n      id: 'multiOutputs_0',\n      type: 'multiOutputs',\n      data: {\n        title: 'MultiOutputs',\n      },\n      blocks: [\n        {\n          id: 'output_0',\n          type: 'output',\n          data: {\n            title: 'output_0',\n          },\n        },\n        {\n          id: 'output_1',\n          type: 'output',\n          data: {\n            title: 'output_1',\n          },\n        },\n        {\n          id: 'output_2',\n          type: 'output',\n          data: {\n            title: 'output_2',\n          },\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/slot.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const slot: FlowDocumentJSON = {\n  nodes: [\n    // 开始节点\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content',\n      },\n      blocks: [],\n    },\n    {\n      id: 'slot_0',\n      type: 'slot',\n      data: {\n        title: 'Slot',\n        content: 'Slot content',\n      },\n      blocks: [\n        {\n          id: 'slot_port_1',\n          type: 'slotBlock',\n          data: {\n            title: 'Slot 1',\n            content: 'slot 1 content',\n          },\n          blocks: [\n            {\n              id: 'custom_1',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'slot_port_2',\n          type: 'slotBlock',\n          data: {\n            title: 'Slot 2',\n            content: 'slot 2 content',\n          },\n          blocks: [\n            {\n              id: 'custom_2',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'slot_port_3',\n          type: 'slotBlock',\n          data: {\n            title: 'Slot 3',\n            content: 'slot 3 content',\n          },\n          blocks: [\n            {\n              id: 'custom_3',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n            {\n              id: 'custom_4',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n      ],\n    },\n    // 结束节点\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/data/tryCatch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const tryCatch: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n      },\n      blocks: [],\n    },\n    {\n      id: 'tryCatch_0',\n      type: 'tryCatch',\n      data: {\n        title: 'TryCatch',\n      },\n      blocks: [\n        {\n          id: 'tryBlock_0',\n          type: 'tryBlock',\n          blocks: [],\n        },\n        {\n          id: 'catchBlock_0',\n          type: 'catchBlock',\n          data: {\n            title: 'Catch Block 1',\n          },\n          blocks: [],\n        },\n        {\n          id: 'catchBlock_1',\n          type: 'catchBlock',\n          data: {\n            title: 'Catch Block 2',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\nimport './index.css';\n\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { FLOW_LIST } from './data';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport { FlowSelect } from './components/flow-select';\n\nexport const Editor = (props: { demo?: string; hideTools?: boolean }) => {\n  const editorProps = useEditorProps(\n    props.demo ? FLOW_LIST[props.demo] : initialData,\n    nodeRegistries\n  );\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-fixed-container\">\n        <div className=\"demo-fixed-layout\">\n          {!props.hideTools ? <NodeAddPanel /> : null}\n          <EditorRenderer className=\"demo-fixed-editor\">\n            {/* add child panel here */}\n          </EditorRenderer>\n        </div>\n      </div>\n      {!props.hideTools ? (\n        <>\n          <Tools />\n          <FlowSelect />\n          <Minimap />\n        </>\n      ) : null}\n    </FixedLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/hooks/use-add-node.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeEntity,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport {\n  Field,\n  type FixedLayoutProps,\n  FlowDocumentJSON,\n  FlowNodeRegistry,\n  FlowRendererKey,\n  FlowTextKey,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { SlotAdder } from '../components/slot-adder';\nimport { NodeAdder } from '../components/node-adder';\nimport { BranchAdder } from '../components/branch-adder';\nimport { BaseNode } from '../components/base-node';\n\n/** semi materials */\n\nexport function useEditorProps(\n  initialData: FlowDocumentJSON, // 初始化数据\n  nodeRegistries: FlowNodeRegistry[] // 节点定义\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * 画布节点定义\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-fixed-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-fixed-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      /**\n       * Materials, components can be customized based on the key\n       * @see https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx\n       * 可以通过 key 自定义 UI 组件\n       */\n      materials: {\n        components: {\n          ...defaultFixedSemiMaterials,\n          /**\n           * Components can be customized based on key business-side requirements.\n           * 这里可以根据 key 业务侧定制组件\n           */\n          [FlowRendererKey.ADDER]: NodeAdder,\n          [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n          [FlowRendererKey.SLOT_ADDER]: SlotAdder,\n          // [FlowRendererKey.DRAG_NODE]: DragNode,\n        },\n        renderDefaultNode: BaseNode, // 节点渲染\n        renderTexts: {\n          [FlowTextKey.LOOP_END_TEXT]: 'loop end',\n          [FlowTextKey.LOOP_TRAVERSE_TEXT]: 'looping',\n        },\n      },\n      /**\n       * Drag/Drop config\n       */\n      dragdrop: {\n        /**\n         * Callback when drag drop\n         */\n        onDrop: (ctx, dropData) => {\n          // console.log(\n          //   '>>> onDrop: ',\n          //   dropData.dropNode.id,\n          //   dropData.dragNodes.map(n => n.id),\n          // );\n        },\n        canDrop: (ctx, dropData) => {\n          // dropData.dragjson\n          console.log('>>> canDrop: ', dropData.isBranch, dropData.dropNode.id, dropData.dragNodes);\n          return true;\n        },\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */\n      nodeEngine: {\n        enable: true,\n      },\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n        onApply(ctx, opt) {\n          // Listen change to trigger auto save\n          console.log('auto save: ', ctx.document.toJSON(), opt);\n        },\n      },\n      /**\n       * Playground init\n       * 画布初始化\n       */\n      onInit: (ctx) => {\n        /**\n         * Data can also be dynamically loaded via fromJSON\n         * 也可以通过 fromJSON 动态加载数据\n         */\n        // ctx.document.fromJSON(initialData)\n        console.log('---- Playground Init ----');\n      },\n      /**\n       * Playground render\n       */\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n        }, 10);\n      },\n      /**\n       * Playground dispose\n       * 画布销毁\n       */\n      onDispose: () => {\n        console.log('---- Playground Dispose ----');\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.fromJSON 调用\n       * Node data transformation, called by ctx.document.fromJSON\n       * @param node\n       * @param json\n       */\n      fromNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.toJSON 调用\n       * Node data transformation, called by ctx.document.toJSON\n       * @param node\n       * @param json\n       */\n      toNodeJSON(node, json) {\n        return json;\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          enableDisplayAllNodes: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n      ],\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-fixed-node {\n  align-items: flex-start;\n  background-color: #fff;\n  border: 1px solid rgba(6, 7, 9, 0.15);\n  border-radius: 8px;\n  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  width: 240px;\n  transition: all 0.3s ease;\n}\n\n.demo-fixed-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 50px 16px 12px 16px;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-fixed-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-fixed-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-fixed-node-title {\n  background-color: #93bfe2;\n  width: 100%;\n  border-radius: 8px 8px 0 0;\n  padding: 4px 12px;\n}\n.demo-fixed-node-content {\n  padding: 16px;\n  flex-grow: 1;\n  width: 100%;\n}\n\ninput {\n  color: black;\n  background-color: white;\n}\n\n.demo-fixed-adder {\n  width: 28px;\n  height: 18px;\n  background: rgb(187, 191, 196);\n  display: flex;\n  border-radius: 9px;\n  justify-content: space-evenly;\n  align-items: center;\n  color: #fff;\n  font-size: 10px;\n  font-weight: bold;\n  div {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    svg {\n      width: 12px;\n      height: 12px;\n    }\n  }\n}\n\n.demo-fixed-adder.activated {\n  background: #82A7FC\n}\n\n.demo-fixed-adder.isHorizontal {\n  transform: rotate(90deg);\n}\n\n\n.gedit-playground * {\n  box-sizing: border-box;\n}\n\n.demo-fixed-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n.demo-fixed-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n    user-select: none;\n}\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFixedLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nimport { condition as conditionDemo } from './data/condition';\n\n/**\n * Initial Data\n */\nexport const initialData: FlowDocumentJSON = conditionDemo;\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/src/node-registries.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 自定义节点注册\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    /**\n     * 自定义节点类型\n     */\n    type: 'condition',\n    /**\n     * 自定义节点扩展:\n     *  - loop: 扩展为循环节点\n     *  - start: 扩展为开始节点\n     *  - dynamicSplit: 扩展为分支节点\n     *  - end: 扩展为结束节点\n     *  - tryCatch: 扩展为 tryCatch 节点\n     *  - break: 分支断开\n     *  - default: 扩展为普通节点 (默认)\n     */\n    extend: 'dynamicSplit',\n    /**\n     * 节点配置信息\n     */\n    meta: {\n      // isStart: false, // 是否为开始节点\n      // isNodeEnd: false, // 是否为结束节点，结束节点后边无法再添加节点\n      // draggable: false, // 是否可拖拽，如开始节点和结束节点无法拖拽\n      // selectable: false, // 触发器等开始节点不能被框选\n      // deleteDisable: true, // 禁止删除\n      // copyDisable: true, // 禁止copy\n      // addDisable: true, // 禁止添加\n    },\n    onAdd() {\n      return {\n        id: `condition_${nanoid(5)}`,\n        type: 'condition',\n        data: {\n          title: 'Condition',\n        },\n        blocks: [\n          {\n            id: nanoid(5),\n            type: 'block',\n            data: {\n              title: 'If_0',\n            },\n          },\n          {\n            id: nanoid(5),\n            type: 'block',\n            data: {\n              title: 'If_1',\n            },\n          },\n        ],\n      };\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    onAdd() {\n      return {\n        id: `custom_${nanoid(5)}`,\n        type: 'custom',\n        data: {\n          title: 'Custom',\n          content: 'this is custom content',\n        },\n      };\n    },\n  },\n];\n"
  },
  {
    "path": "apps/demo-fixed-layout-simple/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/demo-free-layout/README.md",
    "content": "# FlowGram.AI - Demo Free Layout\n\nBest-practice demo for free layout\n\n## Installation\n\n```shell\nnpx @flowgram.ai/create-app@latest free-layout\n```\n\n## Project Overview\n\n### Core Tech Stack\n- **Frontend framework**: React 18 + TypeScript\n- **Build tool**: Rsbuild (a modern build tool based on Rspack)\n- **Styling**: Less + Styled Components + CSS Variables\n- **UI library**: Semi Design (@douyinfe/semi-ui)\n- **State management**: Flowgram’s in-house editor framework\n- **Dependency injection**: Inversify\n\n### Core Dependencies\n\n- **@flowgram.ai/free-layout-editor**: Core dependency for the free layout editor\n- **@flowgram.ai/free-snap-plugin**: Auto-alignment and guide-lines plugin\n- **@flowgram.ai/free-lines-plugin**: Connection line rendering plugin\n- **@flowgram.ai/free-node-panel-plugin**: Node add-panel rendering plugin\n- **@flowgram.ai/minimap-plugin**: Minimap plugin\n- **@flowgram.ai/export-plugin**: Download/export plugin\n- **@flowgram.ai/free-container-plugin**: Sub-canvas plugin\n- **@flowgram.ai/free-group-plugin**: Grouping plugin\n- **@flowgram.ai/form-materials**: Form materials\n- **@flowgram.ai/runtime-interface**: Runtime interfaces\n- **@flowgram.ai/runtime-js**: JS runtime module\n- **@flowgram.ai/panel-manager-plugin**:  Sidebar panel management\n\n## Code Guide\n\n### Directory Structure\n```\nsrc/\n├── app.tsx                  # Application entry file\n├── editor.tsx               # Main editor component\n├── initial-data.ts          # Initial data configuration\n├── assets/                  # Static assets\n├── components/              # Component library\n│   ├── index.ts\n│   ├── add-node/            # Add-node component\n│   ├── base-node/           # Base node components\n│   ├── comment/             # Comment components\n│   ├── group/               # Group components\n│   ├── line-add-button/     # Connection add button\n│   ├── node-menu/           # Node menu\n│   ├── node-panel/          # Node add panel\n│   ├── selector-box-popover/ # Selection box popover\n│   ├── sidebar/             # Sidebar\n│   ├── testrun/             # Test-run module\n│   │   ├── hooks/           # Test-run hooks\n│   │   ├── node-status-bar/ # Node status bar\n│   │   ├── testrun-button/  # Test-run button\n│   │   ├── testrun-form/    # Test-run form\n│   │   ├── testrun-json-input/ # JSON input component\n│   │   └── testrun-panel/   # Test-run panel\n│   └── tools/               # Utility components\n├── context/                 # React Context\n│   ├── node-render-context.ts # Current rendering node context\n│   ├── sidebar-context        # Sidebar context\n├── form-components/         # Form component library\n│   ├── form-content/        # Form content\n│   ├── form-header/         # Form header\n│   ├── form-inputs/         # Form inputs\n│   └── form-item/           # Form item\n│   └── feedback.tsx         # Validation error rendering\n├── hooks/\n│   ├── index.ts\n│   ├── use-editor-props.tsx # Editor props hook\n│   ├── use-is-sidebar.ts    # Sidebar state hook\n│   ├── use-node-render-context.ts # Node render context hook\n│   └── use-port-click.ts    # Port click hook\n├── nodes/                    # Node definitions\n│   ├── index.ts\n│   ├── constants.ts         # Node constants\n│   ├── default-form-meta.ts # Default form metadata\n│   ├── block-end/           # Block end node\n│   ├── block-start/         # Block start node\n│   ├── break/               # Break node\n│   ├── code/                # Code node\n│   ├── comment/             # Comment node\n│   ├── condition/           # Condition node\n│   ├── continue/            # Continue node\n│   ├── end/                 # End node\n│   ├── group/               # Group node\n│   ├── http/                # HTTP node\n│   ├── llm/                 # LLM node\n│   ├── loop/                # Loop node\n│   ├── start/               # Start node\n│   └── variable/            # Variable node\n├── plugins/                 # Plugin system\n│   ├── index.ts\n│   ├── context-menu-plugin/ # Right-click context menu plugin\n│   ├── runtime-plugin/      # Runtime plugin\n│   │   ├── client/          # Client\n│   │   │   ├── browser-client/ # Browser client\n│   │   │   └── server-client/  # Server client\n│   │   └── runtime-service/ # Runtime service\n│   └── variable-panel-plugin/ # Variable panel plugin\n│       └── components/      # Variable panel components\n├── services/                 # Service layer\n│   ├── index.ts\n│   └── custom-service.ts    # Custom service\n├── shortcuts/                # Shortcuts system\n│   ├── index.ts\n│   ├── constants.ts         # Shortcut constants\n│   ├── shortcuts.ts         # Shortcut definitions\n│   ├── type.ts              # Type definitions\n│   ├── collapse/            # Collapse shortcut\n│   ├── copy/                # Copy shortcut\n│   ├── delete/              # Delete shortcut\n│   ├── expand/              # Expand shortcut\n│   ├── paste/               # Paste shortcut\n│   ├── select-all/          # Select-all shortcut\n│   ├── zoom-in/             # Zoom-in shortcut\n│   └── zoom-out/            # Zoom-out shortcut\n├── styles/                   # Styles\n├── typings/                  # Type definitions\n│   ├── index.ts\n│   ├── json-schema.ts       # JSON Schema types\n│   └── node.ts              # Node type definitions\n└── utils/                    # Utility functions\n    ├── index.ts\n    └── on-drag-line-end.ts  # Handle end of drag line\n```\n\n### Key Directory Functions\n\n#### 1. `/components` - Component Library\n- **base-node**: Base rendering components for all nodes\n- **testrun**: Complete test-run module, including status bar, form, and panel\n- **sidebar**: Sidebar components providing tools and property panels\n- **node-panel**: Node add panel with drag-to-add capability\n\n#### 2. `/nodes` - Node System\nEach node type has its own directory, including:\n- Node registration (`index.ts`)\n- Form metadata (`form-meta.ts`)\n- Node-specific components and logic\n\n#### 3. `/plugins` - Plugin System\n- **runtime-plugin**: Supports both browser and server modes\n- **context-menu-plugin**: Right-click context menu\n- **variable-panel-plugin**: Variable management panel\n\n#### 4. `/shortcuts` - Shortcuts System\nComplete keyboard shortcut support, including:\n- Basic actions: copy, paste, delete, select-all\n- View actions: zoom-in, zoom-out, collapse, expand\n- Each shortcut has its own implementation module\n\n## Application Architecture\n\n### Core Design Patterns\n\n#### 1. Plugin Architecture\nHighly modular plugin system; each feature is an independent plugin:\n\n```typescript\nplugins: () => [\n  createFreeLinesPlugin({ renderInsideLine: LineAddButton }),\n  createMinimapPlugin({ /* config */ }),\n  createFreeSnapPlugin({ /* alignment config */ }),\n  createFreeNodePanelPlugin({ renderer: NodePanel }),\n  createContainerNodePlugin({}),\n  createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),\n  createContextMenuPlugin({}),\n  createRuntimePlugin({ mode: 'browser' }),\n  createVariablePanelPlugin({})\n]\n```\n\n#### 2. Node Registry Pattern\nManage different workflow node types via a registry:\n\n```typescript\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,    // Condition node\n  StartNodeRegistry,        // Start node\n  EndNodeRegistry,          // End node\n  LLMNodeRegistry,          // LLM node\n  LoopNodeRegistry,         // Loop node\n  CommentNodeRegistry,      // Comment node\n  HTTPNodeRegistry,         // HTTP node\n  CodeNodeRegistry,         // Code node\n  // ... more node types\n];\n```\n\n#### 3. Dependency Injection\nUse Inversify for service DI:\n\n```typescript\nonBind: ({ bind }) => {\n  bind(CustomService).toSelf().inSingletonScope();\n}\n```\n\n## Core Features\n\n### 1. Editor Configuration System\n\n`useEditorProps` is the configuration center of the editor:\n\n```typescript\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FreeLayoutProps {\n  return useMemo<FreeLayoutProps>(() => ({\n    background: true,                    // Background grid\n    readonly: false,                     // Readonly mode\n    initialData,                         // Initial data\n    nodeRegistries,                      // Node registries\n\n    // Core feature configs\n    playground: { preventGlobalGesture: true /* Prevent Mac browser swipe gestures */ },\n    nodeEngine: { enable: true },\n    variableEngine: { enable: true },\n    history: { enable: true, enableChangeNode: true },\n\n    // Business rules\n    canAddLine: (ctx, fromPort, toPort) => { /* Connection rules */ },\n    canDeleteLine: (ctx, line) => { /* Line deletion rules */ },\n    canDeleteNode: (ctx, node) => { /* Node deletion rules */ },\n    canDropToNode: (ctx, params) => { /* Drag-and-drop rules */ },\n\n    // Plugins\n    plugins: () => [/* Plugin list */],\n\n    // Events\n    onContentChange: debounce((ctx, event) => { /* Auto save */ }, 1000),\n    onInit: (ctx) => { /* Initialization */ },\n    onAllLayersRendered: (ctx) => { /* After render */ }\n  }), []);\n}\n```\n\n### 2. Node Type System\n\nThe app supports multiple workflow node types:\n\n```typescript\nexport enum WorkflowNodeType {\n  Start = 'start',           // Start node\n  End = 'end',               // End node\n  LLM = 'llm',               // Large language model node\n  HTTP = 'http',             // HTTP request node\n  Code = 'code',             // Code execution node\n  Variable = 'variable',     // Variable node\n  Condition = 'condition',   // Conditional node\n  Loop = 'loop',             // Loop node\n  BlockStart = 'block-start', // Sub-canvas start node\n  BlockEnd = 'block-end',    // Sub-canvas end node\n  Comment = 'comment',       // Comment node\n  Continue = 'continue',     // Continue node\n  Break = 'break',           // Break node\n}\n```\n\nEach node follows a unified registration pattern:\n\n```typescript\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,        // Not deletable\n    copyDisable: true,          // Not copyable\n    nodePanelVisible: false,    // Hidden in node panel\n    defaultPorts: [{ type: 'output' }],\n    size: { width: 360, height: 211 }\n  },\n  info: {\n    icon: iconStart,\n    description: 'The starting node of the workflow, used to set up information needed to launch the workflow.'\n  },\n  formMeta,                     // Form configuration\n  canAdd() { return false; }    // Disallow multiple start nodes\n};\n```\n\n### 3. Plugin Architecture\n\nApp features are modularized via the plugin system:\n\n#### Core Plugin List\n1. **FreeLinesPlugin** - Connection rendering and interaction\n2. **MinimapPlugin** - Minimap navigation\n3. **FreeSnapPlugin** - Auto-alignment and guide-lines\n4. **FreeNodePanelPlugin** - Node add panel\n5. **ContainerNodePlugin** - Container nodes (e.g., loop nodes)\n6. **FreeGroupPlugin** - Node grouping\n7. **ContextMenuPlugin** - Right-click context menu\n8. **RuntimePlugin** - Workflow runtime\n9. **VariablePanelPlugin** - Variable management panel\n\n### 4. Runtime System\n\nTwo run modes are supported:\n\n```typescript\ncreateRuntimePlugin({\n  mode: 'browser',              // Browser mode\n  // mode: 'server',            // Server mode\n  // serverConfig: {\n  //   domain: 'localhost',\n  //   port: 4000,\n  //   protocol: 'http',\n  // },\n})\n```\n\n## Design Philosophy and Advantages\n\n### 1. Highly Modular\n- **Plugin architecture**: Each feature is an independent plugin, easy to extend and maintain\n- **Node registry system**: Add new node types without changing core code\n- **Componentized UI**: Highly reusable components with clear responsibilities\n\n### 2. Type Safety\n- **Full TypeScript support**: End-to-end type safety from configuration to runtime\n- **JSON Schema integration**: Node data validated by schemas\n- **Strongly typed plugin interfaces**: Clear type constraints for plugin development\n\n### 3. User Experience\n- **Real-time preview**: Run and debug workflows live\n- **Rich interactions**: Dragging, zooming, snapping, shortcuts for a complete editing experience\n- **Visual feedback**: Minimap, status indicators, line animations\n\n### 4. Extensibility\n- **Open plugin system**: Third parties can easily develop custom plugins\n- **Flexible node system**: Custom node types and form configurations supported\n- **Multiple runtimes**: Both browser and server modes\n\n### 5. Performance\n- **On-demand loading**: Components and plugins support lazy loading\n- **Debounce**: Performance optimizations for high-frequency operations like auto-save\n\n## Technical Highlights\n\n### 1. In-house Editor Framework\nBased on `@flowgram.ai/free-layout-editor`, providing:\n- Free-layout canvas system\n- Full undo/redo functionality\n- Lifecycle management for nodes and connections\n- Variable engine and expression system\n\n### 2. Advanced Build Configuration\nUsing Rsbuild as the build tool:\n\n```typescript\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: { index: './src/app.tsx' },\n    decorators: { version: 'legacy' }  // Enable decorators\n  },\n  tools: {\n    rspack: {\n      ignoreWarnings: [/Critical dependency/]  // Ignore specific warnings\n    }\n  }\n});\n```\n\n### 3. Internationalization\nBuilt-in multilingual support:\n\n```typescript\ni18n: {\n  locale: navigator.language,\n  languages: {\n    'zh-CN': {\n      'Never Remind': '不再提示',\n      'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',\n    },\n    'en-US': {},\n  }\n}\n```\n\n"
  },
  {
    "path": "apps/demo-free-layout/README.zh_CN.md",
    "content": "# FlowGram.AI - Demo Free Layout\n\n自由布局最佳实践 demo\n\n## 安装\n\n```shell\nnpx @flowgram.ai/create-app@latest free-layout\n```\n\n## 项目概览\n\n### 核心技术栈\n- **前端框架**: React 18 + TypeScript\n- **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)\n- **样式方案**: Less + Styled Components + CSS Variables\n- **UI 组件库**: Semi Design (@douyinfe/semi-ui)\n- **状态管理**: 基于 Flowgram 自研的编辑器框架\n- **依赖注入**: Inversify\n\n### 核心依赖包\n\n- **@flowgram.ai/free-layout-editor**: 自由布局编辑器核心依赖\n- **@flowgram.ai/free-snap-plugin**: 自动对齐及辅助线插件\n- **@flowgram.ai/free-lines-plugin**: 连线渲染插件\n- **@flowgram.ai/free-node-panel-plugin**: 节点添加面板渲染插件\n- **@flowgram.ai/minimap-plugin**: 缩略图插件\n- **@flowgram.ai/export-plugin**: 下载导出插件\n- **@flowgram.ai/free-container-plugin**: 子画布插件\n- **@flowgram.ai/free-group-plugin**: 分组插件\n- **@flowgram.ai/form-materials**: 表单物料\n- **@flowgram.ai/runtime-interface**: 运行时接口\n- **@flowgram.ai/runtime-js**: js 运行时模块\n- **@flowgram.ai/panel-manager-plugin**:  侧边栏面板管理\n\n## 代码说明\n\n### 目录结构\n```\nsrc/\n├── app.tsx                  # 应用入口文件\n├── editor.tsx               # 编辑器主组件\n├── initial-data.ts          # 初始化数据配置\n├── assets/                  # 静态资源\n├── components/              # 组件库\n│   ├── index.ts\n│   ├── add-node/            # 添加节点组件\n│   ├── base-node/           # 基础节点组件\n│   ├── comment/             # 注释组件\n│   ├── group/               # 分组组件\n│   ├── line-add-button/     # 连线添加按钮\n│   ├── node-menu/           # 节点菜单\n│   ├── node-panel/          # 节点添加面板\n│   ├── selector-box-popover/ # 选择框弹窗\n│   ├── sidebar/             # 侧边栏\n│   ├── testrun/             # 测试运行组件\n│   │   ├── hooks/           # 测试运行钩子\n│   │   ├── node-status-bar/ # 节点状态栏\n│   │   ├── testrun-button/  # 测试运行按钮\n│   │   ├── testrun-form/    # 测试运行表单\n│   │   ├── testrun-json-input/ # JSON输入组件\n│   │   └── testrun-panel/   # 测试运行面板\n│   └── tools/               # 工具组件\n├── context/                 # React Context\n│   ├── node-render-context.ts # 当前渲染节点 Context\n│   ├── sidebar-context        # 侧边栏 Context\n├── form-components/         # 表单组件库\n│   ├── form-content/        # 表单内容\n│   ├── form-header/         # 表单头部\n│   ├── form-inputs/         # 表单输入\n│   └── form-item/           # 表单项\n│   └── feedback.tsx         # 表单校验错误渲染\n├── hooks/\n│   ├── index.ts\n│   ├── use-editor-props.tsx # 编辑器属性钩子\n│   ├── use-is-sidebar.ts    # 侧边栏状态钩子\n│   ├── use-node-render-context.ts # 节点渲染上下文钩子\n│   └── use-port-click.ts    # 端口点击钩子\n├── nodes/                    # 节点定义\n│   ├── index.ts\n│   ├── constants.ts         # 节点常量定义\n│   ├── default-form-meta.ts # 默认表单元数据\n│   ├── block-end/           # 块结束节点\n│   ├── block-start/         # 块开始节点\n│   ├── break/               # 中断节点\n│   ├── code/                # 代码节点\n│   ├── comment/             # 注释节点\n│   ├── condition/           # 条件节点\n│   ├── continue/            # 继续节点\n│   ├── end/                 # 结束节点\n│   ├── group/               # 分组节点\n│   ├── http/                # HTTP节点\n│   ├── llm/                 # LLM节点\n│   ├── loop/                # 循环节点\n│   ├── start/               # 开始节点\n│   └── variable/            # 变量节点\n├── plugins/                 # 插件系统\n│   ├── index.ts\n│   ├── context-menu-plugin/ # 右键菜单插件\n│   ├── runtime-plugin/      # 运行时插件\n│   │   ├── client/          # 客户端\n│   │   │   ├── browser-client/ # 浏览器客户端\n│   │   │   └── server-client/  # 服务器客户端\n│   │   └── runtime-service/ # 运行时服务\n│   └── variable-panel-plugin/ # 变量面板插件\n│       └── components/      # 变量面板组件\n├── services/                 # 服务层\n│   ├── index.ts\n│   └── custom-service.ts    # 自定义服务\n├── shortcuts/                # 快捷键系统\n│   ├── index.ts\n│   ├── constants.ts         # 快捷键常量\n│   ├── shortcuts.ts         # 快捷键定义\n│   ├── type.ts              # 类型定义\n│   ├── collapse/            # 折叠快捷键\n│   ├── copy/                # 复制快捷键\n│   ├── delete/              # 删除快捷键\n│   ├── expand/              # 展开快捷键\n│   ├── paste/               # 粘贴快捷键\n│   ├── select-all/          # 全选快捷键\n│   ├── zoom-in/             # 放大快捷键\n│   └── zoom-out/            # 缩小快捷键\n├── styles/                   # 样式文件\n├── typings/                  # 类型定义\n│   ├── index.ts\n│   ├── json-schema.ts       # JSON Schema类型\n│   └── node.ts              # 节点类型定义\n└── utils/                    # 工具函数\n    ├── index.ts\n    └── on-drag-line-end.ts  # 拖拽连线结束处理\n```\n\n### 关键目录功能说明\n\n#### 1. `/components` - 组件库\n- **base-node**: 所有节点的基础渲染组件\n- **testrun**: 完整的测试运行功能模块，包含状态栏、表单、面板等\n- **sidebar**: 侧边栏组件，提供工具和属性面板\n- **node-panel**: 节点添加面板，支持拖拽添加新节点\n\n#### 2. `/nodes` - 节点系统\n每个节点类型都有独立的目录，包含：\n- 节点注册信息 (`index.ts`)\n- 表单元数据定义 (`form-meta.ts`)\n- 节点特定的组件和逻辑\n\n#### 3. `/plugins` - 插件系统\n- **runtime-plugin**: 支持浏览器和服务器两种运行模式\n- **context-menu-plugin**: 右键菜单功能\n- **variable-panel-plugin**: 变量管理面板\n\n#### 4. `/shortcuts` - 快捷键系统\n完整的快捷键支持，包括：\n- 基础操作：复制、粘贴、删除、全选\n- 视图操作：放大、缩小、折叠、展开\n- 每个快捷键都有独立的实现模块\n\n## 应用架构设计\n\n### 核心设计模式\n\n#### 1. 插件化架构 (Plugin Architecture)\n应用采用高度模块化的插件系统，每个功能都作为独立插件存在：\n\n```typescript\nplugins: () => [\n  createFreeLinesPlugin({ renderInsideLine: LineAddButton }),\n  createMinimapPlugin({ /* 配置 */ }),\n  createFreeSnapPlugin({ /* 对齐配置 */ }),\n  createFreeNodePanelPlugin({ renderer: NodePanel }),\n  createContainerNodePlugin({}),\n  createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),\n  createContextMenuPlugin({}),\n  createRuntimePlugin({ mode: 'browser' }),\n  createVariablePanelPlugin({})\n]\n```\n\n#### 2. 节点注册系统 (Node Registry Pattern)\n通过注册表模式管理不同类型的工作流节点：\n\n```typescript\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,    // 条件节点\n  StartNodeRegistry,        // 开始节点\n  EndNodeRegistry,          // 结束节点\n  LLMNodeRegistry,          // LLM节点\n  LoopNodeRegistry,         // 循环节点\n  CommentNodeRegistry,      // 注释节点\n  HTTPNodeRegistry,         // HTTP节点\n  CodeNodeRegistry,         // 代码节点\n  // ... 更多节点类型\n];\n```\n\n#### 3. 依赖注入模式 (Dependency Injection)\n使用 Inversify 框架实现服务的依赖注入：\n\n```typescript\nonBind: ({ bind }) => {\n  bind(CustomService).toSelf().inSingletonScope();\n}\n```\n\n## 核心功能分析\n\n### 1. 编辑器配置系统\n\n`useEditorProps` 是整个编辑器的配置中心，包含：\n\n```typescript\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FreeLayoutProps {\n  return useMemo<FreeLayoutProps>(() => ({\n    background: true,                    // 背景网格\n    readonly: false,                     // 是否只读\n    initialData,                         // 初始数据\n    nodeRegistries,                      // 节点注册表\n\n    // 核心功能配置\n    playground: { preventGlobalGesture: true /* 阻止 mac 浏览器手势翻页 */ },\n    nodeEngine: { enable: true },\n    variableEngine: { enable: true },\n    history: { enable: true, enableChangeNode: true },\n\n    // 业务逻辑配置\n    canAddLine: (ctx, fromPort, toPort) => { /* 连线规则 */ },\n    canDeleteLine: (ctx, line) => { /* 删除连线规则 */ },\n    canDeleteNode: (ctx, node) => { /* 删除节点规则 */ },\n    canDropToNode: (ctx, params) => { /* 拖拽规则 */ },\n\n    // 插件配置\n    plugins: () => [/* 插件列表 */],\n\n    // 事件处理\n    onContentChange: debounce((ctx, event) => { /* 自动保存 */ }, 1000),\n    onInit: (ctx) => { /* 初始化 */ },\n    onAllLayersRendered: (ctx) => { /* 渲染完成 */ }\n  }), []);\n}\n```\n\n### 2. 节点类型系统\n\n应用支持多种工作流节点类型：\n\n```typescript\nexport enum WorkflowNodeType {\n  Start = 'start',           // 开始节点\n  End = 'end',               // 结束节点\n  LLM = 'llm',               // 大语言模型节点\n  HTTP = 'http',             // HTTP请求节点\n  Code = 'code',             // 代码执行节点\n  Variable = 'variable',     // 变量节点\n  Condition = 'condition',   // 条件判断节点\n  Loop = 'loop',             // 循环节点\n  BlockStart = 'block-start', // 子画布开始节点\n  BlockEnd = 'block-end',    // 子画布结束节点\n  Comment = 'comment',       // 注释节点\n  Continue = 'continue',     // 继续节点\n  Break = 'break',           // 中断节点\n}\n```\n\n每个节点都遵循统一的注册模式：\n\n```typescript\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,        // 不可删除\n    copyDisable: true,          // 不可复制\n    nodePanelVisible: false,    // 不在节点面板显示\n    defaultPorts: [{ type: 'output' }],\n    size: { width: 360, height: 211 }\n  },\n  info: {\n    icon: iconStart,\n    description: '工作流的起始节点，用于设置启动工作流所需的信息。'\n  },\n  formMeta,                     // 表单配置\n  canAdd() { return false; }    // 不允许添加多个开始节点\n};\n```\n\n### 3. 插件化架构\n\n应用的功能通过插件系统实现模块化：\n\n#### 核心插件列表\n1. **FreeLinesPlugin** - 连线渲染和交互\n2. **MinimapPlugin** - 缩略图导航\n3. **FreeSnapPlugin** - 自动对齐和辅助线\n4. **FreeNodePanelPlugin** - 节点添加面板\n5. **ContainerNodePlugin** - 容器节点（如循环节点）\n6. **FreeGroupPlugin** - 节点分组功能\n7. **ContextMenuPlugin** - 右键菜单\n8. **RuntimePlugin** - 工作流运行时\n9. **VariablePanelPlugin** - 变量管理面板\n\n### 4. 运行时系统\n\n应用支持两种运行模式：\n\n```typescript\ncreateRuntimePlugin({\n  mode: 'browser',              // 浏览器模式\n  // mode: 'server',            // 服务器模式\n  // serverConfig: {\n  //   domain: 'localhost',\n  //   port: 4000,\n  //   protocol: 'http',\n  // },\n})\n```\n\n## 设计理念与架构优势\n\n### 1. 高度模块化\n- **插件化架构**: 每个功能都是独立插件，易于扩展和维护\n- **节点注册系统**: 新节点类型可以轻松添加，无需修改核心代码\n- **组件化设计**: UI组件高度复用，职责清晰\n\n### 2. 类型安全\n- **完整的TypeScript支持**: 从配置到运行时的全链路类型保护\n- **JSON Schema集成**: 节点数据结构通过Schema验证\n- **强类型的插件接口**: 插件开发有明确的类型约束\n\n### 3. 用户体验优化\n- **实时预览**: 支持工作流的实时运行和调试\n- **丰富的交互**: 拖拽、缩放、对齐、快捷键等完整的编辑体验\n- **可视化反馈**: 缩略图、状态指示、连线动画等视觉反馈\n\n### 4. 扩展性设计\n- **开放的插件系统**: 第三方可以轻松开发自定义插件\n- **灵活的节点系统**: 支持自定义节点类型和表单配置\n- **多运行时支持**: 浏览器和服务器双模式运行\n\n### 5. 性能优化\n- **按需加载**: 组件和插件支持按需加载\n- **防抖处理**: 自动保存等高频操作的性能优化\n\n## 技术亮点\n\n### 1. 自研编辑器框架\n基于 `@flowgram.ai/free-layout-editor` 自研框架，提供：\n- 自由布局的画布系统\n- 完整的撤销/重做功能\n- 节点和连线的生命周期管理\n- 变量引擎和表达式系统\n\n### 2. 先进的构建配置\n使用 Rsbuild 作为构建工具：\n\n```typescript\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: { index: './src/app.tsx' },\n    decorators: { version: 'legacy' }  // 支持装饰器\n  },\n  tools: {\n    rspack: {\n      ignoreWarnings: [/Critical dependency/]  // 忽略特定警告\n    }\n  }\n});\n```\n\n### 3. 国际化支持\n内置多语言支持：\n\n```typescript\ni18n: {\n  locale: navigator.language,\n  languages: {\n    'zh-CN': {\n      'Never Remind': '不再提示',\n      'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',\n    },\n    'en-US': {},\n  }\n}\n```\n\n"
  },
  {
    "path": "apps/demo-free-layout/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-free-layout/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-free-layout\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.ts\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\",\n    \"README.md\",\n    \"README.zh_CN.md\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"build:analyze\": \"BUNDLE_ANALYZE=true rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/runtime-interface\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-lines-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-node-panel-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/export-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-container-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-group-plugin\": \"workspace:*\",\n    \"@flowgram.ai/form-materials\": \"workspace:*\",\n    \"@flowgram.ai/panel-manager-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-stack-plugin\": \"workspace:*\",\n    \"@flowgram.ai/runtime-js\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"classnames\": \"^2.5.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@rsbuild/plugin-less\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { pluginLess } from '@rsbuild/plugin-less';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n    /**\n     * support inversify @injectable() and @inject decorators\n     */\n    decorators: {\n      version: 'legacy',\n    },\n  },\n  html: {\n    title: 'demo-free-layout',\n  },\n  tools: {\n    rspack: {\n      /**\n       * ignore warnings from @coze-editor/editor/language-typescript\n       */\n      ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\nimport { unstableSetCreateRoot } from '@flowgram.ai/form-materials';\n\nimport { Editor } from './editor';\n\n/**\n * React 18/19 polyfill for form-materials\n */\nunstableSetCreateRoot(createRoot);\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-auto-layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconAutoLayout = (\n  <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      fill=\"currentColor\"\n      d=\"M3 2C2.44772 2 2 2.44771 2 3V12C2 12.5523 2.44772 13 3 13H10C10.5523 13 11 12.5523 11 12V3C11 2.44772 10.5523 2 10 2H3zM4 11V4H9V11H4zM21 22C21.5523 22 22 21.5523 22 21V12C22 11.4477 21.5523 11 21 11H14C13.4477 11 13 11.4477 13 12V21C13 21.5523 13.4477 22 14 22H21zM20 13V20H15V13H20zM2 16C2 15.4477 2.44772 15 3 15H10C10.5523 15 11 15.4477 11 16V21C11 21.5523 10.5523 22 10 22H3C2.44772 22 2 21.5523 2 21V16zM4 20V17H9V20H4zM21 9C21.5523 9 22 8.55228 22 8V3C22 2.44772 21.5523 2 21 2H14C13.4477 2 13 2.44772 13 3V8C13 8.55228 13.4477 9 14 9H21zM20 4V7H15V4H20z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-cancel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconCancel = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M9.5 8C8.67157 8 8 8.67157 8 9.5V14.5C8 15.3284 8.67157 16 9.5 16H14.5C15.3284 16 16 15.3284 16 14.5V9.5C16 8.67157 15.3284 8 14.5 8H9.5Z\"></path>\n    <path d=\"M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21Z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-comment.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC } from 'react';\n\ninterface IconCommentProps {\n  style?: CSSProperties;\n}\n\nexport const IconComment: FC<IconCommentProps> = ({ style }) => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={style}\n  >\n    <path d=\"M6.5 9C5.94772 9 5.5 9.44772 5.5 10V11C5.5 11.5523 5.94772 12 6.5 12H7.5C8.05228 12 8.5 11.5523 8.5 11V10C8.5 9.44772 8.05228 9 7.5 9H6.5zM11.5 9C10.9477 9 10.5 9.44772 10.5 10V11C10.5 11.5523 10.9477 12 11.5 12H12.5C13.0523 12 13.5 11.5523 13.5 11V10C13.5 9.44772 13.0523 9 12.5 9H11.5zM15.5 10C15.5 9.44772 15.9477 9 16.5 9H17.5C18.0523 9 18.5 9.44772 18.5 10V11C18.5 11.5523 18.0523 12 17.5 12H16.5C15.9477 12 15.5 11.5523 15.5 11V10z\"></path>\n    <path d=\"M23 4C23 2.9 22.1 2 21 2H3C1.9 2 1 2.9 1 4V17.0111C1 18.0211 1.9 19.0111 3 19.0111H7.7586L10.4774 22C10.9822 22.5017 11.3166 22.6311 12 22.7009C12.414 22.707 13.0502 22.5093 13.5 22L16.2414 19.0111H21C22.1 19.0111 23 18.1111 23 17.0111V4ZM3 4H21V17.0111H15.5L12 20.6714L8.5 17.0111H3V4Z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconMinimap = () => (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"g1\">\n      <path\n        id=\"path1\"\n        fill=\"#000000\"\n        stroke=\"none\"\n        d=\"M 18.09091 6.883101 L 5.409091 6.883101 L 5.409091 16.746737 L 10.664648 16.746737 C 10.927091 17.116341 11.30353 17.422749 11.792977 17.611004 L 12.664289 17.946156 L 12.744959 18.155828 L 5.409091 18.155828 C 4.630871 18.155828 4 17.524979 4 16.746737 L 4 6.883101 C 4 6.104881 4.630871 5.47401 5.409091 5.47401 L 18.09091 5.47401 C 18.86915 5.47401 19.5 6.104881 19.5 6.883101 L 19.5 12.52348 C 19.247208 11.883823 18.730145 11.365912 18.09091 11.111994 L 18.09091 6.883101 Z M 18.09091 18.155828 L 17.881165 18.155828 L 19.469212 14.368896 C 19.479921 14.343321 19.490206 14.317817 19.5 14.292241 L 19.5 16.746737 C 19.5 17.524979 18.86915 18.155828 18.09091 18.155828 Z\"\n      />\n      <path\n        id=\"path2\"\n        fill=\"#000000\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 18.494614 13.960189 C 18.982441 12.796985 17.813459 11.628003 16.650255 12.11576 L 12.133272 14.01 C 10.962248 14.501069 10.987188 16.168798 12.172375 16.62464 L 13.482055 17.128389 L 13.985805 18.438068 C 14.441646 19.623184 16.109375 19.648125 16.600443 18.477171 L 18.494614 13.960189 Z M 17.19515 13.415224 L 15.30098 17.932205 L 14.79723 16.622526 C 14.654066 16.250385 14.359989 15.956307 13.987918 15.813213 L 12.678168 15.309464 L 17.19515 13.415224 Z\"\n      />\n    </g>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-mouse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconMouse(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 34}\n      height={height || 52}\n      viewBox=\"0 0 34 52\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.8\"\n      />\n    </svg>\n  );\n}\n\nexport const IconMouseTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-pad.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconPad(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 48}\n      height={height || 38}\n      viewBox=\"0 0 48 38\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect\n        x=\"1.83317\"\n        y=\"1.49998\"\n        width=\"44.3333\"\n        height=\"35\"\n        rx=\"3.5\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n      />\n      <path\n        d=\"M14.6665 30.6667H33.3332\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport const IconPadTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z\"\n    ></path>\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-success.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconSuccessFill = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"20\"\n    height=\"20\"\n    fill=\"none\"\n    viewBox=\"0 0 20 20\"\n  >\n    <g clipPath=\"url(#icon-workflow-run-success_svg__a)\">\n      <path\n        fill=\"#3EC254\"\n        d=\"M.833 10A9.166 9.166 0 0 0 10 19.168a9.166 9.166 0 0 0 9.167-9.166A9.166 9.166 0 0 0 10 .834a9.166 9.166 0 0 0-9.167 9.167\"\n      ></path>\n      <path\n        fill=\"#fff\"\n        d=\"M6.077 9.755a.833.833 0 0 0 0 1.179l2.357 2.357a.833.833 0 0 0 1.179 0l4.714-4.714a.833.833 0 1 0-1.178-1.179l-4.125 4.125-1.768-1.768a.833.833 0 0 0-1.179 0\"\n      ></path>\n    </g>\n    <defs>\n      <clipPath id=\"icon-workflow-run-success_svg__a\">\n        <path fill=\"#fff\" d=\"M0 0h20v20H0z\"></path>\n      </clipPath>\n    </defs>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-switch-line.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconSwitchLine = (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      id=\"switch-line\"\n      fill=\"currentColor\"\n      stroke=\"none\"\n      d=\"M 12.728118 10.060962 C 13.064282 8.716098 14.272528 7.772551 15.65877 7.772343 L 17.689898 7.772343 C 18.0798 7.772343 18.39588 7.456264 18.39588 7.066362 C 18.39588 6.676458 18.0798 6.36038 17.689898 6.36038 L 15.659616 6.36038 C 13.62515 6.360315 11.851767 7.745007 11.358504 9.718771 C 11.02234 11.063635 9.814095 12.007183 8.427853 12.007389 L 7.101437 12.007389 C 6.711768 12.007389 6.395878 12.323277 6.395878 12.712947 C 6.395878 13.102616 6.711768 13.418506 7.101437 13.418506 L 8.426159 13.418506 C 9.812716 13.418323 11.021417 14.361954 11.357657 15.707124 C 11.850921 17.680887 13.624304 19.065578 15.65877 19.065516 L 17.689049 19.065516 C 18.078953 19.065516 18.395033 18.749435 18.395033 18.359533 C 18.395033 17.969631 18.078953 17.653551 17.689049 17.653551 L 15.65877 17.653551 C 14.272528 17.653345 13.064282 16.709797 12.728118 15.364932 C 12.454905 14.27114 11.774856 13.322707 10.826583 12.712947 C 11.774536 12.10303 12.454268 11.154617 12.727271 10.060962 Z\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/assets/icon-warning.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconWarningFill = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12ZM11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8V13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13V8ZM11 16C11 15.4477 11.4477 15 12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/add-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { useAddNode } from './use-add-node';\n\nexport const AddNode = (props: { disabled: boolean }) => {\n  const addNode = useAddNode();\n  return (\n    <Button\n      data-testid=\"demo.free-layout.add-node\"\n      icon={<IconPlus />}\n      color=\"highlight\"\n      style={{ backgroundColor: 'rgba(171,181,255,0.3)', borderRadius: '8px' }}\n      disabled={props.disabled}\n      onClick={(e) => {\n        const rect = e.currentTarget.getBoundingClientRect();\n        addNode(rect);\n      }}\n    >\n      Add Node\n    </Button>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/add-node/use-add-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport { useCallback } from 'react';\n\nimport { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  useService,\n  WorkflowDocument,\n  usePlayground,\n  PositionSchema,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n  WorkflowNodeJSON,\n  getAntiOverlapPosition,\n  WorkflowNodeMeta,\n  FlowNodeBaseType,\n} from '@flowgram.ai/free-layout-editor';\n// hook to get panel position from mouse event - 从鼠标事件获取面板位置的 hook\nconst useGetPanelPosition = () => {\n  const playground = usePlayground();\n  return useCallback(\n    (targetBoundingRect: DOMRect): PositionSchema =>\n      // convert mouse position to canvas position - 将鼠标位置转换为画布位置\n      playground.config.getPosFromMouseEvent({\n        clientX: targetBoundingRect.left + 64,\n        clientY: targetBoundingRect.top - 7,\n      }),\n    [playground]\n  );\n};\n// hook to handle node selection - 处理节点选择的 hook\nconst useSelectNode = () => {\n  const selectService = useService(WorkflowSelectService);\n  return useCallback(\n    (node?: WorkflowNodeEntity) => {\n      if (!node) {\n        return;\n      }\n      // select the target node - 选择目标节点\n      selectService.selectNode(node);\n    },\n    [selectService]\n  );\n};\n\nconst getContainerNode = (selectService: WorkflowSelectService) => {\n  const { activatedNode } = selectService;\n  if (!activatedNode) {\n    return;\n  }\n  const { isContainer } = activatedNode.getNodeMeta<WorkflowNodeMeta>();\n  if (isContainer) {\n    return activatedNode;\n  }\n  const parentNode = activatedNode.parent;\n  if (!parentNode || parentNode.flowNodeType === FlowNodeBaseType.ROOT) {\n    return;\n  }\n  return parentNode;\n};\n\n// main hook for adding new nodes - 添加新节点的主 hook\nexport const useAddNode = () => {\n  const workflowDocument = useService(WorkflowDocument);\n  const nodePanelService = useService<WorkflowNodePanelService>(WorkflowNodePanelService);\n  const selectService = useService(WorkflowSelectService);\n  const playground = usePlayground();\n  const getPanelPosition = useGetPanelPosition();\n  const select = useSelectNode();\n\n  return useCallback(\n    async (targetBoundingRect: DOMRect): Promise<void> => {\n      // calculate panel position based on target element - 根据目标元素计算面板位置\n      const panelPosition = getPanelPosition(targetBoundingRect);\n      const containerNode = getContainerNode(selectService);\n      await new Promise<void>((resolve) => {\n        // call the node panel service to show the panel - 调用节点面板服务来显示面板\n        nodePanelService.callNodePanel({\n          position: panelPosition,\n          enableMultiAdd: true,\n          containerNode,\n          panelProps: {},\n          // handle node selection from panel - 处理从面板中选择节点\n          onSelect: async (panelParams?: NodePanelResult) => {\n            if (!panelParams) {\n              return;\n            }\n            const { nodeType, nodeJSON } = panelParams;\n            const position = Boolean(containerNode)\n              ? getAntiOverlapPosition(workflowDocument, {\n                  x: 0,\n                  y: 200,\n                })\n              : undefined;\n            // create new workflow node based on selected type - 根据选择的类型创建新的工作流节点\n            const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(\n              nodeType,\n              position, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点\n              nodeJSON ?? ({} as WorkflowNodeJSON),\n              containerNode?.id\n            );\n            select(node);\n          },\n          // handle panel close - 处理面板关闭\n          onClose: () => {\n            resolve();\n          },\n        });\n      });\n    },\n    [getPanelPosition, nodePanelService, playground.config.zoom, workflowDocument, select]\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/base-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { FlowNodeEntity, useClientContext, useNodeRender } from '@flowgram.ai/free-layout-editor';\nimport { ConfigProvider } from '@douyinfe/semi-ui';\n\nimport { NodeStatusBar } from '../testrun/node-status-bar';\nimport { NodeRenderContext } from '../../context';\nimport { ErrorIcon } from './styles';\nimport { NodeWrapper } from './node-wrapper';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  /**\n   * Provides methods related to node rendering\n   * 提供节点渲染相关的方法\n   */\n  const nodeRender = useNodeRender();\n  const ctx = useClientContext();\n  /**\n   * It can only be used when nodeEngine is enabled\n   * 只有在节点引擎开启时候才能使用表单\n   */\n  const form = nodeRender.form;\n\n  /**\n   * Used to make the Tooltip scale with the node, which can be implemented by itself depending on the UI library\n   * 用于让 Tooltip 跟随节点缩放, 这个可以根据不同的 ui 库自己实现\n   */\n  const getPopupContainer = useCallback(\n    () => ctx.playground.node.querySelector('.gedit-flow-render-layer') as HTMLDivElement,\n    []\n  );\n\n  return (\n    <ConfigProvider getPopupContainer={getPopupContainer}>\n      <NodeRenderContext.Provider value={nodeRender}>\n        <NodeWrapper>\n          {form?.state.invalid && <ErrorIcon />}\n          {form?.render()}\n        </NodeWrapper>\n        <NodeStatusBar />\n      </NodeRenderContext.Provider>\n    </ConfigProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/base-node/node-wrapper.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport { WorkflowPortRender } from '@flowgram.ai/free-layout-editor';\nimport { useClientContext } from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeMeta } from '../../typings';\nimport { useNodeFormPanel } from '../../plugins/panel-manager-plugin/hooks';\nimport { useNodeRenderContext, usePortClick } from '../../hooks';\nimport { scrollToView } from './utils';\nimport { NodeWrapperStyle } from './styles';\n\nexport interface NodeWrapperProps {\n  isScrollToView?: boolean;\n  children: React.ReactNode;\n}\n\n/**\n * Used for drag-and-drop/click events and ports rendering of nodes\n * 用于节点的拖拽/点击事件和点位渲染\n */\nexport const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {\n  const { children, isScrollToView = false } = props;\n  const nodeRender = useNodeRenderContext();\n  const { node, selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur, readonly } =\n    nodeRender;\n  const [isDragging, setIsDragging] = useState(false);\n  const form = nodeRender.form;\n  const ctx = useClientContext();\n  const onPortClick = usePortClick();\n  const meta = node.getNodeMeta<FlowNodeMeta>();\n\n  const { open } = useNodeFormPanel();\n  const portsRender = ports.map((p) => (\n    <WorkflowPortRender key={p.id} entity={p} onClick={!readonly ? onPortClick : undefined} />\n  ));\n\n  return (\n    <>\n      <NodeWrapperStyle\n        className={selected ? 'selected' : ''}\n        ref={nodeRef}\n        draggable\n        onDragStart={(e) => {\n          startDrag(e);\n          setIsDragging(true);\n        }}\n        onTouchStart={(e) => {\n          startDrag(e as unknown as React.MouseEvent);\n          setIsDragging(true);\n        }}\n        onClick={(e) => {\n          selectNode(e);\n          if (!isDragging) {\n            open({\n              nodeId: nodeRender.node.id,\n            });\n            // 可选：将 isScrollToView 设为 true，可以让节点选中后滚动到画布中间\n            // Optional: Set isScrollToView to true to scroll the node to the center of the canvas after it is selected.\n            if (isScrollToView) {\n              scrollToView(ctx, nodeRender.node);\n            }\n          }\n        }}\n        onMouseUp={() => setIsDragging(false)}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        data-node-selected={String(selected)}\n        style={{\n          ...meta.wrapperStyle,\n          outline: form?.state.invalid ? '1px solid red' : 'none',\n        }}\n      >\n        {children}\n      </NodeWrapperStyle>\n      {portsRender}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/base-node/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { IconInfoCircle } from '@douyinfe/semi-icons';\n\nexport const NodeWrapperStyle = styled.div`\n  align-items: flex-start;\n  background-color: #fff;\n  border: 1px solid rgba(6, 7, 9, 0.15);\n  border-radius: 8px;\n  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  width: 360px;\n  height: auto;\n\n  &.selected {\n    border: 1px solid #4e40e5;\n  }\n`;\n\nexport const ErrorIcon = () => (\n  <IconInfoCircle\n    style={{\n      position: 'absolute',\n      color: 'red',\n      left: -6,\n      top: -6,\n      zIndex: 1,\n      background: 'white',\n      borderRadius: 8,\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/base-node/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FreeLayoutPluginContext, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nexport function scrollToView(\n  ctx: FreeLayoutPluginContext,\n  node: FlowNodeEntity,\n  sidebarWidth = 448\n) {\n  const bounds = node.transform.bounds;\n  ctx.playground.scrollToView({\n    bounds,\n    scrollDelta: {\n      x: sidebarWidth / 2,\n      y: 0,\n    },\n    zoom: 1,\n    scrollToCenter: true,\n  });\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/blank-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FC } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\nimport { DragArea } from './drag-area';\n\ninterface IBlankArea {\n  model: CommentEditorModel;\n}\n\nexport const BlankArea: FC<IBlankArea> = (props) => {\n  const { model } = props;\n  const playground = usePlayground();\n  const { selectNode } = useNodeRender();\n\n  return (\n    <div\n      className=\"workflow-comment-blank-area h-full w-full\"\n      onMouseDown={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        model.setFocus(false);\n        selectNode(e);\n        playground.node.focus(); // 防止节点无法被删除\n      }}\n      onClick={(e) => {\n        model.setFocus(true);\n        model.selectEnd();\n      }}\n    >\n      <DragArea\n        style={{\n          position: 'relative',\n          width: '100%',\n          height: '100%',\n        }}\n        model={model}\n        stopEvent={false}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/border-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC } from 'react';\n\nimport type { CommentEditorModel } from '../model';\nimport { ResizeArea } from './resize-area';\nimport { DragArea } from './drag-area';\n\ninterface IBorderArea {\n  model: CommentEditorModel;\n  overflow: boolean;\n  onResize?: () => {\n    resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;\n    resizeEnd: () => void;\n  };\n}\n\nexport const BorderArea: FC<IBorderArea> = (props) => {\n  const { model, overflow, onResize } = props;\n\n  return (\n    <div style={{ zIndex: 999 }}>\n      {/* 左边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          left: -10,\n          top: 10,\n          width: 20,\n          height: 'calc(100% - 20px)',\n        }}\n        model={model}\n      />\n      {/* 右边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          right: -10,\n          top: 10,\n          height: 'calc(100% - 20px)',\n          width: overflow ? 10 : 20, // 防止遮挡滚动条\n        }}\n        model={model}\n      />\n      {/* 上边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          top: -10,\n          left: 10,\n          width: 'calc(100% - 20px)',\n          height: 20,\n        }}\n        model={model}\n      />\n      {/* 下边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          bottom: -10,\n          left: 10,\n          width: 'calc(100% - 20px)',\n          height: 20,\n        }}\n        model={model}\n      />\n      {/** 左上角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          cursor: 'nwse-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: y, right: 0, bottom: 0, left: x })}\n        onResize={onResize}\n      />\n      {/** 右上角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          right: 0,\n          top: 0,\n          cursor: 'nesw-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: y, right: x, bottom: 0, left: 0 })}\n        onResize={onResize}\n      />\n      {/** 右下角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          right: 0,\n          bottom: 0,\n          cursor: 'nwse-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: 0, right: x, bottom: y, left: 0 })}\n        onResize={onResize}\n      />\n      {/** 左下角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          left: 0,\n          bottom: 0,\n          cursor: 'nesw-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: 0, right: 0, bottom: y, left: x })}\n        onResize={onResize}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/container.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { ReactNode, FC, CSSProperties } from 'react';\n\ninterface ICommentContainer {\n  focused: boolean;\n  children?: ReactNode;\n  style?: React.CSSProperties;\n}\n\nexport const CommentContainer: FC<ICommentContainer> = (props) => {\n  const { focused, children, style } = props;\n\n  const scrollbarStyle = {\n    // 滚动条样式\n    scrollbarWidth: 'thin',\n    scrollbarColor: 'rgb(159 159 158 / 65%) transparent',\n    // 针对 WebKit 浏览器（如 Chrome、Safari）的样式\n    '&:WebkitScrollbar': {\n      width: '4px',\n    },\n    '&::WebkitScrollbarTrack': {\n      background: 'transparent',\n    },\n    '&::WebkitScrollbarThumb': {\n      backgroundColor: 'rgb(159 159 158 / 65%)',\n      borderRadius: '20px',\n      border: '2px solid transparent',\n    },\n  } as unknown as CSSProperties;\n\n  return (\n    <div\n      className=\"workflow-comment-container\"\n      data-flow-editor-selectable=\"false\"\n      style={{\n        // tailwind 不支持 outline 的样式，所以这里需要使用 style 来设置\n        outline: focused ? '1px solid #FF811A' : '1px solid #F2B600',\n        backgroundColor: focused ? '#FFF3EA' : '#FFFBED',\n        ...scrollbarStyle,\n        ...style,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/content-drag-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC, useState, useEffect, type WheelEventHandler } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\nimport { DragArea } from './drag-area';\n\ninterface IContentDragArea {\n  model: CommentEditorModel;\n  focused: boolean;\n  overflow: boolean;\n}\n\nexport const ContentDragArea: FC<IContentDragArea> = (props) => {\n  const { model, focused, overflow } = props;\n  const playground = usePlayground();\n  const { selectNode } = useNodeRender();\n\n  const [active, setActive] = useState(false);\n\n  useEffect(() => {\n    // 当编辑器失去焦点时，取消激活状态\n    if (!focused) {\n      setActive(false);\n    }\n  }, [focused]);\n\n  const handleWheel: WheelEventHandler<HTMLDivElement> = (e) => {\n    const editorElement = model.element;\n    if (active || !overflow || !editorElement) {\n      return;\n    }\n    e.stopPropagation();\n    const maxScroll = editorElement.scrollHeight - editorElement.clientHeight;\n    const newScrollTop = Math.min(Math.max(editorElement.scrollTop + e.deltaY, 0), maxScroll);\n    editorElement.scroll(0, newScrollTop);\n  };\n\n  const handleMouseDown = (mouseDownEvent: React.MouseEvent) => {\n    if (active) {\n      return;\n    }\n    mouseDownEvent.preventDefault();\n    mouseDownEvent.stopPropagation();\n    model.setFocus(false);\n    selectNode(mouseDownEvent);\n    playground.node.focus(); // 防止节点无法被删除\n\n    const startX = mouseDownEvent.clientX;\n    const startY = mouseDownEvent.clientY;\n\n    const handleMouseUp = (mouseMoveEvent: MouseEvent) => {\n      const deltaX = mouseMoveEvent.clientX - startX;\n      const deltaY = mouseMoveEvent.clientY - startY;\n      // 判断是拖拽还是点击\n      const delta = 5;\n      if (Math.abs(deltaX) < delta && Math.abs(deltaY) < delta) {\n        // 点击后隐藏\n        setActive(true);\n      }\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('click', handleMouseUp);\n    };\n\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('click', handleMouseUp);\n  };\n\n  return (\n    <div\n      className=\"workflow-comment-content-drag-area\"\n      onMouseDown={handleMouseDown}\n      onWheel={handleWheel}\n      style={{\n        display: active ? 'none' : undefined,\n      }}\n    >\n      <DragArea\n        style={{\n          position: 'relative',\n          width: '100%',\n          height: '100%',\n        }}\n        model={model}\n        stopEvent={false}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/drag-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, MouseEvent, TouchEvent, type FC } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { type CommentEditorModel } from '../model';\n\ninterface IDragArea {\n  model: CommentEditorModel;\n  stopEvent?: boolean;\n  style?: CSSProperties;\n}\n\nexport const DragArea: FC<IDragArea> = (props) => {\n  const { model, stopEvent = true, style } = props;\n\n  const playground = usePlayground();\n\n  const { startDrag: onStartDrag, onFocus, onBlur, selectNode } = useNodeRender();\n\n  const handleDrag = (e: MouseEvent | TouchEvent) => {\n    if (stopEvent) {\n      e.preventDefault();\n      e.stopPropagation();\n    }\n    model.setFocus(false);\n    onStartDrag(e as MouseEvent);\n    selectNode(e as MouseEvent);\n    playground.node.focus(); // 防止节点无法被删除\n  };\n\n  return (\n    <div\n      className=\"workflow-comment-drag-area\"\n      data-flow-editor-selectable=\"false\"\n      draggable={true}\n      style={style}\n      onMouseDown={handleDrag}\n      onTouchStart={handleDrag}\n      onFocus={onFocus}\n      onBlur={onBlur}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC, type CSSProperties, useEffect, useRef } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { usePlaceholder } from '../hooks';\nimport { CommentEditorEvent } from '../constant';\n\ninterface ICommentEditor {\n  model: CommentEditorModel;\n  style?: CSSProperties;\n  value?: string;\n  onChange?: (value: string) => void;\n}\n\nexport const CommentEditor: FC<ICommentEditor> = (props) => {\n  const { model, style, onChange } = props;\n  const playground = usePlayground();\n  const placeholder = usePlaceholder({ model });\n  const editorRef = useRef<HTMLTextAreaElement | null>(null);\n\n  // 同步编辑器内部值变化\n  useEffect(() => {\n    const disposer = model.on((params) => {\n      if (params.type !== CommentEditorEvent.Change) {\n        return;\n      }\n      onChange?.(model.value);\n    });\n    return () => disposer.dispose();\n  }, [model, onChange]);\n\n  useEffect(() => {\n    if (!editorRef.current) {\n      return;\n    }\n    model.element = editorRef.current;\n  }, [editorRef]);\n\n  return (\n    <div className=\"workflow-comment-editor\">\n      <p className=\"workflow-comment-editor-placeholder\">{placeholder}</p>\n      <textarea\n        className=\"workflow-comment-editor-textarea\"\n        ref={editorRef}\n        style={style}\n        readOnly={playground.config.readonly}\n        onChange={(e) => {\n          const { value } = e.target;\n          model.setValue(value);\n        }}\n        onFocus={() => {\n          model.setFocus(true);\n        }}\n        onBlur={() => {\n          model.setFocus(false);\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.workflow-comment {\n    width: auto;\n    height: auto;\n    min-width: 120px;\n    min-height: 80px;\n}\n\n.workflow-comment-container {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    justify-content: flex-start;\n    width: 100%;\n    height: 100%;\n    border-radius: 8px;\n    outline: 1px solid;\n    padding: 6px 2px 6px 10px;\n    overflow: hidden;\n}\n\n.workflow-comment-drag-area {\n    position: absolute;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    cursor: move;\n}\n\n.workflow-comment-content-drag-area {\n    position: absolute;\n    height: 100%;\n    width: calc(100% - 22px);\n}\n\n.workflow-comment-resize-area {\n    position: absolute;\n    width: 10px;\n    height: 10px;\n}\n\n.workflow-comment-editor {\n    width: 100%;\n    height: 100%;\n}\n\n.workflow-comment-editor-placeholder {\n    margin: 0;\n    position: absolute;\n    pointer-events: none;\n    color: rgba(55, 67, 106, 0.38);\n    font-weight: 500;\n}\n\n.workflow-comment-editor-textarea {\n    width: 100%;\n    height: 100%;\n    box-sizing: border-box;\n    appearance: none;\n    border: none;\n    margin: 0;\n    padding: 0;\n    width: 100%;\n    background: none;\n    color: inherit;\n    font-family: inherit;\n    font-size: 16px;\n    resize: none;\n    outline: none;\n}\n\n.workflow-comment-more-button {\n    position: absolute;\n    right: 6px;\n}\n\n.workflow-comment-more-button > .semi-button {\n    color: rgba(255, 255, 255, 0);\n    background: none;\n}\n\n.workflow-comment-more-button > .semi-button:hover {\n    color: #ffa100;\n    background: #fbf2d2cc;\n    backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button-focused > .semi-button:hover {\n    color: #ff811a;\n    background: #ffe3cecc;\n    backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button > .semi-button:active {\n    color: #f2b600;\n    background: #ede5c7cc;\n    backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button-focused > .semi-button:active {\n    color: #ff811a;\n    background: #eed5c1cc;\n    backdrop-filter: blur(1px);\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.css';\n\nexport { CommentRender } from './render';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/more-button.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeMenu } from '../../node-menu';\n\ninterface IMoreButton {\n  node: WorkflowNodeEntity;\n  focused: boolean;\n  deleteNode: () => void;\n}\n\nexport const MoreButton: FC<IMoreButton> = ({ node, focused, deleteNode }) => (\n  <div\n    className={`workflow-comment-more-button ${\n      focused ? 'workflow-comment-more-button-focused' : ''\n    }`}\n  >\n    <NodeMenu node={node} deleteNode={deleteNode} />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport {\n  Field,\n  FieldRenderProps,\n  FlowNodeFormData,\n  Form,\n  FormModelV2,\n  useNodeRender,\n  WorkflowNodeEntity,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { useOverflow } from '../hooks/use-overflow';\nimport { useModel } from '../hooks/use-model';\nimport { useSize } from '../hooks';\nimport { CommentEditorFormField } from '../constant';\nimport { MoreButton } from './more-button';\nimport { CommentEditor } from './editor';\nimport { ContentDragArea } from './content-drag-area';\nimport { CommentContainer } from './container';\nimport { BorderArea } from './border-area';\n\nexport const CommentRender: FC<{\n  node: WorkflowNodeEntity;\n}> = (props) => {\n  const { node } = props;\n  const model = useModel();\n\n  const { selected: focused, selectNode, nodeRef, deleteNode } = useNodeRender();\n\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formControl = formModel?.formControl;\n\n  const { width, height, onResize } = useSize();\n  const { overflow, updateOverflow } = useOverflow({ model, height });\n\n  return (\n    <div\n      className=\"workflow-comment\"\n      style={{\n        width,\n        height,\n      }}\n      ref={nodeRef}\n      data-node-selected={String(focused)}\n      onMouseEnter={updateOverflow}\n      onMouseDown={(e) => {\n        setTimeout(() => {\n          // 防止 selectNode 拦截事件，导致 slate 编辑器无法聚焦\n          selectNode(e);\n          // eslint-disable-next-line @typescript-eslint/no-magic-numbers -- delay\n        }, 20);\n      }}\n    >\n      <Form control={formControl}>\n        <>\n          {/* 背景 */}\n          <CommentContainer focused={focused} style={{ height }}>\n            <Field name={CommentEditorFormField.Note}>\n              {({ field }: FieldRenderProps<string>) => (\n                <>\n                  {/** 编辑器 */}\n                  <CommentEditor model={model} value={field.value} onChange={field.onChange} />\n                  {/* 内容拖拽区域（点击后隐藏） */}\n                  <ContentDragArea model={model} focused={focused} overflow={overflow} />\n                  {/* 更多按钮 */}\n                  <MoreButton node={node} focused={focused} deleteNode={deleteNode} />\n                </>\n              )}\n            </Field>\n          </CommentContainer>\n          {/* 边框 */}\n          <BorderArea model={model} overflow={overflow} onResize={onResize} />\n        </>\n      </Form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/components/resize-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, type FC } from 'react';\n\nimport { MouseTouchEvent, useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\n\ninterface IResizeArea {\n  model: CommentEditorModel;\n  onResize?: () => {\n    resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;\n    resizeEnd: () => void;\n  };\n  getDelta?: (delta: { x: number; y: number }) => {\n    top: number;\n    right: number;\n    bottom: number;\n    left: number;\n  };\n  style?: CSSProperties;\n}\n\nexport const ResizeArea: FC<IResizeArea> = (props) => {\n  const { model, onResize, getDelta, style } = props;\n\n  const playground = usePlayground();\n\n  const { selectNode } = useNodeRender();\n\n  const handleResizeStart = (\n    startResizeEvent: React.MouseEvent | React.TouchEvent | MouseEvent\n  ) => {\n    MouseTouchEvent.preventDefault(startResizeEvent);\n    startResizeEvent.stopPropagation();\n    if (!onResize) {\n      return;\n    }\n    const { resizing, resizeEnd } = onResize();\n    model.setFocus(false);\n    selectNode(startResizeEvent as React.MouseEvent);\n    playground.node.focus(); // 防止节点无法被删除\n\n    const { clientX: startX, clientY: startY } = MouseTouchEvent.getEventCoord(\n      startResizeEvent as MouseEvent\n    );\n\n    const handleResizing = (mouseMoveEvent: MouseEvent | TouchEvent) => {\n      const { clientX: moveX, clientY: moveY } = MouseTouchEvent.getEventCoord(mouseMoveEvent);\n      const deltaX = moveX - startX;\n      const deltaY = moveY - startY;\n      const delta = getDelta?.({ x: deltaX, y: deltaY });\n      if (!delta || !resizing) {\n        return;\n      }\n      resizing(delta);\n    };\n\n    const handleResizeEnd = () => {\n      resizeEnd();\n      document.removeEventListener('mousemove', handleResizing);\n      document.removeEventListener('mouseup', handleResizeEnd);\n      document.removeEventListener('click', handleResizeEnd);\n      document.removeEventListener('touchmove', handleResizing);\n      document.removeEventListener('touchend', handleResizeEnd);\n      document.removeEventListener('touchcancel', handleResizeEnd);\n    };\n\n    document.addEventListener('mousemove', handleResizing);\n    document.addEventListener('mouseup', handleResizeEnd);\n    document.addEventListener('click', handleResizeEnd);\n    document.addEventListener('touchmove', handleResizing, { passive: false });\n    document.addEventListener('touchend', handleResizeEnd);\n    document.addEventListener('touchcancel', handleResizeEnd);\n  };\n\n  return (\n    <div\n      className=\"workflow-comment-resize-area\"\n      style={style}\n      data-flow-editor-selectable=\"false\"\n      onMouseDown={handleResizeStart}\n      onTouchStart={handleResizeStart}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention -- enum */\n\nexport enum CommentEditorFormField {\n  Size = 'size',\n  Note = 'note',\n}\n\n/** 编辑器事件 */\nexport enum CommentEditorEvent {\n  /** 初始化事件 */\n  Init = 'init',\n  /** 内容变更事件 */\n  Change = 'change',\n  /** 多选事件 */\n  MultiSelect = 'multiSelect',\n  /** 单选事件 */\n  Select = 'select',\n  /** 失焦事件 */\n  Blur = 'blur',\n}\n\nexport const CommentEditorDefaultValue = '';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useSize } from './use-size';\nexport { usePlaceholder } from './use-placeholder';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/hooks/use-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo } from 'react';\n\nimport {\n  FlowNodeFormData,\n  FormModelV2,\n  useEntityFromContext,\n  useNodeRender,\n  WorkflowNodeEntity,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorFormField } from '../constant';\n\nexport const useModel = () => {\n  const node = useEntityFromContext<WorkflowNodeEntity>();\n  const { selected: focused } = useNodeRender();\n\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n\n  const model = useMemo(() => new CommentEditorModel(), []);\n\n  // 同步失焦状态\n  useEffect(() => {\n    if (focused) {\n      return;\n    }\n    model.setFocus(focused);\n  }, [focused, model]);\n\n  // 同步表单值初始化\n  useEffect(() => {\n    const value = formModel.getValueIn<string>(CommentEditorFormField.Note);\n    model.setInitValue(value); // 设置初始值\n    model.selectEnd(); // 设置初始化光标位置\n  }, [formModel, model]);\n\n  // 同步表单外部值变化：undo/redo/协同\n  useEffect(() => {\n    const disposer = formModel.onFormValuesChange(({ name }) => {\n      if (name !== CommentEditorFormField.Note && name !== '') {\n        return;\n      }\n      const value = formModel.getValueIn<string>(CommentEditorFormField.Note);\n      model.setValue(value);\n    });\n    return () => disposer.dispose();\n  }, [formModel, model]);\n\n  return model;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/hooks/use-overflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useState, useEffect } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorEvent } from '../constant';\n\nexport const useOverflow = (params: { model: CommentEditorModel; height: number }) => {\n  const { model, height } = params;\n  const playground = usePlayground();\n\n  const [overflow, setOverflow] = useState(false);\n\n  const isOverflow = useCallback((): boolean => {\n    if (!model.element) {\n      return false;\n    }\n    return model.element.scrollHeight > model.element.clientHeight;\n  }, [model, height, playground]);\n\n  // 更新 overflow\n  const updateOverflow = useCallback(() => {\n    setOverflow(isOverflow());\n  }, [isOverflow]);\n\n  // 监听高度变化\n  useEffect(() => {\n    updateOverflow();\n  }, [height, updateOverflow]);\n\n  // 监听 change 事件\n  useEffect(() => {\n    const changeDisposer = model.on((params) => {\n      if (params.type !== CommentEditorEvent.Change && params.type !== CommentEditorEvent.Init) {\n        return;\n      }\n      updateOverflow();\n    });\n    return () => {\n      changeDisposer.dispose();\n    };\n  }, [model, updateOverflow]);\n\n  return { overflow, updateOverflow };\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/hooks/use-placeholder.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorEvent } from '../constant';\n\nexport const usePlaceholder = (params: { model: CommentEditorModel }): string | undefined => {\n  const { model } = params;\n\n  const [placeholder, setPlaceholder] = useState<string | undefined>('Enter a comment...');\n\n  // 监听 change 事件\n  useEffect(() => {\n    const changeDisposer = model.on((params) => {\n      if (params.type !== CommentEditorEvent.Change && params.type !== CommentEditorEvent.Init) {\n        return;\n      }\n      if (params.value) {\n        setPlaceholder(undefined);\n      } else {\n        setPlaceholder('Enter a comment...');\n      }\n    });\n    return () => {\n      changeDisposer.dispose();\n    };\n  }, [model]);\n\n  return placeholder;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/hooks/use-size.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport {\n  FlowNodeFormData,\n  FormModelV2,\n  FreeOperationType,\n  HistoryService,\n  TransformData,\n  useCurrentEntity,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorFormField } from '../constant';\n\nexport const useSize = () => {\n  const node = useCurrentEntity();\n  const nodeMeta = node.getNodeMeta();\n  const playground = usePlayground();\n  const historyService = useService(HistoryService);\n  const { size = { width: 240, height: 150 } } = nodeMeta;\n  const transform = node.getData(TransformData);\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formSize = formModel.getValueIn<{ width: number; height: number }>(\n    CommentEditorFormField.Size\n  );\n\n  const [width, setWidth] = useState(formSize?.width ?? size.width);\n  const [height, setHeight] = useState(formSize?.height ?? size.height);\n\n  // 初始化表单值\n  useEffect(() => {\n    const initSize = formModel.getValueIn<{ width: number; height: number }>(\n      CommentEditorFormField.Size\n    );\n    if (!initSize) {\n      formModel.setValueIn(CommentEditorFormField.Size, {\n        width,\n        height,\n      });\n    }\n  }, [formModel, width, height]);\n\n  // 同步表单外部值变化：初始化/undo/redo/协同\n  useEffect(() => {\n    const disposer = formModel.onFormValuesChange(({ name }) => {\n      if (name !== CommentEditorFormField.Size && name !== '') {\n        return;\n      }\n      const newSize = formModel.getValueIn<{ width: number; height: number }>(\n        CommentEditorFormField.Size\n      );\n      if (!newSize) {\n        return;\n      }\n      setWidth(newSize.width);\n      setHeight(newSize.height);\n    });\n    return () => disposer.dispose();\n  }, [formModel]);\n\n  const onResize = useCallback(() => {\n    const resizeState = {\n      width,\n      height,\n      originalWidth: width,\n      originalHeight: height,\n      positionX: transform.position.x,\n      positionY: transform.position.y,\n      offsetX: 0,\n      offsetY: 0,\n    };\n    const resizing = (delta: { top: number; right: number; bottom: number; left: number }) => {\n      if (!resizeState) {\n        return;\n      }\n\n      const { zoom } = playground.config;\n\n      const top = delta.top / zoom;\n      const right = delta.right / zoom;\n      const bottom = delta.bottom / zoom;\n      const left = delta.left / zoom;\n\n      const minWidth = 120;\n      const minHeight = 80;\n\n      const newWidth = Math.max(minWidth, resizeState.originalWidth + right - left);\n      const newHeight = Math.max(minHeight, resizeState.originalHeight + bottom - top);\n\n      // 如果宽度或高度小于最小值，则不更新偏移量\n      const newOffsetX =\n        (left > 0 || right < 0) && newWidth <= minWidth\n          ? resizeState.offsetX\n          : left / 2 + right / 2;\n      const newOffsetY =\n        (top > 0 || bottom < 0) && newHeight <= minHeight ? resizeState.offsetY : top;\n\n      const newPositionX = resizeState.positionX + newOffsetX;\n      const newPositionY = resizeState.positionY + newOffsetY;\n\n      resizeState.width = newWidth;\n      resizeState.height = newHeight;\n      resizeState.offsetX = newOffsetX;\n      resizeState.offsetY = newOffsetY;\n\n      // 更新状态\n      setWidth(newWidth);\n      setHeight(newHeight);\n\n      // 更新偏移量\n      transform.update({\n        position: {\n          x: newPositionX,\n          y: newPositionY,\n        },\n      });\n    };\n\n    const resizeEnd = () => {\n      historyService.transact(() => {\n        historyService.pushOperation(\n          {\n            type: FreeOperationType.dragNodes,\n            value: {\n              ids: [node.id],\n              value: [\n                {\n                  x: resizeState.positionX + resizeState.offsetX,\n                  y: resizeState.positionY + resizeState.offsetY,\n                },\n              ],\n              oldValue: [\n                {\n                  x: resizeState.positionX,\n                  y: resizeState.positionY,\n                },\n              ],\n            },\n          },\n          {\n            noApply: true,\n          }\n        );\n        formModel.setValueIn(CommentEditorFormField.Size, {\n          width: resizeState.width,\n          height: resizeState.height,\n        });\n      });\n    };\n\n    return {\n      resizing,\n      resizeEnd,\n    };\n  }, [node, width, height, transform, playground, formModel, historyService]);\n\n  return {\n    width,\n    height,\n    onResize,\n  };\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { CommentRender } from './components';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorEventParams } from './type';\nimport { CommentEditorDefaultValue, CommentEditorEvent } from './constant';\n\nexport class CommentEditorModel {\n  private innerValue: string = CommentEditorDefaultValue;\n\n  private emitter: Emitter<CommentEditorEventParams> = new Emitter();\n\n  private editor: HTMLTextAreaElement;\n\n  /** 注册事件 */\n  public on = this.emitter.event;\n\n  /** 获取当前值 */\n  public get value(): string {\n    return this.innerValue;\n  }\n\n  /** 外部设置模型值 */\n  public setValue(value: string = CommentEditorDefaultValue): void {\n    if (!this.initialized) {\n      return;\n    }\n    if (value === this.innerValue) {\n      return;\n    }\n    this.innerValue = value;\n    this.syncEditorValue();\n    this.emitter.fire({\n      type: CommentEditorEvent.Change,\n      value: this.innerValue,\n    });\n  }\n\n  /** 外部设置模型值 */\n  public setInitValue(value: string = CommentEditorDefaultValue): void {\n    if (!this.initialized) {\n      return;\n    }\n    if (value === this.innerValue) {\n      return;\n    }\n    this.innerValue = value;\n    this.syncEditorValue();\n    this.emitter.fire({\n      type: CommentEditorEvent.Init,\n      value: this.innerValue,\n    });\n  }\n\n  public set element(el: HTMLTextAreaElement) {\n    if (this.initialized) {\n      return;\n    }\n    this.editor = el;\n  }\n\n  /** 获取编辑器 DOM 节点 */\n  public get element(): HTMLTextAreaElement {\n    return this.editor;\n  }\n\n  /** 编辑器聚焦/失焦 */\n  public setFocus(focused: boolean): void {\n    if (!this.initialized) {\n      return;\n    }\n    if (focused && !this.focused) {\n      this.editor.focus();\n    } else if (!focused && this.focused) {\n      this.editor.blur();\n      this.deselect();\n      this.emitter.fire({\n        type: CommentEditorEvent.Blur,\n      });\n    }\n  }\n\n  /** 选择末尾 */\n  public selectEnd(): void {\n    if (!this.initialized) {\n      return;\n    }\n    // 获取文本长度\n    const length = this.editor.value.length;\n    // 将选择范围设置为文本末尾(开始位置和结束位置都是文本长度)\n    this.editor.setSelectionRange(length, length);\n  }\n\n  /** 获取聚焦状态 */\n  public get focused(): boolean {\n    return document.activeElement === this.editor;\n  }\n\n  /** 取消选择文本 */\n  private deselect(): void {\n    const selection: Selection | null = window.getSelection();\n\n    // 清除所有选择区域\n    if (selection) {\n      selection.removeAllRanges();\n    }\n  }\n\n  /** 是否初始化 */\n  private get initialized(): boolean {\n    return Boolean(this.editor);\n  }\n\n  /**\n   * 同步编辑器实例内容\n   * > **NOTICE:** *为确保不影响性能，应仅在外部值变更导致编辑器值与模型值不一致时调用*\n   */\n  private syncEditorValue(): void {\n    if (!this.initialized) {\n      return;\n    }\n    this.editor.value = this.innerValue;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/comment/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CommentEditorEvent } from './constant';\n\ninterface CommentEditorChangeEvent {\n  type: CommentEditorEvent.Change;\n  value: string;\n}\n\ninterface CommentEditorMultiSelectEvent {\n  type: CommentEditorEvent.MultiSelect;\n}\n\ninterface CommentEditorSelectEvent {\n  type: CommentEditorEvent.Select;\n}\n\ninterface CommentEditorBlurEvent {\n  type: CommentEditorEvent.Blur;\n}\n\ninterface CommentEditorInitEvent {\n  type: CommentEditorEvent.Init;\n  value: string;\n}\n\nexport type CommentEditorEventParams =\n  | CommentEditorChangeEvent\n  | CommentEditorMultiSelectEvent\n  | CommentEditorSelectEvent\n  | CommentEditorBlurEvent\n  | CommentEditorInitEvent;\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/color.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ntype GroupColor = {\n  '50': string;\n  '300': string;\n  '400': string;\n};\n\nexport const defaultColor = 'Blue';\n\nexport const groupColors: Record<string, GroupColor> = {\n  Red: {\n    '50': '#fef2f2',\n    '300': '#fca5a5',\n    '400': '#f87171',\n  },\n  Orange: {\n    '50': '#fff7ed',\n    '300': '#fdba74',\n    '400': '#fb923c',\n  },\n  Amber: {\n    '50': '#fffbeb',\n    '300': '#fcd34d',\n    '400': '#fbbf24',\n  },\n  Yellow: {\n    '50': '#fef9c3',\n    '300': '#fde047',\n    '400': '#facc15',\n  },\n  Lime: {\n    '50': '#f7fee7',\n    '300': '#bef264',\n    '400': '#a3e635',\n  },\n  Green: {\n    '50': '#f0fdf4',\n    '300': '#86efac',\n    '400': '#4ade80',\n  },\n  Emerald: {\n    '50': '#ecfdf5',\n    '300': '#6ee7b7',\n    '400': '#34d399',\n  },\n  Teal: {\n    '50': '#f0fdfa',\n    '300': '#5eead4',\n    '400': '#2dd4bf',\n  },\n  Cyan: {\n    '50': '#ecfeff',\n    '300': '#67e8f9',\n    '400': '#22d3ee',\n  },\n  Sky: {\n    '50': '#ecfeff',\n    '300': '#7dd3fc',\n    '400': '#38bdf8',\n  },\n  Blue: {\n    '50': '#eff6ff',\n    '300': '#93c5fd',\n    '400': '#60a5fa',\n  },\n  Indigo: {\n    '50': '#eef2ff',\n    '300': '#a5b4fc',\n    '400': '#818cf8',\n  },\n  Violet: {\n    '50': '#f5f3ff',\n    '300': '#c4b5fd',\n    '400': '#a78bfa',\n  },\n  Purple: {\n    '50': '#faf5ff',\n    '300': '#d8b4fe',\n    '400': '#c084fc',\n  },\n  Fuchsia: {\n    '50': '#fdf4ff',\n    '300': '#f0abfc',\n    '400': '#e879f9',\n  },\n  Pink: {\n    '50': '#fdf2f8',\n    '300': '#f9a8d4',\n    '400': '#f472b6',\n  },\n  Rose: {\n    '50': '#fff1f2',\n    '300': '#fda4af',\n    '400': '#fb7185',\n  },\n  Gray: {\n    '50': '#f9fafb',\n    '300': '#d1d5db',\n    '400': '#9ca3af',\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/background.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC, useEffect } from 'react';\n\nimport { useWatch, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\ninterface GroupBackgroundProps {\n  node: WorkflowNodeEntity;\n  style?: CSSProperties;\n  selected: boolean;\n}\n\nexport const GroupBackground: FC<GroupBackgroundProps> = ({ node, style, selected }) => {\n  const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;\n  const color = groupColors[colorName];\n\n  useEffect(() => {\n    const styleElement = document.createElement('style');\n\n    // 使用独特的选择器\n    const styleContent = `\n      .workflow-group-render[data-group-id=\"${node.id}\"] .workflow-group-background {\n        border: 1px solid ${color['300']};\n      }\n\n      .workflow-group-render.selected[data-group-id=\"${node.id}\"] .workflow-group-background {\n        border: 1px solid #4e40e5;\n      }\n    `;\n\n    styleElement.textContent = styleContent;\n    document.head.appendChild(styleElement);\n\n    return () => {\n      styleElement.remove();\n    };\n  }, [color]);\n\n  return (\n    <div\n      className=\"workflow-group-background\"\n      data-flow-editor-selectable=\"true\"\n      style={{\n        ...style,\n        backgroundColor: `${color['300']}${selected ? '40' : '29'}`,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/color.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { Popover, Tooltip } from '@douyinfe/semi-ui';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\nexport const GroupColor: FC = () => (\n  <Field<string> name={GroupField.Color}>\n    {({ field }) => {\n      const colorName = field.value ?? defaultColor;\n      return (\n        <Popover\n          position=\"top\"\n          mouseLeaveDelay={300}\n          content={\n            <div className=\"workflow-group-color-palette\">\n              {Object.entries(groupColors).map(([name, color]) => (\n                <Tooltip content={name} key={name} mouseEnterDelay={300}>\n                  <span\n                    className=\"workflow-group-color-item\"\n                    key={name}\n                    style={{\n                      backgroundColor: color['300'],\n                      borderColor: name === colorName ? color['400'] : '#fff',\n                    }}\n                    onClick={() => field.onChange(name)}\n                  />\n                </Tooltip>\n              ))}\n            </div>\n          }\n        >\n          <span\n            className=\"workflow-group-color\"\n            style={{\n              backgroundColor: groupColors[colorName]['300'],\n            }}\n          />\n        </Popover>\n      );\n    }}\n  </Field>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/header.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FC, ReactNode, MouseEvent, CSSProperties, TouchEvent } from 'react';\n\nimport { useWatch } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\ninterface GroupHeaderProps {\n  onDrag: (e: MouseEvent | TouchEvent) => void;\n  onFocus: () => void;\n  onBlur: () => void;\n  children: ReactNode;\n  style?: CSSProperties;\n}\n\nexport const GroupHeader: FC<GroupHeaderProps> = ({ onDrag, onFocus, onBlur, children, style }) => {\n  const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;\n  const color = groupColors[colorName];\n  return (\n    <div\n      className=\"workflow-group-header\"\n      data-flow-editor-selectable=\"false\"\n      onMouseDown={onDrag}\n      onTouchStart={onDrag}\n      onFocus={onFocus}\n      onBlur={onBlur}\n      style={{\n        ...style,\n        backgroundColor: color['50'],\n        borderColor: color['300'],\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/icon-group.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\ninterface IconGroupProps {\n  size?: number;\n}\n\nexport const IconGroup: FC<IconGroupProps> = ({ size }) => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 10 10\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={{\n      width: size,\n      height: size,\n    }}\n  >\n    <path\n      id=\"group\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      stroke=\"none\"\n      d=\"M 0.009766 10 L 0.009766 9.990234 L 0 9.990234 L 0 7.5 L 1 7.5 L 1 9 L 2.5 9 L 2.5 10 L 0.009766 10 Z M 3.710938 10 L 3.710938 9 L 6.199219 9 L 6.199219 10 L 3.710938 10 Z M 7.5 10 L 7.5 9 L 9 9 L 9 7.5 L 10 7.5 L 10 9.990234 L 9.990234 9.990234 L 9.990234 10 L 7.5 10 Z M 0 6.289063 L 0 3.800781 L 1 3.800781 L 1 6.289063 L 0 6.289063 Z M 9 6.289063 L 9 3.800781 L 10 3.800781 L 10 6.289063 L 9 6.289063 Z M 0 2.5 L 0 0.009766 L 0.009766 0.009766 L 0.009766 0 L 2.5 0 L 2.5 1 L 1 1 L 1 2.5 L 0 2.5 Z M 9 2.5 L 9 1 L 7.5 1 L 7.5 0 L 9.990234 0 L 9.990234 0.009766 L 10 0.009766 L 10 2.5 L 9 2.5 Z M 3.710938 1 L 3.710938 0 L 6.199219 0 L 6.199219 1 L 3.710938 1 Z\"\n    />\n  </svg>\n);\n\nexport const IconUngroup: FC<IconGroupProps> = ({ size }) => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 10 10\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={{\n      width: size,\n      height: size,\n    }}\n  >\n    <path\n      id=\"ungroup\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      stroke=\"none\"\n      d=\"M 9.654297 10.345703 L 8.808594 9.5 L 7.175781 9.5 L 7.175781 8.609375 L 7.917969 8.609375 L 1.390625 2.082031 L 1.390625 2.824219 L 0.5 2.824219 L 0.5 1.191406 L -0.345703 0.345703 L 0.283203 -0.283203 L 1.166016 0.599609 L 2.724609 0.599609 L 2.724609 1.490234 L 2.056641 1.490234 L 8.509766 7.943359 L 8.509766 7.275391 L 9.400391 7.275391 L 9.400391 8.833984 L 10.283203 9.716797 L 9.654297 10.345703 Z M 0.509766 9.5 L 0.509766 9.490234 L 0.5 9.490234 L 0.5 7.275391 L 1.390625 7.275391 L 1.390625 8.609375 L 2.724609 8.609375 L 2.724609 9.5 L 0.509766 9.5 Z M 3.802734 9.5 L 3.802734 8.609375 L 6.017578 8.609375 L 6.017578 9.5 L 3.802734 9.5 Z M 0.5 6.197266 L 0.5 3.982422 L 1.390625 3.982422 L 1.390625 6.197266 L 0.5 6.197266 Z M 8.509766 6.197266 L 8.509766 3.982422 L 9.400391 3.982422 L 9.400391 6.197266 L 8.509766 6.197266 Z M 8.509766 2.824219 L 8.509766 1.490234 L 7.175781 1.490234 L 7.175781 0.599609 L 9.390625 0.599609 L 9.390625 0.609375 L 9.400391 0.609375 L 9.400391 2.824219 L 8.509766 2.824219 Z M 3.802734 1.490234 L 3.802734 0.599609 L 6.017578 0.599609 L 6.017578 1.490234 L 3.802734 1.490234 Z\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { GroupNodeRender } from './node-render';\nexport { IconGroup } from './icon-group';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MouseEvent, useEffect } from 'react';\n\nimport {\n  FlowNodeFormData,\n  Form,\n  FormModelV2,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\nimport { useNodeSize } from '@flowgram.ai/free-container-plugin';\n\nimport { HEADER_HEIGHT, HEADER_PADDING } from '../constant';\nimport { UngroupButton } from './ungroup';\nimport { GroupTools } from './tools';\nimport { GroupTips } from './tips';\nimport { GroupHeader } from './header';\nimport { GroupBackground } from './background';\n\nexport const GroupNodeRender = () => {\n  const { node, selected, selectNode, nodeRef, startDrag, onFocus, onBlur } = useNodeRender();\n  const nodeSize = useNodeSize();\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formControl = formModel?.formControl;\n\n  const { height, width } = nodeSize ?? {};\n  const nodeHeight = height ?? 0;\n\n  useEffect(() => {\n    // prevent lines in outside cannot be selected - 防止外层线条不可选中\n    const element = node.renderData.node;\n    element.style.pointerEvents = 'none';\n  }, [node]);\n\n  return (\n    <div\n      className={`workflow-group-render ${selected ? 'selected' : ''}`}\n      ref={nodeRef}\n      data-group-id={node.id}\n      data-node-selected={String(selected)}\n      onMouseDown={selectNode}\n      onClick={(e) => {\n        selectNode(e);\n      }}\n      style={{\n        width,\n        height,\n      }}\n    >\n      <Form control={formControl}>\n        <>\n          <GroupHeader\n            onDrag={(e) => {\n              startDrag(e as MouseEvent);\n              e.stopPropagation();\n            }}\n            onFocus={onFocus}\n            onBlur={onBlur}\n            style={{\n              height: HEADER_HEIGHT,\n            }}\n          >\n            <GroupTools />\n          </GroupHeader>\n          <GroupTips />\n          <UngroupButton node={node} />\n          <GroupBackground\n            node={node}\n            selected={selected}\n            style={{\n              top: HEADER_HEIGHT + HEADER_PADDING,\n              height: nodeHeight - HEADER_HEIGHT - HEADER_PADDING,\n            }}\n          />\n        </>\n      </Form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/global-store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention -- no need */\n\nconst STORAGE_KEY = 'workflow-move-into-group-tip-visible';\nconst STORAGE_VALUE = 'false';\n\nexport class TipsGlobalStore {\n  private static _instance?: TipsGlobalStore;\n\n  public static get instance(): TipsGlobalStore {\n    if (!this._instance) {\n      this._instance = new TipsGlobalStore();\n    }\n    return this._instance;\n  }\n\n  private closed = false;\n\n  public isClosed(): boolean {\n    return this.isCloseForever() || this.closed;\n  }\n\n  public close(): void {\n    this.closed = true;\n  }\n\n  public isCloseForever(): boolean {\n    return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;\n  }\n\n  public closeForever(): void {\n    localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/icon-close.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconClose = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"none\" viewBox=\"0 0 16 16\">\n    <path\n      fill=\"#060709\"\n      fillOpacity=\"0.5\"\n      d=\"M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useControlTips } from './use-control';\nimport { GroupTipsStyle } from './style';\nimport { isMacOS } from './is-mac-os';\nimport { IconClose } from './icon-close';\n\nexport const GroupTips = () => {\n  const { visible, close, closeForever } = useControlTips();\n\n  if (!visible) {\n    return null;\n  }\n\n  return (\n    <GroupTipsStyle className={'workflow-group-tips'}>\n      <div className=\"container\">\n        <div className=\"content\">\n          <p className=\"text\">{`Hold ${isMacOS ? 'Cmd ⌘' : 'Ctrl'} to drag node out`}</p>\n          <div\n            className=\"space\"\n            style={{\n              width: 0,\n            }}\n          />\n        </div>\n        <div className=\"actions\">\n          <p className=\"close-forever\" onClick={closeForever}>\n            Never Remind\n          </p>\n          <div className=\"close\" onClick={close}>\n            <IconClose />\n          </div>\n        </div>\n      </div>\n    </GroupTipsStyle>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/is-mac-os.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const GroupTipsStyle = styled.div`\n  position: absolute;\n  top: 35px;\n\n  width: 100%;\n  height: 28px;\n  white-space: nowrap;\n  pointer-events: auto;\n\n  .container {\n    display: inline-flex;\n    justify-content: center;\n    height: 100%;\n    width: 100%;\n    background-color: rgb(255 255 255);\n    border-radius: 8px 8px 0 0;\n\n    .content {\n      overflow: hidden;\n      display: inline-flex;\n      align-items: center;\n      justify-content: flex-start;\n\n      width: fit-content;\n      height: 100%;\n      padding: 0 12px;\n\n      .text {\n        font-size: 14px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 20px;\n        color: rgba(15, 21, 40, 82%);\n        text-overflow: ellipsis;\n        margin: 0;\n      }\n\n      .space {\n        width: 128px;\n      }\n    }\n\n    .actions {\n      display: flex;\n      gap: 8px;\n      align-items: center;\n\n      height: 28px;\n      padding: 0 12px;\n\n      .close-forever {\n        cursor: pointer;\n\n        padding: 0 3px;\n\n        font-size: 12px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 12px;\n        color: rgba(32, 41, 69, 62%);\n        margin: 0;\n      }\n\n      .close {\n        display: flex;\n        cursor: pointer;\n        height: 100%;\n        align-items: center;\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tips/use-control.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { useCurrentEntity, useService } from '@flowgram.ai/free-layout-editor';\nimport {\n  NodeIntoContainerService,\n  NodeIntoContainerType,\n} from '@flowgram.ai/free-container-plugin';\n\nimport { TipsGlobalStore } from './global-store';\n\nexport const useControlTips = () => {\n  const node = useCurrentEntity();\n  const [visible, setVisible] = useState(false);\n  const globalStore = TipsGlobalStore.instance;\n\n  const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);\n\n  const show = useCallback(() => {\n    if (globalStore.isClosed()) {\n      return;\n    }\n\n    setVisible(true);\n  }, [globalStore]);\n\n  const close = useCallback(() => {\n    globalStore.close();\n    setVisible(false);\n  }, [globalStore]);\n\n  const closeForever = useCallback(() => {\n    globalStore.closeForever();\n    close();\n  }, [close, globalStore]);\n\n  useEffect(() => {\n    // 监听移入\n    const inDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.In) {\n        return;\n      }\n      if (e.targetContainer === node) {\n        show();\n      }\n    });\n    // 监听移出事件\n    const outDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.Out) {\n        return;\n      }\n      if (e.sourceContainer === node && !node.blocks.length) {\n        setVisible(false);\n      }\n    });\n    return () => {\n      inDisposer.dispose();\n      outDisposer.dispose();\n    };\n  }, [nodeIntoContainerService, node, show, close, visible]);\n\n  return {\n    visible,\n    close,\n    closeForever,\n  };\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/title.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useState } from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { Input } from '@douyinfe/semi-ui';\n\nimport { GroupField } from '../constant';\n\nexport const GroupTitle: FC = () => {\n  const [inputting, setInputting] = useState(false);\n  return (\n    <Field<string> name={GroupField.Title}>\n      {({ field }) =>\n        inputting ? (\n          <Input\n            autoFocus\n            className=\"workflow-group-title-input\"\n            size=\"small\"\n            value={field.value}\n            onChange={field.onChange}\n            onMouseDown={(e) => e.stopPropagation()}\n            onBlur={() => setInputting(false)}\n            draggable={false}\n            onEnterPress={() => setInputting(false)}\n          />\n        ) : (\n          <p className=\"workflow-group-title\" onDoubleClick={() => setInputting(true)}>\n            {field.value ?? 'Group'}\n          </p>\n        )\n      }\n    </Field>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { IconHandle } from '@douyinfe/semi-icons';\n\nimport { GroupTitle } from './title';\nimport { GroupColor } from './color';\n\nexport const GroupTools: FC = () => (\n  <div className=\"workflow-group-tools\">\n    <IconHandle className=\"workflow-group-tools-drag\" />\n    <GroupTitle />\n    <GroupColor />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/components/ungroup.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC } from 'react';\n\nimport { CommandRegistry, useService, WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';\nimport { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';\nimport { Button, Tooltip } from '@douyinfe/semi-ui';\n\nimport { IconUngroup } from './icon-group';\n\ninterface UngroupButtonProps {\n  node: WorkflowNodeEntity;\n  style?: CSSProperties;\n}\n\nexport const UngroupButton: FC<UngroupButtonProps> = ({ node, style }) => {\n  const commandRegistry = useService(CommandRegistry);\n  return (\n    <Tooltip content=\"Ungroup\">\n      <div className=\"workflow-group-ungroup\" style={style}>\n        <Button\n          icon={<IconUngroup size={14} />}\n          style={{ height: 30, width: 30 }}\n          theme=\"borderless\"\n          type=\"tertiary\"\n          onClick={() => {\n            commandRegistry.executeCommand(WorkflowGroupCommand.Ungroup, node);\n          }}\n        />\n      </div>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const HEADER_HEIGHT = 30;\nexport const HEADER_PADDING = 5;\n\nexport enum GroupField {\n  Title = 'title',\n  Color = 'color',\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.workflow-group-render {\n    border-radius: 8px;\n    pointer-events: none;\n}\n\n.workflow-group-background {\n    box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n}\n.workflow-group-header {\n    height: 30px;\n    width: fit-content;\n    background-color: #fefce8;\n    border: 1px solid #facc15;\n    border-radius: 8px;\n    padding-right: 8px;\n    pointer-events: auto;\n}\n\n.workflow-group-ungroup {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    height: 30px;\n    width: 30px;\n    position: absolute;\n    top: 35px;\n    right: 0;\n    border-radius: 8px;\n    cursor: pointer;\n    pointer-events: auto;\n}\n\n.workflow-group-ungroup .semi-button {\n    color: #9ca3af;\n}\n\n.workflow-group-ungroup:hover .semi-button {\n    color: #374151;\n}\n\n.workflow-group-background {\n    position: absolute;\n    pointer-events: none;\n    top: 0;\n    background-color: #fddf4729;\n    border: 1px solid #fde047;\n    border-radius: 8px;\n    width: 100%;\n}\n\n.workflow-group-render.selected .workflow-group-background {\n    border: 1px solid #facc15;\n}\n\n.workflow-group-tools {\n    display: flex;\n    justify-content: flex-start;\n    align-items: center;\n    gap: 4px;\n    height: 100%;\n    cursor: move;\n    color: oklch(44.6% 0.043 257.281);\n    font-size: 14px;\n}\n.workflow-group-title {\n    margin: 0;\n    max-width: 242px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    font-weight: 500;\n}\n\n.workflow-group-tools-drag {\n    height: 100%;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    padding-left: 4px;\n}\n\n.workflow-group-color {\n    width: 16px;\n    height: 16px;\n    border-radius: 8px;\n    background-color: #fde047;\n    margin-left: 4px;\n    cursor: pointer;\n}\n\n.workflow-group-title-input {\n    width: 242px;\n    border: none;\n    color: #374151;\n}\n\n.workflow-group-color-palette {\n    display: grid;\n    grid-template-columns: repeat(6, 24px);\n    gap: 12px;\n    margin: 8px;\n    padding: 8px;\n}\n\n.workflow-group-color-item {\n    width: 24px;\n    height: 24px;\n    border-radius: 50%;\n    background-color: #fde047;\n    cursor: pointer;\n    border: 3px solid;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/group/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.css';\n\nexport { GroupNodeRender } from './components';\nexport { IconGroup } from './components';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './base-node';\nexport * from './line-add-button';\nexport * from './node-panel';\nexport * from './comment';\nexport * from './group';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/line-add-button/button.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconPlusCircle = () => (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"add\">\n      <path\n        id=\"background\"\n        fill=\"#ffffff\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 24 12 C 24 5.372583 18.627417 0 12 0 C 5.372583 0 -0 5.372583 -0 12 C -0 18.627417 5.372583 24 12 24 C 18.627417 24 24 18.627417 24 12 Z\"\n      />\n      <path\n        id=\"content\"\n        fill=\"currentColor\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 22 12.005 C 22 6.482153 17.522848 2.004999 12 2.004999 C 6.477152 2.004999 2 6.482153 2 12.005 C 2 17.527847 6.477152 22.004999 12 22.004999 C 17.522848 22.004999 22 17.527847 22 12.005 Z\"\n      />\n      <path\n        id=\"cross\"\n        fill=\"#ffffff\"\n        stroke=\"none\"\n        d=\"M 11.411996 16.411797 C 11.411996 16.736704 11.675362 17 12.00023 17 C 12.325109 17 12.588474 16.736704 12.588474 16.411797 L 12.588474 12.58826 L 16.41201 12.58826 C 16.736919 12.58826 17.000216 12.324894 17.000216 12.000015 C 17.000216 11.675147 16.736919 11.411781 16.41201 11.411781 L 12.588474 11.411781 L 12.588474 7.588234 C 12.588474 7.263367 12.325109 7 12.00023 7 C 11.675362 7 11.411996 7.263367 11.411996 7.588234 L 11.411996 11.411781 L 7.588449 11.411781 C 7.263581 11.411781 7.000215 11.675147 7.000215 12.000015 C 7.000215 12.324894 7.263581 12.58826 7.588449 12.58826 L 11.411996 12.58826 L 11.411996 16.411797 Z\"\n      />\n    </g>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/line-add-button/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.line-add-button {\n  position: absolute;\n  width: 24px;\n  height: 24px;\n  cursor: pointer;\n  color: inherit;\n  pointer-events: all;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/line-add-button/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport {\n  WorkflowNodePanelService,\n  WorkflowNodePanelUtils,\n} from '@flowgram.ai/free-node-panel-plugin';\nimport { LineRenderProps } from '@flowgram.ai/free-lines-plugin';\nimport {\n  delay,\n  HistoryService,\n  useService,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n} from '@flowgram.ai/free-layout-editor';\n\nimport './index.less';\nimport { useVisible } from './use-visible';\nimport { IconPlusCircle } from './button';\n\nexport const LineAddButton = (props: LineRenderProps) => {\n  const { line, selected, hovered, color } = props;\n  const visible = useVisible({ line, selected, hovered });\n  const nodePanelService = useService<WorkflowNodePanelService>(WorkflowNodePanelService);\n  const document = useService(WorkflowDocument);\n  const dragService = useService(WorkflowDragService);\n  const linesManager = useService(WorkflowLinesManager);\n  const historyService = useService(HistoryService);\n\n  const { fromPort, toPort } = line;\n\n  const onClick = useCallback(async () => {\n    // calculate the middle point of the line - 计算线条的中点位置\n    const position = {\n      x: (line.position.from.x + line.position.to.x) / 2,\n      y: (line.position.from.y + line.position.to.y) / 2,\n    };\n\n    // get container node for the new node - 获取新节点的容器节点\n    const containerNode = fromPort!.node.parent;\n\n    // show node selection panel - 显示节点选择面板\n    const result = await nodePanelService.singleSelectNodePanel({\n      position,\n      containerNode,\n      panelProps: {\n        enableScrollClose: true,\n        fromPort,\n      },\n    });\n    if (!result) {\n      return;\n    }\n\n    const { nodeType, nodeJSON } = result;\n\n    // adjust position for the new node - 调整新节点的位置\n    const nodePosition = WorkflowNodePanelUtils.adjustNodePosition({\n      nodeType,\n      position,\n      fromPort,\n      toPort,\n      containerNode,\n      document,\n      dragService,\n    });\n\n    // create new workflow node - 创建新的工作流节点\n    const node: WorkflowNodeEntity = document.createWorkflowNodeByType(\n      nodeType,\n      nodePosition,\n      nodeJSON ?? ({} as WorkflowNodeJSON),\n      containerNode?.id\n    );\n\n    // auto offset subsequent nodes - 自动偏移后续节点\n    if (fromPort && toPort) {\n      WorkflowNodePanelUtils.subNodesAutoOffset({\n        node,\n        fromPort,\n        toPort,\n        containerNode,\n        historyService,\n        dragService,\n        linesManager,\n      });\n    }\n\n    // wait for node render - 等待节点渲染\n    await delay(20);\n\n    // build connection lines - 构建连接线\n    WorkflowNodePanelUtils.buildLine({\n      fromPort,\n      node,\n      toPort,\n      linesManager,\n    });\n\n    // remove original line - 移除原始线条\n    line.dispose();\n  }, []);\n\n  if (!visible) {\n    return <></>;\n  }\n\n  return (\n    <div\n      className=\"line-add-button\"\n      style={{\n        transform: `translate(-50%, -50%) translate(${line.center.labelX}px, ${line.center.labelY}px)`,\n        color,\n      }}\n      data-testid=\"sdk.workflow.canvas.line.add\"\n      data-line-id={line.id}\n      onClick={onClick}\n    >\n      <IconPlusCircle />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/line-add-button/use-visible.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { usePlayground, WorkflowLineEntity } from '@flowgram.ai/free-layout-editor';\n\nimport './index.less';\n\nexport const useVisible = (params: {\n  line: WorkflowLineEntity;\n  selected?: boolean;\n  hovered?: boolean;\n}): boolean => {\n  const playground = usePlayground();\n  const { line, selected = false, hovered } = params;\n  if (line.disposed) {\n    // 在 dispose 后，再去获取 line.to | line.from 会导致错误创建端口\n    return false;\n  }\n  if (playground.config.readonly) {\n    return false;\n  }\n  if (!selected && !hovered) {\n    return false;\n  }\n  return true;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/node-menu/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useCallback, useState, type MouseEvent } from 'react';\n\nimport {\n  delay,\n  useClientContext,\n  usePlaygroundTools,\n  useService,\n  WorkflowDragService,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\nimport { NodeIntoContainerService } from '@flowgram.ai/free-container-plugin';\nimport { IconButton, Dropdown } from '@douyinfe/semi-ui';\nimport { IconMore } from '@douyinfe/semi-icons';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport { PasteShortcut } from '../../shortcuts/paste';\nimport { CopyShortcut } from '../../shortcuts/copy';\n\ninterface NodeMenuProps {\n  node: WorkflowNodeEntity;\n  updateTitleEdit?: (setEditing: boolean) => void;\n  deleteNode: () => void;\n}\n\nexport const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode, updateTitleEdit }) => {\n  const [visible, setVisible] = useState(true);\n  const clientContext = useClientContext();\n  const registry = node.getNodeRegistry<FlowNodeRegistry>();\n  const nodeIntoContainerService = useService(NodeIntoContainerService);\n  const selectService = useService(WorkflowSelectService);\n  const dragService = useService(WorkflowDragService);\n  const canMoveOut = nodeIntoContainerService.canMoveOutContainer(node);\n  const tools = usePlaygroundTools();\n\n  const rerenderMenu = useCallback(() => {\n    // force destroy component - 强制销毁组件触发重新渲染\n    setVisible(false);\n    requestAnimationFrame(() => {\n      setVisible(true);\n    });\n  }, []);\n\n  const handleMoveOut = useCallback(\n    async (e: MouseEvent) => {\n      e.stopPropagation();\n      const sourceParent = node.parent;\n      // move out of container - 移出容器\n      nodeIntoContainerService.moveOutContainer({ node });\n      await delay(16);\n      // clear invalid lines - 清除非法线条\n      await nodeIntoContainerService.clearInvalidLines({\n        dragNode: node,\n        sourceParent,\n      });\n      rerenderMenu();\n      // select node - 选中节点\n      selectService.selectNode(node);\n      // start drag node - 开始拖拽\n      dragService.startDragSelectedNodes(e);\n    },\n    [nodeIntoContainerService, node, rerenderMenu]\n  );\n\n  const handleCopy = useCallback(\n    (e: React.MouseEvent) => {\n      const copyShortcut = new CopyShortcut(clientContext);\n      const pasteShortcut = new PasteShortcut(clientContext);\n      const data = copyShortcut.toClipboardData([node]);\n      pasteShortcut.apply(data);\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n\n  const handleDelete = useCallback(\n    (e: React.MouseEvent) => {\n      deleteNode();\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n  const handleEditTitle = useCallback(\n    (e: React.MouseEvent) => {\n      updateTitleEdit?.(true);\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [updateTitleEdit]\n  );\n\n  const handleAutoLayout = useCallback(\n    (e: React.MouseEvent) => {\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n      tools.autoLayout({\n        containerNode: node,\n        enableAnimation: true,\n        animationDuration: 1000,\n        disableFitView: true,\n      });\n    },\n    [tools]\n  );\n\n  if (!visible) {\n    return <></>;\n  }\n\n  return (\n    <Dropdown\n      trigger=\"hover\"\n      position=\"bottomRight\"\n      render={\n        <Dropdown.Menu>\n          <Dropdown.Item onClick={handleEditTitle}>Edit Title</Dropdown.Item>\n          {canMoveOut && <Dropdown.Item onClick={handleMoveOut}>Move out</Dropdown.Item>}\n          <Dropdown.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>\n            Create Copy\n          </Dropdown.Item>\n          {registry.meta.isContainer && (\n            <Dropdown.Item onClick={handleAutoLayout}>Auto Layout</Dropdown.Item>\n          )}\n          <Dropdown.Item\n            onClick={handleDelete}\n            disabled={!!(registry.canDelete?.(clientContext, node) || registry.meta!.deleteDisable)}\n          >\n            Delete\n          </Dropdown.Item>\n        </Dropdown.Menu>\n      }\n    >\n      <IconButton\n        color=\"secondary\"\n        size=\"small\"\n        theme=\"borderless\"\n        icon={<IconMore />}\n        onClick={(e) => e.stopPropagation()}\n      />\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/node-panel/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-placeholder {\n  width: 360px;\n\n  background-color: rgba(252, 252, 255, 1);\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  border-radius: 8px;\n  box-shadow: 0 4px 12px 0 rgba(0, 0, 0, 2%), 0 2px 6px 0 rgba(0, 0, 0, 4%);\n}\n\n\n.node-placeholder-skeleton {\n  width: 100%;\n  padding: 12px;\n  background-color: rgba(252, 252, 255, 1);\n  border-radius: 8px;\n\n\n  .semi-skeleton-avatar {\n    background-color: rgba(68, 83, 130, 0.25);\n  }\n\n  .semi-skeleton-title {\n    height: 16px;\n    background-color: rgba(82, 100, 154, 0.13);\n    border-radius: 4px;\n  }\n}\n\n\n.node-placeholder-hd {\n  display: flex;\n  align-items: center;\n  margin-bottom: 12px;\n}\n\n.node-placeholder-avatar {\n  width: 24px;\n  height: 24px;\n  margin-right: 8px;\n  border-radius: 6px;\n}\n\n.node-placeholder-content {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: 3px;\n}\n\n.node-placeholder-footer {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  gap: 2.5px;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/node-panel/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef } from 'react';\n\nimport { NodePanelRenderProps as NodePanelRenderPropsDefault } from '@flowgram.ai/free-node-panel-plugin';\nimport { WorkflowPortEntity } from '@flowgram.ai/free-layout-editor';\nimport { Popover } from '@douyinfe/semi-ui';\n\nimport { NodePlaceholder } from './node-placeholder';\nimport { NodeList } from './node-list';\nimport './index.less';\n\ninterface NodePanelRenderProps extends NodePanelRenderPropsDefault {\n  panelProps?: {\n    fromPort?: WorkflowPortEntity; // 从哪个端口添加 From which port to add\n    enableNodePlaceholder?: boolean;\n  };\n}\nexport const NodePanel: React.FC<NodePanelRenderProps> = (props) => {\n  const { onSelect, position, onClose, containerNode, panelProps = {} } = props;\n  const { enableNodePlaceholder, fromPort } = panelProps;\n  const ref = useRef<HTMLDivElement>(null);\n\n  return (\n    <Popover\n      trigger=\"click\"\n      visible={true}\n      onVisibleChange={(v) => (v ? null : onClose())}\n      content={<NodeList onSelect={onSelect} containerNode={containerNode} fromPort={fromPort} />}\n      getPopupContainer={containerNode ? () => ref.current || document.body : undefined}\n      placement=\"right\"\n      popupAlign={{ offset: [30, 0] }}\n      overlayStyle={{\n        padding: 0,\n      }}\n    >\n      <div\n        ref={ref}\n        style={\n          enableNodePlaceholder\n            ? {\n                position: 'absolute',\n                top: position.y - 61.5,\n                left: position.x,\n                width: 360,\n                height: 100,\n              }\n            : {\n                position: 'absolute',\n                top: position.y,\n                left: position.x,\n                width: 0,\n                height: 0,\n              }\n        }\n      >\n        {enableNodePlaceholder && <NodePlaceholder />}\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/node-panel/node-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { FC } from 'react';\n\nimport styled from 'styled-components';\nimport { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  useClientContext,\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { canContainNode } from '../../utils';\nimport { FlowNodeRegistry } from '../../typings';\nimport { nodeRegistries } from '../../nodes';\n\nconst NodeWrap = styled.div`\n  width: 100%;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  font-size: 19px;\n  padding: 0 15px;\n  &:hover {\n    background-color: hsl(252deg 62% 55% / 9%);\n    color: hsl(252 62% 54.9%);\n  }\n`;\n\nconst NodeLabel = styled.div`\n  font-size: 12px;\n  margin-left: 10px;\n`;\n\ninterface NodeProps {\n  label: string;\n  icon: JSX.Element;\n  onClick: React.MouseEventHandler<HTMLDivElement>;\n  disabled: boolean;\n}\n\nfunction Node(props: NodeProps) {\n  return (\n    <NodeWrap\n      data-testid={`demo-free-node-list-${props.label}`}\n      onClick={props.disabled ? undefined : props.onClick}\n      style={props.disabled ? { opacity: 0.3 } : {}}\n    >\n      <div style={{ fontSize: 14 }}>{props.icon}</div>\n      <NodeLabel>{props.label}</NodeLabel>\n    </NodeWrap>\n  );\n}\n\nconst NodesWrap = styled.div`\n  max-height: 500px;\n  overflow: auto;\n  &::-webkit-scrollbar {\n    display: none;\n  }\n`;\n\ninterface NodeListProps {\n  onSelect: NodePanelRenderProps['onSelect'];\n  fromPort?: WorkflowPortEntity; // 从哪个端口添加 From which port to add\n  containerNode?: WorkflowNodeEntity;\n}\n\nexport const NodeList: FC<NodeListProps> = (props) => {\n  const { onSelect, containerNode, fromPort } = props;\n  const context = useClientContext();\n  const handleClick = (e: React.MouseEvent, registry: FlowNodeRegistry) => {\n    const json = registry.onAdd?.(context);\n    onSelect({\n      nodeType: registry.type as string,\n      selectEvent: e,\n      nodeJSON: json,\n    });\n  };\n  console.log('>>> fromNode', fromPort?.node);\n  return (\n    <NodesWrap style={{ width: 80 * 2 + 20 }}>\n      {nodeRegistries\n        .filter((register) => register.meta.nodePanelVisible !== false)\n        .filter((register) => {\n          if (register.meta.onlyInContainer) {\n            return register.meta.onlyInContainer === containerNode?.flowNodeType;\n          }\n          /**\n           * 循环节点无法嵌套循环节点\n           * Loop node cannot nest loop node\n           */\n          if (containerNode && !canContainNode(register.type, containerNode.flowNodeType)) {\n            return false;\n          }\n          return true;\n        })\n        .map((registry) => (\n          <Node\n            key={registry.type}\n            disabled={!(registry.canAdd?.(context) ?? true)}\n            icon={\n              <img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info?.icon} />\n            }\n            label={registry.type as string}\n            onClick={(e) => handleClick(e, registry)}\n          />\n        ))}\n    </NodesWrap>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/node-panel/node-placeholder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Skeleton } from '@douyinfe/semi-ui';\n\nexport const NodePlaceholder = () => (\n  <div className=\"node-placeholder\" data-testid=\"workflow.detail.node-panel.placeholder\">\n    <Skeleton\n      className=\"node-placeholder-skeleton\"\n      loading={true}\n      active={true}\n      placeholder={\n        <div className=\"\">\n          <div className=\"node-placeholder-hd\">\n            <Skeleton.Avatar shape=\"square\" className=\"node-placeholder-avatar\" />\n            <Skeleton.Title style={{ width: 141 }} />\n          </div>\n          <div className=\"node-placeholder-content\">\n            <div className=\"node-placeholder-footer\">\n              <Skeleton.Title style={{ width: 85 }} />\n              <Skeleton.Title style={{ width: 241 }} />\n            </div>\n            <Skeleton.Title style={{ width: 220 }} />\n          </div>\n        </div>\n      }\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/problem-panel/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ProblemButton } from './problem-panel';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/problem-panel/problem-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService, WorkflowSelectService } from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Spin, Typography, Avatar, Tooltip } from '@douyinfe/semi-ui';\nimport { IconUploadError, IconClose } from '@douyinfe/semi-icons';\n\nimport { useProblemPanel, useNodeFormPanel } from '../../plugins/panel-manager-plugin/hooks';\nimport { useWatchValidate } from './use-watch-validate';\n\nexport const ProblemPanel = () => {\n  const { results, loading } = useWatchValidate();\n\n  const selectService = useService(WorkflowSelectService);\n\n  const { close: closePanel } = useProblemPanel();\n  const { open: openNodeFormPanel } = useNodeFormPanel();\n\n  return (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        borderRadius: '8px',\n        background: 'rgb(251, 251, 251)',\n        border: '1px solid rgba(82,100,154, 0.13)',\n      }}\n    >\n      <div\n        style={{\n          display: 'flex',\n          height: '50px',\n          alignItems: 'center',\n          justifyContent: 'space-between',\n          padding: '0 12px',\n        }}\n      >\n        <div style={{ display: 'flex', alignItems: 'center', columnGap: '4px', height: '100%' }}>\n          <Typography.Text strong>Problem</Typography.Text>\n          {loading && <Spin size=\"small\" style={{ lineHeight: '0' }} />}\n        </div>\n        <IconButton\n          type=\"tertiary\"\n          theme=\"borderless\"\n          icon={<IconClose />}\n          onClick={() => closePanel()}\n        />\n      </div>\n      <div style={{ padding: '12px', display: 'flex', flexDirection: 'column', rowGap: '4px' }}>\n        {results.map((i) => (\n          <div\n            key={i.node.id}\n            style={{\n              display: 'flex',\n              alignItems: 'center',\n              border: '1px solid #999',\n              borderRadius: '4px',\n              padding: '0 4px',\n              cursor: 'pointer',\n            }}\n            onClick={() => {\n              selectService.selectNodeAndScrollToView(i.node);\n              openNodeFormPanel({ nodeId: i.node.id });\n            }}\n          >\n            <Avatar\n              style={{ flexShrink: '0' }}\n              src={i.node.getNodeRegistry().info.icon}\n              size=\"24px\"\n              shape=\"square\"\n            />\n            <div style={{ marginLeft: '8px' }}>\n              <Typography.Text>{i.node.form?.values.title}</Typography.Text>\n              <br />\n              <Typography.Text type=\"danger\">\n                {i.feedbacks.map((i) => i.feedbackText).join(', ')}\n              </Typography.Text>\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport const ProblemButton = () => {\n  const { open } = useProblemPanel();\n  return (\n    <Tooltip content=\"Problem\">\n      <IconButton\n        type=\"tertiary\"\n        theme=\"borderless\"\n        icon={<IconUploadError />}\n        onClick={() => open()}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/problem-panel/use-watch-validate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { debounce } from 'lodash-es';\nimport { useService, WorkflowDocument } from '@flowgram.ai/free-layout-editor';\n\nimport { ValidateService, type ValidateResult } from '../../services/validate-service';\n\nconst DEBOUNCE_TIME = 1000;\n\nexport const useWatchValidate = () => {\n  const [results, setResults] = useState<ValidateResult[]>([]);\n  const [loading, setLoading] = useState(false);\n\n  const validateService = useService(ValidateService);\n  const workflowDocument = useService(WorkflowDocument);\n\n  const debounceValidate = useCallback(\n    debounce(async () => {\n      const res = await validateService.validateNodes();\n      validateService.validateLines();\n      setResults(res);\n      setLoading(false);\n    }, DEBOUNCE_TIME),\n    [validateService]\n  );\n\n  const validate = () => {\n    setLoading(true);\n    debounceValidate();\n  };\n\n  useEffect(() => {\n    validate();\n    const disposable = workflowDocument.onContentChange(() => {\n      validate();\n    });\n    return () => disposable.dispose();\n  }, []);\n\n  return { results, loading };\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/selector-box-popover/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FunctionComponent } from 'react';\n\nimport { SelectorBoxPopoverProps } from '@flowgram.ai/free-layout-editor';\nimport { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';\nimport { Button, ButtonGroup, Tooltip } from '@douyinfe/semi-ui';\nimport { IconCopy, IconDeleteStroked, IconExpand, IconShrink } from '@douyinfe/semi-icons';\n\nimport { IconGroup } from '../group';\nimport { FlowCommandId } from '../../shortcuts/constants';\n\nconst BUTTON_HEIGHT = 24;\n\nexport const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({\n  bounds,\n  children,\n  flowSelectConfig,\n  commandRegistry,\n}) => (\n  <>\n    <div\n      style={{\n        position: 'absolute',\n        left: bounds.right,\n        top: bounds.top,\n        transform: 'translate(-100%, -100%)',\n      }}\n      onMouseDown={(e) => {\n        e.stopPropagation();\n      }}\n    >\n      <ButtonGroup\n        size=\"small\"\n        style={{ display: 'flex', flexWrap: 'nowrap', height: BUTTON_HEIGHT }}\n      >\n        <Tooltip content={'Collapse'}>\n          <Button\n            icon={<IconShrink />}\n            style={{ height: BUTTON_HEIGHT }}\n            type=\"primary\"\n            theme=\"solid\"\n            onMouseDown={(e) => {\n              commandRegistry.executeCommand(FlowCommandId.COLLAPSE);\n            }}\n          />\n        </Tooltip>\n\n        <Tooltip content={'Expand'}>\n          <Button\n            icon={<IconExpand />}\n            style={{ height: BUTTON_HEIGHT }}\n            type=\"primary\"\n            theme=\"solid\"\n            onMouseDown={(e) => {\n              commandRegistry.executeCommand(FlowCommandId.EXPAND);\n            }}\n          />\n        </Tooltip>\n\n        <Tooltip content={'Create Group'}>\n          <Button\n            icon={<IconGroup size={14} />}\n            style={{ height: BUTTON_HEIGHT }}\n            type=\"primary\"\n            theme=\"solid\"\n            onClick={() => {\n              commandRegistry.executeCommand(WorkflowGroupCommand.Group);\n            }}\n          />\n        </Tooltip>\n\n        <Tooltip content={'Copy'}>\n          <Button\n            icon={<IconCopy />}\n            style={{ height: BUTTON_HEIGHT }}\n            type=\"primary\"\n            theme=\"solid\"\n            onClick={() => {\n              commandRegistry.executeCommand(FlowCommandId.COPY);\n            }}\n          />\n        </Tooltip>\n\n        <Tooltip content={'Delete'}>\n          <Button\n            type=\"primary\"\n            theme=\"solid\"\n            icon={<IconDeleteStroked />}\n            style={{ height: BUTTON_HEIGHT }}\n            onClick={() => {\n              commandRegistry.executeCommand(FlowCommandId.DELETE);\n            }}\n          />\n        </Tooltip>\n      </ButtonGroup>\n    </div>\n    <div>{children}</div>\n  </>\n);\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/sidebar/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/sidebar/node-form-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, startTransition } from 'react';\n\nimport {\n  PlaygroundEntityContext,\n  useRefresh,\n  useClientContext,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeMeta } from '../../typings';\nimport { useNodeFormPanel } from '../../plugins/panel-manager-plugin/hooks';\nimport { IsSidebarContext } from '../../context';\nimport { SidebarNodeRenderer } from './sidebar-node-renderer';\n\nexport interface NodeFormPanelProps {\n  nodeId: string;\n}\n\nexport const NodeFormPanel: React.FC<NodeFormPanelProps> = ({ nodeId }) => {\n  const { selection, playground, document } = useClientContext();\n  const refresh = useRefresh();\n  const { close: closePanel } = useNodeFormPanel();\n  const handleClose = useCallback(() => {\n    // Sidebar delayed closing\n    startTransition(() => {\n      closePanel();\n    });\n  }, []);\n  const node = document.getNode(nodeId);\n  const sidebarDisabled = node?.getNodeMeta<FlowNodeMeta>()?.sidebarDisabled === true;\n  /**\n   * Listen readonly\n   */\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => {\n      handleClose();\n      refresh();\n    });\n    return () => disposable.dispose();\n  }, [playground]);\n  /**\n   * Listen selection\n   */\n  useEffect(() => {\n    const toDispose = selection.onSelectionChanged(() => {\n      /**\n       * 如果没有选中任何节点，则自动关闭侧边栏\n       * If no node is selected, the sidebar is automatically closed\n       */\n      if (selection.selection.length === 0) {\n        handleClose();\n      } else if (selection.selection.length === 1 && selection.selection[0] !== node) {\n        handleClose();\n      }\n    });\n    return () => toDispose.dispose();\n  }, [selection, node, handleClose]);\n  /**\n   * Close when node disposed\n   */\n  useEffect(() => {\n    if (node) {\n      const toDispose = node.onDispose(() => {\n        closePanel();\n      });\n      return () => toDispose.dispose();\n    }\n    return () => {};\n  }, [node, sidebarDisabled, handleClose]);\n  /**\n   * Cloze when sidebar disabled\n   */\n  useEffect(() => {\n    if (!node || sidebarDisabled || playground.config.readonly) {\n      handleClose();\n    }\n  }, [node, sidebarDisabled, playground.config.readonly]);\n\n  if (!node || sidebarDisabled || playground.config.readonly) {\n    return null;\n  }\n\n  return (\n    <IsSidebarContext.Provider value={true}>\n      <PlaygroundEntityContext.Provider key={node.id} value={node}>\n        <SidebarNodeRenderer node={node} />\n      </PlaygroundEntityContext.Provider>\n    </IsSidebarContext.Provider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/sidebar/sidebar-node-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useNodeRender, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRenderContext } from '../../context';\n\nexport function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {\n  const { node } = props;\n  const nodeRender = useNodeRender(node);\n\n  return (\n    <NodeRenderContext.Provider value={nodeRender}>\n      <div\n        style={{\n          background: 'rgb(251, 251, 251)',\n          height: '100%',\n          width: '100%',\n          borderRadius: 8,\n          border: '1px solid rgba(82,100,154, 0.13)',\n          boxSizing: 'border-box',\n        }}\n      >\n        {nodeRender.form?.render()}\n      </div>\n    </NodeRenderContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useFields } from './use-fields';\nexport { useFormMeta } from './use-form-meta';\nexport { useSyncDefault } from './use-sync-default';\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/hooks/use-fields.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TestRunFormField, TestRunFormMeta } from '../testrun-form/type';\n\nexport const useFields = (params: {\n  formMeta: TestRunFormMeta;\n  values: Record<string, unknown>;\n  setValues: (values: Record<string, unknown>) => void;\n}): TestRunFormField[] => {\n  const { formMeta, values, setValues } = params;\n\n  // Convert each meta item to a form field with value and onChange handler\n  const fields: TestRunFormField[] = formMeta.map((meta) => {\n    const currentValue = values[meta.name] ?? meta.defaultValue;\n\n    const handleChange = (newValue: unknown): void => {\n      setValues({\n        ...values,\n        [meta.name]: newValue,\n      });\n    };\n\n    return {\n      ...meta,\n      value: currentValue,\n      onChange: handleChange,\n    };\n  });\n\n  return fields;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/hooks/use-form-meta.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { useService, WorkflowDocument } from '@flowgram.ai/free-layout-editor';\nimport { IJsonSchema, JsonSchemaBasicType } from '@flowgram.ai/form-materials';\n\nimport { TestRunFormMetaItem } from '../testrun-form/type';\nimport { WorkflowNodeType } from '../../../nodes';\n\nconst DEFAULT_DECLARE: IJsonSchema = {\n  type: 'object',\n  properties: {},\n};\n\nexport const useFormMeta = (): TestRunFormMetaItem[] => {\n  const document = useService(WorkflowDocument);\n\n  const startNode = useMemo(\n    () => document.root.blocks.find((node) => node.flowNodeType === WorkflowNodeType.Start),\n    [document]\n  );\n\n  const workflowInputs = startNode?.form?.getValueIn<IJsonSchema>('outputs') || DEFAULT_DECLARE;\n\n  // Add state for form values\n  const formMeta = useMemo(() => {\n    const formFields: TestRunFormMetaItem[] = [];\n    Object.entries(workflowInputs.properties!).forEach(([name, property]) => {\n      formFields.push({\n        type: property.type as JsonSchemaBasicType,\n        name,\n        defaultValue: property.default,\n        required: workflowInputs.required?.includes(name) ?? false,\n        itemsType: property.items?.type as JsonSchemaBasicType,\n      });\n    });\n    return formFields;\n  }, [workflowInputs]);\n\n  return formMeta;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/hooks/use-sync-default.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { TestRunFormMeta, TestRunFormMetaItem } from '../testrun-form/type';\n\nconst getDefaultValue = (meta: TestRunFormMetaItem) => {\n  if (['object', 'array', 'map'].includes(meta.type) && typeof meta.defaultValue === 'string') {\n    return JSON.parse(meta.defaultValue);\n  }\n  return meta.defaultValue;\n};\n\nexport const useSyncDefault = (params: {\n  formMeta: TestRunFormMeta;\n  values: Record<string, unknown>;\n  setValues: (values: Record<string, unknown>) => void;\n}) => {\n  const { formMeta, values, setValues } = params;\n\n  useEffect(() => {\n    let formMetaValues: Record<string, unknown> = {};\n    formMeta.map((meta) => {\n      // If there is no value in values but there is a default value, trigger onChange once\n      if (!(meta.name in values) && meta.defaultValue !== undefined) {\n        formMetaValues = { ...formMetaValues, [meta.name]: getDefaultValue(meta) };\n      }\n    });\n    setValues({\n      ...values,\n      ...formMetaValues,\n    });\n  }, [formMeta]);\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/json-value-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { JsonCodeEditor } from '@flowgram.ai/form-materials';\n\nexport function JsonValueEditor({\n  value,\n  onChange,\n}: {\n  value: Record<string, unknown>;\n  onChange: (value: Record<string, unknown>) => void;\n}) {\n  const defaultJsonText = useMemo(() => JSON.stringify(value, null, 2), [value]);\n\n  const [jsonText, setJsonText] = useState(defaultJsonText);\n\n  const effectVersion = useRef(0);\n  const changeVersion = useRef(0);\n\n  const handleJsonTextChange = (text: string) => {\n    setJsonText(text);\n    try {\n      const jsonValue = JSON.parse(text);\n      onChange(jsonValue);\n      changeVersion.current++;\n    } catch (e) {\n      // ignore\n    }\n  };\n\n  useEffect(() => {\n    // more effect compared with change\n    effectVersion.current = effectVersion.current + 1;\n    if (effectVersion.current === changeVersion.current) {\n      return;\n    }\n    effectVersion.current = changeVersion.current;\n\n    setJsonText(JSON.stringify(value, null, 2));\n  }, [value]);\n\n  return <JsonCodeEditor value={jsonText} onChange={handleJsonTextChange} />;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-status-group {\n  padding: 6px;\n  font-weight: 500;\n  color: #333;\n  font-size: 15px;\n  display: flex;\n  align-items: center;\n\n  &-icon {\n    transform: rotate(-90deg);\n    transition: transform 0.2s;\n    cursor: pointer;\n    margin-right: 4px;\n\n    &-expanded {\n      transform: rotate(0deg);\n    }\n  }\n\n  &-tag {\n    margin-left: 4px;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/group/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useState } from 'react';\n\nimport classNames from 'classnames';\nimport { Tag } from '@douyinfe/semi-ui';\nimport { IconSmallTriangleDown } from '@douyinfe/semi-icons';\n\nimport { DataStructureViewer } from '../viewer';\n\nimport styles from './index.module.less';\n\ninterface NodeStatusGroupProps {\n  title: string;\n  data: unknown;\n  optional?: boolean;\n  disableCollapse?: boolean;\n}\n\nconst isObjectHasContent = (obj: any = {}): boolean => obj && Object.keys(obj).length > 0;\n\nexport const NodeStatusGroup: FC<NodeStatusGroupProps> = ({\n  title,\n  data,\n  optional = false,\n  disableCollapse = false,\n}) => {\n  const hasContent = isObjectHasContent(data);\n  const [isExpanded, setIsExpanded] = useState(true);\n\n  if (optional && !hasContent) {\n    return null;\n  }\n\n  return (\n    <>\n      <div\n        className={styles['node-status-group']}\n        onClick={() => hasContent && !disableCollapse && setIsExpanded(!isExpanded)}\n      >\n        {!disableCollapse && (\n          <IconSmallTriangleDown\n            className={classNames(styles['node-status-group-icon'], {\n              [styles['node-status-group-icon-expanded']]: isExpanded && hasContent,\n            })}\n          />\n        )}\n        <span>{title}:</span>\n        {!hasContent && (\n          <Tag size=\"small\" className={styles['node-status-group-tag']}>\n            null\n          </Tag>\n        )}\n      </div>\n      {hasContent && isExpanded ? <DataStructureViewer data={data} /> : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-status-header {\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  border-radius: 8px;\n  background-color: #fff;\n  position: absolute;\n  top: calc(100% + 8px);\n  left: 0;\n  width: 100%;\n  min-width: 360px;\n\n  &-content {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 6px;\n\n    &-opened {\n      padding-bottom: 0;\n    }\n\n    .status-title {\n      height: 24px;\n      display: flex;\n      align-items: center;\n      column-gap: 8px;\n      min-width: 0;\n\n      :global(.coz-tag) {\n        height: 20px;\n      }\n      :global(.semi-tag-content) {\n        font-weight: 500;\n        line-height: 16px;\n        font-size: 12px;\n      }\n      :global(.semi-tag-suffix-icon > div) {\n        font-size: 14px;\n      }\n    }\n\n    .status-btns {\n      height: 24px;\n      display: flex;\n      align-items: center;\n      column-gap: 4px;\n\n      .is-show-detail {\n        transform: rotate(180deg);\n      }\n    }\n  }\n}"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/header/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport classNames from 'classnames';\nimport { IconChevronDown } from '@douyinfe/semi-icons';\n\nimport { useNodeRenderContext } from '../../../../hooks';\n\nimport styles from './index.module.less';\n\ninterface NodeStatusBarProps {\n  header?: React.ReactNode;\n  defaultShowDetail?: boolean;\n  extraBtns?: React.ReactNode[];\n}\n\nexport const NodeStatusHeader: React.FC<React.PropsWithChildren<NodeStatusBarProps>> = ({\n  header,\n  defaultShowDetail,\n  children,\n  extraBtns = [],\n}) => {\n  const [showDetail, setShowDetail] = useState(defaultShowDetail);\n  const { selectNode } = useNodeRenderContext();\n\n  const handleToggleShowDetail = (e: React.MouseEvent) => {\n    e.stopPropagation();\n    selectNode(e);\n    setShowDetail(!showDetail);\n  };\n\n  return (\n    <div\n      className={styles['node-status-header']}\n      // 必须要禁止 down 冒泡，防止判定圈选和 node hover（不支持多边形）\n      onMouseDown={(e) => e.stopPropagation()}\n    >\n      <div\n        className={classNames(\n          styles['node-status-header-content'],\n          showDetail && styles['node-status-header-content-opened']\n        )}\n        // 必须要禁止 down 冒泡，防止判定圈选和 node hover（不支持多边形）\n        onMouseDown={(e) => e.stopPropagation()}\n        // 其他事件统一走点击事件，且也需要阻止冒泡\n        onClick={handleToggleShowDetail}\n      >\n        <div className={styles['status-title']}>\n          {header}\n          {extraBtns.length > 0 ? extraBtns : null}\n        </div>\n        <div className={styles['status-btns']}>\n          <IconChevronDown\n            className={classNames({\n              [styles['is-show-detail']]: showDetail,\n            })}\n          />\n        </div>\n      </div>\n      {showDetail ? children : null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { NodeReport } from '@flowgram.ai/runtime-interface';\nimport { useCurrentEntity, useService } from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowRuntimeService } from '../../../plugins/runtime-plugin/runtime-service';\nimport { NodeStatusRender } from './render';\n\nconst useNodeReport = () => {\n  const node = useCurrentEntity();\n  const [report, setReport] = useState<NodeReport>();\n\n  const runtimeService = useService(WorkflowRuntimeService);\n\n  useEffect(() => {\n    const reportDisposer = runtimeService.onNodeReportChange((nodeReport) => {\n      if (nodeReport.id !== node.id) {\n        return;\n      }\n      setReport((prev) =>({\n        ...prev,\n        ...nodeReport,\n      }));\n    });\n    const resetDisposer = runtimeService.onReset(() => {\n      setReport(undefined);\n    });\n    return () => {\n      reportDisposer.dispose();\n      resetDisposer.dispose();\n    };\n  }, []);\n\n  return report;\n};\n\nexport const NodeStatusBar = () => {\n  const report = useNodeReport();\n\n  if (!report) {\n    return null;\n  }\n\n  return <NodeStatusRender report={report} />;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.nodeStatus {\n  &Succeed {\n    background-color: rgba(105, 209, 140, 0.3);\n    color: #00b42a;\n  }\n\n  &Processing {\n    background-color: rgba(153, 187, 255, 0.3);\n    color: #4d53e8;\n  }\n\n  &Failed {\n    background-color: rgba(255, 163, 171, 0.3);\n    color: #f53f3f;\n  }\n}\n\n.icon {\n  &.processing {\n    color: rgba(77, 83, 232, 1);\n  }\n}\n\n.round {\n  border-radius: 50%;\n}\n\n.desc {\n  margin: 0;\n}\n\n.count {\n  font-weight: 500;\n  color: #333;\n  font-size: 15px;\n  margin-left: 12px;\n}\n\n.snapshotNavigation {\n  margin: 12px;\n  display: flex;\n  gap: 8px;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.snapshotButton {\n  min-width: 32px;\n  height: 32px;\n  padding: 0;\n  border-radius: 4px;\n  font-size: 12px;\n  border: 1px solid;\n  font-weight: 500;\n\n  &.active {\n    border-color: #4d53e8;\n    font-weight: 800;\n  }\n\n  &.inactive {\n    border-color: rgba(29, 28, 35, 0.08);\n  }\n}\n\n.snapshotSelect {\n  width: 90px;\n  height: 32px;\n  border: 1px solid;\n\n  &.active {\n    border-color: #4d53e8;\n  }\n\n  &.inactive {\n    border-color: rgba(29, 28, 35, 0.08);\n  }\n}\n\n.container {\n  width: 100%;\n  height: 100%;\n  padding: 4px 2px 4px 2px;\n}\n\n.error {\n  padding: 12px;\n  color: red;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useMemo, useState } from 'react';\n\nimport classnames from 'classnames';\nimport { NodeReport, WorkflowStatus } from '@flowgram.ai/runtime-interface';\nimport { Tag, Button, Select } from '@douyinfe/semi-ui';\nimport { IconSpin } from '@douyinfe/semi-icons';\n\nimport { NodeStatusHeader } from '../header';\nimport { NodeStatusGroup } from '../group';\nimport { IconWarningFill } from '../../../../assets/icon-warning';\nimport { IconSuccessFill } from '../../../../assets/icon-success';\n\nimport styles from './index.module.less';\n\ninterface NodeStatusRenderProps {\n  report: NodeReport;\n}\n\nconst msToSeconds = (ms: number): string => (ms / 1000).toFixed(2) + 's';\nconst displayCount = 6;\n\nexport const NodeStatusRender: FC<NodeStatusRenderProps> = ({ report }) => {\n  const { status: nodeStatus } = report;\n  const [currentSnapshotIndex, setCurrentSnapshotIndex] = useState(0);\n\n  const snapshots = report.snapshots || [];\n  const currentSnapshot = snapshots[currentSnapshotIndex] || snapshots[0];\n\n  // 节点 5 个状态\n  const isNodePending = nodeStatus === WorkflowStatus.Pending;\n  const isNodeProcessing = nodeStatus === WorkflowStatus.Processing;\n  const isNodeFailed = nodeStatus === WorkflowStatus.Failed;\n  const isNodeSucceed = nodeStatus === WorkflowStatus.Succeeded;\n  const isNodeCancelled = nodeStatus === WorkflowStatus.Cancelled;\n\n  const tagColor = useMemo(() => {\n    if (isNodeSucceed) {\n      return styles.nodeStatusSucceed;\n    }\n    if (isNodeFailed) {\n      return styles.nodeStatusFailed;\n    }\n    if (isNodeProcessing) {\n      return styles.nodeStatusProcessing;\n    }\n  }, [isNodeSucceed, isNodeFailed, isNodeProcessing]);\n\n  const renderIcon = () => {\n    if (isNodeProcessing) {\n      return <IconSpin spin className={classnames(styles.icon, styles.processing)} />;\n    }\n    if (isNodeSucceed) {\n      return <IconSuccessFill />;\n    }\n    return <IconWarningFill className={classnames(tagColor, styles.round)} />;\n  };\n  const renderDesc = () => {\n    const getDesc = () => {\n      if (isNodeProcessing) {\n        return 'Running';\n      } else if (isNodePending) {\n        return 'Run terminated';\n      } else if (isNodeSucceed) {\n        return 'Succeed';\n      } else if (isNodeFailed) {\n        return 'Failed';\n      } else if (isNodeCancelled) {\n        return 'Cancelled';\n      }\n    };\n\n    const desc = getDesc();\n\n    return desc ? <p className={styles.desc}>{desc}</p> : null;\n  };\n  const renderCost = () => (\n    <Tag size=\"small\" className={tagColor}>\n      {msToSeconds(report.timeCost)}\n    </Tag>\n  );\n\n  const renderSnapshotNavigation = () => {\n    if (snapshots.length <= 1) {\n      return null;\n    }\n\n    const count = <p className={styles.count}>Total: {snapshots.length}</p>;\n\n    if (snapshots.length <= displayCount) {\n      return (\n        <>\n          {count}\n          <div className={styles.snapshotNavigation}>\n            {snapshots.map((_, index) => (\n              <Button\n                key={index}\n                size=\"small\"\n                type={currentSnapshotIndex === index ? 'primary' : 'tertiary'}\n                onClick={() => setCurrentSnapshotIndex(index)}\n                className={classnames(styles.snapshotButton, {\n                  [styles.active]: currentSnapshotIndex === index,\n                  [styles.inactive]: currentSnapshotIndex !== index,\n                })}\n              >\n                {index + 1}\n              </Button>\n            ))}\n          </div>\n        </>\n      );\n    }\n\n    // 超过5个时，前5个显示为按钮，剩余的放在下拉选择中\n    return (\n      <>\n        {count}\n        <div className={styles.snapshotNavigation}>\n          {snapshots.slice(0, displayCount).map((_, index) => (\n            <Button\n              key={index}\n              size=\"small\"\n              type=\"tertiary\"\n              onClick={() => setCurrentSnapshotIndex(index)}\n              className={classnames(styles.snapshotButton, {\n                [styles.active]: currentSnapshotIndex === index,\n                [styles.inactive]: currentSnapshotIndex !== index,\n              })}\n            >\n              {index + 1}\n            </Button>\n          ))}\n          <Select\n            value={currentSnapshotIndex >= displayCount ? currentSnapshotIndex : undefined}\n            onChange={(value) => setCurrentSnapshotIndex(value as number)}\n            className={classnames(styles.snapshotSelect, {\n              [styles.active]: currentSnapshotIndex >= displayCount,\n              [styles.inactive]: currentSnapshotIndex < displayCount,\n            })}\n            size=\"small\"\n            placeholder=\"Select\"\n          >\n            {snapshots.slice(displayCount).map((_, index) => {\n              const actualIndex = index + displayCount;\n              return (\n                <Select.Option key={actualIndex} value={actualIndex}>\n                  {actualIndex + 1}\n                </Select.Option>\n              );\n            })}\n          </Select>\n        </div>\n      </>\n    );\n  };\n\n  if (!report) {\n    return null;\n  }\n\n  return (\n    <NodeStatusHeader\n      header={\n        <>\n          {renderIcon()}\n          {renderDesc()}\n          {renderCost()}\n        </>\n      }\n    >\n      <div className={styles.container}>\n        {isNodeFailed && currentSnapshot?.error && (\n          <div className={styles.error}>{currentSnapshot.error}</div>\n        )}\n        {renderSnapshotNavigation()}\n        <NodeStatusGroup title=\"Inputs\" data={currentSnapshot?.inputs} />\n        <NodeStatusGroup title=\"Outputs\" data={currentSnapshot?.outputs} />\n        <NodeStatusGroup title=\"Branch\" data={currentSnapshot?.branch} optional />\n        <NodeStatusGroup title=\"Data\" data={currentSnapshot?.data} optional />\n      </div>\n    </NodeStatusHeader>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.dataStructureViewer {\n  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n  font-size: 14px;\n  line-height: 1.5;\n  color: #333;\n  background: #fafafa;\n  border-radius: 6px;\n  padding: 12px 12px 12px 0;\n  margin: 12px;\n  border: 1px solid #e1e4e8;\n  overflow: hidden;\n\n  .treeNode {\n    margin: 2px 0;\n\n    &Header {\n      display: flex;\n      align-items: flex-start;\n      gap: 4px;\n      min-height: 20px;\n      padding: 2px 0;\n      border-radius: 3px;\n      transition: background-color 0.15s ease;\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.04);\n      }\n    }\n\n    &Children {\n      margin-left: 8px;\n      padding-left: 8px;\n      position: relative;\n\n      &::before {\n        content: '';\n        position: absolute;\n        left: 0;\n        top: 0;\n        bottom: 0;\n        width: 1px;\n        background: #e1e4e8;\n      }\n    }\n  }\n\n  .expandButton {\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: 10px;\n    color: #666;\n    width: 16px;\n    height: 16px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: 2px;\n    transition: all 0.15s ease;\n    padding: 0;\n    margin: 0;\n\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.1);\n      color: #333;\n    }\n\n    &.expanded {\n      transform: rotate(90deg);\n    }\n\n    &.collapsed {\n      transform: rotate(0deg);\n    }\n  }\n\n  .expandPlaceholder {\n    width: 16px;\n    height: 16px;\n    display: inline-block;\n    flex-shrink: 0;\n  }\n\n  .nodeLabel {\n    color: #0969da;\n    font-weight: 500;\n    cursor: pointer;\n    user-select: auto;\n    margin-right: 4px;\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n\n  .nodeValue {\n    margin-left: 4px;\n  }\n\n  .primitiveValue {\n    cursor: pointer;\n    user-select: all;\n    padding: 1px 3px;\n    border-radius: 3px;\n    transition: background-color 0.15s ease;\n\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.05);\n    }\n\n    &Quote {\n      color: #8f8f8f;\n    }\n\n    &.string {\n      color: #032f62;\n      background-color: rgba(3, 47, 98, 0.05);\n    }\n\n    &.number {\n      color: #005cc5;\n      background-color: rgba(0, 92, 197, 0.05);\n    }\n\n    &.boolean {\n      color: #e36209;\n      background-color: rgba(227, 98, 9, 0.05);\n    }\n\n    &.null,\n    &.undefined {\n      color: #6a737d;\n      font-style: italic;\n      background-color: rgba(106, 115, 125, 0.05);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/node-status-bar/viewer/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport classnames from 'classnames';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport styles from './index.module.less';\n\ninterface DataStructureViewerProps {\n  data: any;\n  level?: number;\n}\n\ninterface TreeNodeProps {\n  label: string;\n  value: any;\n  level: number;\n  isLast?: boolean;\n}\n\nconst TreeNode: React.FC<TreeNodeProps> = ({ label, value, level, isLast = false }) => {\n  const [isExpanded, setIsExpanded] = useState(true);\n\n  const handleCopy = (text: string) => {\n    navigator.clipboard.writeText(text);\n    Toast.success('Copied');\n  };\n\n  const isExpandable = (val: any) =>\n    val !== null &&\n    typeof val === 'object' &&\n    ((Array.isArray(val) && val.length > 0) ||\n      (!Array.isArray(val) && Object.keys(val).length > 0));\n\n  const renderPrimitiveValue = (val: any) => {\n    if (val === null)\n      return <span className={classnames(styles.primitiveValue, styles.null)}>null</span>;\n    if (val === undefined)\n      return <span className={classnames(styles.primitiveValue, styles.undefined)}>undefined</span>;\n\n    switch (typeof val) {\n      case 'string':\n        return (\n          <span>\n            <span className={styles.primitiveValueQuote}>{'\"'}</span>\n            <span\n              className={classnames(styles.primitiveValue, styles.string)}\n              onDoubleClick={() => handleCopy(val)}\n            >\n              {val}\n            </span>\n            <span className={styles.primitiveValueQuote}>{'\"'}</span>\n          </span>\n        );\n      case 'number':\n        return (\n          <span\n            className={classnames(styles.primitiveValue, styles.number)}\n            onDoubleClick={() => handleCopy(String(val))}\n          >\n            {val}\n          </span>\n        );\n      case 'boolean':\n        return (\n          <span\n            className={classnames(styles.primitiveValue, styles.boolean)}\n            onDoubleClick={() => handleCopy(val.toString())}\n          >\n            {val.toString()}\n          </span>\n        );\n      case 'object':\n        // Handle empty objects and arrays\n        if (Array.isArray(val)) {\n          return (\n            <span className={styles.primitiveValue} onDoubleClick={() => handleCopy('[]')}>\n              []\n            </span>\n          );\n        } else {\n          return (\n            <span className={styles.primitiveValue} onDoubleClick={() => handleCopy('{}')}>\n              {'{}'}\n            </span>\n          );\n        }\n      default:\n        return (\n          <span className={styles.primitiveValue} onDoubleClick={() => handleCopy(String(val))}>\n            {String(val)}\n          </span>\n        );\n    }\n  };\n\n  const renderChildren = () => {\n    if (Array.isArray(value)) {\n      return value.map((item, index) => (\n        <TreeNode\n          key={index}\n          label={`${index + 1}.`}\n          value={item}\n          level={level + 1}\n          isLast={index === value.length - 1}\n        />\n      ));\n    } else {\n      const entries = Object.entries(value);\n      return entries.map(([key, val], index) => (\n        <TreeNode\n          key={key}\n          label={`${key}:`}\n          value={val}\n          level={level + 1}\n          isLast={index === entries.length - 1}\n        />\n      ));\n    }\n  };\n\n  return (\n    <div className={styles.treeNode}>\n      <div className={styles.treeNodeHeader}>\n        {isExpandable(value) ? (\n          <button\n            className={classnames(\n              styles.expandButton,\n              isExpanded ? styles.expanded : styles.collapsed\n            )}\n            onClick={() => setIsExpanded(!isExpanded)}\n          >\n            ▶\n          </button>\n        ) : (\n          <span className={styles.expandPlaceholder}></span>\n        )}\n        <span\n          className={styles.nodeLabel}\n          onClick={() =>\n            handleCopy(\n              JSON.stringify({\n                [label]: value,\n              })\n            )\n          }\n        >\n          {label}\n        </span>\n        {!isExpandable(value) && (\n          <span className={styles.nodeValue}>{renderPrimitiveValue(value)}</span>\n        )}\n      </div>\n      {isExpandable(value) && isExpanded && (\n        <div className={styles.treeNodeChildren}>{renderChildren()}</div>\n      )}\n    </div>\n  );\n};\n\nexport const DataStructureViewer: React.FC<DataStructureViewerProps> = ({ data, level = 0 }) => {\n  if (data === null || data === undefined || typeof data !== 'object') {\n    return (\n      <div className={styles.dataStructureViewer}>\n        <TreeNode label=\"value\" value={data} level={0} />\n      </div>\n    );\n  }\n\n  const entries = Object.entries(data);\n\n  return (\n    <div className={styles.dataStructureViewer}>\n      {entries.map(([key, value], index) => (\n        <TreeNode\n          key={key}\n          label={`${key}:`}\n          value={value}\n          level={0}\n          isLast={index === entries.length - 1}\n        />\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-button/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.testrun-success-button {\n  background-color: rgba(0, 178, 60, 1) !important; // override semi style\n  border-radius: 8px;\n  color: #fff !important; // override semi style\n}\n\n.testrun-error-button {\n  background-color: rgba(255, 115, 0, 1) !important; // override semi style\n  border-radius: 8px;\n  color: #fff !important; // override semi style\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-button/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect, useCallback } from 'react';\n\nimport { useClientContext, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\nimport { Button, Badge } from '@douyinfe/semi-ui';\nimport { IconPlay } from '@douyinfe/semi-icons';\n\nimport { useTestRunFormPanel } from '../../../plugins/panel-manager-plugin/hooks';\n\nimport styles from './index.module.less';\n\nexport function TestRunButton(props: { disabled: boolean }) {\n  const [errorCount, setErrorCount] = useState(0);\n  const clientContext = useClientContext();\n  const updateValidateData = useCallback(() => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    const count = allForms.filter((form) => form?.state.invalid).length;\n    setErrorCount(count);\n  }, [clientContext]);\n  const { open: openPanel } = useTestRunFormPanel();\n  /**\n   * Validate all node and Save\n   */\n  const onTestRun = useCallback(async () => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    await Promise.all(allForms.map(async (form) => form?.validate()));\n    console.log('>>>>> save data: ', clientContext.document.toJSON());\n    openPanel();\n  }, [clientContext]);\n\n  /**\n   * Listen single node validate\n   */\n  useEffect(() => {\n    const listenSingleNodeValidate = (node: FlowNodeEntity) => {\n      const { form } = node;\n      if (form) {\n        const formValidateDispose = form.onValidate(() => updateValidateData());\n        node.onDispose(() => formValidateDispose.dispose());\n      }\n    };\n    clientContext.document.getAllNodes().map((node) => listenSingleNodeValidate(node));\n    const dispose = clientContext.document.onNodeCreate(({ node }) =>\n      listenSingleNodeValidate(node)\n    );\n    return () => dispose.dispose();\n  }, [clientContext]);\n\n  const button =\n    errorCount === 0 ? (\n      <Button\n        disabled={props.disabled}\n        onClick={onTestRun}\n        icon={<IconPlay size=\"small\" />}\n        className={styles.testrunSuccessButton}\n      >\n        Test Run\n      </Button>\n    ) : (\n      <Badge count={errorCount} position=\"rightTop\" type=\"danger\">\n        <Button\n          type=\"danger\"\n          disabled={props.disabled}\n          onClick={onTestRun}\n          icon={<IconPlay size=\"small\" />}\n          className={styles.testrunErrorButton}\n        >\n            Test Run\n        </Button>\n      </Badge>\n    );\n\n  return button;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-form/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.formContainer {\n  margin: 8px 0;\n}\n\n.formTitle {\n  font-size: 20px;\n  font-weight: 600;\n  color: #1a1a1a;\n  margin-bottom: 24px;\n  text-align: center;\n}\n\n.fieldGroup {\n  margin-bottom: 8px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n.fieldLabel {\n  display: block;\n  font-size: 14px;\n  font-weight: 500;\n  color: #333333;\n  margin-bottom: 8px;\n  line-height: 1.4;\n}\n\n.fieldInput {\n  width: 100%;\n\n  :global(.semi-input) {\n\n    &:hover {\n      border-color: #4096ff;\n    }\n\n    &:focus {\n      border-color: #4096ff;\n      box-shadow: 0 0 0 2px rgba(64, 150, 255, 0.1);\n    }\n  }\n\n  :global(.semi-input-number) {\n    width: 100%;\n\n    &:hover {\n      border-color: #4096ff;\n    }\n\n    &:focus-within {\n      border-color: #4096ff;\n      box-shadow: 0 0 0 2px rgba(64, 150, 255, 0.1);\n    }\n  }\n}\n\n.codeEditorWrapper {\n  min-height: 100px;\n  max-height: 200px;\n  background: #fff;\n  padding: 8px 8px 8px 4px;\n  border-radius: 8px;\n  border: 1px solid #7f92cd40;\n  width: 348px;\n\n  :global(.cm-editor) {\n    height: 100% !important;\n    overflow: auto !important;\n  }\n\n  :global(.cm-scroller) {\n    min-height: 100px !important;\n    max-height: 200px !important;\n  }\n\n  :global(.cm-content) {\n    min-height: 100px !important;\n    max-height: 200px !important;\n  }\n\n  :global(.cm-activeLine) {\n    background-color: #efefef78;\n  }\n\n  :global(.cm-activeLineGutter) {\n    background-color: #efefef78;\n  }\n\n  :global(.cm-gutters) {\n    background-color: #fff;\n    color: #000A298A;\n    border-right-color: transparent;\n    border-right-width: 0px;\n  }\n}\n\n.fieldTypeIndicator {\n  display: inline-block;\n  padding: 2px 8px;\n  font-size: 12px;\n  font-weight: 500;\n  border-radius: 4px;\n}\n\n.emptyState {\n  text-align: center;\n  padding: 20px 20px;\n  color: #999999;\n  font-size: 14px;\n\n  .emptyText {\n    font-weight: 500;\n  }\n}\n\n.requiredIndicator {\n  color: #ff4d4f;\n  margin-left: 4px;\n  font-weight: 500;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-form/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport classNames from 'classnames';\nimport { DisplaySchemaTag } from '@flowgram.ai/form-materials';\nimport { Input, Switch, InputNumber } from '@douyinfe/semi-ui';\n\nimport { JsonValueEditor } from '../json-value-editor';\nimport { useFormMeta } from '../hooks/use-form-meta';\nimport { useFields } from '../hooks/use-fields';\nimport { useSyncDefault } from '../hooks';\n\nimport styles from './index.module.less';\n\ninterface TestRunFormProps {\n  values: Record<string, unknown>;\n  setValues: (values: Record<string, unknown>) => void;\n}\n\nexport const TestRunForm: FC<TestRunFormProps> = ({ values, setValues }) => {\n  const formMeta = useFormMeta();\n\n  const fields = useFields({\n    formMeta,\n    values,\n    setValues,\n  });\n\n  useSyncDefault({\n    formMeta,\n    values,\n    setValues,\n  });\n\n  const renderField = (field: any) => {\n    switch (field.type) {\n      case 'boolean':\n        return (\n          <div className={styles.fieldInput}>\n            <Switch checked={field.value} onChange={(checked) => field.onChange(checked)} />\n          </div>\n        );\n      case 'integer':\n        return (\n          <div className={styles.fieldInput}>\n            <InputNumber\n              precision={0}\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"Please input integer\"\n            />\n          </div>\n        );\n      case 'number':\n        return (\n          <div className={styles.fieldInput}>\n            <InputNumber\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"Please input number\"\n            />\n          </div>\n        );\n      case 'object':\n        return (\n          <div className={classNames(styles.fieldInput, styles.codeEditorWrapper)}>\n            <JsonValueEditor value={field.value} onChange={(value) => field.onChange(value)} />\n          </div>\n        );\n      case 'array':\n        return (\n          <div className={classNames(styles.fieldInput, styles.codeEditorWrapper)}>\n            <JsonValueEditor value={field.value} onChange={(value) => field.onChange(value)} />\n          </div>\n        );\n      default:\n        return (\n          <div className={styles.fieldInput}>\n            <Input\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"Please input text\"\n            />\n          </div>\n        );\n    }\n  };\n\n  // Show empty state if no fields\n  if (fields.length === 0) {\n    return (\n      <div className={styles.formContainer}>\n        <div className={styles.emptyState}>\n          <div className={styles.emptyText}>Empty</div>\n          <div className={styles.emptyText}>No inputs found in start node</div>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div className={styles.formContainer}>\n      {fields.map((field) => (\n        <div key={field.name} className={styles.fieldGroup}>\n          <label htmlFor={field.name} className={styles.fieldLabel}>\n            {field.name}\n            {field.required && <span className={styles.requiredIndicator}>*</span>}\n            <span className={styles.fieldTypeIndicator}>\n              <DisplaySchemaTag\n                value={{\n                  type: field.type,\n                  items: field.itemsType\n                    ? {\n                        type: field.itemsType,\n                      }\n                    : undefined,\n                }}\n              />\n            </span>\n          </label>\n          {renderField(field)}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-form/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { JsonSchemaBasicType } from '@flowgram.ai/form-materials';\n\nexport interface TestRunFormMetaItem {\n  type: JsonSchemaBasicType;\n  name: string;\n  defaultValue: unknown;\n  required: boolean;\n  itemsType?: JsonSchemaBasicType;\n}\n\nexport type TestRunFormMeta = TestRunFormMetaItem[];\n\nexport interface TestRunFormField extends TestRunFormMetaItem {\n  value: unknown;\n  onChange: (value: unknown) => void;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-json-input/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.testrun-json-input {\n  min-height: 300px;\n  max-height: 400px;\n  background: #fff;\n  padding: 8px 8px 8px 4px;\n  border-radius: 8px;\n  border: 1px solid #7f92cd40;\n  width: 348px;\n\n  :global(.cm-editor) {\n    height: 100% !important;\n    overflow: auto !important;\n  }\n\n  :global(.cm-scroller) {\n    min-height: 300px !important;\n    max-height: 400px !important;\n  }\n\n  :global(.cm-content) {\n    min-height: 300px !important;\n    max-height: 400px !important;\n  }\n\n  :global(.cm-activeLine) {\n    background-color: #efefef78;\n  }\n\n  :global(.cm-activeLineGutter) {\n    background-color: #efefef78;\n  }\n\n  :global(.cm-gutters) {\n    background-color: #fff;\n    color: #000A298A;\n    border-right-color: transparent;\n    border-right-width: 0px;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-json-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { JsonValueEditor } from '../json-value-editor';\nimport { useFormMeta, useSyncDefault } from '../hooks';\n\nimport styles from './index.module.less';\n\ninterface TestRunJsonInputProps {\n  values: Record<string, unknown>;\n  setValues: (values: Record<string, unknown>) => void;\n}\n\nexport const TestRunJsonInput: FC<TestRunJsonInputProps> = ({ values, setValues }) => {\n  const formMeta = useFormMeta();\n\n  useSyncDefault({\n    formMeta,\n    values,\n    setValues,\n  });\n\n  return (\n    <div className={styles['testrun-json-input']}>\n      <JsonValueEditor value={values} onChange={setValues} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-panel/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.testrun-panel-form {\n\n  .testrun-panel-input {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 8px;\n    margin: 0 12px 8px 0;\n\n    .title {\n      font-size: 15px;\n      font-weight: 500;\n      color: #333;\n      flex: 1;\n    }\n  }\n\n\n  .error {\n    color: red;\n    font-size: 14px;\n  }\n\n  .code-editor-container {\n    min-height: 200px;\n    max-height: 400px;\n    background: #fff;\n    padding: 8px 8px 8px 4px;\n    border-radius: 4px;\n    border: 1px solid #52649a0f;\n\n    :global(.cm-editor) {\n      height: 100% !important;\n      overflow: auto !important;\n    }\n\n    :global(.cm-scroller) {\n      min-height: 200px !important;\n      max-height: 400px !important;\n    }\n\n    :global(.cm-content) {\n      min-height: 200px !important;\n      max-height: 400px !important;\n    }\n  }\n}\n\n.testrun-panel-running {\n  width: 100%;\n  height: 80%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  gap: 16px;\n\n  .text {\n    font-size: 18px;\n  }\n}\n\n.button {\n  border-radius: 8px;\n  width: 358px;\n  height: 40px;\n\n  &.running {\n    background-color: rgba(87, 104, 161, 0.08) !important; // override semi style\n    color: rgba(15, 21, 40, 0.82);\n  }\n\n  &.default {\n    background-color: rgba(0, 178, 60, 1) !important; // override semi style\n    color: #fff;\n  }\n}\n\n\n.testrun-panel-container {\n  background: rgb(255, 255, 255);\n  border-radius: 8px;\n  height: 100%;\n  width: 100%;\n  border: 1px solid rgba(82, 100, 154, 0.13);\n  padding: 8px 0 8px 0;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  position: relative;\n\n  .testrun-panel-header {\n    background: var(#fcfcff);\n    border-bottom: 1px solid rgba(82, 100, 154, 0.13);\n    border-top-left-radius: 8px;\n    border-top-right-radius: 8px;\n    display: flex;\n    height: 40px;\n    justify-content: space-between;\n    min-height: 40px;\n    width: 100%;\n    align-items: center;\n\n    .testrun-panel-title {\n      font-size: 16px;\n      font-weight: 500;\n      margin: 8px 8px 8px 16px;\n    }\n\n    .testrun-panel-close {\n      margin: 8px 16px 8px 8px;\n    }\n  }\n\n  .testrun-panel-content {\n    height: calc(100% - 40px);\n    margin: 8px 8px 8px 16px;\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n    overflow: auto;\n    margin-bottom: 72px;\n  }\n\n  .testrun-panel-footer {\n    border-top: 1px solid rgba(82, 100, 154, 0.13);\n    height: 40px;\n    position: absolute;\n    background: #fbfbfb;\n    height: 62px;\n    bottom: 0;\n    border-radius: 0 0 8px 8px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-panel/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/testrun/testrun-panel/test-run-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useState, useEffect } from 'react';\n\nimport classnames from 'classnames';\nimport { WorkflowInputs, WorkflowOutputs } from '@flowgram.ai/runtime-interface';\nimport { useService } from '@flowgram.ai/free-layout-editor';\nimport { Button, Switch } from '@douyinfe/semi-ui';\nimport { IconClose, IconPlay, IconSpin } from '@douyinfe/semi-icons';\n\nimport { TestRunJsonInput } from '../testrun-json-input';\nimport { TestRunForm } from '../testrun-form';\nimport { NodeStatusGroup } from '../node-status-bar/group';\nimport { WorkflowRuntimeService } from '../../../plugins/runtime-plugin/runtime-service';\nimport { useTestRunFormPanel } from '../../../plugins/panel-manager-plugin/hooks';\nimport { IconCancel } from '../../../assets/icon-cancel';\n\nimport styles from './index.module.less';\n\nexport interface TestRunSidePanelProps {}\n\nexport const TestRunSidePanel: FC<TestRunSidePanelProps> = () => {\n  const runtimeService = useService(WorkflowRuntimeService);\n  const { close: closePanel } = useTestRunFormPanel();\n  const [isRunning, setRunning] = useState(false);\n  const [values, setValues] = useState<Record<string, unknown>>({});\n  const [errors, setErrors] = useState<string[]>();\n  const [result, setResult] = useState<\n    | {\n        inputs: WorkflowInputs;\n        outputs: WorkflowOutputs;\n      }\n    | undefined\n  >();\n\n  // en - Use localStorage to persist the JSON mode state\n  const [inputJSONMode, _setInputJSONMode] = useState(() => {\n    const savedMode = localStorage.getItem('testrun-input-json-mode');\n    return savedMode ? JSON.parse(savedMode) : false;\n  });\n\n  const setInputJSONMode = (checked: boolean) => {\n    _setInputJSONMode(checked);\n    localStorage.setItem('testrun-input-json-mode', JSON.stringify(checked));\n  };\n\n  const onTestRun = async () => {\n    if (isRunning) {\n      await runtimeService.taskCancel();\n      return;\n    }\n    setResult(undefined);\n    setErrors(undefined);\n    const taskID = await runtimeService.taskRun(values);\n    if (taskID) {\n      setRunning(true);\n    }\n  };\n\n  const onClose = async () => {\n    await runtimeService.taskCancel();\n    setValues({});\n    setRunning(false);\n    closePanel();\n  };\n\n  const renderRunning = (\n    <div className={styles['testrun-panel-running']}>\n      <IconSpin spin size=\"large\" />\n      <div className={styles.text}>Running...</div>\n    </div>\n  );\n\n  const renderForm = (\n    <div className={styles['testrun-panel-form']}>\n      <div className={styles['testrun-panel-input']}>\n        <div className={styles.title}>Input Form</div>\n        <div>JSON Mode</div>\n        <Switch\n          checked={inputJSONMode}\n          onChange={(checked: boolean) => setInputJSONMode(checked)}\n          size=\"small\"\n        />\n      </div>\n      {inputJSONMode ? (\n        <TestRunJsonInput values={values} setValues={setValues} />\n      ) : (\n        <TestRunForm values={values} setValues={setValues} />\n      )}\n      {errors?.map((e) => (\n        <div className={styles.error} key={e}>\n          {e}\n        </div>\n      ))}\n      <NodeStatusGroup title=\"Inputs Result\" data={result?.inputs} optional disableCollapse />\n      <NodeStatusGroup title=\"Outputs Result\" data={result?.outputs} optional disableCollapse />\n    </div>\n  );\n\n  const renderButton = (\n    <Button\n      onClick={onTestRun}\n      icon={isRunning ? <IconCancel /> : <IconPlay size=\"small\" />}\n      className={classnames(styles.button, {\n        [styles.running]: isRunning,\n        [styles.default]: !isRunning,\n      })}\n    >\n      {isRunning ? 'Cancel' : 'Test Run'}\n    </Button>\n  );\n\n  useEffect(() => {\n    const disposer = runtimeService.onResultChanged(({ result, errors }) => {\n      setRunning(false);\n      setResult(result);\n      if (errors) {\n        setErrors(errors);\n      } else {\n        setErrors(undefined);\n      }\n    });\n    return () => disposer.dispose();\n  }, []);\n\n  useEffect(\n    () => () => {\n      runtimeService.taskCancel();\n    },\n    [runtimeService]\n  );\n\n  return (\n    <div className={styles['testrun-panel-container']}>\n      <div className={styles['testrun-panel-header']}>\n        <div className={styles['testrun-panel-title']}>Test Run</div>\n        <Button\n          className={styles['testrun-panel-title']}\n          type=\"tertiary\"\n          icon={<IconClose />}\n          size=\"small\"\n          theme=\"borderless\"\n          onClick={onClose}\n        />\n      </div>\n      <div className={styles['testrun-panel-content']}>\n        {isRunning ? renderRunning : renderForm}\n      </div>\n      <div className={styles['testrun-panel-footer']}>{renderButton}</div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/auto-layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { usePlayground, usePlaygroundTools } from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\n\nimport { IconAutoLayout } from '../../assets/icon-auto-layout';\n\nexport const AutoLayout = () => {\n  const tools = usePlaygroundTools();\n  const playground = usePlayground();\n  const autoLayout = useCallback(async () => {\n    if (playground.config.readonly) {\n      console.warn('Auto layout is disabled in readonly mode');\n      return;\n    }\n    await tools.autoLayout({\n      enableAnimation: true,\n      animationDuration: 1000,\n      layoutConfig: {\n        rankdir: 'LR',\n        align: undefined,\n        nodesep: 100,\n        ranksep: 100,\n      },\n    });\n  }, [tools]);\n\n  return (\n    <Tooltip content={'Auto Layout'}>\n      <IconButton\n        disabled={playground.config.readonly}\n        type=\"tertiary\"\n        theme=\"borderless\"\n        onClick={autoLayout}\n        icon={IconAutoLayout}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/comment.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useCallback } from 'react';\n\nimport {\n  delay,\n  usePlayground,\n  useService,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\n\nimport { WorkflowNodeType } from '../../nodes';\nimport { IconComment } from '../../assets/icon-comment';\n\nexport const Comment = () => {\n  const playground = usePlayground();\n  const document = useService(WorkflowDocument);\n  const selectService = useService(WorkflowSelectService);\n  const dragService = useService(WorkflowDragService);\n\n  const [tooltipVisible, setTooltipVisible] = useState(false);\n\n  const calcNodePosition = useCallback(\n    (mouseEvent: React.MouseEvent<HTMLButtonElement>) => {\n      const mousePosition = playground.config.getPosFromMouseEvent(mouseEvent);\n      return {\n        x: mousePosition.x,\n        y: mousePosition.y - 75,\n      };\n    },\n    [playground]\n  );\n\n  const createComment = useCallback(\n    async (mouseEvent: React.MouseEvent<HTMLButtonElement>) => {\n      setTooltipVisible(false);\n      const canvasPosition = calcNodePosition(mouseEvent);\n      // create comment node - 创建节点\n      const node = document.createWorkflowNodeByType(WorkflowNodeType.Comment, canvasPosition);\n      // wait comment node render - 等待节点渲染\n      await delay(16);\n      // select comment node - 选中节点\n      selectService.selectNode(node);\n      // maybe touch event - 可能是触摸事件\n      if (mouseEvent.detail !== 0) {\n        // start drag -开始拖拽\n        dragService.startDragSelectedNodes(mouseEvent);\n      }\n    },\n    [selectService, calcNodePosition, document, dragService]\n  );\n\n  return (\n    <Tooltip\n      trigger=\"custom\"\n      visible={tooltipVisible}\n      onVisibleChange={setTooltipVisible}\n      content=\"Comment\"\n    >\n      <IconButton\n        disabled={playground.config.readonly}\n        icon={\n          <IconComment\n            style={{\n              width: 16,\n              height: 16,\n            }}\n          />\n        }\n        type=\"tertiary\"\n        theme=\"borderless\"\n        onClick={createComment}\n        onMouseEnter={() => setTooltipVisible(true)}\n        onMouseLeave={() => setTooltipVisible(false)}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/download.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState, type FC } from 'react';\n\nimport { usePlayground, useService } from '@flowgram.ai/free-layout-editor';\nimport { FlowDownloadFormat, FlowDownloadService } from '@flowgram.ai/export-plugin';\nimport { IconButton, Toast, Dropdown, Tooltip } from '@douyinfe/semi-ui';\nimport { IconFilledArrowDown } from '@douyinfe/semi-icons';\n\nconst formatOptions = [\n  {\n    label: 'PNG',\n    value: FlowDownloadFormat.PNG,\n  },\n  {\n    label: 'JPEG',\n    value: FlowDownloadFormat.JPEG,\n  },\n  {\n    label: 'SVG',\n    value: FlowDownloadFormat.SVG,\n  },\n  {\n    label: 'JSON',\n    value: FlowDownloadFormat.JSON,\n  },\n  {\n    label: 'YAML',\n    value: FlowDownloadFormat.YAML,\n  },\n];\n\nexport const DownloadTool: FC = () => {\n  const [downloading, setDownloading] = useState<boolean>(false);\n  const [visible, setVisible] = useState(false);\n  const playground = usePlayground();\n  const { readonly } = playground.config;\n  const downloadService = useService(FlowDownloadService);\n\n  useEffect(() => {\n    const subscription = downloadService.onDownloadingChange((v) => {\n      setDownloading(v);\n    });\n\n    return () => {\n      subscription.dispose();\n    };\n  }, [downloadService]);\n\n  const handleDownload = async (format: FlowDownloadFormat) => {\n    setVisible(false);\n    await downloadService.download({\n      format,\n    });\n    const formatOption = formatOptions.find((option) => option.value === format);\n    Toast.success(`Download ${formatOption?.label} successfully`);\n  };\n\n  const button = (\n    <IconButton\n      type=\"tertiary\"\n      theme=\"borderless\"\n      className={visible ? '!coz-mg-secondary-pressed' : undefined}\n      icon={<IconFilledArrowDown />}\n      loading={downloading}\n      onClick={() => setVisible(true)}\n    />\n  );\n\n  return (\n    <Dropdown\n      trigger=\"custom\"\n      visible={visible}\n      position=\"topLeft\"\n      onClickOutSide={() => setVisible(false)}\n      render={\n        <Dropdown.Menu className=\"min-w-[120px]\">\n          {formatOptions.map((item) => (\n            <Dropdown.Item\n              disabled={downloading || readonly}\n              key={item.value}\n              onClick={() => handleDownload(item.value)}\n            >\n              {item.label}\n            </Dropdown.Item>\n          ))}\n        </Dropdown.Menu>\n      }\n    >\n      {visible ? (\n        button\n      ) : (\n        <div>\n          <Tooltip content=\"Download\">{button}</Tooltip>\n        </div>\n      )}\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/fit-view.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { usePlaygroundTools } from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\nimport { IconExpand } from '@douyinfe/semi-icons';\n\nexport const FitView = () => {\n  const tools = usePlaygroundTools();\n  return (\n    <Tooltip content=\"FitView\">\n      <IconButton\n        icon={<IconExpand />}\n        type=\"tertiary\"\n        theme=\"borderless\"\n        onClick={() => tools.fitView()}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/free-layout-editor';\nimport { useClientContext } from '@flowgram.ai/free-layout-editor';\nimport { Tooltip, IconButton, Divider } from '@douyinfe/semi-ui';\nimport { IconUndo, IconRedo } from '@douyinfe/semi-icons';\n\nimport { TestRunButton } from '../testrun/testrun-button';\nimport { AddNode } from '../add-node';\nimport { ZoomSelect } from './zoom-select';\nimport { SwitchLine } from './switch-line';\nimport { ToolContainer, ToolSection } from './styles';\nimport { Readonly } from './readonly';\nimport { MinimapSwitch } from './minimap-switch';\nimport { Minimap } from './minimap';\nimport { Interactive } from './interactive';\nimport { FitView } from './fit-view';\nimport { Comment } from './comment';\nimport { AutoLayout } from './auto-layout';\nimport { ProblemButton } from '../problem-panel';\nimport { DownloadTool } from './download';\n\nexport const DemoTools = () => {\n  const { history, playground } = useClientContext();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n  const [minimapVisible, setMinimapVisible] = useState(true);\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());\n    return () => disposable.dispose();\n  }, [playground]);\n\n  return (\n    <ToolContainer className=\"demo-free-layout-tools\">\n      <ToolSection>\n        <Interactive />\n        <AutoLayout />\n        <SwitchLine />\n        <ZoomSelect />\n        <FitView />\n        <MinimapSwitch minimapVisible={minimapVisible} setMinimapVisible={setMinimapVisible} />\n        <Minimap visible={minimapVisible} />\n        <Readonly />\n        <Comment />\n        <Tooltip content=\"Undo\">\n          <IconButton\n            type=\"tertiary\"\n            theme=\"borderless\"\n            icon={<IconUndo />}\n            disabled={!canUndo || playground.config.readonly}\n            onClick={() => history.undo()}\n          />\n        </Tooltip>\n        <Tooltip content=\"Redo\">\n          <IconButton\n            type=\"tertiary\"\n            theme=\"borderless\"\n            icon={<IconRedo />}\n            disabled={!canRedo || playground.config.readonly}\n            onClick={() => history.redo()}\n          />\n        </Tooltip>\n        <ProblemButton />\n        <DownloadTool />\n        <Divider layout=\"vertical\" style={{ height: '16px' }} margin={3} />\n        <AddNode disabled={playground.config.readonly} />\n        <Divider layout=\"vertical\" style={{ height: '16px' }} margin={3} />\n        <TestRunButton disabled={playground.config.readonly} />\n      </ToolSection>\n    </ToolContainer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/interactive.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport {\n  usePlaygroundTools,\n  type InteractiveType as IdeInteractiveType,\n} from '@flowgram.ai/free-layout-editor';\nimport { Tooltip, Popover } from '@douyinfe/semi-ui';\n\nimport { MousePadSelector } from './mouse-pad-selector';\n\nexport const CACHE_KEY = 'workflow_prefer_interactive_type';\nexport const IS_MAC_OS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);\n\nexport const getPreferInteractiveType = () => {\n  const data = localStorage.getItem(CACHE_KEY) as string;\n  if (data && [InteractiveType.Mouse, InteractiveType.Pad].includes(data as InteractiveType)) {\n    return data;\n  }\n  return IS_MAC_OS ? InteractiveType.Pad : InteractiveType.Mouse;\n};\n\nexport const setPreferInteractiveType = (type: InteractiveType) => {\n  localStorage.setItem(CACHE_KEY, type);\n};\n\nexport enum InteractiveType {\n  Mouse = 'MOUSE',\n  Pad = 'PAD',\n}\n\nexport const Interactive = () => {\n  const tools = usePlaygroundTools();\n  const [visible, setVisible] = useState(false);\n\n  const [interactiveType, setInteractiveType] = useState<InteractiveType>(\n    () => getPreferInteractiveType() as InteractiveType\n  );\n\n  const [showInteractivePanel, setShowInteractivePanel] = useState(false);\n\n  const mousePadTooltip =\n    interactiveType === InteractiveType.Mouse ? 'Mouse-Friendly' : 'Touchpad-Friendly';\n\n  useEffect(() => {\n    // read from localStorage\n    const preferInteractiveType = getPreferInteractiveType();\n    tools.setInteractiveType(preferInteractiveType as IdeInteractiveType);\n  }, []);\n\n  const handleClose = () => {\n    setVisible(false);\n  };\n\n  return (\n    <Popover trigger=\"custom\" position=\"top\" visible={visible} onClickOutSide={handleClose}>\n      <Tooltip\n        content={mousePadTooltip}\n        style={{ display: showInteractivePanel ? 'none' : 'block' }}\n      >\n        <div className=\"workflow-toolbar-interactive\">\n          <MousePadSelector\n            value={interactiveType}\n            onChange={(value) => {\n              setInteractiveType(value);\n              setPreferInteractiveType(value);\n              tools.setInteractiveType(value as unknown as IdeInteractiveType);\n            }}\n            onPopupVisibleChange={setShowInteractivePanel}\n            containerStyle={{\n              border: 'none',\n              height: '32px',\n              width: '32px',\n              justifyContent: 'center',\n              alignItems: 'center',\n              gap: '2px',\n              padding: '4px',\n              borderRadius: 'var(--small, 6px)',\n            }}\n            iconStyle={{\n              margin: '0',\n              width: '16px',\n              height: '16px',\n            }}\n            arrowStyle={{\n              width: '12px',\n              height: '12px',\n            }}\n          />\n        </div>\n      </Tooltip>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/minimap-switch.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Tooltip, IconButton } from '@douyinfe/semi-ui';\n\nimport { UIIconMinimap } from './styles';\n\nexport const MinimapSwitch = (props: {\n  minimapVisible: boolean;\n  setMinimapVisible: (visible: boolean) => void;\n}) => {\n  const { minimapVisible, setMinimapVisible } = props;\n\n  return (\n    <Tooltip content=\"Minimap\">\n      <IconButton\n        type=\"tertiary\"\n        theme=\"borderless\"\n        icon={<UIIconMinimap visible={minimapVisible} />}\n        onClick={() => setMinimapVisible(!minimapVisible)}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nimport { MinimapContainer } from './styles';\n\nexport const Minimap = ({ visible }: { visible?: boolean }) => {\n  if (!visible) {\n    return <></>;\n  }\n  return (\n    <MinimapContainer>\n      <MinimapRender\n        panelStyles={{}}\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </MinimapContainer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/mouse-pad-selector.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* stylelint-disable no-descending-specificity */\n/* stylelint-disable selector-class-pattern */\n.ui-mouse-pad-selector {\n  position: relative;\n\n  display: flex;\n  align-items: center;\n\n  box-sizing: border-box;\n  width: 68px;\n  height: 32px;\n  padding: 8px 12px;\n\n  border: 1px solid rgba(29, 28, 35, 8%);\n  border-radius: 8px;\n\n  &-icon {\n    height: 20px;\n    margin-right: 12px;\n  }\n\n  &-arrow {\n    height: 16px;\n    font-size: 12px;\n  }\n\n  &-popover {\n    padding: 16px;\n\n    &-options {\n      display: flex;\n      gap: 12px;\n      margin-top: 12px;\n    }\n\n    .mouse-pad-option {\n      box-sizing: border-box;\n      width: 220px;\n      padding-bottom: 20px;\n\n      text-align: center;\n\n      background: var(--coz-mg-card, #FFF);\n      border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));\n      border-radius: var(--default, 8px);\n\n      &-icon {\n        padding-top: 26px;\n      }\n\n      &-title {\n        padding-top: 8px;\n      }\n\n      &-subTitle {\n        padding: 4px 12px 0;\n      }\n\n      &-icon-selected {\n        color: rgb(19 0 221);\n      }\n\n      &-title-selected {\n        color: var(--coz-fg-hglt, #4E40E5);\n      }\n\n      &-subTitle-selected {\n        color: var(--coz-fg-hglt, #4E40E5);\n      }\n\n      &-selected {\n        cursor: pointer;\n        background-color: var(--coz-mg-hglt, rgba(186, 192, 255, 20%));\n        border: 1px solid var(--coz-stroke-hglt, #4E40E5);\n        border-radius: var(--default, 8px);\n      }\n\n      &:hover:not(&-selected) {\n        cursor: pointer;\n\n        background-color: var(--coz-mg-card-hovered, #FFF);\n        border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));\n        border-radius: var(--default, 8px);\n        box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 16%), 0 16px 48px 0 rgba(0, 0, 0, 8%);\n      }\n\n      &:active:not(&-selected) {\n        background-color: rgba(46, 46, 56, 12%);\n      }\n\n      &:last-of-type {\n        padding-top: 13px;\n      }\n    }\n  }\n\n  &:hover {\n    cursor: pointer;\n    background-color: rgba(46, 46, 56, 8%);\n    border-color: rgba(77, 83, 232, 100%);\n  }\n\n  &:active,\n  &:focus {\n    background-color: rgba(46, 46, 56, 12%);\n    border-color: rgba(77, 83, 232, 100%);\n  }\n\n  &-active {\n    border-color: rgba(77, 83, 232, 100%);\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/mouse-pad-selector.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { type CSSProperties, useState } from 'react';\n\nimport { Popover, Typography } from '@douyinfe/semi-ui';\n\nimport { IconPad, IconPadTool } from '../../assets/icon-pad';\nimport { IconMouse, IconMouseTool } from '../../assets/icon-mouse';\n\nimport './mouse-pad-selector.less';\n\nconst { Title, Paragraph } = Typography;\n\nexport enum InteractiveType {\n  Mouse = 'MOUSE',\n  Pad = 'PAD',\n}\n\nexport interface MousePadSelectorProps {\n  value: InteractiveType;\n  onChange: (value: InteractiveType) => void;\n  onPopupVisibleChange?: (visible: boolean) => void;\n  containerStyle?: CSSProperties;\n  iconStyle?: CSSProperties;\n  arrowStyle?: CSSProperties;\n}\n\nconst InteractiveItem: React.FC<{\n  title: string;\n  subTitle: string;\n  icon: React.ReactNode;\n  value: InteractiveType;\n  selected: boolean;\n  onChange: (value: InteractiveType) => void;\n}> = ({ title, subTitle, icon, onChange, value, selected }) => (\n  <div\n    className={`mouse-pad-option ${selected ? 'mouse-pad-option-selected' : ''}`}\n    onClick={() => onChange(value)}\n  >\n    <div className={`mouse-pad-option-icon ${selected ? 'mouse-pad-option-icon-selected' : ''}`}>\n      {icon}\n    </div>\n    <Title\n      heading={6}\n      className={`mouse-pad-option-title ${selected ? 'mouse-pad-option-title-selected' : ''}`}\n    >\n      {title}\n    </Title>\n    <Paragraph\n      type=\"tertiary\"\n      className={`mouse-pad-option-subTitle ${\n        selected ? 'mouse-pad-option-subTitle-selected' : ''\n      }`}\n    >\n      {subTitle}\n    </Paragraph>\n  </div>\n);\n\nexport const MousePadSelector: React.FC<\n  MousePadSelectorProps & React.RefAttributes<HTMLDivElement>\n> = ({ value, onChange, onPopupVisibleChange, containerStyle, iconStyle, arrowStyle }) => {\n  const isMouse = value === InteractiveType.Mouse;\n  const [visible, setVisible] = useState(false);\n\n  return (\n    <Popover\n      trigger=\"custom\"\n      position=\"topLeft\"\n      closeOnEsc\n      visible={visible}\n      onVisibleChange={(v) => {\n        onPopupVisibleChange?.(v);\n      }}\n      onClickOutSide={() => {\n        setVisible(false);\n      }}\n      spacing={20}\n      content={\n        <div className={'ui-mouse-pad-selector-popover'}>\n          <Typography.Title heading={4}>{'Interaction mode'}</Typography.Title>\n          <div className={'ui-mouse-pad-selector-popover-options'}>\n            <InteractiveItem\n              title={'Mouse-Friendly'}\n              subTitle={'Drag the canvas with the left mouse button, zoom with the scroll wheel.'}\n              value={InteractiveType.Mouse}\n              selected={value === InteractiveType.Mouse}\n              icon={<IconMouse />}\n              onChange={onChange}\n            />\n\n            <InteractiveItem\n              title={'Touchpad-Friendly'}\n              subTitle={\n                'Drag with two fingers moving in the same direction, zoom by pinching or spreading two fingers.'\n              }\n              value={InteractiveType.Pad}\n              selected={value === InteractiveType.Pad}\n              icon={<IconPad />}\n              onChange={onChange}\n            />\n          </div>\n        </div>\n      }\n    >\n      <div\n        className={`ui-mouse-pad-selector ${visible ? 'ui-mouse-pad-selector-active' : ''}`}\n        onClick={() => {\n          setVisible(!visible);\n        }}\n        style={containerStyle}\n      >\n        <div className={'ui-mouse-pad-selector-icon'} style={iconStyle}>\n          {isMouse ? <IconMouseTool /> : <IconPadTool />}\n        </div>\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/readonly.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\nimport { IconUnlock, IconLock } from '@douyinfe/semi-icons';\n\nexport const Readonly = () => {\n  const playground = usePlayground();\n  const toggleReadonly = useCallback(() => {\n    playground.config.readonly = !playground.config.readonly;\n  }, [playground]);\n  return playground.config.readonly ? (\n    <Tooltip content=\"Editable\">\n      <IconButton\n        theme=\"borderless\"\n        type=\"tertiary\"\n        icon={<IconLock size=\"default\" />}\n        onClick={toggleReadonly}\n      />\n    </Tooltip>\n  ) : (\n    <Tooltip content=\"Readonly\">\n      <IconButton\n        theme=\"borderless\"\n        type=\"tertiary\"\n        icon={<IconUnlock size=\"default\" />}\n        onClick={toggleReadonly}\n      />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/save.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect, useCallback } from 'react';\n\nimport { useClientContext, FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\nimport { Button, Badge } from '@douyinfe/semi-ui';\n\nexport function Save(props: { disabled: boolean }) {\n  const [errorCount, setErrorCount] = useState(0);\n  const clientContext = useClientContext();\n\n  const updateValidateData = useCallback(() => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    const count = allForms.filter((form) => form?.state.invalid).length;\n    setErrorCount(count);\n  }, [clientContext]);\n\n  /**\n   * Validate all node and Save\n   */\n  const onSave = useCallback(async () => {\n    const allForms = clientContext.document.getAllNodes().map((node) => node.form);\n    await Promise.all(allForms.map(async (form) => form?.validate()));\n    console.log('>>>>> save data: ', clientContext.document.toJSON());\n  }, [clientContext]);\n\n  /**\n   * Listen single node validate\n   */\n  useEffect(() => {\n    const listenSingleNodeValidate = (node: FlowNodeEntity) => {\n      const { form } = node;\n      if (form) {\n        const formValidateDispose = form.onValidate(() => updateValidateData());\n        node.onDispose(() => formValidateDispose.dispose());\n      }\n    };\n    clientContext.document.getAllNodes().map((node) => listenSingleNodeValidate(node));\n    const dispose = clientContext.document.onNodeCreate(({ node }) =>\n      listenSingleNodeValidate(node)\n    );\n    return () => dispose.dispose();\n  }, [clientContext]);\n\n  if (errorCount === 0) {\n    return (\n      <Button\n        disabled={props.disabled}\n        onClick={onSave}\n        style={{ backgroundColor: 'rgba(171,181,255,0.3)', borderRadius: '8px' }}\n      >\n        Save\n      </Button>\n    );\n  }\n  return (\n    <Badge count={errorCount} position=\"rightTop\" type=\"danger\">\n      <Button\n        type=\"danger\"\n        disabled={props.disabled}\n        onClick={onSave}\n        style={{ backgroundColor: 'rgba(255, 179, 171, 0.3)', borderRadius: '8px' }}\n      >\n          Save\n      </Button>\n    </Badge>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nimport { IconMinimap } from '../../assets/icon-minimap';\n\nexport const ToolContainer = styled.div`\n  position: absolute;\n  bottom: 16px;\n  display: flex;\n  justify-content: left;\n  min-width: 360px;\n  pointer-events: none;\n  gap: 8px;\n\n  z-index: 20;\n`;\n\nexport const ToolSection = styled.div`\n  display: flex;\n  align-items: center;\n  background-color: #fff;\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  border-radius: 10px;\n  box-shadow: rgba(0, 0, 0, 0.04) 0px 2px 6px 0px, rgba(0, 0, 0, 0.02) 0px 4px 12px 0px;\n  column-gap: 2px;\n  height: 40px;\n  padding: 0 4px;\n  pointer-events: auto;\n`;\n\nexport const SelectZoom = styled.span`\n  padding: 4px;\n  border-radius: 8px;\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  font-size: 12px;\n  width: 50px;\n  cursor: pointer;\n`;\n\nexport const MinimapContainer = styled.div`\n  position: absolute;\n  bottom: 60px;\n  width: 198px;\n`;\n\nexport const UIIconMinimap = styled(IconMinimap)<{ visible: boolean }>`\n  color: ${(props) => (props.visible ? undefined : '#060709cc')};\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/switch-line.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport { useService, WorkflowLinesManager } from '@flowgram.ai/free-layout-editor';\nimport { IconButton, Tooltip } from '@douyinfe/semi-ui';\n\nimport { IconSwitchLine } from '../../assets/icon-switch-line';\n\nexport const SwitchLine = () => {\n  const linesManager = useService(WorkflowLinesManager);\n  const switchLine = useCallback(() => {\n    linesManager.switchLineType();\n  }, [linesManager]);\n\n  return (\n    <Tooltip content={'Switch Line'}>\n      <IconButton type=\"tertiary\" theme=\"borderless\" onClick={switchLine} icon={IconSwitchLine} />\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/components/tools/zoom-select.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { usePlayground, usePlaygroundTools } from '@flowgram.ai/free-layout-editor';\nimport { Divider, Dropdown } from '@douyinfe/semi-ui';\n\nimport { SelectZoom } from './styles';\n\nexport const ZoomSelect = () => {\n  const tools = usePlaygroundTools({ maxZoom: 2, minZoom: 0.25 });\n  const playground = usePlayground();\n  const [dropDownVisible, openDropDown] = useState(false);\n  return (\n    <Dropdown\n      position=\"top\"\n      trigger=\"custom\"\n      visible={dropDownVisible}\n      onClickOutSide={() => openDropDown(false)}\n      render={\n        <Dropdown.Menu>\n          <Dropdown.Item onClick={() => tools.zoomin()}>Zoom in</Dropdown.Item>\n          <Dropdown.Item onClick={() => tools.zoomout()}>Zoom out</Dropdown.Item>\n          <Divider layout=\"horizontal\" />\n          <Dropdown.Item onClick={() => playground.config.updateZoom(0.5)}>\n            Zoom to 50%\n          </Dropdown.Item>\n          <Dropdown.Item onClick={() => playground.config.updateZoom(1)}>\n            Zoom to 100%\n          </Dropdown.Item>\n          <Dropdown.Item onClick={() => playground.config.updateZoom(1.5)}>\n            Zoom to 150%\n          </Dropdown.Item>\n          <Dropdown.Item onClick={() => playground.config.updateZoom(2.0)}>\n            Zoom to 200%\n          </Dropdown.Item>\n        </Dropdown.Menu>\n      }\n    >\n      <SelectZoom onClick={() => openDropDown(true)}>{Math.floor(tools.zoom * 100)}%</SelectZoom>\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeRenderContext } from './node-render-context';\nexport { IsSidebarContext } from './sidebar-context';\n"
  },
  {
    "path": "apps/demo-free-layout/src/context/node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { NodeRenderReturnType } from '@flowgram.ai/free-layout-editor';\n\ninterface INodeRenderContext extends NodeRenderReturnType {}\n\n/** 业务自定义节点上下文 */\nexport const NodeRenderContext = React.createContext<INodeRenderContext>({} as INodeRenderContext);\n"
  },
  {
    "path": "apps/demo-free-layout/src/context/sidebar-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const IsSidebarContext = React.createContext<boolean>(false);\n"
  },
  {
    "path": "apps/demo-free-layout/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DockedPanelLayer } from '@flowgram.ai/panel-manager-plugin';\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './styles/index.css';\nimport { nodeRegistries } from './nodes';\nimport { initialData } from './initial-data';\nimport { useEditorProps } from './hooks';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps(initialData, nodeRegistries);\n  return (\n    <div className=\"doc-free-feature-overview\">\n      <FreeLayoutEditorProvider {...editorProps}>\n        <div className=\"demo-container\">\n          <DockedPanelLayer>\n            <EditorRenderer className=\"demo-editor\" />\n          </DockedPanelLayer>\n        </div>\n      </FreeLayoutEditorProvider>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/feedback.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { FieldError, FieldState, FieldWarning } from '@flowgram.ai/free-layout-editor';\n\ninterface StatePanelProps {\n  errors?: FieldState['errors'];\n  warnings?: FieldState['warnings'];\n  invalid?: boolean;\n}\n\nconst Error = styled.span`\n  font-size: 12px;\n  color: red;\n`;\n\nconst Warning = styled.span`\n  font-size: 12px;\n  color: orange;\n`;\n\nexport const Feedback = ({ errors, warnings, invalid }: StatePanelProps) => {\n  const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {\n    if (!fs) return null;\n    return fs.map((f) => <span key={f.name}>{f.message}</span>);\n  };\n  return (\n    <div>\n      <div>\n        <Error>{renderFeedbacks(errors)}</Error>\n      </div>\n      <div>\n        <Warning>{renderFeedbacks(warnings)}</Warning>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-content/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../hooks';\nimport { FormTitleDescription, FormWrapper } from './styles';\n\n/**\n * @param props\n * @constructor\n */\nexport function FormContent(props: { children?: React.ReactNode }) {\n  const { node, expanded } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n  const registry = node.getNodeRegistry<FlowNodeRegistry>();\n  return (\n    <FormWrapper>\n      <>\n        {isSidebar && <FormTitleDescription>{registry.info?.description}</FormTitleDescription>}\n        {(expanded || isSidebar) && props.children}\n      </>\n    </FormWrapper>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-content/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FormWrapper = styled.div`\n  box-sizing: border-box;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  background-color: rgb(251, 251, 251);\n  border-radius: 0 0 8px 8px;\n  padding: 0 12px 12px;\n`;\n\nexport const FormTitleDescription = styled.div`\n  color: var(--semi-color-text-2);\n  font-size: 12px;\n  line-height: 20px;\n  padding: 0px 4px;\n  word-break: break-all;\n  white-space: break-spaces;\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-header/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nimport { useClientContext, CommandService } from '@flowgram.ai/free-layout-editor';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconClose, IconSmallTriangleDown, IconSmallTriangleLeft } from '@douyinfe/semi-icons';\n\nimport { toggleLoopExpanded } from '../../utils';\nimport { FlowCommandId } from '../../shortcuts';\nimport { useNodeFormPanel } from '../../plugins/panel-manager-plugin/hooks';\nimport { useIsSidebar, useNodeRenderContext } from '../../hooks';\nimport { NodeMenu } from '../../components/node-menu';\nimport { getIcon } from './utils';\nimport { TitleInput } from './title-input';\nimport { Header, Operators } from './styles';\n\nexport function FormHeader() {\n  const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();\n  const [titleEdit, updateTitleEdit] = useState<boolean>(false);\n  const ctx = useClientContext();\n  const isSidebar = useIsSidebar();\n  const handleExpand = (e: React.MouseEvent) => {\n    toggleExpand();\n    e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n  };\n  const { close: closePanel } = useNodeFormPanel();\n  const handleDelete = () => {\n    ctx.get<CommandService>(CommandService).executeCommand(FlowCommandId.DELETE, [node]);\n  };\n  const handleClose = () => {\n    closePanel();\n  };\n  useEffect(() => {\n    // 折叠 loop 子节点\n    if (node.flowNodeType === 'loop') {\n      toggleLoopExpanded(node, expanded);\n    }\n  }, [expanded]);\n\n  return (\n    <Header>\n      {getIcon(node)}\n      <TitleInput readonly={readonly} updateTitleEdit={updateTitleEdit} titleEdit={titleEdit} />\n      {node.renderData.expandable && !isSidebar && (\n        <Button\n          type=\"primary\"\n          icon={expanded ? <IconSmallTriangleDown /> : <IconSmallTriangleLeft />}\n          size=\"small\"\n          theme=\"borderless\"\n          onClick={handleExpand}\n        />\n      )}\n      {readonly ? undefined : (\n        <Operators>\n          <NodeMenu node={node} deleteNode={handleDelete} updateTitleEdit={updateTitleEdit} />\n        </Operators>\n      )}\n      {isSidebar && (\n        <Button\n          type=\"primary\"\n          icon={<IconClose />}\n          size=\"small\"\n          theme=\"borderless\"\n          onClick={handleClose}\n        />\n      )}\n    </Header>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-header/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Header = styled.div`\n  box-sizing: border-box;\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  width: 100%;\n  column-gap: 8px;\n  border-radius: 8px 8px 0 0;\n  cursor: move;\n\n  background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);\n  overflow: hidden;\n\n  padding: 8px;\n`;\n\nexport const Title = styled.div`\n  font-size: 20px;\n  flex: 1;\n  width: 0;\n`;\n\nexport const Icon = styled.img`\n  width: 24px;\n  height: 24px;\n  scale: 0.8;\n  border-radius: 4px;\n`;\n\nexport const Operators = styled.div`\n  display: flex;\n  align-items: center;\n  column-gap: 4px;\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-header/title-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef, useEffect } from 'react';\n\nimport { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { Typography, Input } from '@douyinfe/semi-ui';\n\nimport { Title } from './styles';\nimport { Feedback } from '../feedback';\nconst { Text } = Typography;\n\nexport function TitleInput(props: {\n  readonly: boolean;\n  titleEdit: boolean;\n  updateTitleEdit: (setEdit: boolean) => void;\n}): JSX.Element {\n  const { readonly, titleEdit, updateTitleEdit } = props;\n  const ref = useRef<any>();\n  const titleEditing = titleEdit && !readonly;\n  useEffect(() => {\n    if (titleEditing) {\n      ref.current?.focus();\n    }\n  }, [titleEditing]);\n\n  return (\n    <Title>\n      <Field name=\"title\">\n        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n          <div style={{ height: 24 }}>\n            {titleEditing ? (\n              <Input\n                value={value}\n                onChange={onChange}\n                ref={ref}\n                onBlur={() => updateTitleEdit(false)}\n              />\n            ) : (\n              <Text ellipsis={{ showTooltip: true }}>{value}</Text>\n            )}\n            <Feedback errors={fieldState?.errors} />\n          </div>\n        )}\n      </Field>\n    </Title>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-header/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport { Icon } from './styles';\n\nexport const getIcon = (node: FlowNodeEntity) => {\n  const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;\n  if (!icon) return null;\n  return <Icon src={icon} />;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';\n\nimport { FormItem } from '../form-item';\nimport { Feedback } from '../feedback';\nimport { JsonSchema } from '../../typings';\nimport { useNodeRenderContext } from '../../hooks';\n\nexport function FormInputs() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <Field<JsonSchema> name=\"inputs\">\n      {({ field: inputsField }) => {\n        const required = inputsField.value?.required || [];\n        const properties = inputsField.value?.properties;\n        if (!properties) {\n          return <></>;\n        }\n        const content = Object.keys(properties).map((key) => {\n          const property = properties[key];\n\n          const formComponent = property.extra?.formComponent;\n\n          const vertical = ['prompt-editor'].includes(formComponent || '');\n\n          return (\n            <Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>\n              {({ field, fieldState }) => (\n                <FormItem\n                  name={key}\n                  vertical={vertical}\n                  type={property.type as string}\n                  required={required.includes(key)}\n                >\n                  {formComponent === 'prompt-editor' && (\n                    <PromptEditorWithVariables\n                      value={field.value}\n                      onChange={field.onChange}\n                      readonly={readonly}\n                      hasError={Object.keys(fieldState?.errors || {}).length > 0}\n                    />\n                  )}\n                  {!formComponent && (\n                    <DynamicValueInput\n                      value={field.value}\n                      onChange={field.onChange}\n                      readonly={readonly}\n                      hasError={Object.keys(fieldState?.errors || {}).length > 0}\n                      schema={property}\n                    />\n                  )}\n                  <Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />\n                </FormItem>\n              )}\n            </Field>\n          );\n        });\n        return <>{content}</>;\n      }}\n    </Field>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import styled from 'styled-components';\n\n// TODO\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-item/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.form-item-type-tag {\n  color: inherit;\n  padding: 0 2px;\n  height: 18px;\n  width: 18px;\n  vertical-align: middle;\n  flex-shrink: 0;\n  flex-grow: 0;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/form-item/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { DisplaySchemaTag } from '@flowgram.ai/form-materials';\nimport { Typography, Tooltip } from '@douyinfe/semi-ui';\n\nimport './index.css';\n\nconst { Text } = Typography;\n\ninterface FormItemProps {\n  children: React.ReactNode;\n  name: string;\n  type?: string;\n  required?: boolean;\n  description?: string;\n  labelWidth?: number;\n  labelStyle?: React.CSSProperties;\n  vertical?: boolean;\n  style?: React.CSSProperties;\n}\nexport function FormItem({\n  children,\n  name,\n  required,\n  description,\n  type,\n  labelWidth,\n  labelStyle,\n  vertical,\n  style,\n}: FormItemProps): JSX.Element {\n  const renderTitle = useCallback(\n    (showTooltip?: boolean) => (\n      <div style={{ width: '0', display: 'flex', flex: '1' }}>\n        <Text style={{ width: '100%' }} ellipsis={{ showTooltip: !!showTooltip }}>\n          {name}\n          {required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}\n        </Text>\n      </div>\n    ),\n    []\n  );\n  return (\n    <div\n      style={{\n        fontSize: 12,\n        marginBottom: 6,\n        width: '100%',\n        position: 'relative',\n        display: 'flex',\n        gap: 8,\n        ...(vertical\n          ? { flexDirection: 'column' }\n          : {\n              justifyContent: 'center',\n              alignItems: 'center',\n            }),\n        ...style,\n      }}\n    >\n      <div\n        style={{\n          justifyContent: 'center',\n          alignItems: 'center',\n          color: 'var(--semi-color-text-0)',\n          width: labelWidth || 118,\n          minWidth: labelWidth || 118,\n          maxWidth: labelWidth || 118,\n          position: 'relative',\n          display: 'flex',\n          columnGap: 4,\n          flexShrink: 0,\n          ...labelStyle,\n        }}\n      >\n        {type && <DisplaySchemaTag value={{ type }} />}\n        {description ? <Tooltip content={description}>{renderTitle()}</Tooltip> : renderTitle(true)}\n      </div>\n\n      <div\n        style={{\n          flexGrow: 1,\n          minWidth: 0,\n        }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/form-components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './feedback';\nexport * from './form-content';\nexport * from './form-inputs';\nexport * from './form-header';\nexport * from './form-item';\n"
  },
  {
    "path": "apps/demo-free-layout/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useEditorProps } from './use-editor-props';\nexport { useNodeRenderContext } from './use-node-render-context';\nexport { useIsSidebar } from './use-is-sidebar';\nexport { usePortClick } from './use-port-click';\n"
  },
  {
    "path": "apps/demo-free-layout/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { debounce } from 'lodash-es';\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeStackPlugin } from '@flowgram.ai/free-stack-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin';\nimport { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';\nimport {\n  FlowNodeBaseType,\n  FreeLayoutPluginContext,\n  FreeLayoutProps,\n  WorkflowNodeEntity,\n} from '@flowgram.ai/free-layout-editor';\nimport { createFreeGroupPlugin } from '@flowgram.ai/free-group-plugin';\nimport { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\nimport { createDownloadPlugin } from '@flowgram.ai/export-plugin';\n\nimport { canContainNode, onDragLineEnd } from '../utils';\nimport { FlowNodeRegistry, FlowDocumentJSON } from '../typings';\nimport { shortcuts } from '../shortcuts';\nimport { CustomService, ValidateService } from '../services';\nimport { GetGlobalVariableSchema } from '../plugins/variable-panel-plugin';\nimport { WorkflowRuntimeService } from '../plugins/runtime-plugin/runtime-service';\nimport {\n  createRuntimePlugin,\n  createContextMenuPlugin,\n  createVariablePanelPlugin,\n  createPanelManagerPlugin,\n} from '../plugins';\nimport { defaultFormMeta } from '../nodes/default-form-meta';\nimport { WorkflowNodeType } from '../nodes';\nimport { SelectorBoxPopover } from '../components/selector-box-popover';\nimport { BaseNode, CommentRender, GroupNodeRender, LineAddButton, NodePanel } from '../components';\n\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FreeLayoutProps {\n  return useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * 画布相关配置\n       * Canvas-related configurations\n       */\n      playground: {\n        /**\n         * Prevent Mac browser gestures from turning pages\n         * 阻止 mac 浏览器手势翻页\n         */\n        preventGlobalGesture: true,\n      },\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Line support both-way connection (default true)\n       * 线条支持双向连接\n       */\n      twoWayConnection: true,\n      /**\n       * Enable dragging of read-only nodes (default false)\n       * 允许拖拽只读节点\n       */\n      enableReadonlyNodeDragging: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: defaultFormMeta,\n        };\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.fromJSON 调用\n       * Node data transformation, called by ctx.document.fromJSON\n       * @param node\n       * @param json\n       */\n      fromNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.toJSON 调用\n       * Node data transformation, called by ctx.document.toJSON\n       * @param node\n       * @param json\n       */\n      toNodeJSON(node, json) {\n        return json;\n      },\n      lineColor: {\n        hidden: 'var(--g-workflow-line-color-hidden,transparent)',\n        default: 'var(--g-workflow-line-color-default,#4d53e8)',\n        drawing: 'var(--g-workflow-line-color-drawing, #5DD6E3)',\n        hovered: 'var(--g-workflow-line-color-hover,#37d0ff)',\n        selected: 'var(--g-workflow-line-color-selected,#37d0ff)',\n        error: 'var(--g-workflow-line-color-error,red)',\n        flowing: 'var(--g-workflow-line-color-flowing,#4d53e8)',\n      },\n      /*\n       * Check whether the line can be added\n       * 判断是否连线\n       */\n      canAddLine(ctx, fromPort, toPort) {\n        // Cannot be a self-loop on the same node / 不能是同一节点自循环\n        if (fromPort.node === toPort.node) {\n          return false;\n        }\n        // Cannot be in different containers - 不能在不同容器\n        if (\n          fromPort.node.parent?.id !== toPort.node.parent?.id &&\n          ![fromPort.node.parent?.flowNodeType, toPort.node.parent?.flowNodeType].includes(\n            FlowNodeBaseType.GROUP\n          )\n        ) {\n          return false;\n        }\n        /**\n         * 线条环检测，不允许连接到前面的节点\n         * Line loop detection, which is not allowed to connect to the node in front of it\n         */\n        return !fromPort.node.lines.allInputNodes.includes(toPort.node);\n      },\n      /**\n       * Check whether the line can be deleted, this triggers on the default shortcut `Bakspace` or `Delete`\n       * 判断是否能删除连线, 这个会在默认快捷键 (Backspace or Delete) 触发\n       */\n      canDeleteLine(ctx, line, newLineInfo, silent) {\n        return true;\n      },\n      /**\n       * Check whether the node can be deleted, this triggers on the default shortcut `Bakspace` or `Delete`\n       * 判断是否能删除节点, 这个会在默认快捷键 (Backspace or Delete) 触发\n       */\n      canDeleteNode(ctx, node) {\n        return true;\n      },\n      /**\n       * 是否允许拖入子画布 (loop or group)\n       * Whether to allow dragging into the sub-canvas (loop or group)\n       */\n      canDropToNode: (ctx, params) => canContainNode(params.dragNodeType!, params.dropNodeType!),\n      /**\n       * Whether to reset line\n       * 是否允许重连\n       * @param ctx\n       * @param oldLine\n       * @param newLineInfo\n       */\n      canResetLine: (ctx, oldLine, newLineInfo) => true,\n      /**\n       * Drag the end of the line to create an add panel (feature optional)\n       * 拖拽线条结束需要创建一个添加面板 （功能可选）\n       * 希望提供控制线条粗细的配置项\n       */\n      onDragLineEnd,\n      /**\n       * SelectBox config\n       */\n      selectBox: {\n        SelectorBoxPopover,\n      },\n      scroll: {\n        /**\n         * Whether to restrict the node from rolling out of the canvas needs to be closed because there is a running results pane\n         * 是否限制节点不能滚出画布，由于有运行结果面板，所以需要关闭\n         */\n        enableScrollLimit: false,\n      },\n      materials: {\n        components: {},\n        /**\n         * Render Node\n         */\n        renderDefaultNode: BaseNode,\n        renderNodes: {\n          [WorkflowNodeType.Comment]: CommentRender,\n        },\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Variable engine enable\n       */\n      variableEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        /**\n         * Listen form data change, default true\n         */\n        enableChangeNode: true,\n      },\n      /**\n       * Content change\n       */\n      onContentChange: debounce((ctx: FreeLayoutPluginContext, event) => {\n        if (ctx.document.disposed) return;\n\n        console.log('Auto Save: ', event, {\n          ...ctx.document.toJSON(),\n          globalVariable: ctx.get<GetGlobalVariableSchema>(GetGlobalVariableSchema)(),\n        });\n      }, 1000),\n      /**\n       * Running line\n       */\n      isFlowingLine: (ctx, line) => ctx.get(WorkflowRuntimeService).isFlowingLine(line),\n      /**\n       * Shortcuts\n       */\n      shortcuts,\n      /**\n       * Bind custom service\n       */\n      onBind: ({ bind }) => {\n        bind(CustomService).toSelf().inSingletonScope();\n        bind(ValidateService).toSelf().inSingletonScope();\n      },\n      /**\n       * Playground init\n       */\n      onInit(ctx) {\n        console.log('--- Playground init ---');\n      },\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        // ctx.tools.autoLayout(); // init auto layout\n        ctx.tools.fitView(false);\n        console.log('--- Playground rendered ---');\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      i18n: {\n        locale: navigator.language,\n        languages: {\n          'zh-CN': {\n            'Never Remind': '不再提示',\n            'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',\n          },\n          'en-US': {},\n        },\n      },\n      plugins: () => [\n        /**\n         * Custom node sorting, the code below will make the comment nodes always below the normal nodes\n         * 自定义节点排序，下边的代码会让 comment 节点永远在普通节点下边\n         */\n        createFreeStackPlugin({\n          sortNodes: (nodes: WorkflowNodeEntity[]) => {\n            const commentNodes: WorkflowNodeEntity[] = [];\n            const otherNodes: WorkflowNodeEntity[] = [];\n            nodes.forEach((node) => {\n              if (node.flowNodeType === WorkflowNodeType.Comment) {\n                commentNodes.push(node);\n              } else {\n                otherNodes.push(node);\n              }\n            });\n            return [...commentNodes, ...otherNodes];\n          },\n        }),\n        /**\n         * Line render plugin\n         * 连线渲染插件\n         */\n        createFreeLinesPlugin({\n          renderInsideLine: LineAddButton,\n        }),\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(242, 243, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(255, 255, 255, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(6, 7, 9, 0.10)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: undefined,\n            nodeColor: 'rgba(0, 0, 0, 0.10)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0.55)',\n          },\n        }),\n        /**\n         * Download plugin\n         * 下载插件\n         */\n        createDownloadPlugin({}),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n        /**\n         * NodeAddPanel render plugin\n         * 节点添加面板渲染插件\n         */\n        createFreeNodePanelPlugin({\n          renderer: NodePanel,\n        }),\n        /**\n         * This is used for the rendering of the loop node sub-canvas\n         * 这个用于 loop 节点子画布的渲染\n         */\n        createContainerNodePlugin({}),\n        /**\n         * Group plugin\n         */\n        createFreeGroupPlugin({\n          groupNodeRender: GroupNodeRender,\n        }),\n        /**\n         * ContextMenu plugin\n         */\n        createContextMenuPlugin({}),\n        /**\n         * Runtime plugin\n         * ⚠️ Browser mode is for demo only; for production, please deploy the server-side runtime\n         * https://flowgram.ai/guide/runtime/introduction.html\n         */\n        createRuntimePlugin({\n          mode: 'browser', // browser mode is for demo only!\n          // mode: 'server',\n          // serverConfig: {\n          //   domain: 'localhost',\n          //   port: 4000,\n          //   protocol: 'http',\n          // },\n        }),\n\n        /**\n         * Variable panel plugin\n         * 变量面板插件\n         */\n        createVariablePanelPlugin({\n          initialData: initialData.globalVariable,\n        }),\n        /** Float layout plugin */\n        createPanelManagerPlugin(),\n      ],\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/hooks/use-is-sidebar.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { IsSidebarContext } from '../context';\n\nexport function useIsSidebar() {\n  return useContext(IsSidebarContext);\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/hooks/use-node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { NodeRenderContext } from '../context';\n\nexport function useNodeRenderContext() {\n  return useContext(NodeRenderContext);\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/hooks/use-port-click.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useState } from 'react';\n\nimport {\n  WorkflowNodePanelService,\n  WorkflowNodePanelUtils,\n  type CallNodePanelParams,\n  type NodePanelResult,\n} from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  delay,\n  usePlayground,\n  useService,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-editor';\n\n/**\n * click port to trigger node select panel\n * 点击端口后唤起节点选择面板\n */\nexport const usePortClick = () => {\n  const playground = usePlayground();\n  const nodePanelService = useService(WorkflowNodePanelService);\n  const document = useService(WorkflowDocument);\n  const dragService = useService(WorkflowDragService);\n  const linesManager = useService(WorkflowLinesManager);\n  const [active, setActive] = useState(false);\n\n  const singleSelectNodePanel = useCallback(\n    async (\n      params: Omit<CallNodePanelParams, 'onSelect' | 'onClose' | 'enableMultiAdd'>\n    ): Promise<NodePanelResult | undefined> => {\n      if (active) {\n        return;\n      }\n      setActive(true);\n      return new Promise((resolve) => {\n        nodePanelService.callNodePanel({\n          ...params,\n          enableMultiAdd: false,\n          onSelect: async (panelParams?: NodePanelResult) => {\n            resolve(panelParams);\n          },\n          onClose: () => {\n            setActive(false);\n            resolve(undefined);\n          },\n        });\n      });\n    },\n    [active]\n  );\n\n  const onPortClick = useCallback(\n    async (e: React.MouseEvent, port: WorkflowPortEntity) => {\n      if (port.portType === 'input') return;\n      const mousePos = playground.config.getPosFromMouseEvent(e);\n      const containerNode = port.node.parent;\n      // open node selection panel - 打开节点选择面板\n      const result = await singleSelectNodePanel({\n        position: mousePos,\n        containerNode,\n        panelProps: {\n          enableScrollClose: true,\n          fromPort: port,\n        },\n      });\n\n      // return if no node selected - 如果没有选择节点则返回\n      if (!result) {\n        return;\n      }\n\n      // get selected node type and data - 获取选择的节点类型和数据\n      const { nodeType, nodeJSON } = result;\n\n      // calculate position for the new node - 计算新节点的位置\n      const nodePosition = WorkflowNodePanelUtils.adjustNodePosition({\n        nodeType,\n        position:\n          port.location === 'bottom'\n            ? {\n                x: mousePos.x,\n                y: mousePos.y + 100,\n              }\n            : {\n                x: mousePos.x + 100,\n                y: mousePos.y,\n              },\n        fromPort: port,\n        containerNode,\n        document,\n        dragService,\n      });\n\n      // create new workflow node - 创建新的工作流节点\n      const node: WorkflowNodeEntity = document.createWorkflowNodeByType(\n        nodeType,\n        nodePosition,\n        nodeJSON ?? ({} as WorkflowNodeJSON),\n        containerNode?.id\n      );\n\n      // wait for node render - 等待节点渲染\n      await delay(20);\n\n      // build connection line - 构建连接线\n      WorkflowNodePanelUtils.buildLine({\n        fromPort: port,\n        node,\n        linesManager,\n      });\n    },\n    [singleSelectNodePanel]\n  );\n\n  return onPortClick;\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFreeLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-free-layout/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from './typings';\n\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 601.2,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            query: {\n              type: 'string',\n              default: 'Hello Flow.',\n            },\n            enable: {\n              type: 'boolean',\n              default: true,\n            },\n            array_obj: {\n              type: 'array',\n              items: {\n                type: 'object',\n                properties: {\n                  int: {\n                    type: 'number',\n                  },\n                  str: {\n                    type: 'string',\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 1100,\n          y: 546.2,\n        },\n      },\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            key: 'if_0',\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'query'],\n              },\n              operator: 'contains',\n              right: {\n                type: 'constant',\n                content: 'Hello Flow.',\n              },\n            },\n          },\n        ],\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 2968,\n          y: 601.2,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          success: {\n            type: 'constant',\n            content: true,\n            schema: {\n              type: 'boolean',\n            },\n          },\n          query: {\n            type: 'ref',\n            content: ['start_0', 'query'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            success: {\n              type: 'boolean',\n            },\n            query: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: '159623',\n      type: 'comment',\n      meta: {\n        position: {\n          x: 180,\n          y: 775.2,\n        },\n      },\n      data: {\n        size: {\n          width: 240,\n          height: 150,\n        },\n        note: 'hi ~\\n\\nthis is a comment node\\n\\n- flowgram.ai',\n      },\n    },\n    {\n      id: 'http_rDGIH',\n      type: 'http',\n      meta: {\n        position: {\n          x: 640,\n          y: 421.35,\n        },\n      },\n      data: {\n        title: 'HTTP_1',\n        outputs: {\n          type: 'object',\n          properties: {\n            body: {\n              type: 'string',\n            },\n            headers: {\n              type: 'object',\n            },\n            statusCode: {\n              type: 'integer',\n            },\n          },\n        },\n        api: {\n          method: 'GET',\n          url: {\n            type: 'template',\n            content: '',\n          },\n        },\n        body: {\n          bodyType: 'JSON',\n        },\n        timeout: {\n          timeout: 10000,\n          retryTimes: 1,\n        },\n      },\n    },\n    {\n      id: 'loop_Ycnsk',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 1460,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'Loop_1',\n        loopFor: {\n          type: 'ref',\n          content: ['start_0', 'array_obj'],\n        },\n        loopOutputs: {\n          acm: {\n            type: 'ref',\n            content: ['llm_6aSyo', 'result'],\n          },\n        },\n        outputs: {\n          type: 'object',\n          required: [],\n          properties: {\n            acm: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n      blocks: [\n        {\n          id: 'llm_6aSyo',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 344,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_3',\n            inputsValues: {\n              modelName: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              apiKey: {\n                type: 'constant',\n                content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n              },\n              apiHost: {\n                type: 'constant',\n                content: 'https://mock-ai-url/api/v3',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'template',\n                content: '# Role\\nYou are an AI assistant.\\n',\n              },\n              prompt: {\n                type: 'template',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n              properties: {\n                modelName: {\n                  type: 'string',\n                },\n                apiKey: {\n                  type: 'string',\n                },\n                apiHost: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n                prompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'llm_ZqKlP',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 804,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_4',\n            inputsValues: {\n              modelName: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              apiKey: {\n                type: 'constant',\n                content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n              },\n              apiHost: {\n                type: 'constant',\n                content: 'https://mock-ai-url/api/v3',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'template',\n                content: '# Role\\nYou are an AI assistant.\\n',\n              },\n              prompt: {\n                type: 'template',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n              properties: {\n                modelName: {\n                  type: 'string',\n                },\n                apiKey: {\n                  type: 'string',\n                },\n                apiHost: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n                prompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'block_start_PUDtS',\n          type: 'block-start',\n          meta: {\n            position: {\n              x: 32,\n              y: 167.1,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'block_end_leBbs',\n          type: 'block-end',\n          meta: {\n            position: {\n              x: 1116,\n              y: 167.1,\n            },\n          },\n          data: {},\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'block_start_PUDtS',\n          targetNodeID: 'llm_6aSyo',\n        },\n        {\n          sourceNodeID: 'llm_6aSyo',\n          targetNodeID: 'llm_ZqKlP',\n        },\n        {\n          sourceNodeID: 'llm_ZqKlP',\n          targetNodeID: 'block_end_leBbs',\n        },\n      ],\n    },\n    {\n      id: 'group_nYl6D',\n      type: 'group',\n      meta: {\n        position: {\n          x: 1624,\n          y: 698.2,\n        },\n      },\n      data: {\n        parentID: 'root',\n        blockIDs: ['llm_8--A3', 'llm_vTyMa'],\n      },\n    },\n    {\n      id: 'llm_8--A3',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 180,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: '# Role\\nYou are an AI assistant.\\n',\n          },\n          prompt: {\n            type: 'template',\n            content: '# User Input\\nquery:{{start_0.query}}\\nenable:{{start_0.enable}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_vTyMa',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 640,\n          y: 10,\n        },\n      },\n      data: {\n        title: 'LLM_2',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: '# Role\\nYou are an AI assistant.\\n',\n          },\n          prompt: {\n            type: 'template',\n            content: '# LLM Input\\nresult:{{llm_8--A3.result}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'http_rDGIH',\n    },\n    {\n      sourceNodeID: 'http_rDGIH',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'loop_Ycnsk',\n      sourcePortID: 'if_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_8--A3',\n      sourcePortID: 'else',\n    },\n    {\n      sourceNodeID: 'llm_vTyMa',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'loop_Ycnsk',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_8--A3',\n      targetNodeID: 'llm_vTyMa',\n    },\n  ],\n  globalVariable: {\n    type: 'object',\n    required: [],\n    properties: {\n      userId: {\n        type: 'string',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/block-end/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { Avatar } from '@douyinfe/semi-ui';\n\nimport { FlowNodeJSON } from '../../typings';\nimport iconEnd from '../../assets/icon-end.jpg';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <div\n      style={{\n        width: 60,\n        height: 60,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n    >\n      <Avatar\n        shape=\"circle\"\n        style={{\n          width: 40,\n          height: 40,\n          borderRadius: '50%',\n          cursor: 'move',\n        }}\n        alt=\"Icon\"\n        src={iconEnd}\n      />\n    </div>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/block-end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconStart from '../../assets/icon-start.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nexport const BlockEndNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.BlockEnd,\n  meta: {\n    isNodeEnd: true,\n    deleteDisable: true,\n    copyDisable: true,\n    sidebarDisabled: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 100,\n      height: 100,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      borderRadius: 12,\n      cursor: 'move',\n    },\n  },\n  info: {\n    icon: iconStart,\n    description: 'The final node of the block.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/block-start/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { Avatar } from '@douyinfe/semi-ui';\n\nimport { FlowNodeJSON } from '../../typings';\nimport iconStart from '../../assets/icon-start.jpg';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <div\n      style={{\n        width: 60,\n        height: 60,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n    >\n      <Avatar\n        shape=\"circle\"\n        style={{\n          width: 40,\n          height: 40,\n          borderRadius: '50%',\n          cursor: 'move',\n        }}\n        alt=\"Icon\"\n        src={iconStart}\n      />\n    </div>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/block-start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconStart from '../../assets/icon-start.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nexport const BlockStartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.BlockStart,\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    sidebarDisabled: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 100,\n      height: 100,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      borderRadius: 12,\n      cursor: 'move',\n    },\n  },\n  info: {\n    icon: iconStart,\n    description: 'The starting node of the block.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/break/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/free-layout-editor';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent } from '../../form-components';\n\nexport const renderForm = () => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent />\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent />\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/break/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconBreak from '../../assets/icon-break.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nlet index = 0;\nexport const BreakNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Break,\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n    sidebarDisabled: true,\n    size: {\n      width: 360,\n      height: 54,\n    },\n    expandable: false,\n    onlyInContainer: WorkflowNodeType.Loop,\n  },\n  info: {\n    icon: iconBreak,\n    description:\n      'The final node of the workflow, used to return the result information after the workflow is run.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  onAdd() {\n    return {\n      id: `break_${nanoid(5)}`,\n      type: 'break',\n      data: {\n        title: `Break_${++index}`,\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/components/code.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { TypeScriptCodeEditor } from '@flowgram.ai/form-materials';\nimport { Divider } from '@douyinfe/semi-ui';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../../hooks';\n\nexport function Code() {\n  const isSidebar = useIsSidebar();\n  const { readonly } = useNodeRenderContext();\n\n  if (!isSidebar) {\n    return null;\n  }\n\n  return (\n    <>\n      <Divider />\n      <Field<string> name=\"script.content\">\n        {({ field }) => (\n          <TypeScriptCodeEditor\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            readonly={readonly}\n          />\n        )}\n      </Field>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/components/inputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Inputs() {\n  const isSidebar = useIsSidebar();\n\n  const { readonly } = useNodeRenderContext();\n\n  if (!isSidebar) {\n    return (\n      <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n        {({ field }) => <DisplayInputsValues value={field.value} />}\n      </Field>\n    );\n  }\n\n  return (\n    <FormItem name=\"inputs\" type=\"object\" vertical>\n      <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n        {({ field }) => (\n          <InputsValues\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            readonly={readonly}\n          />\n        )}\n      </Field>\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/components/outputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DisplayOutputs, IJsonSchema, JsonSchemaEditor } from '@flowgram.ai/form-materials';\nimport { Divider } from '@douyinfe/semi-ui';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Outputs() {\n  const { readonly } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n\n  if (!isSidebar) {\n    return (\n      <>\n        <Divider />\n        <Field<IJsonSchema> name=\"outputs\">\n          {({ field }) => <DisplayOutputs value={field.value} />}\n        </Field>\n      </>\n    );\n  }\n\n  return (\n    <>\n      <Divider />\n      <FormItem name=\"outputs\" type=\"object\" vertical>\n        <Field<IJsonSchema> name=\"outputs\">\n          {({ field }) => (\n            <JsonSchemaEditor\n              readonly={readonly}\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n          )}\n        </Field>\n      </FormItem>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { createInferInputsPlugin } from '@flowgram.ai/form-materials';\n\nimport { FormHeader, FormContent } from '../../form-components';\nimport { CodeNodeJSON } from './types';\nimport { Outputs } from './components/outputs';\nimport { Inputs } from './components/inputs';\nimport { Code } from './components/code';\nimport { defaultFormMeta } from '../default-form-meta';\n\nexport const FormRender = ({ form }: FormRenderProps<CodeNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <Inputs />\n      <Code />\n      <Outputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: (props) => <FormRender {...props} />,\n  effect: defaultFormMeta.effect,\n  validate: defaultFormMeta.validate,\n  plugins: [createInferInputsPlugin({ sourceKey: 'inputsValues', targetKey: 'inputs' })],\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCode from '../../assets/icon-script.png';\nimport { formMeta } from './form-meta';\n\nlet index = 0;\n\nconst defaultCode = `// Here, you can retrieve input variables from the node using 'params' and output results using 'ret'.\n// 'params' has been correctly injected into the environment.\n// Here's an example of getting the value of the parameter named 'input' from the node input:\n// const input = params.input;\n// Here's an example of outputting a 'ret' object containing multiple data types:\n// const ret = { \"name\": 'Xiaoming', \"hobbies\": [\"Reading\", \"Traveling\"] };\n\nasync function main({ params }) {\n  // Build the output object\n  const ret = {\n    key0: params.input + params.input, // Concatenate the input parameter 'input' twice\n    key1: [\"hello\", \"world\"], // Output an array\n    key2: { // Output an Object\n      key21: \"hi\"\n    },\n  };\n\n  return ret;\n}`;\n\nexport const CodeNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Code,\n  info: {\n    icon: iconCode,\n    description: 'Run the Script',\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    return {\n      id: `code_${nanoid(5)}`,\n      type: 'code',\n      data: {\n        title: `Code_${++index}`,\n        inputsValues: {\n          input: { type: 'constant', content: '' },\n        },\n        script: {\n          language: 'javascript',\n          content: defaultCode,\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            key0: {\n              type: 'string',\n            },\n            key1: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            key2: {\n              type: 'object',\n              properties: {\n                key21: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      },\n    };\n  },\n  formMeta: formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/code/types.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';\nimport { IFlowValue, IJsonSchema } from '@flowgram.ai/form-materials';\n\nexport interface CodeNodeJSON extends FlowNodeJSON {\n  data: {\n    title: string;\n    inputsValues: Record<string, IFlowValue>;\n    inputs: IJsonSchema<'object'>;\n    outputs: IJsonSchema<'object'>;\n    script: {\n      language: 'javascript';\n      content: string;\n    };\n  };\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/comment/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\n\nexport const CommentNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Comment,\n  meta: {\n    sidebarDisabled: true,\n    nodePanelVisible: false,\n    defaultPorts: [],\n    renderKey: WorkflowNodeType.Comment,\n    size: {\n      width: 240,\n      height: 150,\n    },\n  },\n  formMeta: {\n    render: () => <></>,\n  },\n  getInputPoints: () => [], // Comment 节点没有输入\n  getOutputPoints: () => [], // Comment 节点没有输出\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/condition/condition-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { Field, FieldArray, I18n } from '@flowgram.ai/free-layout-editor';\nimport { ConditionRow, ConditionRowValueType } from '@flowgram.ai/form-materials';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus, IconCrossCircleStroked } from '@douyinfe/semi-icons';\n\nimport { useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\nimport { Feedback } from '../../../form-components';\nimport { ConditionPort } from './styles';\n\ninterface ConditionValue {\n  key: string;\n  value?: ConditionRowValueType;\n}\n\nexport function ConditionInputs() {\n  const { node, readonly } = useNodeRenderContext();\n\n  useLayoutEffect(() => {\n    window.requestAnimationFrame(() => {\n      node.ports.updateDynamicPorts();\n    });\n  }, [node]);\n\n  return (\n    <FieldArray name=\"conditions\">\n      {({ field }) => (\n        <>\n          {field.map((child, index) => (\n            <Field<ConditionValue> key={child.name} name={child.name}>\n              {({ field: childField, fieldState: childState }) => (\n                <FormItem name=\"if\" type=\"boolean\" required={true} labelWidth={50}>\n                  <div style={{ display: 'flex', alignItems: 'center' }}>\n                    <ConditionRow\n                      readonly={readonly}\n                      style={{ flexGrow: 1, overflow: 'hidden' }}\n                      value={childField.value.value}\n                      onChange={(v) => childField.onChange({ value: v, key: childField.value.key })}\n                    />\n\n                    {!readonly && (\n                      <Button\n                        theme=\"borderless\"\n                        disabled={readonly}\n                        icon={<IconCrossCircleStroked />}\n                        onClick={() => field.delete(index)}\n                      />\n                    )}\n                  </div>\n\n                  <Feedback errors={childState?.errors} invalid={childState?.invalid} />\n                  <ConditionPort data-port-id={childField.value.key} data-port-type=\"output\" />\n                </FormItem>\n              )}\n            </Field>\n          ))}\n          <FormItem name=\"else\" type=\"boolean\" required={true} labelWidth={100}>\n            <ConditionPort data-port-id=\"else\" data-port-type=\"output\" />\n          </FormItem>\n          {!readonly && (\n            <div>\n              <Button\n                theme=\"borderless\"\n                icon={<IconPlus />}\n                onClick={() =>\n                  field.append({\n                    key: `if_${nanoid(6)}`,\n                    value: { type: 'expression', content: '' },\n                  })\n                }\n              >\n                {I18n.t('Add')}\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </FieldArray>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/condition/condition-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const ConditionPort = styled.div`\n  position: absolute;\n  right: -12px;\n  top: 50%;\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/condition/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent } from '../../form-components';\nimport { ConditionInputs } from './condition-inputs';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <ConditionInputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n    'conditions.*': ({ value }) => {\n      if (!value?.value) return 'Condition is required';\n      return undefined;\n    },\n  },\n  effect: {\n    conditions: autoRenameRefEffect,\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconCondition from '../../assets/icon-condition.svg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nexport const ConditionNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Condition,\n  info: {\n    icon: iconCondition,\n    description:\n      'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',\n  },\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n    // Condition Outputs use dynamic port\n    useDynamicPort: true,\n    expandable: false, // disable expanded\n    size: {\n      width: 360,\n      height: 210,\n    },\n  },\n  formMeta,\n  onAdd() {\n    return {\n      id: `condition_${nanoid(5)}`,\n      type: 'condition',\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            key: `if_${nanoid(5)}`,\n            value: {},\n          },\n          {\n            key: `if_${nanoid(5)}`,\n            value: {},\n          },\n        ],\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowNodeType {\n  Start = 'start',\n  End = 'end',\n  LLM = 'llm',\n  HTTP = 'http',\n  Code = 'code',\n  Variable = 'variable',\n  Condition = 'condition',\n  MultiCondition = 'multi-condition',\n  Loop = 'loop',\n  BlockStart = 'block-start',\n  BlockEnd = 'block-end',\n  Comment = 'comment',\n  Continue = 'continue',\n  Break = 'break',\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/continue/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/free-layout-editor';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent } from '../../form-components';\n\nexport const renderForm = () => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent />\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent />\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/continue/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconContinue from '../../assets/icon-continue.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nlet index = 0;\nexport const ContinueNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Continue,\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n    sidebarDisabled: true,\n    size: {\n      width: 360,\n      height: 54,\n    },\n    expandable: false,\n    onlyInContainer: WorkflowNodeType.Loop,\n  },\n  info: {\n    icon: iconContinue,\n    description:\n      'The final node of the workflow, used to return the result information after the workflow is run.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  onAdd() {\n    return {\n      id: `continue_${nanoid(5)}`,\n      type: 'continue',\n      data: {\n        title: `Continue_${++index}`,\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/default-form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\nimport {\n  autoRenameRefEffect,\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n  DisplayOutputs,\n  validateFlowValue,\n  validateWhenVariableSync,\n  listenRefSchemaChange,\n} from '@flowgram.ai/form-materials';\nimport { Divider } from '@douyinfe/semi-ui';\n\nimport { FlowNodeJSON } from '../typings';\nimport { FormHeader, FormContent, FormInputs } from '../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <Divider />\n      <DisplayOutputs displayFromScope />\n    </FormContent>\n  </>\n);\n\nexport const defaultFormMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  /**\n   * Supported writing as:\n   * 1: validate as options: { title: () => {} , ... }\n   * 2: validate as dynamic function: (values,  ctx) => ({ title: () => {}, ... })\n   */\n  validate: {\n    title: ({ value }) => (value ? undefined : 'Title is required'),\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropertyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n\n      return validateFlowValue(value, {\n        node: context.node,\n        required: required.includes(valuePropertyKey),\n        errorMessages: {\n          required: `${valuePropertyKey} is required`,\n        },\n      });\n    },\n  },\n  /**\n   * Initialize (fromJSON) data transformation\n   * 初始化(fromJSON) 数据转换\n   * @param value\n   * @param ctx\n   */\n  formatOnInit: (value, ctx) => value,\n  /**\n   * Save (toJSON) data transformation\n   * 保存(toJSON) 数据转换\n   * @param value\n   * @param ctx\n   */\n  formatOnSubmit: (value, ctx) => value,\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n    inputsValues: [...autoRenameRefEffect, ...validateWhenVariableSync({ scope: 'public' })],\n    'inputsValues.*': listenRefSchemaChange((params) => {\n      console.log(`[${params.context.node.id}][${params.name}] Schema Of Ref Updated`);\n    }),\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/end/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  createInferInputsPlugin,\n  DisplayInputsValues,\n  IFlowValue,\n  InputsValues,\n} from '@flowgram.ai/form-materials';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent } from '../../form-components';\n\nexport const renderForm = () => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n            {({ field: { value, onChange } }) => (\n              <>\n                <InputsValues value={value} onChange={(_v) => onChange(_v)} />\n              </>\n            )}\n          </Field>\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<Record<string, IFlowValue | undefined> | undefined> name=\"inputsValues\">\n          {({ field: { value } }) => (\n            <>\n              <DisplayInputsValues value={value} />\n            </>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: renderForm,\n  plugins: [\n    createInferInputsPlugin({\n      sourceKey: 'inputsValues',\n      targetKey: 'inputs',\n    }),\n  ],\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconEnd from '../../assets/icon-end.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nexport const EndNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.End,\n  meta: {\n    deleteDisable: true,\n    copyDisable: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconEnd,\n    description:\n      'The final node of the workflow, used to return the result information after the workflow is run.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * End Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/group/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport {\n  FlowNodeBaseType,\n  WorkflowNodeEntity,\n  PositionSchema,\n  FlowNodeTransformData,\n  nanoid,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\n\nlet index = 0;\nexport const GroupNodeRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.GROUP,\n  meta: {\n    renderKey: FlowNodeBaseType.GROUP,\n    defaultPorts: [],\n    isContainer: true,\n    disableSideBar: true,\n    size: {\n      width: 560,\n      height: 400,\n    },\n    padding: () => ({\n      top: 80,\n      bottom: 40,\n      left: 65,\n      right: 65,\n    }),\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    expandable: false,\n    /**\n     * It cannot be added through the panel\n     * 不能通过面板添加\n     */\n    nodePanelVisible: false,\n  },\n  formMeta: {\n    render: () => <></>,\n  },\n  onAdd() {\n    return {\n      type: FlowNodeBaseType.GROUP,\n      id: `group_${nanoid(5)}`,\n      meta: {\n        position: {\n          x: 0,\n          y: 0,\n        },\n      },\n      data: {\n        color: 'Green',\n        title: `Group_${++index}`,\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/components/api.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { IFlowTemplateValue, PromptEditorWithVariables } from '@flowgram.ai/form-materials';\nimport { Select } from '@douyinfe/semi-ui';\n\nimport { useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Api() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <div>\n      <FormItem name=\"API\" required vertical type=\"string\">\n        <div style={{ display: 'flex', gap: 5 }}>\n          <Field<string> name=\"api.method\" defaultValue=\"GET\">\n            {({ field }) => (\n              <Select\n                value={field.value}\n                onChange={(value) => {\n                  field.onChange(value as string);\n                }}\n                style={{ width: 85, maxWidth: 85, minWidth: 85 }}\n                size=\"small\"\n                disabled={readonly}\n                optionList={[\n                  { label: 'GET', value: 'GET' },\n                  { label: 'POST', value: 'POST' },\n                  { label: 'PUT', value: 'PUT' },\n                  { label: 'DELETE', value: 'DELETE' },\n                  { label: 'PATCH', value: 'PATCH' },\n                  { label: 'HEAD', value: 'HEAD' },\n                ]}\n              />\n            )}\n          </Field>\n\n          <Field<IFlowTemplateValue> name=\"api.url\">\n            {({ field }) => (\n              <PromptEditorWithVariables\n                disableMarkdownHighlight\n                readonly={readonly}\n                style={{ flexGrow: 1 }}\n                placeholder=\"Input URL, use var by '{'\"\n                value={field.value}\n                onChange={(value) => {\n                  field.onChange(value!);\n                }}\n              />\n            )}\n          </Field>\n        </div>\n      </FormItem>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/components/body.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport {\n  IFlowTemplateValue,\n  JsonEditorWithVariables,\n  PromptEditorWithVariables,\n} from '@flowgram.ai/form-materials';\nimport { Select } from '@douyinfe/semi-ui';\n\nimport { useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nconst BODY_TYPE_OPTIONS = [\n  {\n    label: 'None',\n    value: 'none',\n  },\n  {\n    label: 'JSON',\n    value: 'JSON',\n  },\n  {\n    label: 'Raw Text',\n    value: 'raw-text',\n  },\n];\n\nexport function Body() {\n  const { readonly } = useNodeRenderContext();\n\n  const renderBodyEditor = (bodyType: string) => {\n    switch (bodyType) {\n      case 'JSON':\n        return (\n          <Field<IFlowTemplateValue> name=\"body.json\">\n            {({ field }) => (\n              <JsonEditorWithVariables\n                value={field.value?.content}\n                readonly={readonly}\n                activeLinePlaceholder=\"use var by '@'\"\n                onChange={(value) => {\n                  field.onChange({ type: 'template', content: value });\n                }}\n              />\n            )}\n          </Field>\n        );\n      case 'raw-text':\n        return (\n          <Field<IFlowTemplateValue> name=\"body.rawText\">\n            {({ field }) => (\n              <PromptEditorWithVariables\n                disableMarkdownHighlight\n                readonly={readonly}\n                style={{ flexGrow: 1 }}\n                placeholder=\"Input raw text, use var by '{'\"\n                onChange={(value) => {\n                  field.onChange(value!);\n                }}\n              />\n            )}\n          </Field>\n        );\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <Field<string> name=\"body.bodyType\" defaultValue=\"JSON\">\n      {({ field }) => (\n        <div style={{ marginTop: 5 }}>\n          <FormItem name=\"Body\" vertical type=\"object\">\n            <Select\n              value={field.value}\n              onChange={(value) => {\n                field.onChange(value as string);\n              }}\n              style={{ width: '100%', marginBottom: 10 }}\n              disabled={readonly}\n              size=\"small\"\n              optionList={BODY_TYPE_OPTIONS}\n            />\n            {renderBodyEditor(field.value)}\n          </FormItem>\n        </div>\n      )}\n    </Field>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/components/headers.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Headers() {\n  const { readonly } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n\n  if (!isSidebar) {\n    return (\n      <FormItem name=\"headers\" type=\"object\" vertical>\n        <Field<Record<string, IFlowValue | undefined> | undefined> name=\"headersValues\">\n          {({ field }) => <DisplayInputsValues value={field.value} />}\n        </Field>\n      </FormItem>\n    );\n  }\n\n  return (\n    <FormItem name=\"headers\" type=\"object\" vertical>\n      <Field<Record<string, IFlowValue | undefined> | undefined> name=\"headersValues\">\n        {({ field }) => (\n          <InputsValues\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            readonly={readonly}\n          />\n        )}\n      </Field>\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/components/params.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DisplayInputsValues, IFlowValue, InputsValues } from '@flowgram.ai/form-materials';\n\nimport { useIsSidebar, useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Params() {\n  const { readonly } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n\n  if (!isSidebar) {\n    return (\n      <FormItem name=\"params\" type=\"object\" vertical>\n        <Field<Record<string, IFlowValue | undefined> | undefined> name=\"paramsValues\">\n          {({ field }) => <DisplayInputsValues value={field.value} />}\n        </Field>\n      </FormItem>\n    );\n  }\n\n  return (\n    <FormItem name=\"params\" type=\"object\" vertical>\n      <Field<Record<string, IFlowValue | undefined> | undefined> name=\"paramsValues\">\n        {({ field }) => (\n          <InputsValues\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            readonly={readonly}\n          />\n        )}\n      </Field>\n    </FormItem>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/components/timeout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { InputNumber } from '@douyinfe/semi-ui';\n\nimport { useNodeRenderContext } from '../../../hooks';\nimport { FormItem } from '../../../form-components';\n\nexport function Timeout() {\n  const { readonly } = useNodeRenderContext();\n\n  return (\n    <div>\n      <FormItem name=\"Timeout(ms)\" required style={{ flex: 1 }} type=\"number\">\n        <Field<number> name=\"timeout.timeout\" defaultValue={10000}>\n          {({ field }) => (\n            <InputNumber\n              size=\"small\"\n              value={field.value}\n              onChange={(value) => {\n                field.onChange(value as number);\n              }}\n              disabled={readonly}\n              style={{ width: '100%' }}\n              min={0}\n            />\n          )}\n        </Field>\n      </FormItem>\n      <FormItem name=\"Retry Times\" required type=\"number\">\n        <Field<number> name=\"timeout.retryTimes\" defaultValue={1}>\n          {({ field }) => (\n            <InputNumber\n              size=\"small\"\n              value={field.value}\n              onChange={(value) => {\n                field.onChange(value as number);\n              }}\n              disabled={readonly}\n              style={{ width: '100%' }}\n              min={0}\n            />\n          )}\n        </Field>\n      </FormItem>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { createInferInputsPlugin, DisplayOutputs } from '@flowgram.ai/form-materials';\nimport { Divider } from '@douyinfe/semi-ui';\n\nimport { FormHeader, FormContent } from '../../form-components';\nimport { HTTPNodeJSON } from './types';\nimport { Timeout } from './components/timeout';\nimport { Params } from './components/params';\nimport { Headers } from './components/headers';\nimport { Body } from './components/body';\nimport { Api } from './components/api';\nimport { defaultFormMeta } from '../default-form-meta';\n\nexport const FormRender = ({ form }: FormRenderProps<HTTPNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <Api />\n      <Divider />\n      <Headers />\n      <Divider />\n      <Params />\n      <Divider />\n      <Body />\n      <Divider />\n      <Timeout />\n      <Divider />\n      <DisplayOutputs displayFromScope />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: (props) => <FormRender {...props} />,\n  effect: defaultFormMeta.effect,\n  plugins: [\n    createInferInputsPlugin({ sourceKey: 'headersValues', targetKey: 'headers' }),\n    createInferInputsPlugin({ sourceKey: 'paramsValues', targetKey: 'params' }),\n  ],\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconHTTP from '../../assets/icon-http.svg';\nimport { formMeta } from './form-meta';\n\nlet index = 0;\n\nexport const HTTPNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.HTTP,\n  info: {\n    icon: iconHTTP,\n    description: 'Call the HTTP API',\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    return {\n      id: `http_${nanoid(5)}`,\n      type: 'http',\n      data: {\n        title: `HTTP_${++index}`,\n        api: {\n          method: 'GET',\n        },\n        body: {\n          bodyType: 'JSON',\n        },\n        headers: {},\n        params: {},\n        outputs: {\n          type: 'object',\n          properties: {\n            body: { type: 'string' },\n            headers: { type: 'object' },\n            statusCode: { type: 'integer' },\n          },\n        },\n      },\n    };\n  },\n  formMeta: formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/http/types.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue } from '@flowgram.ai/runtime-interface';\nimport { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';\nimport { IFlowTemplateValue, IJsonSchema } from '@flowgram.ai/form-materials';\n\nexport interface HTTPNodeJSON extends FlowNodeJSON {\n  data: {\n    title: string;\n    outputs: IJsonSchema<'object'>;\n    api: {\n      method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';\n      url: IFlowTemplateValue;\n    };\n    headers: IJsonSchema<'object'>;\n    headersValues: Record<string, IFlowConstantRefValue>;\n    params: IJsonSchema<'object'>;\n    paramsValues: Record<string, IFlowConstantRefValue>;\n    body: {\n      bodyType: 'none' | 'form-data' | 'x-www-form-urlencoded' | 'raw-text' | 'JSON';\n      json?: IFlowTemplateValue;\n      formData?: IJsonSchema<'object'>;\n      formDataValues?: Record<string, IFlowConstantRefValue>;\n      rawText?: IFlowTemplateValue;\n      xWwwFormUrlencoded?: IJsonSchema<'object'>;\n      xWwwFormUrlencodedValues?: Record<string, IFlowConstantRefValue>;\n    };\n    timeout: {\n      retryTimes: number;\n      timeout: number;\n    };\n  };\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../typings';\nimport { VariableNodeRegistry } from './variable';\nimport { StartNodeRegistry } from './start';\nimport { LoopNodeRegistry } from './loop';\nimport { LLMNodeRegistry } from './llm';\nimport { HTTPNodeRegistry } from './http';\nimport { GroupNodeRegistry } from './group';\nimport { EndNodeRegistry } from './end';\nimport { ContinueNodeRegistry } from './continue';\nimport { ConditionNodeRegistry } from './condition';\nimport { CommentNodeRegistry } from './comment';\nimport { CodeNodeRegistry } from './code';\nimport { BreakNodeRegistry } from './break';\nimport { BlockStartNodeRegistry } from './block-start';\nimport { BlockEndNodeRegistry } from './block-end';\nimport { MultiConditionNodeRegistry } from \"./multi-condition\";\nexport { WorkflowNodeType } from './constants';\n\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,\n  StartNodeRegistry,\n  EndNodeRegistry,\n  LLMNodeRegistry,\n  LoopNodeRegistry,\n  CommentNodeRegistry,\n  BlockStartNodeRegistry,\n  BlockEndNodeRegistry,\n  HTTPNodeRegistry,\n  CodeNodeRegistry,\n  ContinueNodeRegistry,\n  BreakNodeRegistry,\n  VariableNodeRegistry,\n  GroupNodeRegistry,\n  MultiConditionNodeRegistry,\n];\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/llm/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconLLM from '../../assets/icon-llm.jpg';\n\nlet index = 0;\nexport const LLMNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.LLM,\n  info: {\n    icon: iconLLM,\n    description:\n      'Call the large language model and use variables and prompt words to generate responses.',\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    return {\n      id: `llm_${nanoid(5)}`,\n      type: 'llm',\n      data: {\n        title: `LLM_${++index}`,\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: '# Role\\nYou are an AI assistant.\\n',\n          },\n          prompt: {\n            type: 'template',\n            content: '',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/loop/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  DisplayOutputs,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar, useNodeRenderContext } from '../../hooks';\nimport { FormHeader, FormContent, FormItem, Feedback } from '../../form-components';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n  const { readonly } = useNodeRenderContext();\n  const formHeight = 115;\n\n  const loopFor = (\n    <Field<IFlowRefValue> name={`loopFor`}>\n      {({ field, fieldState }) => (\n        <FormItem name={'loopFor'} type={'array'} required>\n          <BatchVariableSelector\n            style={{ width: '100%' }}\n            value={field.value?.content}\n            onChange={(val) => field.onChange({ type: 'ref', content: val })}\n            readonly={readonly}\n            hasError={Object.keys(fieldState?.errors || {}).length > 0}\n          />\n          <Feedback errors={fieldState?.errors} />\n        </FormItem>\n      )}\n    </Field>\n  );\n\n  const loopOutputs = (\n    <Field<Record<string, IFlowRefValue | undefined> | undefined> name={`loopOutputs`}>\n      {({ field, fieldState }) => (\n        <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n          <BatchOutputs\n            style={{ width: '100%' }}\n            value={field.value}\n            onChange={(val) => field.onChange(val)}\n            readonly={readonly}\n            hasError={Object.keys(fieldState?.errors || {}).length > 0}\n          />\n          <Feedback errors={fieldState?.errors} />\n        </FormItem>\n      )}\n    </Field>\n  );\n\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          {loopFor}\n          {loopOutputs}\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        {loopFor}\n        <SubCanvasRender offsetY={-formHeight} />\n        <DisplayOutputs displayFromScope />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport {\n  WorkflowNodeEntity,\n  PositionSchema,\n  FlowNodeTransformData,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconLoop from '../../assets/icon-loop.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nlet index = 0;\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Loop,\n  info: {\n    icon: iconLoop,\n    description:\n      'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',\n  },\n  meta: {\n    /**\n     * Mark as subcanvas\n     * 子画布标记\n     */\n    isContainer: true,\n    /**\n     * The subcanvas default size setting\n     * 子画布默认大小设置\n     */\n    size: {\n      width: 424,\n      height: 244,\n    },\n    // autoResizeDisable: true,\n    /**\n     * The subcanvas padding setting\n     * 子画布 padding 设置\n     */\n    padding: (transform) => {\n      if (!transform.isContainer) {\n        return {\n          top: 0,\n          bottom: 0,\n          left: 0,\n          right: 0,\n        };\n      }\n      return {\n        top: 120,\n        bottom: 80,\n        left: 80,\n        right: 80,\n      };\n    },\n    /**\n     * Controls the node selection status within the subcanvas\n     * 控制子画布内的节点选中状态\n     */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // 鼠标开始时所在位置不包括当前节点时才可选中\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    // expandable: false, // disable expanded\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n    },\n    // defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left'}, { type: 'output', location: 'bottom', portID: 'bottom' }, { type: 'input', location: 'top', portID: 'top'}]\n  },\n  onAdd() {\n    return {\n      id: `loop_${nanoid(5)}`,\n      type: WorkflowNodeType.Loop,\n      data: {\n        title: `Loop_${++index}`,\n      },\n      blocks: [\n        {\n          id: `block_start_${nanoid(5)}`,\n          type: WorkflowNodeType.BlockStart,\n          meta: {\n            position: {\n              x: 32,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n        {\n          id: `block_end_${nanoid(5)}`,\n          type: WorkflowNodeType.BlockEnd,\n          meta: {\n            position: {\n              x: 192,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n      ],\n    };\n  },\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/multi-condition/condition-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { Field, FieldArray, I18n, WorkflowNodePortsData } from '@flowgram.ai/free-layout-editor';\nimport { ConditionRow, ConditionRowValueType } from '@flowgram.ai/form-materials';\nimport { Button, Select, Space } from '@douyinfe/semi-ui';\nimport { IconCrossCircleStroked, IconDelete, IconPlus } from '@douyinfe/semi-icons';\n\nimport { useNodeRenderContext, useIsSidebar } from '../../../hooks';\nimport { Feedback, FormItem } from '../../../form-components';\nimport { ConditionBranch, ConditionBranchLogic, ConditionPort } from './styles';\n\ninterface ConditionValue {\n  key: string;\n  value?: ConditionRowValueType;\n}\n\ninterface BranchItem {\n  logic: string; // 'and' | 'or'\n  conditions: ConditionValue[];\n}\n\nexport function ConditionInputs() {\n  const { node, readonly } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n\n  useLayoutEffect(() => {\n    window.requestAnimationFrame(() => {\n      node.getData<WorkflowNodePortsData>(WorkflowNodePortsData).updateDynamicPorts();\n    });\n  }, [node]);\n\n  return (\n    <FieldArray name=\"branch\">\n      {({ field: conditions }) => (\n        <>\n          {conditions.map((branch, index) => (\n            <Field<BranchItem> name={branch.name} key={branch.name}>\n              {({ field, fieldState }) => (\n                <FormItem\n                  type=\"boolean\"\n                  labelWidth={100}\n                  name={index === 0 ? I18n.t('IF') : I18n.t('ELSE-IF')}\n                  vertical\n                  required={index === 0}\n                >\n                  <ConditionBranch>\n                    {field.value.conditions.length > 1 && (\n                      <ConditionBranchLogic>\n                        <Select\n                          size=\"small\"\n                          value={field.value.logic}\n                          style={{ backgroundColor: 'var(--semi-color-bg-0)' }}\n                          onChange={(v) =>\n                            field.onChange({\n                              ...field.value,\n                              logic: (v as string) ?? 'and',\n                            })\n                          }\n                        >\n                          <Select.Option value=\"and\">{I18n.t('AND')}</Select.Option>\n                          <Select.Option value=\"or\">{I18n.t('OR')}</Select.Option>\n                        </Select>\n                      </ConditionBranchLogic>\n                    )}\n                    <div style={{ flex: 1 }}>\n                      {field.value.conditions.map((condition, childIndex) => (\n                        <Field<ConditionValue>\n                          name={`${field.name}.conditions.${childIndex}`}\n                          key={condition.key}\n                        >\n                          {({ field: conditionField }) => (\n                            <Space align=\"center\" style={{ padding: '6px 0', width: '100%' }}>\n                              <div style={{ flex: 1 }}>\n                                <ConditionRow\n                                  readonly={readonly}\n                                  value={conditionField.value.value}\n                                  onChange={(v) => {\n                                    conditionField.onChange({\n                                      value: v,\n                                      key: conditionField.value.key,\n                                    });\n                                  }}\n                                />\n                              </div>\n                              {/*remove current branch condition*/}\n                              {isSidebar && !readonly && (\n                                <Button\n                                  theme=\"borderless\"\n                                  disabled={field.value?.conditions.length === 1}\n                                  icon={<IconCrossCircleStroked />}\n                                  onClick={() =>\n                                    field.onChange({\n                                      ...field.value,\n                                      conditions: field.value.conditions.filter(\n                                        (i: ConditionValue) => i.key !== condition.key\n                                      ),\n                                    })\n                                  }\n                                />\n                              )}\n                            </Space>\n                          )}\n                        </Field>\n                      ))}\n                    </div>\n\n                    <ConditionPort data-port-id={`${branch.name}`} data-port-type=\"output\" />\n                  </ConditionBranch>\n\n                  {/* remove current branch and add new condition*/}\n                  {isSidebar && !readonly && (\n                    <div style={{ display: 'flex', justifyContent: 'flex-end' }}>\n                      <Button\n                        size=\"small\"\n                        theme=\"borderless\"\n                        icon={<IconPlus />}\n                        onClick={() => {\n                          field.onChange({\n                            ...field.value,\n                            conditions: [\n                              ...field.value.conditions,\n                              {\n                                key: `condition_${nanoid(6)}`,\n                                value: {},\n                              },\n                            ],\n                          });\n                        }}\n                      >\n                        {I18n.t('Add condition')}\n                      </Button>\n                      <Button\n                        disabled={conditions.value?.length === 1}\n                        size=\"small\"\n                        theme=\"borderless\"\n                        icon={<IconDelete />}\n                        onClick={() => conditions.remove(index)}\n                      >\n                        {I18n.t('Remove branch')}\n                      </Button>\n                    </div>\n                  )}\n                  <Feedback errors={fieldState?.errors} invalid={fieldState?.invalid} />\n                </FormItem>\n              )}\n            </Field>\n          ))}\n\n          {/*  else */}\n          <FormItem name={I18n.t('ELSE')} type=\"boolean\" required={true} labelWidth={100}>\n            <ConditionPort data-port-id=\"else\" data-port-type=\"output\" />\n          </FormItem>\n\n          {!readonly && (\n            <div>\n              <Button\n                theme=\"borderless\"\n                icon={<IconPlus />}\n                onClick={() =>\n                  conditions.append({\n                    logic: 'and',\n                    conditions: [\n                      {\n                        key: `condition_${nanoid(6)}`,\n                        value: {},\n                      },\n                    ],\n                  })\n                }\n              >\n                {I18n.t('Add branch')}\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </FieldArray>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/multi-condition/condition-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from \"styled-components\";\n\nexport const ConditionPort = styled.div`\n  position: absolute;\n  right: -12px;\n  top: 50%;\n`;\n\nexport const ConditionBranch = styled.div`\n  display: flex;\n  width: 100%;\n  align-items: stretch;\n  position: relative;\n`;\n\nexport const ConditionBranchLogic = styled.div`\n  position: relative;\n  width: 80px;\n  display: flex;\n  align-items: center;\n\n  &::before {\n    content: '';\n    position: absolute;\n    width: 50%;\n    border: 1px solid var(--semi-color-tertiary-light-active);\n    border-radius: 6px 0 0 6px;\n    border-right: none;\n    left: 50%;\n    top: 32px;\n    bottom: 32px;\n  }\n`;\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/multi-condition/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nimport { FlowNodeJSON } from '../../typings';\nimport { FormHeader, FormContent } from '../../form-components';\n\nimport { ConditionInputs } from './condition-inputs';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <ConditionInputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n    'branch.*': ({ value }) => {\n      const haveEmptyCondition =\n        value.conditions.filter((item: any) => {\n          return Object.keys(item.value).length === 0;\n        }).length > 0;\n      if (haveEmptyCondition) return 'Condition is required';\n      return undefined;\n    },\n  },\n  effect: {\n    conditions: autoRenameRefEffect,\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/multi-condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '../../typings';\nimport { WorkflowNodeType } from '../constants';\nimport iconCondition from '../../assets/icon-condition.svg';\n\nimport { formMeta } from './form-meta';\n\nlet index = 0;\nexport const MultiConditionNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.MultiCondition,\n  info: {\n    icon: iconCondition,\n    description:\n      'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',\n  },\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n    // Condition Outputs use dynamic port\n    useDynamicPort: true,\n    expandable: false, // disable expanded\n    size: {\n      width: 360,\n      height: 210,\n    },\n  },\n  formMeta,\n  onAdd() {\n    return {\n      id: `multi_condition_${nanoid(5)}`,\n      type: 'condition',\n      data: {\n        title: `multi_condition_${++index}`,\n        branch: [\n          {\n            logic: 'and',\n            conditions: [\n              {\n                key: `condition_${nanoid(5)}`,\n                value: {},\n              },\n            ],\n          },\n        ],\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/start/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Field,\n  FieldRenderProps,\n  FormRenderProps,\n  FormMeta,\n  ValidateTrigger,\n} from '@flowgram.ai/free-layout-editor';\nimport {\n  DisplayOutputs,\n  JsonSchemaEditor,\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n} from '@flowgram.ai/form-materials';\n\nimport { FlowNodeJSON, JsonSchema } from '../../typings';\nimport { useIsSidebar } from '../../hooks';\nimport { FormHeader, FormContent } from '../../form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field\n            name=\"outputs\"\n            render={({ field: { value, onChange } }: FieldRenderProps<JsonSchema>) => (\n              <>\n                <JsonSchemaEditor\n                  value={value}\n                  onChange={(value) => onChange(value as JsonSchema)}\n                />\n              </>\n            )}\n          />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <DisplayOutputs displayFromScope />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n  },\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '../../typings';\nimport iconStart from '../../assets/icon-start.jpg';\nimport { formMeta } from './form-meta';\nimport { WorkflowNodeType } from '../constants';\n\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconStart,\n    description:\n      'The starting node of the workflow, used to set the information needed to initiate the workflow.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/variable/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { AssignRows, createInferAssignPlugin, DisplayOutputs } from '@flowgram.ai/form-materials';\n\nimport { FormHeader, FormContent } from '../../form-components';\nimport { VariableNodeJSON } from './types';\nimport { defaultFormMeta } from '../default-form-meta';\nimport { useIsSidebar } from '../../hooks';\n\nexport const FormRender = ({ form }: FormRenderProps<VariableNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        {isSidebar ? <AssignRows name=\"assign\" /> : <DisplayOutputs displayFromScope />}\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: (props) => <FormRender {...props} />,\n  effect: defaultFormMeta.effect,\n  plugins: [\n    createInferAssignPlugin({\n      assignKey: 'assign',\n      outputKey: 'outputs',\n    }),\n  ],\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/variable/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { WorkflowNodeType } from '../constants';\nimport { FlowNodeRegistry } from '../../typings';\nimport iconVariable from '../../assets/icon-variable.png';\nimport { formMeta } from './form-meta';\n\nlet index = 0;\n\nexport const VariableNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Variable,\n  info: {\n    icon: iconVariable,\n    description: 'Variable Assign and Declaration',\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 390,\n    },\n  },\n  onAdd() {\n    return {\n      id: `variable_${nanoid(5)}`,\n      type: 'variable',\n      data: {\n        title: `Variable_${++index}`,\n        assign: [\n          {\n            operator: 'declare',\n            left: 'sum',\n            right: {\n              type: 'constant',\n              content: 0,\n              schema: { type: 'integer' },\n            },\n          },\n        ],\n      },\n    };\n  },\n  formMeta: formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/nodes/variable/types.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeJSON } from '@flowgram.ai/free-layout-editor';\nimport { AssignValueType, IJsonSchema } from '@flowgram.ai/form-materials';\n\nexport interface VariableNodeJSON extends FlowNodeJSON {\n  data: {\n    title: string;\n    assign: AssignValueType[];\n    outputs: IJsonSchema<'object'>;\n  };\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/context-menu-plugin/context-menu-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  Layer,\n  injectable,\n  inject,\n  FreeLayoutPluginContext,\n  WorkflowHoverService,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  WorkflowSelectService,\n  WorkflowDocument,\n  PositionSchema,\n  WorkflowDragService,\n} from '@flowgram.ai/free-layout-editor';\nimport { ContainerUtils } from '@flowgram.ai/free-container-plugin';\n\n@injectable()\nexport class ContextMenuLayer extends Layer {\n  @inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext;\n\n  @inject(WorkflowNodePanelService) nodePanelService: WorkflowNodePanelService;\n\n  @inject(WorkflowHoverService) hoverService: WorkflowHoverService;\n\n  @inject(WorkflowSelectService) selectService: WorkflowSelectService;\n\n  @inject(WorkflowDocument) document: WorkflowDocument;\n\n  @inject(WorkflowDragService) dragService: WorkflowDragService;\n\n  onReady() {\n    this.listenPlaygroundEvent('contextmenu', (e) => {\n      if (this.config.readonlyOrDisabled) return;\n      this.openNodePanel(e);\n      e.preventDefault();\n      e.stopPropagation();\n    });\n  }\n\n  openNodePanel(e: MouseEvent) {\n    const mousePos = this.getPosFromMouseEvent(e);\n    const containerNode = this.getContainerNode(mousePos);\n    this.nodePanelService.callNodePanel({\n      position: mousePos,\n      containerNode,\n      panelProps: {},\n      // handle node selection from panel - 处理从面板中选择节点\n      onSelect: async (panelParams?: NodePanelResult) => {\n        if (!panelParams) {\n          return;\n        }\n        const { nodeType, nodeJSON } = panelParams;\n        const position = this.dragService.adjustSubNodePosition(nodeType, containerNode, mousePos);\n        // create new workflow node based on selected type - 根据选择的类型创建新的工作流节点\n        const node: WorkflowNodeEntity = this.ctx.document.createWorkflowNodeByType(\n          nodeType,\n          position,\n          nodeJSON ?? ({} as WorkflowNodeJSON),\n          containerNode?.id\n        );\n        // select the newly created node - 选择新创建的节点\n        this.selectService.select(node);\n      },\n      // handle panel close - 处理面板关闭\n      onClose: () => {},\n    });\n  }\n\n  private getContainerNode(mousePos: PositionSchema): WorkflowNodeEntity | undefined {\n    const allNodes = this.document.getAllNodes();\n    const containerTransforms = ContainerUtils.getContainerTransforms(allNodes);\n    const collisionTransform = ContainerUtils.getCollisionTransform({\n      targetPoint: mousePos,\n      transforms: containerTransforms,\n      document: this.document,\n    });\n    return collisionTransform?.entity;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/context-menu-plugin/context-menu-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  definePluginCreator,\n  PluginCreator,\n  FreeLayoutPluginContext,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { ContextMenuLayer } from './context-menu-layer';\n\nexport interface ContextMenuPluginOptions {}\n\n/**\n * Creates a plugin of contextmenu\n * @param ctx - The plugin context, containing the document and other relevant information.\n * @param options - Plugin options, currently an empty object.\n */\nexport const createContextMenuPlugin: PluginCreator<ContextMenuPluginOptions> = definePluginCreator<\n  ContextMenuPluginOptions,\n  FreeLayoutPluginContext\n>({\n  onInit(ctx, options) {\n    ctx.playground.registerLayer(ContextMenuLayer);\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/context-menu-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createContextMenuPlugin } from './context-menu-plugin';\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createContextMenuPlugin } from './context-menu-plugin';\nexport { createRuntimePlugin } from './runtime-plugin';\nexport { createVariablePanelPlugin } from './variable-panel-plugin';\nexport { createPanelManagerPlugin } from './panel-manager-plugin';\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/panel-manager-plugin/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum PanelType {\n  NodeFormPanel = 'nodeFormPanel',\n  TestRunFormPanel = 'testRunFormPanel',\n  ProblemPanel = 'problemPanel',\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/panel-manager-plugin/hooks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { usePanelManager } from '@flowgram.ai/panel-manager-plugin';\n\nimport type { NodeFormPanelProps } from '../../components/sidebar/node-form-panel';\nimport { PanelType } from './constants';\n\nexport const useNodeFormPanel = () => {\n  const panelManager = usePanelManager();\n\n  const open = (props: NodeFormPanelProps) => {\n    panelManager.open(PanelType.NodeFormPanel, 'right', {\n      props: props,\n    });\n  };\n  const close = () => panelManager.close(PanelType.NodeFormPanel);\n\n  return { open, close };\n};\n\nexport const useTestRunFormPanel = () => {\n  const panelManager = usePanelManager();\n\n  const open = () => {\n    panelManager.open(PanelType.TestRunFormPanel, 'docked-right');\n  };\n  const close = () => panelManager.close(PanelType.TestRunFormPanel);\n\n  return { open, close };\n};\n\nexport const useProblemPanel = () => {\n  const panelManager = usePanelManager();\n\n  const open = () => {\n    panelManager.open(PanelType.ProblemPanel, 'bottom');\n  };\n  const close = () => panelManager.close(PanelType.ProblemPanel);\n\n  return { open, close };\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/panel-manager-plugin/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  createPanelManagerPlugin as create,\n  PanelFactory,\n} from '@flowgram.ai/panel-manager-plugin';\n\nimport { DemoTools } from '../../components/tools';\nimport {\n  TestRunSidePanel,\n  TestRunSidePanelProps,\n} from '../../components/testrun/testrun-panel/test-run-panel';\nimport { NodeFormPanel, NodeFormPanelProps } from '../../components/sidebar/node-form-panel';\nimport { ProblemPanel } from '../../components/problem-panel/problem-panel';\nimport { PanelType } from './constants';\n\nconst nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {\n  key: PanelType.NodeFormPanel,\n  defaultSize: 500,\n  maxSize: 800,\n  minSize: 300,\n  render: (props: NodeFormPanelProps) => <NodeFormPanel {...props} />,\n};\n\nconst testRunPanelFactory: PanelFactory<TestRunSidePanelProps> = {\n  key: PanelType.TestRunFormPanel,\n  defaultSize: 400,\n  render: () => <TestRunSidePanel />,\n};\n\nconst problemPanelFactory: PanelFactory<void> = {\n  key: PanelType.ProblemPanel,\n  defaultSize: 200,\n  render: () => <ProblemPanel />,\n};\n\nexport const createPanelManagerPlugin = () =>\n  create({\n    factories: [nodeFormPanelFactory, testRunPanelFactory, problemPanelFactory],\n    layerProps: {\n      children: <DemoTools />,\n    },\n  });\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/base-client.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramAPIName, IRuntimeClient } from '@flowgram.ai/runtime-interface';\nimport { injectable } from '@flowgram.ai/free-layout-editor';\n\n@injectable()\nexport class WorkflowRuntimeClient implements IRuntimeClient {\n  constructor() {}\n\n  public [FlowGramAPIName.TaskRun]: IRuntimeClient[FlowGramAPIName.TaskRun];\n\n  public [FlowGramAPIName.TaskReport]: IRuntimeClient[FlowGramAPIName.TaskReport];\n\n  public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult];\n\n  public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel];\n\n  public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate];\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/browser-client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable no-console */\nimport { FlowGramAPIName, IRuntimeClient } from '@flowgram.ai/runtime-interface';\nimport { injectable } from '@flowgram.ai/free-layout-editor';\n\n@injectable()\nexport class WorkflowRuntimeBrowserClient implements IRuntimeClient {\n  constructor() {}\n\n  public [FlowGramAPIName.TaskRun]: IRuntimeClient[FlowGramAPIName.TaskRun] = async (input) => {\n    const { TaskRunAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载\n    return TaskRunAPI(input);\n  };\n\n  public [FlowGramAPIName.TaskReport]: IRuntimeClient[FlowGramAPIName.TaskReport] = async (\n    input\n  ) => {\n    const { TaskReportAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载\n    return TaskReportAPI(input);\n  };\n\n  public [FlowGramAPIName.TaskResult]: IRuntimeClient[FlowGramAPIName.TaskResult] = async (\n    input\n  ) => {\n    const { TaskResultAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载\n    return TaskResultAPI(input);\n  };\n\n  public [FlowGramAPIName.TaskCancel]: IRuntimeClient[FlowGramAPIName.TaskCancel] = async (\n    input\n  ) => {\n    const { TaskCancelAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载\n    return TaskCancelAPI(input);\n  };\n\n  public [FlowGramAPIName.TaskValidate]: IRuntimeClient[FlowGramAPIName.TaskValidate] = async (\n    input\n  ) => {\n    const { TaskValidateAPI } = await import('@flowgram.ai/runtime-js'); // Load on demand - 按需加载\n    return TaskValidateAPI(input);\n  };\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeClient } from './base-client';\nexport { WorkflowRuntimeBrowserClient } from './browser-client';\nexport { WorkflowRuntimeServerClient } from './server-client';\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/server-client/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ServerConfig } from '../../type';\n\nexport const DEFAULT_SERVER_CONFIG: ServerConfig = {\n  domain: 'localhost',\n  port: 4000,\n  protocol: 'http',\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/server-client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowGramAPIName,\n  IRuntimeClient,\n  TaskCancelDefine,\n  TaskCancelInput,\n  TaskCancelOutput,\n  TaskReportDefine,\n  TaskReportInput,\n  TaskReportOutput,\n  TaskResultDefine,\n  TaskResultInput,\n  TaskResultOutput,\n  TaskRunDefine,\n  TaskRunInput,\n  TaskRunOutput,\n  TaskValidateDefine,\n  TaskValidateInput,\n  TaskValidateOutput,\n} from '@flowgram.ai/runtime-interface';\nimport { injectable } from '@flowgram.ai/free-layout-editor';\n\nimport { ServerConfig } from '../../type';\nimport type { ServerError } from './type';\nimport { DEFAULT_SERVER_CONFIG } from './constant';\n\n@injectable()\nexport class WorkflowRuntimeServerClient implements IRuntimeClient {\n  private config: ServerConfig = DEFAULT_SERVER_CONFIG;\n\n  constructor() {}\n\n  public init(config: ServerConfig) {\n    this.config = config;\n  }\n\n  public async [FlowGramAPIName.TaskRun](input: TaskRunInput): Promise<TaskRunOutput | undefined> {\n    return this.request<TaskRunOutput>(TaskRunDefine.path, TaskRunDefine.method, {\n      body: input,\n      errorMessage: 'TaskRun failed',\n    });\n  }\n\n  public async [FlowGramAPIName.TaskReport](\n    input: TaskReportInput\n  ): Promise<TaskReportOutput | undefined> {\n    return this.request<TaskReportOutput>(TaskReportDefine.path, TaskReportDefine.method, {\n      queryParams: { taskID: input.taskID },\n      errorMessage: 'TaskReport failed',\n    });\n  }\n\n  public async [FlowGramAPIName.TaskResult](\n    input: TaskResultInput\n  ): Promise<TaskResultOutput | undefined> {\n    return this.request<TaskResultOutput>(TaskResultDefine.path, TaskResultDefine.method, {\n      queryParams: { taskID: input.taskID },\n      errorMessage: 'TaskResult failed',\n      fallbackValue: { success: false },\n    });\n  }\n\n  public async [FlowGramAPIName.TaskCancel](input: TaskCancelInput): Promise<TaskCancelOutput> {\n    const result = await this.request<TaskCancelOutput>(\n      TaskCancelDefine.path,\n      TaskCancelDefine.method,\n      {\n        body: input,\n        errorMessage: 'TaskCancel failed',\n        fallbackValue: { success: false },\n      }\n    );\n    return result ?? { success: false };\n  }\n\n  public async [FlowGramAPIName.TaskValidate](\n    input: TaskValidateInput\n  ): Promise<TaskValidateOutput | undefined> {\n    return this.request<TaskValidateOutput>(TaskValidateDefine.path, TaskValidateDefine.method, {\n      body: input,\n      errorMessage: 'TaskValidate failed',\n    });\n  }\n\n  // Generic request method to reduce code duplication\n  private async request<T>(\n    path: string,\n    method: string,\n    options: {\n      body?: unknown;\n      queryParams?: Record<string, string>;\n      errorMessage: string;\n      fallbackValue?: T;\n    }\n  ): Promise<T | undefined> {\n    try {\n      const url = this.url(path, options.queryParams);\n      const requestOptions: RequestInit = {\n        method,\n        redirect: 'follow',\n      };\n\n      if (options.body) {\n        requestOptions.headers = {\n          'Content-Type': 'application/json',\n        };\n        requestOptions.body = JSON.stringify(options.body);\n      }\n\n      const response = await fetch(url, requestOptions);\n      const output: T | ServerError = await response.json();\n\n      if (this.isError(output)) {\n        console.error(options.errorMessage, output);\n        return options.fallbackValue;\n      }\n\n      return output;\n    } catch (error) {\n      console.error(error);\n      return options.fallbackValue;\n    }\n  }\n\n  // Build URL with query parameters\n  private url(path: string, queryParams?: Record<string, string>): string {\n    const baseURL = this.getURL(`/api${path}`);\n    if (!queryParams) {\n      return baseURL;\n    }\n\n    const searchParams = new URLSearchParams(queryParams);\n    return `${baseURL}?${searchParams.toString()}`;\n  }\n\n  private isError(output: unknown | undefined): output is ServerError {\n    return !!output && (output as ServerError).code !== undefined;\n  }\n\n  private getURL(path: string): string {\n    const protocol = this.config.protocol ?? window.location.protocol;\n    const host = this.config.port\n      ? `${this.config.domain}:${this.config.port}`\n      : this.config.domain;\n    return `${protocol}://${host}${path}`;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/client/server-client/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface ServerError {\n  code: string;\n  message: string;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/create-runtime-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/free-layout-editor';\n\nimport { RuntimePluginOptions } from './type';\nimport { WorkflowRuntimeService } from './runtime-service';\nimport {\n  WorkflowRuntimeBrowserClient,\n  WorkflowRuntimeClient,\n  WorkflowRuntimeServerClient,\n} from './client';\n\nexport const createRuntimePlugin = definePluginCreator<RuntimePluginOptions, PluginContext>({\n  onBind({ bind, rebind }, options) {\n    bind(WorkflowRuntimeClient).toSelf().inSingletonScope();\n    bind(WorkflowRuntimeServerClient).toSelf().inSingletonScope();\n    bind(WorkflowRuntimeBrowserClient).toSelf().inSingletonScope();\n    if (options.mode === 'server') {\n      rebind(WorkflowRuntimeClient).to(WorkflowRuntimeServerClient);\n    } else {\n      rebind(WorkflowRuntimeClient).to(WorkflowRuntimeBrowserClient);\n    }\n    bind(WorkflowRuntimeService).toSelf().inSingletonScope();\n  },\n  onInit(ctx, options) {\n    if (options.mode === 'server') {\n      const serverClient = ctx.get<WorkflowRuntimeServerClient>(WorkflowRuntimeClient);\n      serverClient.init(options.serverConfig);\n    }\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createRuntimePlugin } from './create-runtime-plugin';\nexport { WorkflowRuntimeClient } from './client';\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/runtime-service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  IReport,\n  NodeReport,\n  WorkflowInputs,\n  WorkflowOutputs,\n  WorkflowStatus,\n} from '@flowgram.ai/runtime-interface';\nimport {\n  injectable,\n  inject,\n  WorkflowDocument,\n  Playground,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  Emitter,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowRuntimeClient } from '../client';\nimport { GetGlobalVariableSchema } from '../../variable-panel-plugin';\nimport { WorkflowNodeType } from '../../../nodes';\n\nconst SYNC_TASK_REPORT_INTERVAL = 500;\n\ninterface NodeRunningStatus {\n  nodeID: string;\n  status: WorkflowStatus;\n  nodeResultLength: number;\n}\n\n@injectable()\nexport class WorkflowRuntimeService {\n  @inject(Playground) playground: Playground;\n\n  @inject(WorkflowDocument) document: WorkflowDocument;\n\n  @inject(WorkflowRuntimeClient) runtimeClient: WorkflowRuntimeClient;\n\n  @inject(GetGlobalVariableSchema) getGlobalVariableSchema: GetGlobalVariableSchema;\n\n  private runningNodes: WorkflowNodeEntity[] = [];\n\n  private taskID?: string;\n\n  private syncTaskReportIntervalID?: ReturnType<typeof setInterval>;\n\n  private reportEmitter = new Emitter<NodeReport>();\n\n  private resetEmitter = new Emitter<{}>();\n\n  private resultEmitter = new Emitter<{\n    errors?: string[];\n    result?: {\n      inputs: WorkflowInputs;\n      outputs: WorkflowOutputs;\n    };\n  }>();\n\n  private nodeRunningStatus: Map<string, NodeRunningStatus>;\n\n  public onNodeReportChange = this.reportEmitter.event;\n\n  public onReset = this.resetEmitter.event;\n\n  public onResultChanged = this.resultEmitter.event;\n\n  public isFlowingLine(line: WorkflowLineEntity) {\n    return this.runningNodes.some((node) => node.lines.inputLines.includes(line));\n  }\n\n  public async taskRun(inputs: WorkflowInputs): Promise<string | undefined> {\n    if (this.taskID) {\n      await this.taskCancel();\n    }\n    const isFormValid = await this.validateForm();\n    if (!isFormValid) {\n      this.resultEmitter.fire({\n        errors: ['Form validation failed'],\n      });\n      return;\n    }\n    const schema = {\n      ...this.document.toJSON(),\n      globalVariable: this.getGlobalVariableSchema(),\n    };\n\n    const validateResult = await this.runtimeClient.TaskValidate({\n      schema: JSON.stringify(schema),\n      inputs,\n    });\n    if (!validateResult?.valid) {\n      this.resultEmitter.fire({\n        errors: validateResult?.errors ?? ['Internal Server Error'],\n      });\n      return;\n    }\n    this.reset();\n    let taskID: string | undefined;\n    try {\n      const output = await this.runtimeClient.TaskRun({\n        schema: JSON.stringify(schema),\n        inputs,\n      });\n      taskID = output?.taskID;\n    } catch (e) {\n      this.resultEmitter.fire({\n        errors: [(e as Error)?.message],\n      });\n      return;\n    }\n    if (!taskID) {\n      this.resultEmitter.fire({\n        errors: ['Task run failed'],\n      });\n      return;\n    }\n    this.taskID = taskID;\n    this.syncTaskReportIntervalID = setInterval(() => {\n      this.syncTaskReport();\n    }, SYNC_TASK_REPORT_INTERVAL);\n    return this.taskID;\n  }\n\n  public async taskCancel(): Promise<void> {\n    if (!this.taskID) {\n      return;\n    }\n    await this.runtimeClient.TaskCancel({\n      taskID: this.taskID,\n    });\n  }\n\n  private async validateForm(): Promise<boolean> {\n    const allForms = this.document.getAllNodes().map((node) => node.form);\n    const formValidations = await Promise.all(allForms.map(async (form) => form?.validate()));\n    const validations = formValidations.filter((validation) => validation !== undefined);\n    const isValid = validations.every((validation) => validation);\n    return isValid;\n  }\n\n  private reset(): void {\n    this.taskID = undefined;\n    this.nodeRunningStatus = new Map();\n    this.runningNodes = [];\n    if (this.syncTaskReportIntervalID) {\n      clearInterval(this.syncTaskReportIntervalID);\n    }\n    this.resetEmitter.fire({});\n  }\n\n  private async syncTaskReport(): Promise<void> {\n    if (!this.taskID) {\n      return;\n    }\n    const report = await this.runtimeClient.TaskReport({\n      taskID: this.taskID,\n    });\n    if (!report) {\n      clearInterval(this.syncTaskReportIntervalID);\n      console.error('Sync task report failed');\n      return;\n    }\n    const { workflowStatus, inputs, outputs, messages } = report;\n    if (workflowStatus.terminated) {\n      clearInterval(this.syncTaskReportIntervalID);\n      if (Object.keys(outputs).length > 0) {\n        this.resultEmitter.fire({ result: { inputs, outputs } });\n      } else {\n        this.resultEmitter.fire({\n          errors: messages?.error?.map((message) =>\n            message.nodeID ? `${message.nodeID}: ${message.message}` : message.message\n          ),\n        });\n      }\n    }\n    this.updateReport(report);\n  }\n\n  private updateReport(report: IReport): void {\n    const { reports } = report;\n    this.runningNodes = [];\n    this.document\n      .getAllNodes()\n      .filter(\n        (node) =>\n          ![WorkflowNodeType.BlockStart, WorkflowNodeType.BlockEnd].includes(\n            node.flowNodeType as WorkflowNodeType\n          )\n      )\n      .forEach((node) => {\n        const nodeID = node.id;\n        const nodeReport = reports[nodeID];\n        if (!nodeReport) {\n          return;\n        }\n        if (nodeReport.status === WorkflowStatus.Processing) {\n          this.runningNodes.push(node);\n        }\n        const runningStatus = this.nodeRunningStatus.get(nodeID);\n        if (\n          !runningStatus ||\n          nodeReport.status !== runningStatus.status ||\n          nodeReport.snapshots.length !== runningStatus.nodeResultLength\n        ) {\n          this.nodeRunningStatus.set(nodeID, {\n            nodeID,\n            status: nodeReport.status,\n            nodeResultLength: nodeReport.snapshots.length,\n          });\n          this.reportEmitter.fire(nodeReport);\n          this.document.linesManager.forceUpdate();\n        } else if (nodeReport.status === WorkflowStatus.Processing) {\n          this.reportEmitter.fire(nodeReport);\n        }\n      });\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/runtime-plugin/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface RuntimeBrowserOptions {\n  mode?: 'browser';\n}\n\nexport interface RuntimeServerOptions {\n  mode: 'server';\n  serverConfig: ServerConfig;\n}\n\nexport type RuntimePluginOptions = RuntimeBrowserOptions | RuntimeServerOptions;\n\nexport interface ServerConfig {\n  domain: string;\n  port?: number;\n  protocol?: string;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/components/full-variable-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useVariableTree } from '@flowgram.ai/form-materials';\nimport { Tree } from '@douyinfe/semi-ui';\n\nexport function FullVariableList() {\n  const treeData = useVariableTree({});\n\n  return <Tree treeData={treeData} />;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/components/global-variable-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport {\n  BaseVariableField,\n  GlobalScope,\n  useRefresh,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\nimport { JsonSchemaEditor, JsonSchemaUtils } from '@flowgram.ai/form-materials';\n\nexport function GlobalVariableEditor() {\n  const globalScope = useService(GlobalScope);\n\n  const refresh = useRefresh();\n\n  const globalVar = globalScope.getVar() as BaseVariableField;\n\n  useEffect(() => {\n    const disposable = globalScope.output.onVariableListChange(() => {\n      refresh();\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  if (!globalVar) {\n    return null;\n  }\n\n  const value = globalVar.type ? JsonSchemaUtils.astToSchema(globalVar.type) : { type: 'object' };\n\n  return (\n    <JsonSchemaEditor\n      value={value}\n      onChange={(_schema) => globalVar.updateType(JsonSchemaUtils.schemaToAST(_schema))}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/components/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.panel-wrapper {\n  position: relative;\n}\n\n.variable-panel-button {\n  position: absolute;\n  top: 0;\n  right: 0;\n  border-radius: 50%;\n  width: 50px;\n  height: 50px;\n  z-index: 1;\n\n  &.close {\n    width: 30px;\n    height: 30px;\n    top: 10px;\n    right: 10px;\n  }\n}\n\n.panel-container {\n  width: 500px;\n  border-radius: 5px;\n  background-color: #fff;\n  overflow: hidden;\n  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);\n  z-index: 30;\n\n  :global(.semi-tabs-bar) {\n    padding-left: 20px;\n  }\n\n  :global(.semi-tabs-content) {\n    padding: 20px;\n    height: 500px;\n    overflow: auto;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/components/variable-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { Button, Collapsible, Tabs, Tooltip } from '@douyinfe/semi-ui';\nimport { IconMinus } from '@douyinfe/semi-icons';\n\nimport iconVariable from '../../../assets/icon-variable.png';\nimport { GlobalVariableEditor } from './global-variable-editor';\nimport { FullVariableList } from './full-variable-list';\n\nimport styles from './index.module.less';\n\nexport function VariablePanel() {\n  const [isOpen, setOpen] = useState<boolean>(false);\n\n  return (\n    <div className={styles['panel-wrapper']}>\n      <Tooltip content=\"Toggle Variable Panel\">\n        <Button\n          className={`${styles['variable-panel-button']} ${isOpen ? styles.close : ''}`}\n          theme={isOpen ? 'borderless' : 'light'}\n          onClick={() => setOpen((_open) => !_open)}\n        >\n          {isOpen ? <IconMinus /> : <img src={iconVariable} width={20} height={20} />}\n        </Button>\n      </Tooltip>\n      <Collapsible isOpen={isOpen}>\n        <div className={styles['panel-container']}>\n          <Tabs>\n            <Tabs.TabPane itemKey=\"variables\" tab=\"Variable List\">\n              <FullVariableList />\n            </Tabs.TabPane>\n            <Tabs.TabPane itemKey=\"global\" tab=\"Global Editor\">\n              <GlobalVariableEditor />\n            </Tabs.TabPane>\n          </Tabs>\n        </div>\n      </Collapsible>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createVariablePanelPlugin, GetGlobalVariableSchema } from './variable-panel-plugin';\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { domUtils, injectable, Layer } from '@flowgram.ai/free-layout-editor';\n\nimport { VariablePanel } from './components/variable-panel';\n\n@injectable()\nexport class VariablePanelLayer extends Layer {\n  onReady(): void {\n    // Fix variable panel in the right of canvas\n    this.config.onDataChange(() => {\n      const { scrollX, scrollY } = this.config.config;\n      domUtils.setStyle(this.node, {\n        position: 'absolute',\n        right: 25 - scrollX,\n        top: scrollY + 25,\n      });\n    });\n  }\n\n  render(): JSX.Element {\n    return <VariablePanel />;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/plugins/variable-panel-plugin/variable-panel-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  definePluginCreator,\n  GlobalScope,\n  VariableDeclaration,\n} from '@flowgram.ai/free-layout-editor';\nimport { IJsonSchema, JsonSchemaUtils } from '@flowgram.ai/form-materials';\n\nimport iconVariable from '../../assets/icon-variable.png';\nimport { VariablePanelLayer } from './variable-panel-layer';\n\nconst fetchMockVariableFromRemote = async () => {\n  await new Promise((resolve) => setTimeout(resolve, 1000));\n  return {\n    type: 'object',\n    properties: {\n      userId: { type: 'string' },\n    },\n  };\n};\n\nexport type GetGlobalVariableSchema = () => IJsonSchema;\nexport const GetGlobalVariableSchema = Symbol('GlobalVariableSchemaGetter');\n\nexport const createVariablePanelPlugin = definePluginCreator<{ initialData?: IJsonSchema }>({\n  onBind({ bind }) {\n    bind(GetGlobalVariableSchema).toDynamicValue((ctx) => () => {\n      const variable = ctx.container.get(GlobalScope).getVar() as VariableDeclaration;\n      return JsonSchemaUtils.astToSchema(variable?.type);\n    });\n  },\n  onInit(ctx, opts) {\n    ctx.playground.registerLayer(VariablePanelLayer);\n\n    const globalScope = ctx.get(GlobalScope);\n\n    if (opts.initialData) {\n      globalScope.setVar(\n        ASTFactory.createVariableDeclaration({\n          key: 'global',\n          meta: {\n            title: 'Global',\n            icon: iconVariable,\n          },\n          type: JsonSchemaUtils.schemaToAST(opts.initialData),\n        })\n      );\n    } else {\n      // You can also fetch global variable from remote\n      fetchMockVariableFromRemote().then((v) => {\n        globalScope.setVar(\n          ASTFactory.createVariableDeclaration({\n            key: 'global',\n            meta: {\n              title: 'Global',\n              icon: iconVariable,\n            },\n            type: JsonSchemaUtils.schemaToAST(v),\n          })\n        );\n      });\n    }\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout/src/services/custom-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from '@flowgram.ai/free-layout-editor';\nimport {\n  FreeLayoutPluginContext,\n  SelectionService,\n  Playground,\n  WorkflowDocument,\n} from '@flowgram.ai/free-layout-editor';\n\n/**\n * Docs: https://inversify.io/docs/introduction/getting-started/\n * Warning: Use decorator legacy\n *   // rsbuild.config.ts\n *   {\n *     source: {\n *       decorators: {\n *         version: 'legacy'\n *       }\n *     }\n *   }\n * Usage:\n *  1.\n *    const myService = useService(CustomService)\n *    myService.save()\n *  2.\n *    const myService = useClientContext().get(CustomService)\n *  3.\n *    const myService = node.getService(CustomService)\n */\n@injectable()\nexport class CustomService {\n  @inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext;\n\n  @inject(SelectionService) selectionService: SelectionService;\n\n  @inject(Playground) playground: Playground;\n\n  @inject(WorkflowDocument) document: WorkflowDocument;\n\n  save() {\n    console.log(this.document.toJSON());\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { CustomService } from './custom-service';\nexport { ValidateService } from './validate-service';\n"
  },
  {
    "path": "apps/demo-free-layout/src/services/validate-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  inject,\n  injectable,\n  WorkflowLinesManager,\n  FlowNodeEntity,\n  FlowNodeFormData,\n  FormModelV2,\n  WorkflowDocument,\n} from '@flowgram.ai/free-layout-editor';\n\nexport interface ValidateResult {\n  node: FlowNodeEntity;\n  feedbacks: any[];\n}\n\n@injectable()\nexport class ValidateService {\n  @inject(WorkflowLinesManager)\n  protected readonly linesManager: WorkflowLinesManager;\n\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  validateLines() {\n    const allLines = this.linesManager.getAllLines();\n    allLines.forEach((line) => line.validate());\n  }\n\n  async validateNode(node: FlowNodeEntity) {\n    const feedbacks = await node\n      .getData(FlowNodeFormData)\n      .getFormModel<FormModelV2>()\n      .validateWithFeedbacks();\n    return feedbacks;\n  }\n\n  async validateNodes(): Promise<ValidateResult[]> {\n    const nodes = this.document.getAssociatedNodes();\n    const results = await Promise.all(\n      nodes.map(async (node) => {\n        const feedbacks = await this.validateNode(node);\n        return {\n          feedbacks,\n          node,\n        };\n      })\n    );\n\n    return results.filter((i) => i.feedbacks.length);\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/collapse/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  ShortcutsHandler,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class CollapseShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.COLLAPSE;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Collapse',\n  };\n\n  public shortcuts = ['meta alt openbracket', 'ctrl alt openbracket'];\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    this.selectService.selectedNodes.forEach((node) => {\n      node.renderData.expanded = false;\n    });\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const WorkflowClipboardDataID = 'flowgram-workflow-clipboard-data';\n\nexport enum FlowCommandId {\n  COPY = 'COPY',\n  PASTE = 'PASTE',\n  CUT = 'CUT',\n  GROUP = 'GROUP',\n  UNGROUP = 'UNGROUP',\n  COLLAPSE = 'COLLAPSE',\n  EXPAND = 'EXPAND',\n  DELETE = 'DELETE',\n  ZOOM_IN = 'ZOOM_IN',\n  ZOOM_OUT = 'ZOOM_OUT',\n  RESET_ZOOM = 'RESET_ZOOM',\n  SELECT_ALL = 'SELECT_ALL',\n  CANCEL_SELECT = 'CANCEL_SELECT',\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/copy/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeBaseType,\n  FreeLayoutPluginContext,\n  PlaygroundConfigEntity,\n  Rectangle,\n  ShortcutsHandler,\n  TransformData,\n  WorkflowDocument,\n  WorkflowEdgeJSON,\n  WorkflowJSON,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport type {\n  WorkflowClipboardRect,\n  WorkflowClipboardSource,\n  WorkflowClipboardData,\n} from '../type';\nimport { FlowCommandId, WorkflowClipboardDataID } from '../constants';\nimport { WorkflowNodeType } from '../../nodes';\n\nexport class CopyShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.COPY;\n\n  public shortcuts = ['meta c', 'ctrl c'];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.playground.config;\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute copy operation - 执行复制操作\n   */\n  public async execute(): Promise<void> {\n    if (this.readonly || (await this.hasSelectedText())) {\n      return;\n    }\n    if (!this.isValid(this.selectedNodes)) {\n      return;\n    }\n    const data = this.toClipboardData();\n    await this.write(data);\n  }\n\n  /**\n   * create clipboard data - 转换为剪贴板数据\n   */\n  public toClipboardData(nodes?: WorkflowNodeEntity[]): WorkflowClipboardData {\n    const validNodes = this.getValidNodes(nodes ? nodes : this.selectedNodes);\n    const source = this.toSource();\n    const json = this.toJSON(validNodes);\n    const bounds = this.getEntireBounds(validNodes);\n    return {\n      type: WorkflowClipboardDataID,\n      source,\n      json,\n      bounds,\n    };\n  }\n\n  /**\n   * readonly - 是否只读\n   */\n  private get readonly(): boolean {\n    return this.playgroundConfig.readonly;\n  }\n\n  /**\n   * has selected text - 是否有文字被选中\n   */\n  private async hasSelectedText(): Promise<boolean> {\n    if (!window.getSelection()?.toString()) {\n      return false;\n    }\n    await navigator.clipboard.writeText(window.getSelection()?.toString() ?? '');\n    Toast.success({\n      content: 'Text copied',\n    });\n    return true;\n  }\n\n  /**\n   * get selected nodes - 获取选中的节点\n   */\n  private get selectedNodes(): WorkflowNodeEntity[] {\n    return this.selectService.selection.filter(\n      (n) => n instanceof WorkflowNodeEntity\n    ) as WorkflowNodeEntity[];\n  }\n\n  /**\n   * validate selected nodes - 验证选中的节点\n   */\n  private isValid(nodes: WorkflowNodeEntity[]): boolean {\n    if (nodes.length === 0) {\n      Toast.warning({\n        content: 'No nodes selected',\n      });\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * get valid nodes - 获取有效的节点\n   */\n  private getValidNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {\n    return nodes.filter((n) => {\n      if (\n        [WorkflowNodeType.Start, WorkflowNodeType.End].includes(n.flowNodeType as WorkflowNodeType)\n      ) {\n        return false;\n      }\n      if (n.getNodeMeta<WorkflowNodeMeta>().copyDisable) {\n        return false;\n      }\n      return true;\n    });\n  }\n\n  /**\n   * get source data - 获取来源数据\n   */\n  private toSource(): WorkflowClipboardSource {\n    return {\n      host: window.location.host,\n    };\n  }\n\n  /**\n   * convert nodes to JSON - 将节点转换为JSON\n   */\n  private toJSON(nodes: WorkflowNodeEntity[]): WorkflowJSON {\n    const nodeJSONs = this.getNodeJSONs(nodes);\n    const edgeJSONs = this.getEdgeJSONs(nodes);\n    return {\n      nodes: nodeJSONs,\n      edges: edgeJSONs,\n    };\n  }\n\n  /**\n   * get JSON representation of nodes - 获取节点的JSON表示\n   */\n  private getNodeJSONs(nodes: WorkflowNodeEntity[]): WorkflowNodeJSON[] {\n    const nodeJSONs = nodes.map((node) =>\n      node.flowNodeType === FlowNodeBaseType.GROUP\n        ? this.getGroupNodeJSON(node)\n        : this.document.toNodeJSON(node)\n    );\n    return nodeJSONs.filter(Boolean);\n  }\n\n  /**\n   * get JSON representation of group node - 获取分组节点的JSON\n   */\n  private getGroupNodeJSON(node: WorkflowNodeEntity): WorkflowNodeJSON {\n    const rawJSON = this.document.toNodeJSON(node);\n    return {\n      ...rawJSON,\n      blocks: node.blocks.map((block) => this.document.toNodeJSON(block)),\n    };\n  }\n\n  /**\n   * get edges of all nodes - 获取所有节点的边\n   */\n  private getEdgeJSONs(nodes: WorkflowNodeEntity[]): WorkflowEdgeJSON[] {\n    const lineSet = new Set<WorkflowLineEntity>();\n    const expandedNodes = this.expandGroupNodes(nodes);\n    const nodeIdSet = new Set(expandedNodes.map((n) => n.id));\n    expandedNodes.forEach((node) => {\n      const linesData = node.lines;\n      const lines = [...linesData.inputLines, ...linesData.outputLines];\n      lines.forEach((line) => {\n        if (\n          line.from?.id &&\n          nodeIdSet.has(line.from.id) &&\n          line.to?.id &&\n          nodeIdSet.has(line.to.id)\n        ) {\n          lineSet.add(line);\n        }\n      });\n    });\n    return Array.from(lineSet).map((line) => line.toJSON());\n  }\n\n  /**\n   * expand group nodes - 展开分组子节点\n   */\n  private expandGroupNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {\n    return nodes.flatMap((node) => {\n      if (node.flowNodeType === FlowNodeBaseType.GROUP) {\n        return [node, ...node.blocks];\n      }\n      return node;\n    });\n  }\n\n  /**\n   * get bounding rectangle of all nodes - 获取所有节点的边界矩形\n   */\n  private getEntireBounds(nodes: WorkflowNodeEntity[]): WorkflowClipboardRect {\n    const bounds = nodes.map((node) => node.getData<TransformData>(TransformData).bounds);\n    const rect = Rectangle.enlarge(bounds);\n    return {\n      x: rect.x,\n      y: rect.y,\n      width: rect.width,\n      height: rect.height,\n    };\n  }\n\n  /**\n   * write data to clipboard - 将数据写入剪贴板\n   */\n  private async write(data: WorkflowClipboardData): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(JSON.stringify(data));\n      this.notifySuccess();\n    } catch (err) {\n      console.error('Failed to write text: ', err);\n    }\n  }\n\n  /**\n   * show success notification - 显示成功通知\n   */\n  private notifySuccess(): void {\n    const startEndNodeTypes: WorkflowNodeType[] = [\n      WorkflowNodeType.Start,\n      WorkflowNodeType.End,\n      WorkflowNodeType.BlockStart,\n      WorkflowNodeType.BlockEnd,\n    ];\n    if (\n      this.selectedNodes.some((node) =>\n        startEndNodeTypes.includes(node.flowNodeType as WorkflowNodeType)\n      )\n    ) {\n      Toast.warning({\n        content:\n          'The Start/End node cannot be duplicated, other nodes have been copied to the clipboard',\n        showClose: false,\n      });\n      return;\n    }\n    Toast.success({\n      content: 'Nodes have been copied to the clipboard',\n      showClose: false,\n    });\n    return;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/delete/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  ShortcutsHandler,\n  WorkflowDocument,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n  HistoryService,\n  PlaygroundConfigEntity,\n} from '@flowgram.ai/free-layout-editor';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport { FlowCommandId } from '../constants';\nimport { WorkflowNodeType } from '../../nodes';\n\nexport class DeleteShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.DELETE;\n\n  public shortcuts = ['backspace', 'delete'];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  private historyService: HistoryService;\n\n  /**\n   * initialize delete shortcut - 初始化删除快捷键\n   */\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.playground.config;\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.historyService = context.get(HistoryService);\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute delete operation - 执行删除操作\n   */\n  public async execute(nodes?: WorkflowNodeEntity[]): Promise<void> {\n    if (this.readonly) {\n      return;\n    }\n    const selection = Array.isArray(nodes) ? nodes : this.selectService.selection;\n    if (\n      !this.isValid(\n        selection.filter((n) => n instanceof WorkflowNodeEntity) as WorkflowNodeEntity[]\n      )\n    ) {\n      return;\n    }\n    // Merge actions to redo/undo\n    this.historyService.startTransaction();\n    // delete selected entities - 删除选中实体\n    selection.forEach((entity) => {\n      if (entity instanceof WorkflowNodeEntity) {\n        this.removeNode(entity);\n      } else if (entity instanceof WorkflowLineEntity) {\n        this.removeLine(entity);\n      } else {\n        entity.dispose();\n      }\n    });\n    // filter out disposed entities - 过滤掉已删除的实体\n    this.selectService.selection = this.selectService.selection.filter((s) => !s.disposed);\n    this.historyService.endTransaction();\n  }\n\n  /**\n   * readonly - 是否只读\n   */\n  private get readonly(): boolean {\n    return this.playgroundConfig.readonly;\n  }\n\n  /**\n   * validate if nodes can be deleted - 验证节点是否可以删除\n   */\n  private isValid(nodes: WorkflowNodeEntity[]): boolean {\n    const hasSystemNodes = nodes.some((n) =>\n      [WorkflowNodeType.Start, WorkflowNodeType.End].includes(n.flowNodeType as WorkflowNodeType)\n    );\n    if (hasSystemNodes) {\n      Toast.error({\n        content: 'Start or End node cannot be deleted',\n        showClose: false,\n      });\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * remove node from workflow - 从工作流中删除节点\n   */\n  private removeNode(node: WorkflowNodeEntity): void {\n    if (!this.document.canRemove(node)) {\n      return;\n    }\n    const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(node);\n    if (subCanvas?.isCanvas) {\n      subCanvas.parentNode.dispose();\n      return;\n    }\n    node.dispose();\n  }\n\n  /**\n   * remove line from workflow - 从工作流中删除连线\n   */\n  private removeLine(line: WorkflowLineEntity): void {\n    if (!this.document.linesManager.canRemove(line)) {\n      return;\n    }\n    line.dispose();\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/expand/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  ShortcutsHandler,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ExpandShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.EXPAND;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Expand',\n  };\n\n  public shortcuts = ['meta alt closebracket', 'ctrl alt openbracket'];\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    this.selectService.selectedNodes.forEach((node) => {\n      node.renderData.expanded = true;\n    });\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './constants';\nexport * from './shortcuts';\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/paste/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  delay,\n  EntityManager,\n  FlowNodeTransformData,\n  FreeLayoutPluginContext,\n  IPoint,\n  PlaygroundConfigEntity,\n  Rectangle,\n  ShortcutsHandler,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowHoverService,\n  WorkflowJSON,\n  WorkflowNodeEntity,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n  Playground,\n} from '@flowgram.ai/free-layout-editor';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport { WorkflowClipboardData, WorkflowClipboardRect } from '../type';\nimport { FlowCommandId, WorkflowClipboardDataID } from '../constants';\nimport { canContainNode } from '../../utils';\nimport { generateUniqueWorkflow } from './unique-workflow';\n\nexport class PasteShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.PASTE;\n\n  public shortcuts = ['meta v', 'ctrl v'];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  private entityManager: EntityManager;\n\n  private hoverService: WorkflowHoverService;\n\n  private dragService: WorkflowDragService;\n\n  private playground: Playground;\n\n  /**\n   * initialize paste shortcut handler - 初始化粘贴快捷键处理器\n   */\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.playground.config;\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.entityManager = context.get(EntityManager);\n    this.hoverService = context.get(WorkflowHoverService);\n    this.dragService = context.get(WorkflowDragService);\n    this.playground = context.playground;\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute paste action - 执行粘贴操作\n   */\n  public async execute(): Promise<WorkflowNodeEntity[] | undefined> {\n    if (this.readonly) {\n      return;\n    }\n    const data = await this.tryReadClipboard();\n    if (!data) {\n      return;\n    }\n    if (!this.isValidData(data)) {\n      return;\n    }\n    const nodes = this.apply(data);\n    if (nodes.length > 0) {\n      Toast.success({\n        content: 'Copy successfully',\n        showClose: false,\n      });\n      // wait for nodes to render - 等待节点渲染\n      await this.nextTick();\n      // scroll to visible area - 滚动到可视区域\n      this.scrollNodesToView(nodes);\n    }\n    return nodes;\n  }\n\n  /** apply clipboard data - 应用剪切板数据 */\n  public apply(data: WorkflowClipboardData): WorkflowNodeEntity[] {\n    // extract raw json from clipboard data - 从剪贴板数据中提取原始JSON\n    const { json: rawJSON } = data;\n    const json = generateUniqueWorkflow({\n      json: rawJSON,\n      isUniqueId: (id: string) => !this.entityManager.getEntityById(id),\n    });\n\n    const offset = this.calcPasteOffset(data.bounds);\n    let parent = this.getSelectedContainer();\n    // loop 不支持嵌套\n    if (parent && json.nodes.some((n) => !canContainNode(n.type, parent!.flowNodeType))) {\n      parent = undefined;\n    }\n    this.applyOffset({ json, offset, parent });\n    const { nodes } = this.document.batchAddFromJSON(json, {\n      parent,\n    });\n    this.selectNodes(nodes);\n    // 这里需要 focus 画布才能继续使用快捷键\n    // The focus canvas is needed here to continue using the shortcuts\n    this.playground.node.focus();\n    return nodes;\n  }\n\n  /**\n   * readonly - 是否只读\n   */\n  private get readonly(): boolean {\n    return this.playgroundConfig.readonly;\n  }\n\n  private isValidData(data?: WorkflowClipboardData): boolean {\n    if (data?.type !== WorkflowClipboardDataID) {\n      Toast.error({\n        content: 'Invalid clipboard data',\n      });\n      return false;\n    }\n    // Cross-domain means different environments, different plugins, cannot be copied - 跨域名表示不同环境，上架插件不同，不能复制\n    if (data.source.host !== window.location.host) {\n      Toast.error({\n        content: 'Cannot paste nodes from different host',\n      });\n      return false;\n    }\n    // Check container - 检查容器\n    const parent = this.getSelectedContainer();\n    for (const nodeJSON of data.json.nodes) {\n      const res = this.dragService.canDropToNode({\n        dragNodeType: nodeJSON.type,\n        dropNodeType: parent?.flowNodeType,\n        dropNode: parent,\n      });\n      if (!res.allowDrop) {\n        Toast.error({\n          content: res.message ?? 'Cannot paste nodes to invalid container',\n        });\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /** try to read clipboard - 尝试读取剪贴板 */\n  private async tryReadClipboard(): Promise<WorkflowClipboardData | undefined> {\n    try {\n      // need user permission to access clipboard, may throw NotAllowedError - 需要用户授予网页剪贴板读取权限, 如果用户没有授予权限, 代码可能会抛出异常 NotAllowedError\n      const text: string = (await navigator.clipboard.readText()) || '';\n      const clipboardData: WorkflowClipboardData = JSON.parse(text);\n      return clipboardData;\n    } catch (e) {\n      // clipboard data is not fixed, no need to show error - 这里本身剪贴板里的数据就不固定，所以没必要报错\n      return;\n    }\n  }\n\n  /** calculate paste offset - 计算粘贴偏移 */\n  private calcPasteOffset(boundsData: WorkflowClipboardRect): IPoint {\n    // extract bounds data - 提取边界数据\n    const { x, y, width, height } = boundsData;\n    const rect = new Rectangle(x, y, width, height);\n    const { center } = rect;\n    const mousePos = this.hoverService.hoveredPos;\n    return {\n      x: mousePos.x - center.x,\n      y: mousePos.y - center.y,\n    };\n  }\n\n  /**\n   * apply offset to node positions - 应用偏移到节点位置\n   */\n  private applyOffset(params: {\n    json: WorkflowJSON;\n    offset: IPoint;\n    parent?: WorkflowNodeEntity;\n  }): void {\n    const { json, offset, parent } = params;\n    json.nodes.forEach((nodeJSON) => {\n      if (!nodeJSON.meta?.position) {\n        return;\n      }\n      // calculate new position - 计算新位置\n      let position = {\n        x: nodeJSON.meta.position.x + offset.x,\n        y: nodeJSON.meta.position.y + offset.y,\n      };\n      if (parent) {\n        position = this.dragService.adjustSubNodePosition(\n          nodeJSON.type as string,\n          parent,\n          position\n        );\n      }\n      nodeJSON.meta.position = position;\n    });\n  }\n\n  /** get selected container node - 获取鼠标选中的容器 */\n  private getSelectedContainer(): WorkflowNodeEntity | undefined {\n    const { activatedNode } = this.selectService;\n    return activatedNode?.getNodeMeta<WorkflowNodeMeta>().isContainer ? activatedNode : undefined;\n  }\n\n  /** select nodes - 选中节点 */\n  private selectNodes(nodes: WorkflowNodeEntity[]): void {\n    this.selectService.selection = nodes;\n  }\n\n  /** scroll to nodes - 滚动到节点 */\n  private async scrollNodesToView(nodes: WorkflowNodeEntity[]): Promise<void> {\n    const nodeBounds = nodes.map((node) => node.getData(FlowNodeTransformData).bounds);\n    await this.document.playgroundConfig.scrollToView({\n      bounds: Rectangle.enlarge(nodeBounds),\n    });\n  }\n\n  /** wait for next frame - 等待下一帧 */\n  private async nextTick(): Promise<void> {\n    // 16ms is one render frame - 16ms 为一个渲染帧\n    const frameTime = 16;\n    await delay(frameTime);\n    await new Promise((resolve) => requestAnimationFrame(resolve));\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/paste/traverse.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// traverse value type - 遍历值类型\nexport type TraverseValue = any;\n\n// traverse node interface - 遍历节点接口\nexport interface TraverseNode {\n  value: TraverseValue; // node value - 节点值\n  container?: TraverseValue; // parent container - 父容器\n  parent?: TraverseNode; // parent node - 父节点\n  key?: string; // object key - 对象键名\n  index?: number; // array index - 数组索引\n}\n\n// traverse context interface - 遍历上下文接口\nexport interface TraverseContext {\n  node: TraverseNode; // current node - 当前节点\n  setValue: (value: TraverseValue) => void; // set value function - 设置值函数\n  getParents: () => TraverseNode[]; // get parents function - 获取父节点函数\n  getPath: () => Array<string | number>; // get path function - 获取路径函数\n  getStringifyPath: () => string; // get string path function - 获取字符串路径函数\n  deleteSelf: () => void; // delete self function - 删除自身函数\n}\n\n// traverse handler type - 遍历处理器类型\nexport type TraverseHandler = (context: TraverseContext) => void;\n\n/**\n * traverse object deeply and handle each value - 深度遍历对象并处理每个值\n * @param value traverse target - 遍历目标\n * @param handle handler function - 处理函数\n */\nexport const traverse = <T extends TraverseValue = TraverseValue>(\n  value: T,\n  handler: TraverseHandler | TraverseHandler[]\n): T => {\n  const traverseHandler: TraverseHandler = Array.isArray(handler)\n    ? (context: TraverseContext) => {\n        handler.forEach((handlerFn) => handlerFn(context));\n      }\n    : handler;\n  TraverseUtils.traverseNodes({ value }, traverseHandler);\n  return value;\n};\n\nnamespace TraverseUtils {\n  /**\n   * traverse nodes deeply and handle each value - 深度遍历节点并处理每个值\n   * @param node traverse node - 遍历节点\n   * @param handle handler function - 处理函数\n   */\n  export const traverseNodes = (node: TraverseNode, handle: TraverseHandler): void => {\n    const { value } = node;\n    if (!value) {\n      // handle null value - 处理空值\n      return;\n    }\n    if (Object.prototype.toString.call(value) === '[object Object]') {\n      // traverse object properties - 遍历对象属性\n      Object.entries(value).forEach(([key, item]) =>\n        traverseNodes(\n          {\n            value: item,\n            container: value,\n            key,\n            parent: node,\n          },\n          handle\n        )\n      );\n    } else if (Array.isArray(value)) {\n      // traverse array elements from end to start - 从末尾开始遍历数组元素\n      for (let index = value.length - 1; index >= 0; index--) {\n        const item: string = value[index];\n        traverseNodes(\n          {\n            value: item,\n            container: value,\n            index,\n            parent: node,\n          },\n          handle\n        );\n      }\n    }\n    const context: TraverseContext = createContext({ node });\n    handle(context);\n  };\n\n  /**\n   * create traverse context - 创建遍历上下文\n   * @param node traverse node - 遍历节点\n   */\n  const createContext = ({ node }: { node: TraverseNode }): TraverseContext => ({\n    node,\n    setValue: (value: unknown) => setValue(node, value),\n    getParents: () => getParents(node),\n    getPath: () => getPath(node),\n    getStringifyPath: () => getStringifyPath(node),\n    deleteSelf: () => deleteSelf(node),\n  });\n\n  /**\n   * set node value - 设置节点值\n   * @param node traverse node - 遍历节点\n   * @param value new value - 新值\n   */\n  const setValue = (node: TraverseNode, value: unknown) => {\n    // handle empty value - 处理空值\n    if (!value || !node) {\n      return;\n    }\n    node.value = value;\n    // get container info from parent scope - 从父作用域获取容器信息\n    const { container, key, index } = node;\n    if (key && container) {\n      container[key] = value;\n    } else if (typeof index === 'number') {\n      container[index] = value;\n    }\n  };\n\n  /**\n   * get parent nodes - 获取父节点列表\n   * @param node traverse node - 遍历节点\n   */\n  const getParents = (node: TraverseNode): TraverseNode[] => {\n    const parents: TraverseNode[] = [];\n    let currentNode: TraverseNode | undefined = node;\n    while (currentNode) {\n      parents.unshift(currentNode);\n      currentNode = currentNode.parent;\n    }\n    return parents;\n  };\n\n  /**\n   * get node path - 获取节点路径\n   * @param node traverse node - 遍历节点\n   */\n  const getPath = (node: TraverseNode): Array<string | number> => {\n    const path: Array<string | number> = [];\n    const parents = getParents(node);\n    parents.forEach((parent) => {\n      if (parent.key) {\n        path.unshift(parent.key);\n      } else if (parent.index) {\n        path.unshift(parent.index);\n      }\n    });\n    return path;\n  };\n\n  /**\n   * get stringify path - 获取字符串路径\n   * @param node traverse node - 遍历节点\n   */\n  const getStringifyPath = (node: TraverseNode): string => {\n    const path = getPath(node);\n    return path.reduce((stringifyPath: string, pathItem: string | number) => {\n      if (typeof pathItem === 'string') {\n        const re = /\\W/g;\n        if (re.test(pathItem)) {\n          // handle special characters - 处理特殊字符\n          return `${stringifyPath}[\"${pathItem}\"]`;\n        }\n        return `${stringifyPath}.${pathItem}`;\n      } else {\n        return `${stringifyPath}[${pathItem}]`;\n      }\n    }, '');\n  };\n\n  /**\n   * delete current node - 删除当前节点\n   * @param node traverse node - 遍历节点\n   */\n  const deleteSelf = (node: TraverseNode): void => {\n    const { container, key, index } = node;\n    if (key && container) {\n      delete container[key];\n    } else if (typeof index === 'number') {\n      container.splice(index, 1);\n    }\n  };\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/paste/unique-workflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { customAlphabet } from 'nanoid';\nimport type { WorkflowJSON, WorkflowNodeJSON } from '@flowgram.ai/free-layout-editor';\n\nimport { traverse, TraverseContext } from './traverse';\n\nnamespace UniqueWorkflowUtils {\n  /** generate unique id - 生成唯一ID */\n  const generateUniqueId = customAlphabet('1234567890', 6); // create a function to generate 6-digit number - 创建一个生成6位数字的函数\n\n  /** get all node ids from workflow json - 从工作流JSON中获取所有节点ID */\n  export const getAllNodeIds = (json: WorkflowJSON): string[] => {\n    const nodeIds = new Set<string>(); // use set to store unique ids - 使用Set存储唯一ID\n    const addNodeId = (node: WorkflowNodeJSON) => {\n      nodeIds.add(node.id);\n      if (node.blocks?.length) {\n        node.blocks.forEach((child) => addNodeId(child)); // recursively add child node ids - 递归添加子节点ID\n      }\n    };\n    json.nodes.forEach((node) => addNodeId(node));\n    return Array.from(nodeIds);\n  };\n\n  /** generate node replacement mapping - 生成节点替换映射 */\n  export const generateNodeReplaceMap = (\n    nodeIds: string[],\n    isUniqueId: (id: string) => boolean\n  ): Map<string, string> => {\n    const nodeReplaceMap = new Map<string, string>(); // create map for id replacement - 创建ID替换映射\n    nodeIds.forEach((id) => {\n      if (isUniqueId(id)) {\n        nodeReplaceMap.set(id, id); // keep original id if unique - 如果ID唯一则保持不变\n      } else {\n        let newId: string;\n        do {\n          newId = generateUniqueId(); // generate new id until unique - 生成新ID直到唯一\n        } while (!isUniqueId(newId));\n        nodeReplaceMap.set(id, newId);\n      }\n    });\n    return nodeReplaceMap;\n  };\n\n  /** check if value exists - 检查值是否存在 */\n  const isExist = (value: unknown): boolean => value !== null && value !== undefined;\n\n  /** check if node should be handled - 检查节点是否需要处理 */\n  const shouldHandle = (context: TraverseContext): boolean => {\n    const { node } = context;\n    // check edge data - 检查边数据\n    if (\n      node?.key &&\n      ['sourceNodeID', 'targetNodeID'].includes(node.key) &&\n      node.parent?.parent?.key === 'edges'\n    ) {\n      return true;\n    }\n    // check node data - 检查节点数据\n    if (\n      node?.key === 'id' &&\n      isExist(node.container?.type) &&\n      isExist(node.container?.meta) &&\n      isExist(node.container?.data)\n    ) {\n      return true;\n    }\n    // check variable data - 检查变量数据\n    if (\n      node?.key === 'blockID' &&\n      isExist(node.container?.name) &&\n      node.container?.source === 'block-output'\n    ) {\n      return true;\n    }\n    return false;\n  };\n\n  /**\n   * replace node ids in workflow json - 替换工作流JSON中的节点ID\n   * notice: this method has side effects, it will modify the input json to avoid deep copy overhead\n   * - 注意：此方法有副作用，会修改输入的json以避免深拷贝开销\n   */\n  export const replaceNodeId = (\n    json: WorkflowJSON,\n    nodeReplaceMap: Map<string, string>\n  ): WorkflowJSON => {\n    traverse(json, (context) => {\n      if (!shouldHandle(context)) {\n        return;\n      }\n      const { node } = context;\n      if (nodeReplaceMap.has(node.value)) {\n        context.setValue(nodeReplaceMap.get(node.value)); // replace old id with new id - 用新ID替换旧ID\n      }\n    });\n    return json;\n  };\n}\n\n/** generate unique workflow json - 生成唯一工作流JSON */\nexport const generateUniqueWorkflow = (params: {\n  json: WorkflowJSON;\n  isUniqueId: (id: string) => boolean;\n}): WorkflowJSON => {\n  const { json, isUniqueId } = params;\n  const nodeIds = UniqueWorkflowUtils.getAllNodeIds(json); // get all existing node ids - 获取所有现有节点ID\n  const nodeReplaceMap = UniqueWorkflowUtils.generateNodeReplaceMap(nodeIds, isUniqueId); // generate id replacement map - 生成ID替换映射\n  return UniqueWorkflowUtils.replaceNodeId(json, nodeReplaceMap); // replace all node ids - 替换所有节点ID\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/select-all/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  Playground,\n  ShortcutsHandler,\n  WorkflowDocument,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class SelectAllShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.SELECT_ALL;\n\n  public shortcuts = ['meta a', 'ctrl a'];\n\n  private document: WorkflowDocument;\n\n  private playground: Playground;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.document = context.get(WorkflowDocument);\n    this.playground = context.playground;\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    const allNodes = this.document.getAllNodes();\n    this.playground.selectionService.selection = allNodes;\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/shortcuts.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FreeLayoutPluginContext, ShortcutsRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { ZoomOutShortcut } from './zoom-out';\nimport { ZoomInShortcut } from './zoom-in';\nimport { SelectAllShortcut } from './select-all';\nimport { PasteShortcut } from './paste';\nimport { ExpandShortcut } from './expand';\nimport { DeleteShortcut } from './delete';\nimport { CopyShortcut } from './copy';\nimport { CollapseShortcut } from './collapse';\n\nexport function shortcuts(shortcutsRegistry: ShortcutsRegistry, ctx: FreeLayoutPluginContext) {\n  shortcutsRegistry.addHandlers(\n    new CopyShortcut(ctx),\n    new PasteShortcut(ctx),\n    new SelectAllShortcut(ctx),\n    new CollapseShortcut(ctx),\n    new ExpandShortcut(ctx),\n    new DeleteShortcut(ctx),\n    new ZoomInShortcut(ctx),\n    new ZoomOutShortcut(ctx)\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nimport type { WorkflowClipboardDataID } from './constants';\n\nexport interface WorkflowClipboardSource {\n  host: string;\n  // more: id?, workspaceId? etc.\n}\n\nexport interface WorkflowClipboardRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface WorkflowClipboardData {\n  type: typeof WorkflowClipboardDataID;\n  json: WorkflowJSON;\n  source: WorkflowClipboardSource;\n  bounds: WorkflowClipboardRect;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/zoom-in/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  PlaygroundConfigEntity,\n  ShortcutsHandler,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ZoomInShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.ZOOM_IN;\n\n  public shortcuts = ['meta =', 'ctrl ='];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.get(PlaygroundConfigEntity);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    if (this.playgroundConfig.zoom > 1.9) {\n      return;\n    }\n    this.playgroundConfig.zoomin();\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/shortcuts/zoom-out/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  PlaygroundConfigEntity,\n  ShortcutsHandler,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ZoomOutShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.ZOOM_OUT;\n\n  public shortcuts = ['meta -', 'ctrl -'];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.get(PlaygroundConfigEntity);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    if (this.playgroundConfig.zoom > 1.9) {\n      return;\n    }\n    this.playgroundConfig.zoomout();\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/styles/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  /* Port colors */\n  --g-workflow-port-color-primary: #4d53e8;\n  --g-workflow-port-color-secondary: #9197f1;\n  --g-workflow-port-color-error: #ff0000;\n  --g-workflow-port-color-background: #ffffff;\n\n  /* Line colors */\n  --g-workflow-line-color-hidden: transparent;\n  --g-workflow-line-color-default: #4d53e8;\n  --g-workflow-line-color-drawing: #5dd6e3;\n  --g-workflow-line-color-hover: #37d0ff;\n  --g-workflow-line-color-selected: #37d0ff;\n  --g-workflow-line-color-error: red;\n}\n\n.gedit-selector-bounds-foreground {\n  cursor: move;\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  z-index: 33;\n  background-color: var(--g-playground-selectBox-background);\n}\n\n@keyframes blink {\n  0% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0;\n  }\n  100% {\n    opacity: 1;\n  }\n}\n\n.node-running {\n  border: 1px dashed rgb(78, 64, 229) !important;\n  border-radius: 8px;\n}\n.demo-editor {\n  flex-grow: 1;\n  position: relative;\n  height: 100%;\n}\n\n.demo-container {\n  position: absolute;\n  left: 0px;\n  top: 0px;\n  display: flex;\n  width: 100%;\n  height: 100%;\n  flex-direction: column;\n}\n\n.demo-tools {\n  padding: 10px;\n  display: flex;\n  justify-content: space-between;\n}\n\n.demo-tools-group > * {\n  margin-right: 8px;\n}\n\n.mouse-pad-option-icon {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/type.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ndeclare module '*.svg'\ndeclare module '*.png'\ndeclare module '*.jpg'\ndeclare module '*.module.less'\n"
  },
  {
    "path": "apps/demo-free-layout/src/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node';\nexport * from './json-schema';\n"
  },
  {
    "path": "apps/demo-free-layout/src/typings/json-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IJsonSchema, JsonSchemaBasicType } from '@flowgram.ai/form-materials';\n\nexport type BasicType = JsonSchemaBasicType;\nexport type JsonSchema = IJsonSchema;\n"
  },
  {
    "path": "apps/demo-free-layout/src/typings/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowNodeJSON as FlowNodeJSONDefault,\n  WorkflowNodeRegistry as FlowNodeRegistryDefault,\n  FreeLayoutPluginContext,\n  FlowNodeEntity,\n  type WorkflowEdgeJSON,\n  WorkflowNodeMeta,\n} from '@flowgram.ai/free-layout-editor';\nimport { IFlowValue } from '@flowgram.ai/form-materials';\n\nimport { type JsonSchema } from './json-schema';\nimport { WorkflowNodeType } from '../nodes';\n\n/**\n * You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node\n * 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出\n */\nexport interface FlowNodeJSON extends FlowNodeJSONDefault {\n  data: {\n    /**\n     * Node title\n     */\n    title?: string;\n    /**\n     * Inputs data values\n     */\n    inputsValues?: Record<string, IFlowValue>;\n    /**\n     * Define the inputs data of the node by JsonSchema\n     */\n    inputs?: JsonSchema;\n    /**\n     * Define the outputs data of the node by JsonSchema\n     */\n    outputs?: JsonSchema;\n    /**\n     * Rest properties\n     */\n    [key: string]: any;\n  };\n}\n\n/**\n * You can customize your own node meta\n * 你可以自定义节点的meta\n */\nexport interface FlowNodeMeta extends WorkflowNodeMeta {\n  sidebarDisabled?: boolean;\n  nodePanelHidden?: boolean;\n  wrapperStyle?: React.CSSProperties;\n  onlyInContainer?: WorkflowNodeType;\n}\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport interface FlowNodeRegistry extends FlowNodeRegistryDefault {\n  meta: FlowNodeMeta;\n  info?: {\n    icon: string;\n    description: string;\n  };\n  canAdd?: (ctx: FreeLayoutPluginContext) => boolean;\n  canDelete?: (ctx: FreeLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  onAdd?: (ctx: FreeLayoutPluginContext) => FlowNodeJSON;\n}\n\nexport interface FlowDocumentJSON {\n  nodes: FlowNodeJSON[];\n  edges: WorkflowEdgeJSON[];\n  /**\n   * Global Variable Schema Definition\n   * 全局变量的 Schema 定义\n   *\n   * Warning: In real occasion, it's better to store the schema and value of these global variables in a reliable place, since the value of a variable might be leaked in saved schema.\n   * 注意：在真实场景下，全局变量的 Schema 定义和值都应该存储在更可靠的地方，因为全局变量的值可能会泄露在保存的 Schema 中。\n   */\n  globalVariable?: JsonSchema;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/utils/can-contain-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeType } from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowNodeType } from '../nodes';\n\n/**\n * 判断父节点是否可以包含对应子节点\n * Determine whether the parent node can contain the corresponding child node\n * @param childNodeType\n * @param parentNodeType\n */\nexport function canContainNode(\n  childNodeType: WorkflowNodeType | FlowNodeType,\n  parentNodeType: WorkflowNodeType | FlowNodeType\n) {\n  /**\n   * 开始/结束节点无法更改容器\n   * The start and end nodes cannot change container\n   */\n  if (\n    [\n      WorkflowNodeType.Start,\n      WorkflowNodeType.End,\n      WorkflowNodeType.BlockStart,\n      WorkflowNodeType.BlockEnd,\n    ].includes(childNodeType as WorkflowNodeType)\n  ) {\n    return false;\n  }\n  /**\n   * 继续循环与终止循环只能在循环节点中\n   * Continue loop and break loop can only be in loop nodes\n   */\n  if (\n    [WorkflowNodeType.Continue, WorkflowNodeType.Break].includes(\n      childNodeType as WorkflowNodeType\n    ) &&\n    parentNodeType !== WorkflowNodeType.Loop\n  ) {\n    return false;\n  }\n  /**\n   * 循环节点无法嵌套循环节点\n   * Loop node cannot nest loop node\n   */\n  if (childNodeType === WorkflowNodeType.Loop && parentNodeType === WorkflowNodeType.Loop) {\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "apps/demo-free-layout/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { onDragLineEnd } from './on-drag-line-end';\nexport { toggleLoopExpanded } from './toggle-loop-expanded';\nexport { canContainNode } from './can-contain-node';\n"
  },
  {
    "path": "apps/demo-free-layout/src/utils/on-drag-line-end.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowNodePanelService,\n  WorkflowNodePanelUtils,\n} from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  delay,\n  FreeLayoutPluginContext,\n  onDragLineEndParams,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n} from '@flowgram.ai/free-layout-editor';\n\n/**\n * Drag the end of the line to create an add panel (feature optional)\n * 拖拽线条结束需要创建一个添加面板 （功能可选）\n */\nexport const onDragLineEnd = async (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => {\n  // get services from context - 从上下文获取服务\n  const nodePanelService = ctx.get(WorkflowNodePanelService);\n  const document = ctx.document;\n  const dragService = ctx.get(WorkflowDragService);\n  const linesManager = ctx.get(WorkflowLinesManager);\n\n  // get params from drag event - 从拖拽事件获取参数\n  const { fromPort, toPort, mousePos, line, originLine } = params;\n\n  // return if invalid line state - 如果线条状态无效则返回\n  if (originLine || !line) {\n    return;\n  }\n\n  // return if target port exists - 如果目标端口存在则返回\n  if (toPort || !fromPort) {\n    return;\n  }\n\n  // get container node for the new node - 获取新节点的容器节点\n  const containerNode = fromPort.node.parent;\n  const isVertical = fromPort.location === 'bottom';\n\n  // open node selection panel - 打开节点选择面板\n  const result = await nodePanelService.singleSelectNodePanel({\n    position: isVertical\n      ? {\n          x: mousePos.x - 165,\n          y: mousePos.y + 60,\n        }\n      : mousePos,\n    containerNode,\n    panelProps: {\n      enableNodePlaceholder: true,\n      enableScrollClose: true,\n      fromPort,\n    },\n  });\n\n  // return if no node selected - 如果没有选择节点则返回\n  if (!result) {\n    return;\n  }\n\n  // get selected node type and data - 获取选择的节点类型和数据\n  const { nodeType, nodeJSON } = result;\n\n  // calculate position for the new node - 计算新节点的位置\n  const nodePosition = WorkflowNodePanelUtils.adjustNodePosition({\n    nodeType,\n    position: mousePos,\n    fromPort,\n    toPort,\n    containerNode,\n    document,\n    dragService,\n  });\n\n  // create new workflow node - 创建新的工作流节点\n  const node: WorkflowNodeEntity = document.createWorkflowNodeByType(\n    nodeType,\n    nodePosition,\n    nodeJSON ?? ({} as WorkflowNodeJSON),\n    containerNode?.id\n  );\n\n  // wait for node render - 等待节点渲染\n  await delay(20);\n\n  // build connection line - 构建连接线\n  WorkflowNodePanelUtils.buildLine({\n    fromPort,\n    node,\n    linesManager,\n  });\n};\n"
  },
  {
    "path": "apps/demo-free-layout/src/utils/toggle-loop-expanded.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nconst HeightCollapsed = 54;\nconst HeightExpanded = 225;\n\nexport function toggleLoopExpanded(\n  node: WorkflowNodeEntity,\n  expanded: boolean = node.transform.collapsed\n) {\n  if (node.transform.collapsed === !expanded) {\n    if (!node.getNodeMeta().isContainer && node.blocks.length !== 0) {\n      return;\n    }\n    const bounds = node.bounds.clone();\n    node.transform.size = {\n      width: bounds.width,\n      height: node.transform.collapsed === expanded ? HeightCollapsed : HeightExpanded,\n    };\n    node.transform.transform.fireChange();\n    return;\n  }\n  const bounds = node.bounds.clone();\n  const prePosition = {\n    x: node.transform.position.x,\n    y: node.transform.position.y,\n  };\n  node.transform.collapsed = !expanded;\n  if (!expanded) {\n    node.transform.transform.clearChildren();\n    node.transform.transform.update({\n      position: {\n        x: prePosition.x - node.transform.padding.left,\n        y: prePosition.y - node.transform.padding.top,\n      },\n      origin: {\n        x: 0,\n        y: 0,\n      },\n    });\n    // When folded, the width and height no longer change according to the child nodes, and need to be set manually\n    // 折叠起来，宽高不再根据子节点变化，需要手动设置\n    node.transform.size = {\n      width: bounds.width,\n      height: HeightCollapsed,\n    };\n  } else {\n    node.transform.transform.update({\n      position: {\n        x: prePosition.x + node.transform.padding.left,\n        y: prePosition.y + node.transform.padding.top,\n      },\n      origin: {\n        x: 0,\n        y: 0,\n      },\n    });\n  }\n\n  // 隐藏子节点线条\n  // Hide the child node lines\n  node.blocks.forEach((block) => {\n    block.lines.allLines.forEach((line) => {\n      line.updateUIState({\n        style: !expanded\n          ? { ...line.uiState.style, display: 'none' }\n          : { ...line.uiState.style, display: 'block' },\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "apps/demo-free-layout/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    },\n  },\n  \"include\": [\n    \"./src\"\n  ],\n}\n"
  },
  {
    "path": "apps/demo-free-layout-simple/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout-simple/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-free-layout-simple/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-free-layout-simple\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-container-plugin\": \"workspace:*\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-free-layout-simple/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n  },\n  html: {\n    title: 'demo-free-layout-simple',\n  },\n});\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { Editor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/components/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 226,\n      bottom: 51,\n      zIndex: 100,\n      width: 198,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/components/node-add-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  WorkflowDocument,\n  WorkflowDragService,\n  useClientContext,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { createBatchFunction } from '../nodes/batch-function';\n\nconst cardkeys = ['Node1', 'Node2', 'Condition', 'Chain', 'Tool', 'Twoway', 'Loop', 'Batch'];\n\nexport const NodeAddPanel: React.FC = (props) => {\n  const startDragService = useService(WorkflowDragService);\n  const workflowDocument = useService(WorkflowDocument);\n  const context = useClientContext();\n\n  return (\n    <div className=\"demo-free-sidebar\">\n      {cardkeys.map((nodeType) => (\n        <div\n          key={nodeType}\n          className=\"demo-free-card\"\n          onMouseDown={async (e) => {\n            const type = nodeType.toLowerCase();\n            const registry = workflowDocument.getNodeRegistry(type);\n            const json = registry.onAdd?.(context);\n            const node = await startDragService.startDragCard(type, e, {\n              ...json,\n              data: {\n                title: `New ${nodeType}`,\n                content: 'xxxx',\n              },\n            });\n            if (node?.flowNodeType === 'batch') {\n              createBatchFunction(node, node.getNodeMeta().position);\n            }\n          }}\n        >\n          {nodeType}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext, LineType } from '@flowgram.ai/free-layout-editor';\n\nimport { getBatchID } from '../nodes/batch-function';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button\n        onClick={() =>\n          tools.autoLayout({\n            getFollowNode: (node, context) => {\n              if (node.entity.flowNodeType !== 'batch_function') {\n                return;\n              }\n              const batchNodeID = getBatchID(node.entity.id);\n              return {\n                followTo: batchNodeID,\n              };\n            },\n          })\n        }\n      >\n        AutoLayout\n      </button>\n      <button\n        onClick={() =>\n          tools.switchLineType(\n            tools.lineType === LineType.BEZIER ? LineType.LINE_CHART : LineType.BEZIER\n          )\n        }\n      >\n        {tools.lineType === LineType.BEZIER ? 'Bezier' : 'Fold'}\n      </button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <NodeAddPanel />\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n        <Tools />\n        <Minimap />\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n  FlowNodeMeta,\n} from '@flowgram.ai/free-layout-editor';\nimport { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\n\nimport { nodeRegistries } from '../nodes';\nimport { initialData } from '../initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * 节点数据转换, 由 ctx.document.fromJSON 调用\n       * Node data transformation, called by ctx.document.fromJSON\n       * @param node\n       * @param json\n       */\n      fromNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * 节点数据转换, 由 ctx.document.toJSON 调用\n       * Node data transformation, called by ctx.document.toJSON\n       * @param node\n       * @param json\n       */\n      toNodeJSON(node, json) {\n        return json;\n      },\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { node, form } = useNodeRender();\n          const meta = node.getNodeMeta<FlowNodeMeta>();\n          return (\n            <WorkflowNodeRenderer\n              className=\"demo-free-node\"\n              node={props.node}\n              style={meta.wrapperStyle}\n            >\n              {form?.render()}\n            </WorkflowNodeRenderer>\n          );\n        },\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      canDeleteLine: (ctx, line) => {\n        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {\n          return false;\n        }\n        return true;\n      },\n      isHideArrowLine(ctx, line) {\n        if (line.from?.flowNodeType === 'batch' && line.to?.flowNodeType === 'batch_function') {\n          return true;\n        }\n        return false;\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n        /**\n         * This is used for the rendering of the loop node sub-canvas\n         * 这个用于 loop 节点子画布的渲染\n         */\n        createContainerNodePlugin({}),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-node {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    position: relative;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-free-node-title {\n    background-color: #93bfe2;\n    width: 100%;\n    border-radius: 8px 8px 0 0;\n    padding: 4px 12px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n    background-color: white;\n    border-radius: 0 0 8px 8px;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFreeLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 86.5,\n          y: 57.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content',\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 359.5,\n          y: 43.25,\n        },\n      },\n      data: {\n        portKeys: ['if', 'else'],\n        title: 'Condition',\n        content: 'Condition node content',\n        ports: ['if', 'else'],\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1393.5,\n          y: 52.5,\n        },\n      },\n      data: {\n        title: 'End',\n        content: 'End content',\n      },\n    },\n    {\n      id: '100260',\n      type: 'tool',\n      meta: {\n        position: {\n          x: 86.5,\n          y: 399.75,\n        },\n      },\n      data: {\n        title: 'New Tool',\n        content: 'xxxx',\n      },\n    },\n    {\n      id: '105108',\n      type: 'tool',\n      meta: {\n        position: {\n          x: 359.5,\n          y: 399.75,\n        },\n      },\n      data: {\n        title: 'New Tool',\n        content: 'xxxx',\n      },\n    },\n    {\n      id: '106070',\n      type: 'twoway',\n      meta: {\n        position: {\n          x: 86.5,\n          y: 563.75,\n        },\n      },\n      data: {\n        title: 'New Twoway',\n        content: 'xxxx',\n      },\n    },\n    {\n      id: '122116',\n      type: 'twoway',\n      meta: {\n        position: {\n          x: 359.5,\n          y: 563.75,\n        },\n      },\n      data: {\n        title: 'New Twoway',\n        content: 'xxxx',\n      },\n    },\n    {\n      id: 'BatchFunction_193210',\n      type: 'batch_function',\n      meta: {\n        position: {\n          x: 626,\n          y: 420.38853503184714,\n        },\n      },\n      data: {},\n      blocks: [\n        {\n          id: '118937',\n          type: 'node2',\n          meta: {\n            position: {\n              x: 250.5,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'New Node2',\n            content: 'xxxx',\n          },\n        },\n        {\n          id: 'block_start_Y04Mt',\n          type: 'block_start',\n          meta: {\n            position: {\n              x: 32,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'block_end_7QesT',\n          type: 'block_end',\n          meta: {\n            position: {\n              x: 469,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'block_start_Y04Mt',\n          targetNodeID: '118937',\n        },\n        {\n          sourceNodeID: '118937',\n          targetNodeID: 'block_end_7QesT',\n        },\n      ],\n    },\n    {\n      id: 'loop_9OpIm',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 626,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'New Loop',\n        content: 'xxxx',\n      },\n      blocks: [\n        {\n          id: '144150',\n          type: 'node1',\n          meta: {\n            position: {\n              x: 250.5,\n              y: 1.4210854715202004e-14,\n            },\n          },\n          data: {\n            title: 'New Node1',\n            content: 'xxxx',\n          },\n        },\n        {\n          id: 'block_start_ptqXx',\n          type: 'block_start',\n          meta: {\n            position: {\n              x: 32,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'block_end_1zf_a',\n          type: 'block_end',\n          meta: {\n            position: {\n              x: 469,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'block_start_ptqXx',\n          targetNodeID: '144150',\n        },\n        {\n          sourceNodeID: '144150',\n          targetNodeID: 'block_end_1zf_a',\n        },\n      ],\n    },\n    {\n      id: '193210',\n      type: 'batch',\n      meta: {\n        position: {\n          x: 876.5,\n          y: 197.69426751592357,\n        },\n      },\n      data: {\n        title: 'New Batch',\n        content: 'xxxx',\n      },\n    },\n    {\n      id: 'chain0',\n      type: 'chain',\n      meta: {\n        position: {\n          x: 221.02229299363057,\n          y: 197.69426751592357,\n        },\n      },\n      data: {\n        title: 'Chain',\n        content: 'xxxx',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'loop_9OpIm',\n      sourcePortID: 'if',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: '193210',\n      sourcePortID: 'else',\n    },\n    {\n      sourceNodeID: '193210',\n      targetNodeID: 'end_0',\n      sourcePortID: 'batch-output',\n    },\n    {\n      sourceNodeID: 'loop_9OpIm',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'chain0',\n      targetNodeID: '100260',\n      sourcePortID: 'p4',\n    },\n    {\n      sourceNodeID: 'chain0',\n      targetNodeID: '105108',\n      sourcePortID: 'p5',\n    },\n    {\n      sourceNodeID: '122116',\n      targetNodeID: '106070',\n      sourcePortID: 'output-left',\n      targetPortID: 'input-right',\n    },\n    {\n      sourceNodeID: '106070',\n      targetNodeID: '122116',\n      sourcePortID: 'output-right',\n      targetPortID: 'input-left',\n    },\n    {\n      sourceNodeID: '193210',\n      targetNodeID: 'BatchFunction_193210',\n      sourcePortID: 'batch-output-to-function',\n      targetPortID: 'batch-function-input',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { getBatchFunctionID } from '../batch-function';\n\nexport const BatchNodeRegistry: FlowNodeRegistry = {\n  type: 'batch',\n  meta: {\n    defaultPorts: [\n      { type: 'input' },\n      { type: 'output', portID: 'batch-output' },\n      {\n        type: 'output',\n        portID: 'batch-output-to-function',\n        location: 'bottom',\n      },\n    ],\n  },\n  onCreate(node, json) {\n    node.onDispose(() => {\n      node.document.getNode(getBatchFunctionID(node.id))?.dispose();\n    });\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-json.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, WorkflowNodeJSON, nanoid } from '@flowgram.ai/free-layout-editor';\n\nexport const createBatchFunctionJSON = (id: string, position: IPoint): WorkflowNodeJSON => ({\n  id,\n  type: 'batch_function',\n  data: {},\n  meta: {\n    position,\n  },\n  blocks: [\n    {\n      id: `block_start_${nanoid(5)}`,\n      type: 'block_start',\n      meta: {\n        position: {\n          x: 32,\n          y: 0,\n        },\n      },\n      data: {},\n    },\n    {\n      id: `block_end_${nanoid(5)}`,\n      type: 'block_end',\n      meta: {\n        position: {\n          x: 192,\n          y: 0,\n        },\n      },\n      data: {},\n    },\n  ],\n});\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function-lines.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowDocument, delay } from '@flowgram.ai/free-layout-editor';\n\n/** 生成连线 */\nexport const createBatchFunctionLines = async (params: {\n  document: WorkflowDocument;\n  batchId: string;\n  batchFunctionId: string;\n}) => {\n  await delay(30); // 等待节点创建完毕\n  const { document, batchId, batchFunctionId } = params;\n  document.linesManager.createLine({\n    from: batchId,\n    to: batchFunctionId,\n    fromPort: 'batch-output-to-function',\n    toPort: 'batch-function-input',\n  });\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/create-batch-function.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, WorkflowDocument, IPoint } from '@flowgram.ai/free-layout-editor';\n\nimport { BatchFunctionIDPrefix } from './relation';\nimport { createBatchFunctionLines } from './create-batch-function-lines';\nimport { createBatchFunctionJSON } from './create-batch-function-json';\n\n/** 创建 Batch 循环体节点 */\nexport const createBatchFunction = (batchNode: WorkflowNodeEntity, batchPosition: IPoint) => {\n  const document = batchNode.document as WorkflowDocument;\n  const id = `${BatchFunctionIDPrefix}${batchNode.id}`;\n  const offset: IPoint = {\n    x: -112,\n    y: 230,\n  };\n  const position = {\n    x: batchPosition.x + offset.x,\n    y: batchPosition.y + offset.y,\n  };\n  const batchFunctionJSON = createBatchFunctionJSON(id, position);\n  const batchFunctionNode = document.createWorkflowNode(batchFunctionJSON);\n  createBatchFunctionLines({\n    document,\n    batchId: batchNode.id,\n    batchFunctionId: batchFunctionNode.id,\n  });\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\n\nconst formHeight = 48;\n\nexport const BatchFunctionFormRender = () => (\n  <>\n    <div className=\"demo-free-node-title\">Batch Function</div>\n    <SubCanvasRender offsetY={-formHeight} />\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: BatchFunctionFormRender,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createBatchFunctionJSON } from './create-batch-function-json';\nexport { createBatchFunctionLines } from './create-batch-function-lines';\nexport { createBatchFunction } from './create-batch-function';\nexport { BatchFunctionFormRender, formMeta } from './form-meta';\nexport { BatchFunctionNodeRegistry } from './registry';\nexport { BatchFunctionIDPrefix, getBatchFunctionID, getBatchID } from './relation';\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowNodeEntity,\n  PositionSchema,\n  FlowNodeTransformData,\n  FlowNodeRegistry,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { getBatchID } from './relation';\nimport { formMeta } from './form-meta';\n\nexport const BatchFunctionNodeRegistry: FlowNodeRegistry = {\n  type: 'batch_function',\n  meta: {\n    defaultPorts: [{ type: 'input', location: 'top', portID: 'batch-function-input' }],\n    /**\n     * Mark as subcanvas\n     * 子画布标记\n     */\n    isContainer: true,\n    /**\n     * The subcanvas default size setting\n     * 子画布默认大小设置\n     */\n\n    size: {\n      width: 424,\n      height: 244,\n    },\n    // autoResizeDisable: true,\n    /**\n     * The subcanvas padding setting\n     * 子画布 padding 设置\n     */\n    padding: (transform) => {\n      if (!transform.isContainer) {\n        return {\n          top: 0,\n          bottom: 0,\n          left: 0,\n          right: 0,\n        };\n      }\n      return {\n        top: 65,\n        bottom: 40,\n        left: 80,\n        right: 80,\n      };\n    },\n    /**\n     * Controls the node selection status within the subcanvas\n     * 控制子画布内的节点选中状态\n     */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // 鼠标开始时所在位置不包括当前节点时才可选中\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    // expandable: false, // disable expanded\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n    },\n    // defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left'}, { type: 'output', location: 'bottom', portID: 'bottom' }, { type: 'input', location: 'top', portID: 'top'}]\n  },\n  formMeta,\n  onCreate(node, json) {\n    node.onDispose(() => {\n      node.document.getNode(getBatchID(node.id))?.dispose();\n    });\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/batch-function/relation.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable  @typescript-eslint/naming-convention*/\nexport const BatchFunctionIDPrefix = 'BatchFunction_';\nexport const getBatchFunctionID = (batchID: string) => BatchFunctionIDPrefix + batchID;\nexport const getBatchID = (batchFunctionID: string) =>\n  batchFunctionID.replace(BatchFunctionIDPrefix, '');\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/block-end/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/free-layout-editor';\n\nexport const renderForm = () => (\n  <>\n    <div\n      style={{\n        width: 60,\n        height: 60,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n    >\n      <div\n        style={{\n          width: 40,\n          height: 40,\n          cursor: 'move',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        END\n      </div>\n    </div>\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/block-end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { formMeta } from './form-meta';\n\nexport const BlockEndNodeRegistry: FlowNodeRegistry = {\n  type: 'block_end',\n  meta: {\n    isNodeEnd: true,\n    deleteDisable: true,\n    copyDisable: true,\n    sidebarDisabled: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 100,\n      height: 100,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      cursor: 'move',\n    },\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/block-start/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta } from '@flowgram.ai/free-layout-editor';\n\nexport const renderForm = () => (\n  <>\n    <div\n      style={{\n        width: 60,\n        height: 60,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n    >\n      <div\n        style={{\n          width: 40,\n          height: 40,\n          cursor: 'move',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n        }}\n      >\n        START\n      </div>\n    </div>\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/block-start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { formMeta } from './form-meta';\n\nexport const BlockStartNodeRegistry: FlowNodeRegistry = {\n  type: 'block_start',\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    sidebarDisabled: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 100,\n      height: 100,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      cursor: 'move',\n    },\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/chain/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const ChainNodeRegistry: FlowNodeRegistry = {\n  type: 'chain',\n  meta: {\n    defaultPorts: [\n      { type: 'input' },\n      { type: 'output' },\n      {\n        portID: 'p4',\n        location: 'bottom',\n        locationConfig: { left: '33%', bottom: 0 },\n        type: 'output',\n      },\n      {\n        portID: 'p5',\n        location: 'bottom',\n        locationConfig: { left: '66%', bottom: 0 },\n        type: 'output',\n      },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/condition/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Field,\n  DataEvent,\n  EffectFuncProps,\n  WorkflowPorts,\n  FormMeta,\n} from '@flowgram.ai/free-layout-editor';\n\nconst CONDITION_ITEM_HEIGHT = 35;\n\nexport const formMeta: FormMeta = {\n  formatOnInit: (value) => ({\n    portKeys: ['if', 'else'],\n    ...value,\n  }),\n  effect: {\n    portKeys: [\n      {\n        event: DataEvent.onValueInitOrChange,\n        effect: ({ value, context }: EffectFuncProps<Array<string>, FormData>) => {\n          const { node } = context;\n          const defaultPorts: WorkflowPorts = [{ type: 'input' }];\n          const newPorts: WorkflowPorts = value.map((portID: string, i: number) => ({\n            type: 'output',\n            portID,\n            location: 'right',\n            locationConfig: {\n              right: 0,\n              top: (i + 1) * CONDITION_ITEM_HEIGHT,\n            },\n          }));\n          node.ports.updateAllPorts([...defaultPorts, ...newPorts]);\n        },\n      },\n    ],\n  },\n  render: () => (\n    <>\n      <Field<string> name=\"title\">\n        {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n      </Field>\n      <Field<Array<string>> name=\"portKeys\">\n        {({ field: { value, onChange } }) => (\n          <div\n            className=\"demo-free-node-content\"\n            style={{\n              width: 160,\n              height: value.length * CONDITION_ITEM_HEIGHT,\n              minHeight: 2 * CONDITION_ITEM_HEIGHT,\n            }}\n          >\n            <div>\n              <button onClick={() => onChange(value.concat(`if_${value.length}`))}>Add Port</button>\n            </div>\n            <div style={{ marginTop: 8 }}>\n              <button onClick={() => onChange(value.filter((v, i, arr) => i !== arr.length - 1))}>\n                Delete Port\n              </button>\n            </div>\n          </div>\n        )}\n      </Field>\n    </>\n  ),\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { formMeta } from './form-meta';\n\nexport const ConditionNodeRegistry: FlowNodeRegistry = {\n  type: 'condition',\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n  },\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/custom/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const CustomNodeRegistry: FlowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const EndNodeRegistry: FlowNodeRegistry = {\n  type: 'end',\n  meta: {\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'input' }],\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { TwowayNodeRegistry } from './twoway';\nimport { ToolNodeRegistry } from './tool';\nimport { StartNodeRegistry } from './start';\nimport { LoopNodeRegistry } from './loop';\nimport { EndNodeRegistry } from './end';\nimport { CustomNodeRegistry } from './custom';\nimport { ConditionNodeRegistry } from './condition';\nimport { ChainNodeRegistry } from './chain';\nimport { BlockStartNodeRegistry } from './block-start';\nimport { BlockEndNodeRegistry } from './block-end';\nimport { BatchFunctionNodeRegistry } from './batch-function';\nimport { BatchNodeRegistry } from './batch';\n\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  LoopNodeRegistry,\n  BlockStartNodeRegistry,\n  BlockEndNodeRegistry,\n  BatchNodeRegistry,\n  BatchFunctionNodeRegistry,\n  StartNodeRegistry,\n  ConditionNodeRegistry,\n  ChainNodeRegistry,\n  ToolNodeRegistry,\n  TwowayNodeRegistry,\n  EndNodeRegistry,\n  CustomNodeRegistry,\n];\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/loop/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\n\nconst formHeight = 48;\n\nexport const LoopFormRender = () => (\n  <>\n    <Field<string> name=\"title\">\n      {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n    </Field>\n    <SubCanvasRender offsetY={-formHeight} />\n  </>\n);\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowNodeEntity,\n  PositionSchema,\n  FlowNodeTransformData,\n  FlowNodeRegistry,\n  nanoid,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { formMeta } from './form-meta';\n\nlet index = 0;\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: 'loop',\n  meta: {\n    /**\n     * Mark as subcanvas\n     * 子画布标记\n     */\n    isContainer: true,\n    /**\n     * The subcanvas default size setting\n     * 子画布默认大小设置\n     */\n    size: {\n      width: 424,\n      height: 244,\n    },\n    // autoResizeDisable: true,\n    /**\n     * The subcanvas padding setting\n     * 子画布 padding 设置\n     */\n    padding: (transform) => {\n      if (!transform.isContainer) {\n        return {\n          top: 0,\n          bottom: 0,\n          left: 0,\n          right: 0,\n        };\n      }\n      return {\n        top: 65,\n        bottom: 40,\n        left: 80,\n        right: 80,\n      };\n    },\n    /**\n     * Controls the node selection status within the subcanvas\n     * 控制子画布内的节点选中状态\n     */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // 鼠标开始时所在位置不包括当前节点时才可选中\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    // expandable: false, // disable expanded\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n    },\n    // defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left'}, { type: 'output', location: 'bottom', portID: 'bottom' }, { type: 'input', location: 'top', portID: 'top'}]\n  },\n  onAdd() {\n    return {\n      id: `loop_${nanoid(5)}`,\n      type: 'loop',\n      data: {\n        title: `loop_${++index}`,\n      },\n      blocks: [\n        {\n          id: `block_start_${nanoid(5)}`,\n          type: 'block_start',\n          meta: {\n            position: {\n              x: 32,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n        {\n          id: `block_end_${nanoid(5)}`,\n          type: 'block_end',\n          meta: {\n            position: {\n              x: 192,\n              y: 0,\n            },\n          },\n          data: {},\n        },\n      ],\n    };\n  },\n  formMeta,\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: 'start',\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'output' }],\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/tool/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const ToolNodeRegistry: FlowNodeRegistry = {\n  type: 'tool',\n  meta: {\n    defaultPorts: [{ location: 'top', type: 'input' }],\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/src/nodes/twoway/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const TwowayNodeRegistry: FlowNodeRegistry = {\n  type: 'twoway',\n  meta: {\n    defaultPorts: [\n      { type: 'input', portID: 'input-left', location: 'left' },\n      { type: 'output', portID: 'output-left', location: 'left' },\n      { type: 'input', portID: 'input-right', location: 'right' },\n      { type: 'output', portID: 'output-right', location: 'right' },\n    ],\n  },\n};\n"
  },
  {
    "path": "apps/demo-free-layout-simple/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/demo-materials/.storybook/main.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { StorybookConfig } from 'storybook-react-rsbuild'\n\nconst config: StorybookConfig = {\n  stories: [\n    '../src/**/stories/**/*.stories.tsx',\n    '../src/**/stories/**/*.story.tsx',\n    '../src/**/stories/**/*.mdx',\n  ],\n  framework: 'storybook-react-rsbuild',\n  rsbuildFinal: (config) => {\n    // Customize the final Rsbuild config here\n    return config\n  },\n}\n\nexport default config\n"
  },
  {
    "path": "apps/demo-materials/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-materials/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-materials\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"private\": true,\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app storybook build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development storybook dev -p 6006\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development storybook dev -p 6006\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/editor\": \"workspace:*\",\n    \"@flowgram.ai/form-materials\": \"workspace:*\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"lodash-es\": \"^4.17.21\",\n    \"styled-components\": \"^5\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"storybook-react-rsbuild\": \"^2.1.2\",\n    \"storybook\": \"^9.0.0\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-materials/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact()],\n  tools: {\n    rspack: {\n      /**\n       * ignore warnings from @coze-editor/editor/language-typescript\n       */\n      ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-auto-layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconAutoLayout = (\n  <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      fill=\"currentColor\"\n      d=\"M3 2C2.44772 2 2 2.44771 2 3V12C2 12.5523 2.44772 13 3 13H10C10.5523 13 11 12.5523 11 12V3C11 2.44772 10.5523 2 10 2H3zM4 11V4H9V11H4zM21 22C21.5523 22 22 21.5523 22 21V12C22 11.4477 21.5523 11 21 11H14C13.4477 11 13 11.4477 13 12V21C13 21.5523 13.4477 22 14 22H21zM20 13V20H15V13H20zM2 16C2 15.4477 2.44772 15 3 15H10C10.5523 15 11 15.4477 11 16V21C11 21.5523 10.5523 22 10 22H3C2.44772 22 2 21.5523 2 21V16zM4 20V17H9V20H4zM21 9C21.5523 9 22 8.55228 22 8V3C22 2.44772 21.5523 2 21 2H14C13.4477 2 13 2.44772 13 3V8C13 8.55228 13.4477 9 14 9H21zM20 4V7H15V4H20z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-cancel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconCancel = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path d=\"M9.5 8C8.67157 8 8 8.67157 8 9.5V14.5C8 15.3284 8.67157 16 9.5 16H14.5C15.3284 16 16 15.3284 16 14.5V9.5C16 8.67157 15.3284 8 14.5 8H9.5Z\"></path>\n    <path d=\"M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21Z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-comment.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC } from 'react';\n\ninterface IconCommentProps {\n  style?: CSSProperties;\n}\n\nexport const IconComment: FC<IconCommentProps> = ({ style }) => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={style}\n  >\n    <path d=\"M6.5 9C5.94772 9 5.5 9.44772 5.5 10V11C5.5 11.5523 5.94772 12 6.5 12H7.5C8.05228 12 8.5 11.5523 8.5 11V10C8.5 9.44772 8.05228 9 7.5 9H6.5zM11.5 9C10.9477 9 10.5 9.44772 10.5 10V11C10.5 11.5523 10.9477 12 11.5 12H12.5C13.0523 12 13.5 11.5523 13.5 11V10C13.5 9.44772 13.0523 9 12.5 9H11.5zM15.5 10C15.5 9.44772 15.9477 9 16.5 9H17.5C18.0523 9 18.5 9.44772 18.5 10V11C18.5 11.5523 18.0523 12 17.5 12H16.5C15.9477 12 15.5 11.5523 15.5 11V10z\"></path>\n    <path d=\"M23 4C23 2.9 22.1 2 21 2H3C1.9 2 1 2.9 1 4V17.0111C1 18.0211 1.9 19.0111 3 19.0111H7.7586L10.4774 22C10.9822 22.5017 11.3166 22.6311 12 22.7009C12.414 22.707 13.0502 22.5093 13.5 22L16.2414 19.0111H21C22.1 19.0111 23 18.1111 23 17.0111V4ZM3 4H21V17.0111H15.5L12 20.6714L8.5 17.0111H3V4Z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconMinimap = () => (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"g1\">\n      <path\n        id=\"path1\"\n        fill=\"#000000\"\n        stroke=\"none\"\n        d=\"M 18.09091 6.883101 L 5.409091 6.883101 L 5.409091 16.746737 L 10.664648 16.746737 C 10.927091 17.116341 11.30353 17.422749 11.792977 17.611004 L 12.664289 17.946156 L 12.744959 18.155828 L 5.409091 18.155828 C 4.630871 18.155828 4 17.524979 4 16.746737 L 4 6.883101 C 4 6.104881 4.630871 5.47401 5.409091 5.47401 L 18.09091 5.47401 C 18.86915 5.47401 19.5 6.104881 19.5 6.883101 L 19.5 12.52348 C 19.247208 11.883823 18.730145 11.365912 18.09091 11.111994 L 18.09091 6.883101 Z M 18.09091 18.155828 L 17.881165 18.155828 L 19.469212 14.368896 C 19.479921 14.343321 19.490206 14.317817 19.5 14.292241 L 19.5 16.746737 C 19.5 17.524979 18.86915 18.155828 18.09091 18.155828 Z\"\n      />\n      <path\n        id=\"path2\"\n        fill=\"#000000\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 18.494614 13.960189 C 18.982441 12.796985 17.813459 11.628003 16.650255 12.11576 L 12.133272 14.01 C 10.962248 14.501069 10.987188 16.168798 12.172375 16.62464 L 13.482055 17.128389 L 13.985805 18.438068 C 14.441646 19.623184 16.109375 19.648125 16.600443 18.477171 L 18.494614 13.960189 Z M 17.19515 13.415224 L 15.30098 17.932205 L 14.79723 16.622526 C 14.654066 16.250385 14.359989 15.956307 13.987918 15.813213 L 12.678168 15.309464 L 17.19515 13.415224 Z\"\n      />\n    </g>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-mouse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconMouse(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 34}\n      height={height || 52}\n      viewBox=\"0 0 34 52\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.8\"\n      />\n    </svg>\n  );\n}\n\nexport const IconMouseTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-pad.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconPad(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 48}\n      height={height || 38}\n      viewBox=\"0 0 48 38\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect\n        x=\"1.83317\"\n        y=\"1.49998\"\n        width=\"44.3333\"\n        height=\"35\"\n        rx=\"3.5\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n      />\n      <path\n        d=\"M14.6665 30.6667H33.3332\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport const IconPadTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z\"\n    ></path>\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-success.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconSuccessFill = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"20\"\n    height=\"20\"\n    fill=\"none\"\n    viewBox=\"0 0 20 20\"\n  >\n    <g clipPath=\"url(#icon-workflow-run-success_svg__a)\">\n      <path\n        fill=\"#3EC254\"\n        d=\"M.833 10A9.166 9.166 0 0 0 10 19.168a9.166 9.166 0 0 0 9.167-9.166A9.166 9.166 0 0 0 10 .834a9.166 9.166 0 0 0-9.167 9.167\"\n      ></path>\n      <path\n        fill=\"#fff\"\n        d=\"M6.077 9.755a.833.833 0 0 0 0 1.179l2.357 2.357a.833.833 0 0 0 1.179 0l4.714-4.714a.833.833 0 1 0-1.178-1.179l-4.125 4.125-1.768-1.768a.833.833 0 0 0-1.179 0\"\n      ></path>\n    </g>\n    <defs>\n      <clipPath id=\"icon-workflow-run-success_svg__a\">\n        <path fill=\"#fff\" d=\"M0 0h20v20H0z\"></path>\n      </clipPath>\n    </defs>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-switch-line.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconSwitchLine = (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      id=\"switch-line\"\n      fill=\"currentColor\"\n      stroke=\"none\"\n      d=\"M 12.728118 10.060962 C 13.064282 8.716098 14.272528 7.772551 15.65877 7.772343 L 17.689898 7.772343 C 18.0798 7.772343 18.39588 7.456264 18.39588 7.066362 C 18.39588 6.676458 18.0798 6.36038 17.689898 6.36038 L 15.659616 6.36038 C 13.62515 6.360315 11.851767 7.745007 11.358504 9.718771 C 11.02234 11.063635 9.814095 12.007183 8.427853 12.007389 L 7.101437 12.007389 C 6.711768 12.007389 6.395878 12.323277 6.395878 12.712947 C 6.395878 13.102616 6.711768 13.418506 7.101437 13.418506 L 8.426159 13.418506 C 9.812716 13.418323 11.021417 14.361954 11.357657 15.707124 C 11.850921 17.680887 13.624304 19.065578 15.65877 19.065516 L 17.689049 19.065516 C 18.078953 19.065516 18.395033 18.749435 18.395033 18.359533 C 18.395033 17.969631 18.078953 17.653551 17.689049 17.653551 L 15.65877 17.653551 C 14.272528 17.653345 13.064282 16.709797 12.728118 15.364932 C 12.454905 14.27114 11.774856 13.322707 10.826583 12.712947 C 11.774536 12.10303 12.454268 11.154617 12.727271 10.060962 Z\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/assets/icon-warning.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface Props {\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const IconWarningFill = ({ className, style }: Props) => (\n  <svg\n    className={className}\n    style={style}\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12ZM11 8C11 7.44772 11.4477 7 12 7C12.5523 7 13 7.44772 13 8V13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13V8ZM11 16C11 15.4477 11.4477 15 12 15C12.5523 15 13 15.4477 13 16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-materials/src/components/form-header/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, useCurrentEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { getIcon } from './utils';\nimport { TitleInput } from './title-input';\nimport { Header, HeaderInner } from './styles';\n\nexport function FormHeader() {\n  const node: WorkflowNodeEntity = useCurrentEntity();\n\n  return (\n    <Header>\n      <HeaderInner>\n        {getIcon(node)}\n\n        <TitleInput />\n      </HeaderInner>\n    </Header>\n  );\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/form-header/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Header = styled.div`\n  height: 40px;\n`;\n\nexport const HeaderInner = styled.div`\n  box-sizing: border-box;\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  width: 100%;\n  column-gap: 8px;\n  border-radius: 8px 8px 0 0;\n  cursor: move;\n\n  background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);\n  overflow: hidden;\n\n  padding: 8px;\n\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n`;\n\nexport const Title = styled.div`\n  font-size: 20px;\n  flex: 1;\n  width: 0;\n`;\n\nexport const Icon = styled.img`\n  width: 24px;\n  height: 24px;\n  scale: 0.8;\n  border-radius: 4px;\n`;\n\nexport const Operators = styled.div`\n  display: flex;\n  align-items: center;\n  column-gap: 4px;\n`;\n"
  },
  {
    "path": "apps/demo-materials/src/components/form-header/title-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef, useEffect, useState } from 'react';\n\nimport { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { BlurInput } from '@flowgram.ai/form-materials';\nimport { IconButton, Tooltip, Typography } from '@douyinfe/semi-ui';\nimport { IconEdit } from '@douyinfe/semi-icons';\n\nimport { Title } from './styles';\nconst { Text } = Typography;\n\nexport function TitleInput(): JSX.Element {\n  const [titleEdit, updateTitleEdit] = useState<boolean>(false);\n\n  const ref = useRef<any>();\n  useEffect(() => {\n    if (titleEdit) {\n      ref.current?.focus();\n    }\n  }, [titleEdit]);\n\n  return (\n    <Title>\n      <Field name=\"title\">\n        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n          <div\n            style={{\n              height: 24,\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'space-between',\n              gap: 5,\n            }}\n          >\n            {titleEdit ? (\n              <BlurInput\n                ref={ref}\n                value={value}\n                onDoubleClick={() => {\n                  updateTitleEdit(true);\n                }}\n                onChange={(v) => {\n                  onChange(v);\n                }}\n                onBlur={() => {\n                  updateTitleEdit(false);\n                }}\n              />\n            ) : (\n              <>\n                <Text ellipsis={{ showTooltip: true }}>{value}</Text>\n                {!titleEdit && (\n                  <Tooltip content=\"Edit Title\">\n                    <IconButton\n                      size=\"small\"\n                      theme=\"borderless\"\n                      type=\"tertiary\"\n                      icon={<IconEdit size=\"small\" />}\n                      onClick={() => updateTitleEdit(!titleEdit)}\n                    />\n                  </Tooltip>\n                )}\n              </>\n            )}\n          </div>\n        )}\n      </Field>\n    </Title>\n  );\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/form-header/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport { type FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport iconScript from '../../assets/icon-script.png';\nimport { Icon } from './styles';\n\nexport const getIcon = (node: FlowNodeEntity) => {\n  const icon = node.getNodeRegistry().info?.icon;\n  if (!icon) return <Icon src={iconScript} />;\n  return <Icon src={icon} />;\n};\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  useNodeRender,\n  FlowNodeRegistry,\n  WorkflowJSON,\n  WorkflowAutoLayoutTool,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { createDebugPanelPlugin } from '../plugins/debug-panel-plugin';\n\ninterface EditorProps {\n  registries: FlowNodeRegistry[];\n  initialData: WorkflowJSON;\n  plugins?: FreeLayoutProps['plugins'];\n  onSave?: (data: WorkflowJSON) => void;\n  transformRegistry?: (registry: FreeLayoutProps) => FreeLayoutProps;\n}\n\nfunction passthrough<T>(a: T) {\n  return a;\n}\n\nexport const useEditorProps = ({\n  registries,\n  initialData,\n  plugins,\n  onSave,\n  transformRegistry = passthrough,\n}: EditorProps) =>\n  useMemo<FreeLayoutProps>(\n    () =>\n      transformRegistry({\n        /**\n         * Whether to enable the background\n         */\n        background: true,\n        /**\n         * Whether it is read-only or not, the node cannot be dragged in read-only mode\n         */\n        readonly: false,\n        /**\n         * Initial data\n         * 初始化数据\n         */\n        initialData,\n\n        scroll: {\n          enableScrollLimit: true,\n        },\n        /**\n         * Node registries\n         * 节点注册\n         */\n        nodeRegistries: registries,\n        variableEngine: {\n          enable: true,\n        },\n        /**\n         * Get the default node registry, which will be merged with the 'nodeRegistries'\n         * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n         */\n        getNodeDefaultRegistry(type) {\n          return {\n            type,\n            meta: {\n              defaultExpanded: true,\n            },\n          };\n        },\n        materials: {\n          /**\n           * Render Node\n           */\n          renderDefaultNode: (props: WorkflowNodeProps) => {\n            const { form } = useNodeRender();\n\n            return (\n              <WorkflowNodeRenderer\n                className={`demo-free-material-node ${\n                  ['block-start', 'block-end'].includes(props.node.flowNodeType as string)\n                    ? 'demo-free-material-shrink-node'\n                    : ''\n                }`}\n                node={props.node}\n              >\n                <div className=\"demo-free-material-node-wrapper\" style={{ padding: 12 }}>\n                  {form?.render()}\n                </div>\n              </WorkflowNodeRenderer>\n            );\n          },\n        },\n        /**\n         * Content change\n         */\n        onContentChange(ctx, event) {\n          console.log('Auto Save: ', event, ctx.document.toJSON());\n          onSave?.(ctx.document.toJSON());\n        },\n        // /**\n        //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n        //  */\n        nodeEngine: {\n          enable: true,\n        },\n        /**\n         * Redo/Undo enable\n         */\n        history: {\n          enable: true,\n          enableChangeNode: true, // Listen Node engine data change\n        },\n        /**\n         * Playground init\n         */\n        onInit: (ctx) => {},\n        /**\n         * Playground render\n         */\n        onAllLayersRendered(ctx) {\n          const autoLayoutTool = ctx.get(WorkflowAutoLayoutTool);\n          autoLayoutTool.handle();\n          //  Fitview\n          ctx.document.fitView(false);\n        },\n        /**\n         * Playground dispose\n         */\n        onDispose() {\n          console.log('---- Playground Dispose ----');\n        },\n        plugins: (ctx) => [createDebugPanelPlugin({}), ...(plugins?.(ctx) || [])],\n      }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-material-node {\n  display: flex;\n  min-width: 400px;\n  min-height: 100px;\n  flex-direction: column;\n  align-items: flex-start;\n  box-sizing: border-box;\n  border-radius: 8px;\n  border: 1px solid\n    var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n  background: #fff;\n  box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n  position: relative;\n}\n\n.demo-free-material-shrink-node {\n  min-width: 50px;\n  min-height: 50px;\n  border-radius: 25px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.demo-free-material-node-wrapper {\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  EditorRenderer,\n  FlowNodeRegistry,\n  FreeLayoutEditorProvider,\n  FreeLayoutProps,\n  WorkflowJSON,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\ninterface EditorProps {\n  registries: FlowNodeRegistry[];\n  initialData: WorkflowJSON;\n  plugins?: FreeLayoutProps['plugins'];\n  onSave?: (data: WorkflowJSON) => void;\n  transformRegistry?: (registry: FreeLayoutProps) => FreeLayoutProps;\n}\n\nexport const FreeEditor = ({\n  registries,\n  initialData,\n  plugins = () => [],\n  onSave,\n  transformRegistry,\n}: EditorProps) => {\n  const editorProps = useEditorProps({\n    registries,\n    initialData,\n    plugins,\n    onSave,\n    transformRegistry,\n  });\n\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/components/debug-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport styled from 'styled-components';\nimport { Button, SideSheet, Tabs, Tooltip } from '@douyinfe/semi-ui';\nimport { IconTerminal } from '@douyinfe/semi-icons';\n\nimport { WorkflowJsonEditor } from './workflow-json-editor';\nimport { FullVariableList } from './full-variable-list';\n\nconst PanelWrapper = styled.div`\n  position: relative;\n  box-shadow: 4px 4px 4px rgba(0, 0, 0, 0.1);\n`;\n\nconst DebugPanelButton = styled(Button)`\n  position: absolute;\n  top: 0;\n  right: 0;\n  border-radius: 20px;\n  width: 90px;\n  height: 40px;\n  z-index: 999;\n`;\n\nexport function DebugPanel() {\n  const [isOpen, setOpen] = useState<boolean>(false);\n\n  return (\n    <PanelWrapper>\n      <Tooltip content=\"Toggle Debug Panel\">\n        <DebugPanelButton theme=\"light\" onClick={() => setOpen((_open) => !_open)}>\n          <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>\n            <IconTerminal />\n            Debug\n          </div>\n        </DebugPanelButton>\n      </Tooltip>\n      <SideSheet\n        title=\"Debug Panel\"\n        visible={isOpen}\n        onCancel={() => setOpen(false)}\n        width={1000}\n        footer={null}\n      >\n        <Tabs>\n          <Tabs.TabPane itemKey=\"workflow_json\" tab=\"Workflow JSON\">\n            <WorkflowJsonEditor />\n          </Tabs.TabPane>\n          <Tabs.TabPane itemKey=\"variables\" tab=\"Variable List\">\n            <FullVariableList />\n          </Tabs.TabPane>\n        </Tabs>\n      </SideSheet>\n    </PanelWrapper>\n  );\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/components/full-variable-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useVariableTree } from '@flowgram.ai/form-materials';\nimport { Tree } from '@douyinfe/semi-ui';\n\nexport function FullVariableList() {\n  const treeData = useVariableTree({});\n\n  return <Tree treeData={treeData} defaultExpandAll />;\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/components/workflow-json-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo, useState } from 'react';\n\nimport { useClientContext } from '@flowgram.ai/free-layout-editor';\nimport { JsonCodeEditor } from '@flowgram.ai/form-materials';\nimport { Button } from '@douyinfe/semi-ui';\n\nexport function WorkflowJsonEditor() {\n  const ctx = useClientContext();\n\n  const initJson = useMemo(() => JSON.stringify(ctx.document?.toJSON() || {}, null, 2) || '', []);\n\n  const [json, setJson] = useState<string>(initJson);\n  const [isUpdated, setIsUpdated] = useState<boolean>(false);\n\n  const handleJsonChange = (newJson: string) => {\n    if (newJson === json) {\n      return;\n    }\n    setJson(newJson);\n    setIsUpdated(true);\n  };\n\n  const handleSync = () => {\n    try {\n      const newJson = JSON.parse(json);\n      ctx.document?.reload(newJson);\n    } catch (e) {\n      console.error('Invalid JSON', e);\n    }\n  };\n\n  // const handleRefresh = () => {\n  //   setJson(JSON.stringify(ctx.document?.toJSON() || {}, null, 2));\n  //   setIsUpdated(false);\n  // };\n\n  return (\n    <div>\n      <div style={{ display: 'flex', gap: 5, marginBottom: 5 }}>\n        {isUpdated && <Button onClick={handleSync}>Sync</Button>}\n      </div>\n\n      <JsonCodeEditor value={json} onChange={handleJsonChange} options={{ minHeight: 300 }} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/debug-panel-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { domUtils, injectable, Layer } from '@flowgram.ai/free-layout-editor';\n\nimport { DebugPanel } from './components/debug-panel';\n\n@injectable()\nexport class DebugPanelLayer extends Layer {\n  onReady(): void {\n    // Fix panel in the right of canvas\n    this.config.onDataChange(() => {\n      const { scrollX, scrollY } = this.config.config;\n      domUtils.setStyle(this.node, {\n        position: 'absolute',\n        right: 25 - scrollX,\n        top: scrollY + 25,\n        zIndex: 999,\n      });\n    });\n  }\n\n  render(): JSX.Element {\n    return <DebugPanel />;\n  }\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/debug-panel-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/free-layout-editor';\n\nimport { DebugPanelLayer } from './debug-panel-layer';\n\nexport const createDebugPanelPlugin = definePluginCreator<{}>({\n  onInit(ctx, opts) {\n    ctx.playground.registerLayer(DebugPanelLayer);\n  },\n});\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-editor/plugins/debug-panel-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createDebugPanelPlugin } from './debug-panel-plugin';\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-form-meta-story-builder/constants.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Field,\n  FieldRenderProps,\n  FlowNodeMeta,\n  FlowNodeRegistry,\n} from '@flowgram.ai/free-layout-editor';\nimport {\n  IJsonSchema,\n  InputsValues,\n  JsonSchemaEditor,\n  provideJsonSchemaOutputs,\n  AssignRows,\n  createInferAssignPlugin,\n  DisplayOutputs,\n} from '@flowgram.ai/form-materials';\n\nimport { Icon } from '../form-header/styles';\nimport { FormHeader } from '../form-header';\nimport iconVariable from '../../assets/icon-variable.png';\nimport iconStart from '../../assets/icon-start.jpg';\nimport iconScript from '../../assets/icon-script.png';\nimport iconEnd from '../../assets/icon-end.jpg';\n\nexport const DEFAULT_FORM_META = {\n  render: () => (\n    <div>\n      <FormHeader />\n      <div>TODO</div>\n    </div>\n  ),\n};\n\nexport const START_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'start',\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconStart,\n    description: 'You can add variables here to test variable reference',\n  },\n  canAdd() {\n    return false;\n  },\n  formMeta: {\n    render: () => (\n      <div>\n        <FormHeader />\n        <Field\n          name=\"outputs\"\n          render={({ field: { value, onChange } }: FieldRenderProps<IJsonSchema>) => (\n            <>\n              <JsonSchemaEditor\n                value={value}\n                onChange={(value) => onChange(value as IJsonSchema)}\n              />\n            </>\n          )}\n        />\n      </div>\n    ),\n    effect: {\n      outputs: provideJsonSchemaOutputs,\n    },\n  },\n};\n\nexport const END_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'end',\n  meta: {\n    isEnd: true,\n    deleteDisable: true,\n    copyDisable: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconEnd,\n    description: 'You can test variables created in the previous nodes',\n  },\n  canAdd() {\n    return false;\n  },\n  formMeta: {\n    render: () => (\n      <div>\n        <FormHeader />\n        <Field\n          name=\"inputsValues\"\n          render={({ field: { value, onChange } }: FieldRenderProps<any>) => (\n            <>\n              <InputsValues value={value} onChange={(value) => onChange(value)} />\n            </>\n          )}\n        />\n      </div>\n    ),\n  },\n};\n\nexport const BLOCK_START_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'block-start',\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 60,\n      height: 60,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      borderRadius: 8,\n      cursor: 'move',\n    },\n  },\n  canAdd: () => false,\n  formMeta: {\n    render: () => <Icon src={iconStart} />,\n  },\n};\n\nexport const BLOCK_END_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'block-end',\n  meta: {\n    isNodeEnd: true,\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 60,\n      height: 60,\n    },\n    wrapperStyle: {\n      minWidth: 'unset',\n      width: '100%',\n      borderWidth: 2,\n      borderRadius: 8,\n      cursor: 'move',\n    },\n  },\n  canAdd: () => false,\n  formMeta: {\n    render: () => <Icon src={iconEnd} />,\n  },\n};\n\nexport const VARIABLE_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'variable',\n  meta: {\n    size: {\n      width: 240,\n      height: 150,\n    },\n  },\n  info: {\n    icon: iconVariable,\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <FormHeader />\n        <div>\n          <AssignRows name=\"assign\" />\n          <DisplayOutputs style={{ paddingTop: 10 }} displayFromScope />\n        </div>\n      </>\n    ),\n    plugins: [\n      createInferAssignPlugin({\n        assignKey: 'assign',\n        outputKey: 'outputs',\n      }),\n    ],\n  },\n};\n\nexport const CUSTOM_REGISTRY: FlowNodeRegistry<FlowNodeMeta> = {\n  type: 'custom',\n  meta: {\n    deleteDisable: true,\n    copyDisable: true,\n    nodePanelVisible: false,\n    defaultPorts: [{ type: 'input' }, { type: 'output' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconScript,\n    description: 'You can add custom form meta here',\n  },\n  canAdd() {\n    return true;\n  },\n  formMeta: DEFAULT_FORM_META,\n};\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-form-meta-story-builder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { cloneDeep } from 'lodash-es';\nimport {\n  FormMeta,\n  FreeLayoutProps,\n  WorkflowJSON,\n  WorkflowNodeJSON,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FreeEditor } from '../free-editor';\nimport { insertNodesInEdges } from './utils';\nimport { INITIAL_DATA } from './initial-data';\nimport {\n  CUSTOM_REGISTRY,\n  DEFAULT_FORM_META,\n  END_REGISTRY,\n  START_REGISTRY,\n  BLOCK_START_REGISTRY,\n  BLOCK_END_REGISTRY,\n  VARIABLE_REGISTRY,\n} from './constants';\n\ntype NodeId = string;\ninterface PropsType {\n  formMeta?: FormMeta;\n  initialData?: WorkflowJSON;\n  filterEndNode?: boolean;\n  filterStartNode?: boolean;\n  addNodeBeforeCustom?: WorkflowNodeJSON[];\n  addNodeAfterCustom?: WorkflowNodeJSON[];\n  transformInitialNode?: Record<NodeId, (node: WorkflowNodeJSON) => WorkflowNodeJSON>;\n  plugins?: FreeLayoutProps['plugins'];\n  transformRegistry?: (registry: FreeLayoutProps) => FreeLayoutProps;\n}\n\nexport function FreeFormMetaStoryBuilder(props: PropsType) {\n  const {\n    formMeta = DEFAULT_FORM_META,\n    initialData,\n    filterEndNode = false,\n    filterStartNode = false,\n    addNodeBeforeCustom,\n    addNodeAfterCustom,\n    transformInitialNode,\n    transformRegistry,\n    plugins,\n  } = props;\n\n  const registries = useMemo(\n    () => [\n      START_REGISTRY,\n      END_REGISTRY,\n      BLOCK_START_REGISTRY,\n      BLOCK_END_REGISTRY,\n      VARIABLE_REGISTRY,\n      {\n        ...CUSTOM_REGISTRY,\n        formMeta: {\n          ...formMeta,\n        },\n      },\n    ],\n    [formMeta]\n  );\n\n  const initialDataWithDefault = useMemo(() => {\n    const nodes = [\n      ...(initialData?.nodes || []),\n      ...(addNodeBeforeCustom || []),\n      ...(addNodeAfterCustom || []),\n      ...INITIAL_DATA.nodes\n        .filter((node) => !initialData?.nodes?.find((item) => item.id === node.id))\n        .filter((node) => {\n          if (node.type === 'start') {\n            return !filterStartNode;\n          }\n          if (node.type === 'end') {\n            return !filterEndNode;\n          }\n          return true;\n        })\n        .map((node) => {\n          if (transformInitialNode?.[node.id]) {\n            return transformInitialNode[node.id](cloneDeep(node));\n          }\n          return node;\n        }),\n    ];\n\n    let edges = [...(initialData?.edges || []), ...INITIAL_DATA.edges];\n\n    if (addNodeBeforeCustom?.length) {\n      edges = [\n        ...insertNodesInEdges(\n          addNodeBeforeCustom,\n          edges.filter((edge) => edge.targetNodeID === 'custom_0')\n        ).newEdges,\n        ...edges.filter((edge) => edge.targetNodeID !== 'custom_0'),\n      ];\n    }\n\n    if (addNodeAfterCustom?.length) {\n      edges = [\n        ...insertNodesInEdges(\n          addNodeAfterCustom,\n          edges.filter((edge) => edge.sourceNodeID === 'custom_0')\n        ).newEdges,\n        ...edges.filter((edge) => edge.sourceNodeID !== 'custom_0'),\n      ];\n    }\n\n    // remove edges that connected to non-existent nodes\n    edges = [...(initialData?.edges || []), ...INITIAL_DATA.edges].filter(\n      (edge) =>\n        nodes.find((_node) => _node.id === edge.sourceNodeID) ||\n        nodes.find((_node) => _node.id === edge.targetNodeID)\n    );\n\n    return {\n      nodes,\n      edges,\n    };\n  }, [initialData, filterEndNode, filterStartNode]);\n\n  return (\n    <div>\n      <FreeEditor\n        registries={registries}\n        initialData={initialDataWithDefault}\n        plugins={plugins}\n        transformRegistry={transformRegistry}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-form-meta-story-builder/initial-data.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const INITIAL_DATA: WorkflowJSON = {\n  nodes: [\n    {\n      type: 'custom',\n      id: 'custom_0',\n      data: {\n        title: 'Custom',\n      },\n    },\n    {\n      type: 'start',\n      id: 'start_0',\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            str: { type: 'string' },\n            obj: {\n              type: 'object',\n              properties: {\n                obj2: {\n                  type: 'object',\n                  properties: {\n                    num: { type: 'number' },\n                  },\n                },\n              },\n            },\n            arr: {\n              type: 'object',\n              properties: {\n                arr_str: { type: 'array', items: { type: 'string' } },\n                arr_num: { type: 'array', items: { type: 'number' } },\n                arr_bool: { type: 'array', items: { type: 'boolean' } },\n                arr_int: { type: 'array', items: { type: 'integer' } },\n                arr_obj: {\n                  type: 'array',\n                  items: {\n                    type: 'object',\n                    properties: {\n                      str: { type: 'string' },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n    {\n      type: 'end',\n      id: 'end_0',\n      data: {\n        title: 'End',\n        inputsValues: {\n          success: { type: 'constant', content: true, schema: { type: 'boolean' } },\n          message: { type: 'ref', content: ['start_0', 'str'] },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'custom_0',\n    },\n    {\n      sourceNodeID: 'custom_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-materials/src/components/free-form-meta-story-builder/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowEdgeJSON, WorkflowNodeJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const insertNodesInEdges = (\n  insertNodes: WorkflowNodeJSON[],\n  insertInEdges: WorkflowEdgeJSON[]\n) => {\n  const newEdges = insertNodes.flatMap((_node, idx) => {\n    const before = insertNodes[idx - 1];\n    const next = insertNodes[idx + 1];\n\n    if (!before) {\n      return [...insertInEdges.map((edge) => ({ ...edge, targetNodeID: _node.id }))];\n    }\n    const beforeEdge = { sourceNodeID: before.id, targetNodeID: _node.id };\n\n    return [\n      beforeEdge,\n      ...(next ? [] : [...insertInEdges.map((edge) => ({ ...edge, sourceNodeID: _node.id }))]),\n    ];\n  });\n\n  return { newEdges };\n};\n"
  },
  {
    "path": "apps/demo-materials/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FreeFormMetaStoryBuilder } from './components/free-form-meta-story-builder';\nexport { FormHeader } from './components/form-header';\nexport {\n  BLOCK_START_REGISTRY,\n  BLOCK_END_REGISTRY,\n  VARIABLE_REGISTRY,\n} from './components/free-form-meta-story-builder/constants';\n"
  },
  {
    "path": "apps/demo-materials/src/stories/components/blur-input.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { BlurInput } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder } from '../../components/free-form-meta-story-builder';\nimport { FormHeader } from '../../components/form-header';\n\nconst Story = (args: { formMeta: FormMeta }) => (\n  <FreeFormMetaStoryBuilder formMeta={args.formMeta} />\n);\n\nconst meta: Meta<typeof Story> = {\n  title: 'Form Components/BlurInput',\n  component: Story,\n  tags: ['autodocs'],\n};\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"input\" defaultValue=\"Initial text\">\n            {({ field }) => (\n              <BlurInput\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                placeholder=\"Please enter text\"\n              />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport const WithPlaceholder: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"inputWithPlaceholder\" defaultValue=\"\">\n            {({ field }) => (\n              <BlurInput\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                placeholder=\"This is an input field with placeholder\"\n              />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport const Disabled: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"disabledInput\" defaultValue=\"Disabled state text\">\n            {({ field }) => (\n              <BlurInput value={field.value} onChange={(value) => field.onChange(value)} disabled />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport default meta;\n"
  },
  {
    "path": "apps/demo-materials/src/stories/components/inputs-values-tree.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { IInputsValues, InputsValuesTree } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder } from '../../components/free-form-meta-story-builder';\nimport { FormHeader } from '../../components/form-header';\n\nconst Story = (args: { formMeta: FormMeta }) => (\n  <FreeFormMetaStoryBuilder formMeta={args.formMeta} />\n);\n\nconst meta: Meta<typeof Story> = {\n  title: 'Form Components/InputsValuesTree',\n  component: Story,\n  tags: ['autodocs'],\n};\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IInputsValues | undefined> name=\"inputsValues\">\n            {({ field }) => (\n              <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport const WithDefaultValue: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IInputsValues | undefined>\n            name=\"inputsValues\"\n            defaultValue={{\n              a: {\n                b: { type: 'constant', content: '123', schema: { type: 'string' } },\n                c: { type: 'constant', content: 456, schema: { type: 'number' } },\n              },\n              d: {\n                type: 'constant',\n                content: `{\"hello\": \"world\"}`,\n                schema: { type: 'object' },\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport default meta;\n"
  },
  {
    "path": "apps/demo-materials/src/stories/components/json-schema-creator.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport type { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { JsonSchemaCreator, IJsonSchema } from '@flowgram.ai/form-materials';\n\nconst JsonSchemaCreatorDemo: React.FC = () => {\n  const [schema, setSchema] = useState<IJsonSchema | null>(null);\n\n  return (\n    <div style={{ padding: '20px' }}>\n      <h2>JSON Schema Creator</h2>\n      <JsonSchemaCreator\n        onSchemaCreate={(generatedSchema) => {\n          console.log('生成的 schema:', generatedSchema);\n          setSchema(generatedSchema);\n        }}\n      />\n\n      {schema && (\n        <div style={{ marginTop: '20px' }}>\n          <h3>生成的 Schema:</h3>\n          <pre\n            style={{\n              background: '#f5f5f5',\n              padding: '16px',\n              borderRadius: '4px',\n              overflow: 'auto',\n            }}\n          >\n            {JSON.stringify(schema, null, 2)}\n          </pre>\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst meta: Meta<typeof JsonSchemaCreatorDemo> = {\n  title: 'Form Components/JsonSchemaCreator',\n  component: JsonSchemaCreatorDemo,\n  parameters: {\n    layout: 'centered',\n    docs: {\n      description: {\n        component: '从 JSON 字符串自动生成 JSONSchema 的组件',\n      },\n    },\n  },\n  tags: ['autodocs'],\n};\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n"
  },
  {
    "path": "apps/demo-materials/src/stories/components/sql-editor-with-variables.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { SQLEditorWithVariables } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder } from '../../components/free-form-meta-story-builder';\nimport { FormHeader } from '../../components/form-header';\n\nconst Story = (args: { formMeta: FormMeta }) => (\n  <FreeFormMetaStoryBuilder formMeta={args.formMeta} />\n);\n\nconst meta: Meta<typeof Story> = {\n  title: 'Form Components/SqlEditorWithVariables',\n  component: Story,\n  tags: ['autodocs'],\n};\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string | undefined>\n            name=\"editor\"\n            defaultValue=\"SELECT * FROM users WHERE user_id = {{start_0.str}}\"\n          >\n            {({ field }) => (\n              <SQLEditorWithVariables\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport default meta;\n"
  },
  {
    "path": "apps/demo-materials/src/stories/components/variable-selector.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Meta, StoryObj } from 'storybook-react-rsbuild';\nimport { Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder } from '../../components/free-form-meta-story-builder';\nimport { FormHeader } from '../../components/form-header';\n\nconst Story = (args: { formMeta: FormMeta }) => (\n  <FreeFormMetaStoryBuilder formMeta={args.formMeta} />\n);\n\nconst meta: Meta<typeof Story> = {\n  title: 'Form Components/VariableSelector',\n  component: Story,\n  tags: ['autodocs'],\n};\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {\n  args: {\n    formMeta: {\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    },\n  },\n};\n\nexport default meta;\n"
  },
  {
    "path": "apps/demo-materials/src/stories/hello.stories.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { Meta, StoryObj } from 'storybook-react-rsbuild';\n\ninterface HelloWorldProps {\n  name?: string;\n  message?: string;\n}\n\nconst HelloWorld: React.FC<HelloWorldProps> = ({\n  name = 'World',\n  message = 'Welcome to Flowgram Materials!',\n}) => (\n  <div\n    style={{\n      display: 'flex',\n      flexDirection: 'column',\n      alignItems: 'center',\n      justifyContent: 'center',\n      minHeight: '200px',\n      padding: '40px',\n      background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n      borderRadius: '12px',\n      color: 'white',\n      fontFamily: 'Arial, sans-serif',\n      boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',\n    }}\n  >\n    <h1\n      style={{\n        margin: 0,\n        fontSize: '2.5rem',\n        fontWeight: 'bold',\n        marginBottom: '10px',\n      }}\n    >\n      Hello, {name}! 👋\n    </h1>\n    <p\n      style={{\n        margin: 0,\n        fontSize: '1.2rem',\n        opacity: 0.9,\n      }}\n    >\n      {message}\n    </p>\n    <div\n      style={{\n        marginTop: '20px',\n        padding: '10px 20px',\n        backgroundColor: 'rgba(255, 255, 255, 0.2)',\n        borderRadius: '20px',\n        fontSize: '0.9rem',\n      }}\n    >\n      ✨ Powered by Flowgram\n    </div>\n  </div>\n);\n\nconst meta: Meta<typeof HelloWorld> = {\n  title: 'Welcome/HelloWorld',\n  component: HelloWorld,\n  parameters: {\n    layout: 'centered',\n    docs: {\n      description: {\n        component: 'A beautiful hello world component to welcome users to Flowgram materials.',\n      },\n    },\n  },\n  tags: ['autodocs'],\n  argTypes: {\n    name: {\n      control: 'text',\n      description: 'Name to greet',\n      defaultValue: 'World',\n    },\n    message: {\n      control: 'text',\n      description: 'Welcome message',\n      defaultValue: 'Welcome to Flowgram Materials!',\n    },\n  },\n};\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {\n  args: {},\n};\n\nexport const CustomGreeting: Story = {\n  args: {\n    name: 'Flowgram Developer',\n    message: 'Ready to build amazing workflows! 🚀',\n  },\n};\n\nexport const Simple: Story = {\n  args: {\n    name: 'Friend',\n    message: \"Let's get started!\",\n  },\n};\n"
  },
  {
    "path": "apps/demo-materials/src/type.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ndeclare module '*.svg'\ndeclare module '*.png'\ndeclare module '*.jpg'\ndeclare module '*.module.less'\n"
  },
  {
    "path": "apps/demo-materials/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    },\n  },\n  \"include\": [\n    \"./src\",\n    \"src/stories\"\n  ],\n}\n"
  },
  {
    "path": "apps/demo-nextjs/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "apps/demo-nextjs/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "apps/demo-nextjs/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  ignore: ['eslint.config.js'],\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  plugins: [],\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-nextjs/next.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport type { NextConfig } from 'next';\n\nconst __dirname = new URL('.', import.meta.url).pathname;\n\nconst nextConfig: NextConfig = {\n  reactStrictMode: false,\n  webpack: (config) => {\n    config.resolve.alias = {\n      ...config.resolve.alias,\n      '@app': path.resolve(__dirname, 'src/app'),\n      '@editor': path.resolve(__dirname, 'src/editor'),\n      '@runtime': path.resolve(__dirname, 'src/runtime'),\n    };\n    return config;\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/demo-nextjs/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-nextjs\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"files\": [\n    \"public/\",\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"next.config.ts\",\n    \"pnpm-lock.yaml\",\n    \"postcss.config.mjs\",\n    \"package.json\",\n    \"tsconfig.json\",\n    \"README.md\"\n  ],\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"exit 0\",\n    \"build:prod\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"next\": \"^15.5.7\",\n    \"lodash-es\": \"^4.17.21\",\n    \"classnames\": \"^2.5.1\",\n    \"server-only\": \"^0.0.1\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-lines-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-node-panel-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-container-plugin\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"typescript\": \"^5.8.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/next\": \"^9.0.0\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"tailwindcss\": \"^4\",\n    \"eslint\": \"^9.0.0\",\n    \"@babel/eslint-parser\": \"~7.19.1\",\n    \"eslint-plugin-json\": \"^4.0.1\",\n    \"eslint-plugin-next\": \"0.0.0\",\n    \"eslint-config-next\": \"^15.3.1\",\n    \"@eslint/eslintrc\": \"3.3.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs/postcss.config.mjs",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst config = {\n  plugins: ['@tailwindcss/postcss'],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/demo-nextjs/src/app/api/runtime/route.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NextResponse } from 'next/server';\n\nimport { main } from '../../../runtime';\n\nexport async function POST(request: Request) {\n  try {\n    const body = await request.json();\n    const result = await main(body.json);\n    return NextResponse.json({ success: true, data: result });\n  } catch (error) {\n    return NextResponse.json(\n      {\n        success: false,\n        error: error instanceof Error ? error.message : 'Unknown error',\n      },\n      { status: 500 }\n    );\n  }\n}\n\nexport async function GET() {\n  return NextResponse.json({ message: 'Please use POST method' }, { status: 405 });\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/app/globals.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n@import \"tailwindcss\";\n\n:root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n  }\n}\n\nbody {\n  background: var(--background);\n  color: var(--foreground);\n  font-family: Arial, Helvetica, sans-serif;\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/app/layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Geist, Geist_Mono } from 'next/font/google';\nimport type { Metadata } from 'next';\nimport './globals.css';\n\nconst geistSans = Geist({\n  variable: '--font-geist-sans',\n  subsets: ['latin'],\n});\n\nconst geistMono = Geist_Mono({\n  variable: '--font-geist-mono',\n  subsets: ['latin'],\n});\n\nexport const metadata: Metadata = {\n  title: 'Workflow Demo',\n  description: 'Workflow Demo Next.js',\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/app/page.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\nimport { EditorClient } from '@editor/index';\n\nexport default function Home() {\n  return <EditorClient />;\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/components/editor-client.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\nimport { useEffect, useState } from 'react';\n\nimport { Editor } from './editor';\n\nexport const EditorClient = () => {\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  if (!isMounted) {\n    // only render <Editor /> in browser client\n    return null;\n  }\n\n  return <Editor />;\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/components/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport { useEditorProps } from '../hooks/use-editor-props';\nimport { Tools } from './tools';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <Tools />\n      <EditorRenderer className=\"mastra-workflow-editor\" />\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/components/form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nexport const FormRender = () => (\n  <>\n    <div className=\"w-full cursor-move\">\n      <Field<string> name=\"title\">\n        {({ field }) => <h1 className=\"text-xl font-bold\">{field.value}</h1>}\n      </Field>\n    </div>\n    <div className=\"content flex flex-col gap-3\">\n      <Field<string> name=\"input\">\n        {({ field }) => (\n          <div className=\"inline-flex justify-between w-full\">\n            <h2 className=\"text-lg\">Input</h2>\n            <input\n              className=\"border border-gray-400 rounded\"\n              value={field.value}\n              onChange={field.onChange}\n            />\n          </div>\n        )}\n      </Field>\n      <Field<string> name=\"output\">\n        {({ field }) => (\n          <div className=\"inline-flex justify-between w-full\">\n            <h2 className=\"text-lg\">Output</h2>\n            <input\n              className=\"border border-gray-400 rounded\"\n              value={field.value}\n              onChange={field.onChange}\n            />\n          </div>\n        )}\n      </Field>\n    </div>\n  </>\n);\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/components/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport classnames from 'classnames';\nimport {\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      className={classnames(\n        'workflow-node-render min-w-xs p-4 bg-node-bg rounded-node-radius shadow-[var(--node-shadow)] border border-solid border-node-border',\n        {\n          'border-node-selected ': selected,\n        }\n      )}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { useService, WorkflowDocument } from '@flowgram.ai/free-layout-editor';\n\nexport const Tools = () => {\n  const [isLoading, setIsLoading] = useState(false);\n  const document = useService(WorkflowDocument);\n\n  const handleRun = async () => {\n    try {\n      setIsLoading(true);\n      const response = await fetch('/api/runtime', {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          json: document.toJSON(),\n        }),\n      });\n      const data = await response.json();\n\n      if (!data.success) {\n        throw new Error(data.error || 'process failed');\n      }\n\n      console.log('run success', data.data);\n    } catch (error) {\n      console.error(error instanceof Error ? error.message : 'run failed');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"mastra-workflow-tools absolute z-999 bottom-4 left-1/2\">\n      <button\n        className=\"bg-blue-400 cursor-pointer active:bg-blue-500 p-2 rounded\"\n        onClick={handleRun}\n        disabled={isLoading}\n      >\n        <p className=\"text-white\">TEST RUN</p>\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/data/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content',\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom',\n        content: 'Custom node content',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End',\n        content: 'End content',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/data/node-registries.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useEditorProps } from './use-editor-props';\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';\n\nimport { FormRender } from '@editor/components/form-render';\nimport { nodeRegistries } from '../data/node-registries';\nimport { initialData } from '../data/initial-data';\nimport { NodeRender } from '../components/node-render';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n            size: {\n              width: 360,\n              height: 70,\n            },\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: FormRender,\n          },\n        };\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: NodeRender,\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        // console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './style/index.css';\n\nexport { EditorClient } from './components/editor-client';\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/style/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n@import 'tailwindcss';\n@import './theme.css';\n@import './var.css';\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/style/theme.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n@import 'tailwindcss';\n\n@theme {\n    /* color */\n    --color-node-bg: rgba(252, 252, 255, 1);\n    --color-node-border: rgba(68, 83, 130, 0.25);\n    --color-node-selected: rgba(81, 71, 255, 1);\n\n    /* others */\n    --radius-node-radius: 8px;\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/editor/style/var.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n    --node-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04),\n        0 4px 12px 0 rgba(0, 0, 0, 0.02);\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './models';\n\nexport { main } from './main';\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/main.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'server-only';\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowRuntimeModel } from '@runtime/models';\n\nexport const main = async (json: WorkflowJSON) => {\n  WorkflowRuntimeModel.instance.run();\n  return {\n    timestamp: new Date().toISOString(),\n    message: 'Server processing completed',\n    input: json,\n  };\n};\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/models/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './runtime';\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/models/runtime/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeModel } from './model';\nexport type { IWorkflowRuntimeModel } from './type';\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/models/runtime/model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IWorkflowRuntimeModel } from './type';\n\nexport class WorkflowRuntimeModel implements IWorkflowRuntimeModel {\n  private static _instance?: WorkflowRuntimeModel;\n\n  public static get instance(): WorkflowRuntimeModel {\n    if (!this._instance) {\n      this._instance = new WorkflowRuntimeModel();\n    }\n    return this._instance;\n  }\n\n  public async run(): Promise<void> {}\n}\n"
  },
  {
    "path": "apps/demo-nextjs/src/runtime/models/runtime/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface IWorkflowRuntimeModel {\n  run: () => Promise<void>;\n}\n"
  },
  {
    "path": "apps/demo-nextjs/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n          \"name\": \"next\"\n      }\n    ],\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@app/*\": [\"app/*\"],\n      \"@editor/*\": [\"editor/*\"],\n      \"@runtime/*\": [\"runtime/*\"]\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n"
  },
  {
    "path": "apps/demo-nextjs-antd/README.md",
    "content": "This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.\n"
  },
  {
    "path": "apps/demo-nextjs-antd/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  ignore: ['eslint.config.js', 'src/editor/plugins/context-menu-plugin/context-menu-layer.tsx'],\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n    'react-hooks/exhaustive-deps': 'off',\n    '@next/next/no-img-element': 'off',\n    'jsx-a11y/alt-text': 'off',\n  },\n  plugins: [],\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-nextjs-antd/next-env.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n/// <reference path=\"./.next/types/routes.d.ts\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "apps/demo-nextjs-antd/next.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport type { NextConfig } from 'next';\n\nconst __dirname = new URL('.', import.meta.url).pathname;\n\nconst nextConfig: NextConfig = {\n  reactStrictMode: false,\n  webpack: (config) => {\n    config.resolve.alias = {\n      ...config.resolve.alias,\n      '@app': path.resolve(__dirname, 'src/app'),\n      '@editor': path.resolve(__dirname, 'src/editor'),\n    };\n    return config;\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-nextjs-antd\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"files\": [\n    \"public/\",\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"next.config.ts\",\n    \"pnpm-lock.yaml\",\n    \"postcss.config.mjs\",\n    \"package.json\",\n    \"tsconfig.json\",\n    \"README.md\"\n  ],\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"exit 0\",\n    \"build:prod\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"antd\": \"^5.25.4\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"next\": \"^15.5.7\",\n    \"lodash-es\": \"^4.17.21\",\n    \"classnames\": \"^2.5.1\",\n    \"server-only\": \"^0.0.1\",\n    \"styled-components\": \"^5\",\n    \"nanoid\": \"^5.0.9\",\n    \"@ant-design/icons\": \"5.x\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-lines-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-node-panel-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-container-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-group-plugin\": \"workspace:*\",\n    \"@flowgram.ai/form-antd-materials\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/styled-components\": \"^5\",\n    \"typescript\": \"^5.8.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/next\": \"^9.0.0\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@tailwindcss/postcss\": \"^4\",\n    \"tailwindcss\": \"^4\",\n    \"eslint\": \"^9.0.0\",\n    \"@babel/eslint-parser\": \"~7.19.1\",\n    \"eslint-plugin-json\": \"^4.0.1\",\n    \"eslint-plugin-next\": \"0.0.0\",\n    \"eslint-config-next\": \"^15.3.1\",\n    \"@eslint/eslintrc\": \"3.3.3\",\n    \"sass\": \"^1.89.1\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/postcss.config.mjs",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst config = {\n  plugins: ['@tailwindcss/postcss'],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/app/globals.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n@import \"tailwindcss\";\n\n:root {\n  --background: #ffffff;\n  --foreground: #171717;\n}\n\n@theme inline {\n  --color-background: var(--background);\n  --color-foreground: var(--foreground);\n  --font-sans: var(--font-geist-sans);\n  --font-mono: var(--font-geist-mono);\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #0a0a0a;\n    --foreground: #ededed;\n  }\n}\n\nbody {\n  background: var(--background);\n  color: var(--foreground);\n  font-family: Arial, Helvetica, sans-serif;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/app/layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Geist, Geist_Mono } from 'next/font/google';\nimport type { Metadata } from 'next';\nimport './globals.css';\n\nconst geistSans = Geist({\n  variable: '--font-geist-sans',\n  subsets: ['latin'],\n});\n\nconst geistMono = Geist_Mono({\n  variable: '--font-geist-mono',\n  subsets: ['latin'],\n});\n\nexport const metadata: Metadata = {\n  title: 'Workflow Demo',\n  description: 'Workflow Demo Next.js',\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  return (\n    <html lang=\"en\">\n      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/app/page.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\nimport { EditorClient } from '@editor/index';\n\nexport default function Home() {\n  return <EditorClient />;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-auto-layout.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconAutoLayout = (\n  <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      fill=\"currentColor\"\n      d=\"M3 2C2.44772 2 2 2.44771 2 3V12C2 12.5523 2.44772 13 3 13H10C10.5523 13 11 12.5523 11 12V3C11 2.44772 10.5523 2 10 2H3zM4 11V4H9V11H4zM21 22C21.5523 22 22 21.5523 22 21V12C22 11.4477 21.5523 11 21 11H14C13.4477 11 13 11.4477 13 12V21C13 21.5523 13.4477 22 14 22H21zM20 13V20H15V13H20zM2 16C2 15.4477 2.44772 15 3 15H10C10.5523 15 11 15.4477 11 16V21C11 21.5523 10.5523 22 10 22H3C2.44772 22 2 21.5523 2 21V16zM4 20V17H9V20H4zM21 9C21.5523 9 22 8.55228 22 8V3C22 2.44772 21.5523 2 21 2H14C13.4477 2 13 2.44772 13 3V8C13 8.55228 13.4477 9 14 9H21zM20 4V7H15V4H20z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-comment.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC } from 'react';\n\ninterface IconCommentProps {\n  style?: CSSProperties;\n}\n\nexport const IconComment: FC<IconCommentProps> = ({ style }) => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={style}\n  >\n    <path d=\"M6.5 9C5.94772 9 5.5 9.44772 5.5 10V11C5.5 11.5523 5.94772 12 6.5 12H7.5C8.05228 12 8.5 11.5523 8.5 11V10C8.5 9.44772 8.05228 9 7.5 9H6.5zM11.5 9C10.9477 9 10.5 9.44772 10.5 10V11C10.5 11.5523 10.9477 12 11.5 12H12.5C13.0523 12 13.5 11.5523 13.5 11V10C13.5 9.44772 13.0523 9 12.5 9H11.5zM15.5 10C15.5 9.44772 15.9477 9 16.5 9H17.5C18.0523 9 18.5 9.44772 18.5 10V11C18.5 11.5523 18.0523 12 17.5 12H16.5C15.9477 12 15.5 11.5523 15.5 11V10z\"></path>\n    <path d=\"M23 4C23 2.9 22.1 2 21 2H3C1.9 2 1 2.9 1 4V17.0111C1 18.0211 1.9 19.0111 3 19.0111H7.7586L10.4774 22C10.9822 22.5017 11.3166 22.6311 12 22.7009C12.414 22.707 13.0502 22.5093 13.5 22L16.2414 19.0111H21C22.1 19.0111 23 18.1111 23 17.0111V4ZM3 4H21V17.0111H15.5L12 20.6714L8.5 17.0111H3V4Z\"></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconMinimap = () => (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"g1\">\n      <path\n        id=\"path1\"\n        fill=\"#000000\"\n        stroke=\"none\"\n        d=\"M 18.09091 6.883101 L 5.409091 6.883101 L 5.409091 16.746737 L 10.664648 16.746737 C 10.927091 17.116341 11.30353 17.422749 11.792977 17.611004 L 12.664289 17.946156 L 12.744959 18.155828 L 5.409091 18.155828 C 4.630871 18.155828 4 17.524979 4 16.746737 L 4 6.883101 C 4 6.104881 4.630871 5.47401 5.409091 5.47401 L 18.09091 5.47401 C 18.86915 5.47401 19.5 6.104881 19.5 6.883101 L 19.5 12.52348 C 19.247208 11.883823 18.730145 11.365912 18.09091 11.111994 L 18.09091 6.883101 Z M 18.09091 18.155828 L 17.881165 18.155828 L 19.469212 14.368896 C 19.479921 14.343321 19.490206 14.317817 19.5 14.292241 L 19.5 16.746737 C 19.5 17.524979 18.86915 18.155828 18.09091 18.155828 Z\"\n      />\n      <path\n        id=\"path2\"\n        fill=\"#000000\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 18.494614 13.960189 C 18.982441 12.796985 17.813459 11.628003 16.650255 12.11576 L 12.133272 14.01 C 10.962248 14.501069 10.987188 16.168798 12.172375 16.62464 L 13.482055 17.128389 L 13.985805 18.438068 C 14.441646 19.623184 16.109375 19.648125 16.600443 18.477171 L 18.494614 13.960189 Z M 17.19515 13.415224 L 15.30098 17.932205 L 14.79723 16.622526 C 14.654066 16.250385 14.359989 15.956307 13.987918 15.813213 L 12.678168 15.309464 L 17.19515 13.415224 Z\"\n      />\n    </g>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-mouse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconMouse(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 34}\n      height={height || 52}\n      viewBox=\"0 0 34 52\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.8\"\n      />\n    </svg>\n  );\n}\n\nexport const IconMouseTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-pad.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function IconPad(props: { width?: number; height?: number }) {\n  const { width, height } = props;\n  return (\n    <svg\n      width={width || 48}\n      height={height || 38}\n      viewBox=\"0 0 48 38\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <rect\n        x=\"1.83317\"\n        y=\"1.49998\"\n        width=\"44.3333\"\n        height=\"35\"\n        rx=\"3.5\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n      />\n      <path\n        d=\"M14.6665 30.6667H33.3332\"\n        stroke=\"currentColor\"\n        strokeOpacity=\"0.8\"\n        strokeWidth=\"2.33333\"\n        strokeLinecap=\"round\"\n      />\n    </svg>\n  );\n}\n\nexport const IconPadTool = () => (\n  <svg\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z\"\n    ></path>\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/assets/icon-switch-line.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconSwitchLine = (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <path\n      id=\"switch-line\"\n      fill=\"currentColor\"\n      stroke=\"none\"\n      d=\"M 12.728118 10.060962 C 13.064282 8.716098 14.272528 7.772551 15.65877 7.772343 L 17.689898 7.772343 C 18.0798 7.772343 18.39588 7.456264 18.39588 7.066362 C 18.39588 6.676458 18.0798 6.36038 17.689898 6.36038 L 15.659616 6.36038 C 13.62515 6.360315 11.851767 7.745007 11.358504 9.718771 C 11.02234 11.063635 9.814095 12.007183 8.427853 12.007389 L 7.101437 12.007389 C 6.711768 12.007389 6.395878 12.323277 6.395878 12.712947 C 6.395878 13.102616 6.711768 13.418506 7.101437 13.418506 L 8.426159 13.418506 C 9.812716 13.418323 11.021417 14.361954 11.357657 15.707124 C 11.850921 17.680887 13.624304 19.065578 15.65877 19.065516 L 17.689049 19.065516 C 18.078953 19.065516 18.395033 18.749435 18.395033 18.359533 C 18.395033 17.969631 18.078953 17.653551 17.689049 17.653551 L 15.65877 17.653551 C 14.272528 17.653345 13.064282 16.709797 12.728118 15.364932 C 12.454905 14.27114 11.774856 13.322707 10.826583 12.712947 C 11.774536 12.10303 12.454268 11.154617 12.727271 10.060962 Z\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/base-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, useNodeRender } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRenderContext } from '@editor/context';\nimport { ErrorIcon } from './styles';\nimport { NodeWrapper } from './node-wrapper';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  /**\n   * Provides methods related to node rendering\n   * 提供节点渲染相关的方法\n   */\n  const nodeRender = useNodeRender();\n  /**\n   * It can only be used when nodeEngine is enabled\n   * 只有在节点引擎开启时候才能使用表单\n   */\n  const form = nodeRender.form;\n\n  return (\n    <NodeRenderContext.Provider value={nodeRender}>\n      <NodeWrapper>\n        {form?.state.invalid && <ErrorIcon />}\n        {form?.render()}\n      </NodeWrapper>\n    </NodeRenderContext.Provider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/base-node/node-wrapper.scss",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-wrapper {\n  align-items: flex-start;\n  background-color: #fff;\n  border: 1px solid rgba(6, 7, 9, 0.15);\n  border-radius: 8px;\n  box-shadow:\n    0 2px 6px 0 rgba(0, 0, 0, 0.04),\n    0 4px 12px 0 rgba(0, 0, 0, 0.02);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  min-width: 360px;\n  width: 100%;\n  height: auto;\n\n  &.selected {\n    border: 1px solid #4e40e5;\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/base-node/node-wrapper.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useContext, useState } from 'react';\n\nimport { useClientContext, WorkflowPortRender } from '@flowgram.ai/free-layout-editor';\n\nimport { SidebarContext } from '@editor/context';\nimport { useNodeRenderContext } from '../../hooks';\nimport { scrollToView } from './utils';\nimport './node-wrapper.scss';\n\n// import { NodeWrapperStyle } from \"./styles\";\n\nexport interface NodeWrapperProps {\n  isScrollToView?: boolean;\n  children: React.ReactNode;\n}\n\n/**\n * Used for drag-and-drop/click events and ports rendering of nodes\n * 用于节点的拖拽/点击事件和点位渲染\n */\nexport const NodeWrapper: React.FC<NodeWrapperProps> = (props) => {\n  // IMPORTANT 这里写了如何处理node的数据\n  const { children, isScrollToView = false } = props;\n  const nodeRender = useNodeRenderContext();\n  const { selected, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } = nodeRender;\n  const [isDragging, setIsDragging] = useState(false);\n  const sidebar = useContext(SidebarContext);\n  const form = nodeRender.form;\n  const ctx = useClientContext();\n\n  const portsRender = ports.map((p) => <WorkflowPortRender key={p.id} entity={p} />);\n\n  return (\n    <>\n      <div\n        className={`node-wrapper ${selected ? 'selected' : ''}`}\n        ref={nodeRef}\n        draggable\n        onDragStart={(e) => {\n          startDrag(e);\n          setIsDragging(true);\n        }}\n        onClick={(e) => {\n          selectNode(e);\n          if (!isDragging) {\n            sidebar.setNodeId(nodeRender.node.id);\n            // 可选：将 isScrollToView 设为 true，可以让节点选中后滚动到画布中间\n            // Optional: Set isScrollToView to true to scroll the node to the center of the canvas after it is selected.\n            if (isScrollToView) {\n              scrollToView(ctx, nodeRender.node);\n            }\n          }\n        }}\n        onMouseUp={() => setIsDragging(false)}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        data-node-selected={String(selected)}\n        style={{\n          outline: form?.state.invalid ? '1px solid red' : 'none',\n        }}\n      >\n        {children}\n      </div>\n      {portsRender}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/base-node/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ExclamationCircleOutlined } from '@ant-design/icons';\n\nexport const ErrorIcon = () => (\n  <ExclamationCircleOutlined\n    style={{\n      color: 'red',\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/base-node/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor';\n\nexport function scrollToView(\n  ctx: FreeLayoutPluginContext,\n  node: FlowNodeEntity,\n  sidebarWidth = 448\n) {\n  const bounds = node.transform.bounds;\n  ctx.playground.scrollToView({\n    bounds,\n    scrollDelta: {\n      x: sidebarWidth / 2,\n      y: 0,\n    },\n    zoom: 1,\n    scrollToCenter: true,\n  });\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/editor-client.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport dynamic from 'next/dynamic';\n\nconst Editor = dynamic(() => import('./editor').then((module) => module.Editor), { ssr: false });\n\nexport const EditorClient = () => {\n  const [isMounted, setIsMounted] = useState(false);\n\n  useEffect(() => {\n    setIsMounted(true);\n  }, []);\n\n  if (!isMounted) {\n    // only render <Editor /> in browser client\n    return null;\n  }\n\n  return <Editor />;\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { SidebarProvider, SidebarRenderer } from '@editor/components/sidebar';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport { useEditorProps } from '../hooks/use-editor-props';\nimport { nodeRegistries } from '../data/node-registries';\nimport { initialData } from '../data/initial-data';\nimport { Tools } from './tools';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps(initialData, nodeRegistries);\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <SidebarProvider>\n        <Tools />\n        <EditorRenderer className=\"mastra-workflow-editor\" />\n        <SidebarRenderer />\n      </SidebarProvider>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nexport const FormRender = () => (\n  <>\n    <div className=\"w-full cursor-move\">\n      <Field<string> name=\"title\">\n        {({ field }) => <h1 className=\"text-xl font-bold\">{field.value}</h1>}\n      </Field>\n    </div>\n    <div className=\"content flex flex-col gap-3\">\n      <Field<string> name=\"input\">\n        {({ field }) => (\n          <div className=\"inline-flex justify-between w-full\">\n            <h2 className=\"text-lg\">Input</h2>\n            <input\n              className=\"border border-gray-400 rounded\"\n              value={field.value}\n              onChange={field.onChange}\n            />\n          </div>\n        )}\n      </Field>\n      <Field<string> name=\"output\">\n        {({ field }) => (\n          <div className=\"inline-flex justify-between w-full\">\n            <h2 className=\"text-lg\">Output</h2>\n            <input\n              className=\"border border-gray-400 rounded\"\n              value={field.value}\n              onChange={field.onChange}\n            />\n          </div>\n        )}\n      </Field>\n    </div>\n  </>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/color.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ntype GroupColor = {\n  '50': string;\n  '300': string;\n  '400': string;\n};\n\nexport const defaultColor = 'Blue';\n\nexport const groupColors: Record<string, GroupColor> = {\n  Red: {\n    '50': '#fef2f2',\n    '300': '#fca5a5',\n    '400': '#f87171',\n  },\n  Orange: {\n    '50': '#fff7ed',\n    '300': '#fdba74',\n    '400': '#fb923c',\n  },\n  Amber: {\n    '50': '#fffbeb',\n    '300': '#fcd34d',\n    '400': '#fbbf24',\n  },\n  Yellow: {\n    '50': '#fef9c3',\n    '300': '#fde047',\n    '400': '#facc15',\n  },\n  Lime: {\n    '50': '#f7fee7',\n    '300': '#bef264',\n    '400': '#a3e635',\n  },\n  Green: {\n    '50': '#f0fdf4',\n    '300': '#86efac',\n    '400': '#4ade80',\n  },\n  Emerald: {\n    '50': '#ecfdf5',\n    '300': '#6ee7b7',\n    '400': '#34d399',\n  },\n  Teal: {\n    '50': '#f0fdfa',\n    '300': '#5eead4',\n    '400': '#2dd4bf',\n  },\n  Cyan: {\n    '50': '#ecfeff',\n    '300': '#67e8f9',\n    '400': '#22d3ee',\n  },\n  Sky: {\n    '50': '#ecfeff',\n    '300': '#7dd3fc',\n    '400': '#38bdf8',\n  },\n  Blue: {\n    '50': '#eff6ff',\n    '300': '#93c5fd',\n    '400': '#60a5fa',\n  },\n  Indigo: {\n    '50': '#eef2ff',\n    '300': '#a5b4fc',\n    '400': '#818cf8',\n  },\n  Violet: {\n    '50': '#f5f3ff',\n    '300': '#c4b5fd',\n    '400': '#a78bfa',\n  },\n  Purple: {\n    '50': '#faf5ff',\n    '300': '#d8b4fe',\n    '400': '#c084fc',\n  },\n  Fuchsia: {\n    '50': '#fdf4ff',\n    '300': '#f0abfc',\n    '400': '#e879f9',\n  },\n  Pink: {\n    '50': '#fdf2f8',\n    '300': '#f9a8d4',\n    '400': '#f472b6',\n  },\n  Rose: {\n    '50': '#fff1f2',\n    '300': '#fda4af',\n    '400': '#fb7185',\n  },\n  Gray: {\n    '50': '#f9fafb',\n    '300': '#d1d5db',\n    '400': '#9ca3af',\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/background.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC, useEffect } from 'react';\n\nimport { WorkflowNodeEntity, useWatch } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\ninterface GroupBackgroundProps {\n  node: WorkflowNodeEntity;\n  style?: CSSProperties;\n}\n\nexport const GroupBackground: FC<GroupBackgroundProps> = ({ node, style }) => {\n  const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;\n  const color = groupColors[colorName];\n\n  useEffect(() => {\n    const styleElement = document.createElement('style');\n\n    // 使用独特的选择器\n    const styleContent = `\n      .workflow-group-render[data-group-id=\"${node.id}\"] .workflow-group-background {\n        border: 1px solid ${color['300']};\n      }\n\n      .workflow-group-render.selected[data-group-id=\"${node.id}\"] .workflow-group-background {\n        border: 1px solid ${color['400']};\n      }\n    `;\n\n    styleElement.textContent = styleContent;\n    document.head.appendChild(styleElement);\n\n    return () => {\n      styleElement.remove();\n    };\n  }, [color]);\n\n  return (\n    <div\n      className=\"workflow-group-background\"\n      data-flow-editor-selectable=\"true\"\n      style={{\n        ...style,\n        backgroundColor: `${color['300']}29`,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/color.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { Popover, Tooltip } from 'antd';\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\nexport const GroupColor: FC = () => (\n  <Field<string> name={GroupField.Color}>\n    {({ field }) => {\n      const colorName = field.value ?? defaultColor;\n      return (\n        <Popover\n          placement=\"top\"\n          mouseLeaveDelay={300}\n          content={\n            <div className=\"workflow-group-color-palette\">\n              {Object.entries(groupColors).map(([name, color]) => (\n                <Tooltip title={name} key={name} mouseEnterDelay={300}>\n                  <span\n                    className=\"workflow-group-color-item\"\n                    key={name}\n                    style={{\n                      backgroundColor: color['300'],\n                      borderColor: name === colorName ? color['400'] : '#fff',\n                    }}\n                    onClick={() => field.onChange(name)}\n                  />\n                </Tooltip>\n              ))}\n            </div>\n          }\n        >\n          <span\n            className=\"workflow-group-color\"\n            style={{\n              backgroundColor: groupColors[colorName]['300'],\n            }}\n          />\n        </Popover>\n      );\n    }}\n  </Field>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/header.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CSSProperties, FC, MouseEvent, ReactNode } from 'react';\n\nimport { useWatch } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\nimport { defaultColor, groupColors } from '../color';\n\ninterface GroupHeaderProps {\n  onMouseDown: (e: MouseEvent) => void;\n  onFocus: () => void;\n  onBlur: () => void;\n  children: ReactNode;\n  style?: CSSProperties;\n}\n\nexport const GroupHeader: FC<GroupHeaderProps> = ({\n  onMouseDown,\n  onFocus,\n  onBlur,\n  children,\n  style,\n}) => {\n  const colorName = useWatch<string>(GroupField.Color) ?? defaultColor;\n  const color = groupColors[colorName];\n  return (\n    <div\n      className=\"workflow-group-header\"\n      data-flow-editor-selectable=\"false\"\n      onMouseDown={onMouseDown}\n      onFocus={onFocus}\n      onBlur={onBlur}\n      style={{\n        ...style,\n        backgroundColor: color['50'],\n        borderColor: color['300'],\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/icon-group.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\ninterface IconGroupProps {\n  size?: number;\n}\n\nexport const IconGroup: FC<IconGroupProps> = ({ size }) => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 10 10\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={{\n      width: size,\n      height: size,\n    }}\n  >\n    <path\n      id=\"group\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      stroke=\"none\"\n      d=\"M 0.009766 10 L 0.009766 9.990234 L 0 9.990234 L 0 7.5 L 1 7.5 L 1 9 L 2.5 9 L 2.5 10 L 0.009766 10 Z M 3.710938 10 L 3.710938 9 L 6.199219 9 L 6.199219 10 L 3.710938 10 Z M 7.5 10 L 7.5 9 L 9 9 L 9 7.5 L 10 7.5 L 10 9.990234 L 9.990234 9.990234 L 9.990234 10 L 7.5 10 Z M 0 6.289063 L 0 3.800781 L 1 3.800781 L 1 6.289063 L 0 6.289063 Z M 9 6.289063 L 9 3.800781 L 10 3.800781 L 10 6.289063 L 9 6.289063 Z M 0 2.5 L 0 0.009766 L 0.009766 0.009766 L 0.009766 0 L 2.5 0 L 2.5 1 L 1 1 L 1 2.5 L 0 2.5 Z M 9 2.5 L 9 1 L 7.5 1 L 7.5 0 L 9.990234 0 L 9.990234 0.009766 L 10 0.009766 L 10 2.5 L 9 2.5 Z M 3.710938 1 L 3.710938 0 L 6.199219 0 L 6.199219 1 L 3.710938 1 Z\"\n    />\n  </svg>\n);\n\nexport const IconUngroup: FC<IconGroupProps> = ({ size }) => (\n  <svg\n    width=\"10\"\n    height=\"10\"\n    viewBox=\"0 0 10 10\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    style={{\n      width: size,\n      height: size,\n    }}\n  >\n    <path\n      id=\"ungroup\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      stroke=\"none\"\n      d=\"M 9.654297 10.345703 L 8.808594 9.5 L 7.175781 9.5 L 7.175781 8.609375 L 7.917969 8.609375 L 1.390625 2.082031 L 1.390625 2.824219 L 0.5 2.824219 L 0.5 1.191406 L -0.345703 0.345703 L 0.283203 -0.283203 L 1.166016 0.599609 L 2.724609 0.599609 L 2.724609 1.490234 L 2.056641 1.490234 L 8.509766 7.943359 L 8.509766 7.275391 L 9.400391 7.275391 L 9.400391 8.833984 L 10.283203 9.716797 L 9.654297 10.345703 Z M 0.509766 9.5 L 0.509766 9.490234 L 0.5 9.490234 L 0.5 7.275391 L 1.390625 7.275391 L 1.390625 8.609375 L 2.724609 8.609375 L 2.724609 9.5 L 0.509766 9.5 Z M 3.802734 9.5 L 3.802734 8.609375 L 6.017578 8.609375 L 6.017578 9.5 L 3.802734 9.5 Z M 0.5 6.197266 L 0.5 3.982422 L 1.390625 3.982422 L 1.390625 6.197266 L 0.5 6.197266 Z M 8.509766 6.197266 L 8.509766 3.982422 L 9.400391 3.982422 L 9.400391 6.197266 L 8.509766 6.197266 Z M 8.509766 2.824219 L 8.509766 1.490234 L 7.175781 1.490234 L 7.175781 0.599609 L 9.390625 0.599609 L 9.390625 0.609375 L 9.400391 0.609375 L 9.400391 2.824219 L 8.509766 2.824219 Z M 3.802734 1.490234 L 3.802734 0.599609 L 6.017578 0.599609 L 6.017578 1.490234 L 3.802734 1.490234 Z\"\n    />\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { GroupNodeRender } from './node-render';\nexport { IconGroup } from './icon-group';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport {\n  FlowNodeFormData,\n  Form,\n  FormModelV2,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\nimport { useNodeSize } from '@flowgram.ai/free-container-plugin';\n\nimport { HEADER_HEIGHT, HEADER_PADDING } from '../constant';\nimport { UngroupButton } from './ungroup';\nimport { GroupTools } from './tools';\nimport { GroupTips } from './tips';\nimport { GroupHeader } from './header';\nimport { GroupBackground } from './background';\n\nexport const GroupNodeRender = () => {\n  const { node, selected, selectNode, nodeRef, startDrag, onFocus, onBlur } = useNodeRender();\n  const nodeSize = useNodeSize();\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formControl = formModel?.formControl;\n\n  const { height, width } = nodeSize ?? {};\n  const nodeHeight = height ?? 0;\n\n  useEffect(() => {\n    // prevent lines in outside cannot be selected - 防止外层线条不可选中\n    const element = node.renderData.node;\n    element.style.pointerEvents = 'none';\n  }, [node]);\n\n  return (\n    <div\n      className={`workflow-group-render ${selected ? 'selected' : ''}`}\n      ref={nodeRef}\n      data-group-id={node.id}\n      data-node-selected={String(selected)}\n      onMouseDown={selectNode}\n      onClick={(e) => {\n        selectNode(e);\n      }}\n      style={{\n        width,\n        height,\n      }}\n    >\n      <Form control={formControl}>\n        <>\n          <GroupHeader\n            onMouseDown={(e) => {\n              startDrag(e);\n            }}\n            onFocus={onFocus}\n            onBlur={onBlur}\n            style={{\n              height: HEADER_HEIGHT,\n            }}\n          >\n            <GroupTools />\n          </GroupHeader>\n          <GroupTips />\n          <UngroupButton node={node} />\n          <GroupBackground\n            node={node}\n            style={{\n              top: HEADER_HEIGHT + HEADER_PADDING,\n              height: nodeHeight - HEADER_HEIGHT - HEADER_PADDING,\n            }}\n          />\n        </>\n      </Form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/global-store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst STORAGE_KEY = 'workflow-move-into-group-tip-visible';\nconst STORAGE_VALUE = 'false';\n\nexport class TipsGlobalStore {\n  private static _instance?: TipsGlobalStore;\n\n  public static get instance(): TipsGlobalStore {\n    if (!this._instance) {\n      this._instance = new TipsGlobalStore();\n    }\n    return this._instance;\n  }\n\n  private closed = false;\n\n  public isClosed(): boolean {\n    return this.isCloseForever() || this.closed;\n  }\n\n  public close(): void {\n    this.closed = true;\n  }\n\n  public isCloseForever(): boolean {\n    return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;\n  }\n\n  public closeForever(): void {\n    localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/icon-close.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconClose = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"none\" viewBox=\"0 0 16 16\">\n    <path\n      fill=\"#060709\"\n      fillOpacity=\"0.5\"\n      d=\"M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useControlTips } from './use-control';\nimport { GroupTipsStyle } from './style';\nimport { isMacOS } from './is-mac-os';\nimport { IconClose } from './icon-close';\n\nexport const GroupTips = () => {\n  const { visible, close, closeForever } = useControlTips();\n\n  if (!visible) {\n    return null;\n  }\n\n  return (\n    <GroupTipsStyle className={'workflow-group-tips'}>\n      <div className=\"container\">\n        <div className=\"content\">\n          <p className=\"text\">{`Hold ${isMacOS ? 'Cmd ⌘' : 'Ctrl'} to drag node out`}</p>\n          <div\n            className=\"space\"\n            style={{\n              width: 0,\n            }}\n          />\n        </div>\n        <div className=\"actions\">\n          <p className=\"close-forever\" onClick={closeForever}>\n            Never Remind\n          </p>\n          <div className=\"close\" onClick={close}>\n            <IconClose />\n          </div>\n        </div>\n      </div>\n    </GroupTipsStyle>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/is-mac-os.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const GroupTipsStyle = styled.div`\n  position: absolute;\n  top: 35px;\n\n  width: 100%;\n  height: 28px;\n  white-space: nowrap;\n  pointer-events: auto;\n\n  .container {\n    display: inline-flex;\n    justify-content: center;\n    height: 100%;\n    width: 100%;\n    background-color: rgb(255 255 255);\n    border-radius: 8px 8px 0 0;\n\n    .content {\n      overflow: hidden;\n      display: inline-flex;\n      align-items: center;\n      justify-content: flex-start;\n\n      width: fit-content;\n      height: 100%;\n      padding: 0 12px;\n\n      .text {\n        font-size: 14px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 20px;\n        color: rgba(15, 21, 40, 82%);\n        text-overflow: ellipsis;\n        margin: 0;\n      }\n\n      .space {\n        width: 128px;\n      }\n    }\n\n    .actions {\n      display: flex;\n      gap: 8px;\n      align-items: center;\n\n      height: 28px;\n      padding: 0 12px;\n\n      .close-forever {\n        cursor: pointer;\n\n        padding: 0 3px;\n\n        font-size: 12px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 12px;\n        color: rgba(32, 41, 69, 62%);\n        margin: 0;\n      }\n\n      .close {\n        display: flex;\n        cursor: pointer;\n        height: 100%;\n        align-items: center;\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tips/use-control.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { useCurrentEntity, useService } from '@flowgram.ai/free-layout-editor';\nimport {\n  NodeIntoContainerService,\n  NodeIntoContainerType,\n} from '@flowgram.ai/free-container-plugin';\n\nimport { TipsGlobalStore } from './global-store';\n\nexport const useControlTips = () => {\n  const node = useCurrentEntity();\n  const [visible, setVisible] = useState(false);\n  const globalStore = TipsGlobalStore.instance;\n\n  const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);\n\n  const show = useCallback(() => {\n    if (globalStore.isClosed()) {\n      return;\n    }\n\n    setVisible(true);\n  }, [globalStore]);\n\n  const close = useCallback(() => {\n    globalStore.close();\n    setVisible(false);\n  }, [globalStore]);\n\n  const closeForever = useCallback(() => {\n    globalStore.closeForever();\n    close();\n  }, [close, globalStore]);\n\n  useEffect(() => {\n    // 监听移入\n    const inDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.In) {\n        return;\n      }\n      if (e.targetContainer === node) {\n        show();\n      }\n    });\n    // 监听移出事件\n    const outDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.Out) {\n        return;\n      }\n      if (e.sourceContainer === node && !node.blocks.length) {\n        setVisible(false);\n      }\n    });\n    return () => {\n      inDisposer.dispose();\n      outDisposer.dispose();\n    };\n  }, [nodeIntoContainerService, node, show, close, visible]);\n\n  return {\n    visible,\n    close,\n    closeForever,\n  };\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/title.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useState } from 'react';\n\nimport { Input } from 'antd';\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { GroupField } from '../constant';\n\nexport const GroupTitle: FC = () => {\n  const [inputting, setInputting] = useState(false);\n  return (\n    <Field<string> name={GroupField.Title}>\n      {({ field }) =>\n        inputting ? (\n          <Input\n            autoFocus\n            className=\"workflow-group-title-input\"\n            size=\"small\"\n            value={field.value}\n            onChange={field.onChange}\n            onMouseDown={(e) => e.stopPropagation()}\n            onBlur={() => setInputting(false)}\n            draggable={false}\n            onSubmit={() => setInputting(false)}\n          />\n        ) : (\n          <p className=\"workflow-group-title\" onDoubleClick={() => setInputting(true)}>\n            {field.value ?? 'Group'}\n          </p>\n        )\n      }\n    </Field>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { HolderOutlined } from '@ant-design/icons';\n\nimport { GroupTitle } from './title';\nimport { GroupColor } from './color';\n\nexport const GroupTools: FC = () => (\n  <div className=\"workflow-group-tools\">\n    <HolderOutlined className=\"workflow-group-tools-drag\" />\n    <GroupTitle />\n    <GroupColor />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/components/ungroup.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, FC } from 'react';\n\nimport { Button, Tooltip } from 'antd';\nimport { CommandRegistry, WorkflowNodeEntity, useService } from '@flowgram.ai/free-layout-editor';\nimport { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';\n\nimport { IconUngroup } from './icon-group';\n\ninterface UngroupButtonProps {\n  node: WorkflowNodeEntity;\n  style?: CSSProperties;\n}\n\nexport const UngroupButton: FC<UngroupButtonProps> = ({ node, style }) => {\n  const commandRegistry = useService(CommandRegistry);\n  return (\n    <Tooltip title=\"Ungroup\">\n      <div className=\"workflow-group-ungroup\" style={style}>\n        <Button\n          icon={<IconUngroup size={14} />}\n          style={{ height: 30, width: 30 }}\n          type=\"text\"\n          onClick={() => {\n            commandRegistry.executeCommand(WorkflowGroupCommand.Ungroup, node);\n          }}\n        />\n      </div>\n    </Tooltip>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const HEADER_HEIGHT = 30;\nexport const HEADER_PADDING = 5;\n\nexport enum GroupField {\n  Title = 'title',\n  Color = 'color',\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.workflow-group-render {\n  border-radius: 8px;\n  pointer-events: none;\n}\n\n.workflow-group-header {\n  height: 30px;\n  width: fit-content;\n  background-color: #fefce8;\n  border: 1px solid #facc15;\n  border-radius: 8px;\n  padding-right: 8px;\n  pointer-events: auto;\n}\n\n.workflow-group-ungroup {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 30px;\n  width: 30px;\n  position: absolute;\n  top: 35px;\n  right: 0;\n  border-radius: 8px;\n  cursor: pointer;\n  pointer-events: auto;\n}\n\n.workflow-group-ungroup .ant-btn {\n  color: #9ca3af;\n}\n\n.workflow-group-ungroup:hover .ant-btn {\n  color: #374151;\n}\n\n.workflow-group-background {\n  position: absolute;\n  pointer-events: none;\n  top: 0;\n  background-color: #fddf4729;\n  border: 1px solid #fde047;\n  border-radius: 8px;\n  width: 100%;\n}\n\n.workflow-group-render.selected .workflow-group-background {\n  border: 1px solid #facc15;\n}\n\n.workflow-group-tools {\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  gap: 4px;\n  height: 100%;\n  cursor: move;\n  color: oklch(44.6% 0.043 257.281);\n  font-size: 14px;\n}\n.workflow-group-title {\n  margin: 0;\n  max-width: 242px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  font-weight: 500;\n}\n\n.workflow-group-tools-drag {\n  height: 100%;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding-left: 4px;\n}\n\n.workflow-group-color {\n  width: 16px;\n  height: 16px;\n  border-radius: 8px;\n  background-color: #fde047;\n  margin-left: 4px;\n  cursor: pointer;\n}\n\n.workflow-group-title-input {\n  width: 242px;\n  border: none;\n  color: #374151;\n}\n\n.workflow-group-color-palette {\n  display: grid;\n  grid-template-columns: repeat(6, 24px);\n  gap: 12px;\n  margin: 8px;\n  padding: 8px;\n}\n\n.workflow-group-color-item {\n  width: 24px;\n  height: 24px;\n  border-radius: 50%;\n  background-color: #fde047;\n  cursor: pointer;\n  border: 3px solid;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/group/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.css';\n\nexport { GroupNodeRender } from './components';\nexport { IconGroup } from './components';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './line-add-button';\nexport * from './node-panel';\nexport * from './node-comment';\nexport * from './group';\nexport * from './selector-box-popover';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/line-add-button/button.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const IconPlusCircle = () => (\n  <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g id=\"add\">\n      <path\n        id=\"background\"\n        fill=\"#ffffff\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 24 12 C 24 5.372583 18.627417 0 12 0 C 5.372583 0 -0 5.372583 -0 12 C -0 18.627417 5.372583 24 12 24 C 18.627417 24 24 18.627417 24 12 Z\"\n      />\n      <path\n        id=\"content\"\n        fill=\"currentColor\"\n        fillRule=\"evenodd\"\n        stroke=\"none\"\n        d=\"M 22 12.005 C 22 6.482153 17.522848 2.004999 12 2.004999 C 6.477152 2.004999 2 6.482153 2 12.005 C 2 17.527847 6.477152 22.004999 12 22.004999 C 17.522848 22.004999 22 17.527847 22 12.005 Z\"\n      />\n      <path\n        id=\"cross\"\n        fill=\"#ffffff\"\n        stroke=\"none\"\n        d=\"M 11.411996 16.411797 C 11.411996 16.736704 11.675362 17 12.00023 17 C 12.325109 17 12.588474 16.736704 12.588474 16.411797 L 12.588474 12.58826 L 16.41201 12.58826 C 16.736919 12.58826 17.000216 12.324894 17.000216 12.000015 C 17.000216 11.675147 16.736919 11.411781 16.41201 11.411781 L 12.588474 11.411781 L 12.588474 7.588234 C 12.588474 7.263367 12.325109 7 12.00023 7 C 11.675362 7 11.411996 7.263367 11.411996 7.588234 L 11.411996 11.411781 L 7.588449 11.411781 C 7.263581 11.411781 7.000215 11.675147 7.000215 12.000015 C 7.000215 12.324894 7.263581 12.58826 7.588449 12.58826 L 11.411996 12.58826 L 11.411996 16.411797 Z\"\n      />\n    </g>\n  </svg>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/line-add-button/index.scss",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.line-add-button {\n  position: absolute;\n  transform: translate(-50%, -60%);\n  width: 24px;\n  height: 24px;\n  cursor: pointer;\n  color: inherit;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/line-add-button/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback } from 'react';\n\nimport {\n  WorkflowNodePanelService,\n  WorkflowNodePanelUtils,\n} from '@flowgram.ai/free-node-panel-plugin';\nimport { LineRenderProps } from '@flowgram.ai/free-lines-plugin';\nimport {\n  HistoryService,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  delay,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { useVisible } from './use-visible';\nimport { IconPlusCircle } from './button';\n\nimport './index.scss';\nexport const LineAddButton = (props: LineRenderProps) => {\n  const { line, selected, hovered, color } = props;\n  const visible = useVisible({ line, selected, hovered });\n  const nodePanelService = useService<WorkflowNodePanelService>(WorkflowNodePanelService);\n  const document = useService(WorkflowDocument);\n  const dragService = useService(WorkflowDragService);\n  const linesManager = useService(WorkflowLinesManager);\n  const historyService = useService(HistoryService);\n\n  const { fromPort, toPort } = line;\n\n  const onClick = useCallback(async () => {\n    // calculate the middle point of the line - 计算线条的中点位置\n    const position = {\n      x: (line.position.from.x + line.position.to.x) / 2,\n      y: (line.position.from.y + line.position.to.y) / 2,\n    };\n\n    // get container node for the new node - 获取新节点的容器节点\n    const containerNode = WorkflowNodePanelUtils.getContainerNode({\n      fromPort,\n    });\n\n    // show node selection panel - 显示节点选择面板\n    const result = await nodePanelService.singleSelectNodePanel({\n      position,\n      containerNode,\n      panelProps: {\n        enableScrollClose: true,\n      },\n    });\n    if (!result) {\n      return;\n    }\n\n    const { nodeType, nodeJSON } = result;\n\n    // adjust position for the new node - 调整新节点的位置\n    const nodePosition = WorkflowNodePanelUtils.adjustNodePosition({\n      nodeType,\n      position,\n      fromPort,\n      toPort,\n      containerNode,\n      document,\n      dragService,\n    });\n\n    // create new workflow node - 创建新的工作流节点\n    const node: WorkflowNodeEntity = document.createWorkflowNodeByType(\n      nodeType,\n      nodePosition,\n      nodeJSON ?? ({} as WorkflowNodeJSON),\n      containerNode?.id\n    );\n\n    // auto offset subsequent nodes - 自动偏移后续节点\n    if (fromPort && toPort) {\n      WorkflowNodePanelUtils.subNodesAutoOffset({\n        node,\n        fromPort,\n        toPort,\n        containerNode,\n        historyService,\n        dragService,\n        linesManager,\n      });\n    }\n\n    // wait for node render - 等待节点渲染\n    await delay(20);\n\n    // build connection lines - 构建连接线\n    WorkflowNodePanelUtils.buildLine({\n      fromPort,\n      node,\n      toPort,\n      linesManager,\n    });\n\n    // remove original line - 移除原始线条\n    line.dispose();\n  }, []);\n\n  if (!visible) {\n    return <></>;\n  }\n\n  return (\n    <div\n      className=\"line-add-button\"\n      style={{\n        left: '50%',\n        top: '50%',\n        color,\n      }}\n      data-testid=\"sdk.workflow.canvas.line.add\"\n      data-line-id={line.id}\n      onClick={onClick}\n    >\n      <IconPlusCircle />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/line-add-button/use-visible.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.scss';\nimport { WorkflowLineEntity, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nexport const useVisible = (params: {\n  line: WorkflowLineEntity;\n  selected?: boolean;\n  hovered?: boolean;\n}): boolean => {\n  const playground = usePlayground();\n  const { line, selected = false, hovered } = params;\n  if (line.disposed) {\n    // 在 dispose 后，再去获取 line.to | line.from 会导致错误创建端口\n    return false;\n  }\n  if (playground.config.readonly) {\n    return false;\n  }\n  if (!selected && !hovered) {\n    return false;\n  }\n  return true;\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/blank-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FC } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\nimport { DragArea } from './drag-area';\n\ninterface IBlankArea {\n  model: CommentEditorModel;\n}\n\nexport const BlankArea: FC<IBlankArea> = (props) => {\n  const { model } = props;\n  const playground = usePlayground();\n  const { selectNode } = useNodeRender();\n\n  return (\n    <div\n      className=\"workflow-comment-blank-area h-full w-full\"\n      onMouseDown={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        model.setFocus(false);\n        selectNode(e);\n        playground.node.focus(); // 防止节点无法被删除\n      }}\n      onClick={(e) => {\n        model.setFocus(true);\n        model.selectEnd();\n      }}\n    >\n      <DragArea\n        style={{\n          position: 'relative',\n          width: '100%',\n          height: '100%',\n        }}\n        model={model}\n        stopEvent={false}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/border-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC } from 'react';\n\nimport type { CommentEditorModel } from '../model';\nimport { ResizeArea } from './resize-area';\nimport { DragArea } from './drag-area';\n\ninterface IBorderArea {\n  model: CommentEditorModel;\n  overflow: boolean;\n  onResize?: () => {\n    resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;\n    resizeEnd: () => void;\n  };\n}\n\nexport const BorderArea: FC<IBorderArea> = (props) => {\n  const { model, overflow, onResize } = props;\n\n  return (\n    <div style={{ zIndex: 999 }}>\n      {/* 左边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          left: -10,\n          top: 10,\n          width: 20,\n          height: 'calc(100% - 20px)',\n        }}\n        model={model}\n      />\n      {/* 右边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          right: -10,\n          top: 10,\n          height: 'calc(100% - 20px)',\n          width: overflow ? 10 : 20, // 防止遮挡滚动条\n        }}\n        model={model}\n      />\n      {/* 上边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          top: -10,\n          left: 10,\n          width: 'calc(100% - 20px)',\n          height: 20,\n        }}\n        model={model}\n      />\n      {/* 下边 */}\n      <DragArea\n        style={{\n          position: 'absolute',\n          bottom: -10,\n          left: 10,\n          width: 'calc(100% - 20px)',\n          height: 20,\n        }}\n        model={model}\n      />\n      {/** 左上角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          left: 0,\n          top: 0,\n          cursor: 'nwse-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: y, right: 0, bottom: 0, left: x })}\n        onResize={onResize}\n      />\n      {/** 右上角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          right: 0,\n          top: 0,\n          cursor: 'nesw-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: y, right: x, bottom: 0, left: 0 })}\n        onResize={onResize}\n      />\n      {/** 右下角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          right: 0,\n          bottom: 0,\n          cursor: 'nwse-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: 0, right: x, bottom: y, left: 0 })}\n        onResize={onResize}\n      />\n      {/** 左下角 */}\n      <ResizeArea\n        style={{\n          position: 'absolute',\n          left: 0,\n          bottom: 0,\n          cursor: 'nesw-resize',\n        }}\n        model={model}\n        getDelta={({ x, y }) => ({ top: 0, right: 0, bottom: y, left: x })}\n        onResize={onResize}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/container.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CSSProperties, FC, ReactNode } from 'react';\n\ninterface ICommentContainer {\n  focused: boolean;\n  children?: ReactNode;\n  style?: React.CSSProperties;\n}\n\nexport const CommentContainer: FC<ICommentContainer> = (props) => {\n  const { focused, children, style } = props;\n\n  const scrollbarStyle = {\n    // 滚动条样式\n    scrollbarWidth: 'thin',\n    scrollbarColor: 'rgb(159 159 158 / 65%) transparent',\n    // 针对 WebKit 浏览器（如 Chrome、Safari）的样式\n    '&:WebkitScrollbar': {\n      width: '4px',\n    },\n    '&::WebkitScrollbarTrack': {\n      background: 'transparent',\n    },\n    '&::WebkitScrollbarThumb': {\n      backgroundColor: 'rgb(159 159 158 / 65%)',\n      borderRadius: '20px',\n      border: '2px solid transparent',\n    },\n  } as unknown as CSSProperties;\n\n  return (\n    <div\n      className=\"workflow-comment-container\"\n      data-flow-editor-selectable=\"false\"\n      style={{\n        // tailwind 不支持 outline 的样式，所以这里需要使用 style 来设置\n        outline: focused ? '1px solid #FF811A' : '1px solid #F2B600',\n        backgroundColor: focused ? '#FFF3EA' : '#FFFBED',\n        ...scrollbarStyle,\n        ...style,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/content-drag-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC, type WheelEventHandler, useEffect, useState } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\nimport { DragArea } from './drag-area';\n\ninterface IContentDragArea {\n  model: CommentEditorModel;\n  focused: boolean;\n  overflow: boolean;\n}\n\nexport const ContentDragArea: FC<IContentDragArea> = (props) => {\n  const { model, focused, overflow } = props;\n  const playground = usePlayground();\n  const { selectNode } = useNodeRender();\n\n  const [active, setActive] = useState(false);\n\n  useEffect(() => {\n    // 当编辑器失去焦点时，取消激活状态\n    if (!focused) {\n      setActive(false);\n    }\n  }, [focused]);\n\n  const handleWheel: WheelEventHandler<HTMLDivElement> = (e) => {\n    const editorElement = model.element;\n    if (active || !overflow || !editorElement) {\n      return;\n    }\n    e.stopPropagation();\n    const maxScroll = editorElement.scrollHeight - editorElement.clientHeight;\n    const newScrollTop = Math.min(Math.max(editorElement.scrollTop + e.deltaY, 0), maxScroll);\n    editorElement.scroll(0, newScrollTop);\n  };\n\n  const handleMouseDown = (mouseDownEvent: React.MouseEvent) => {\n    if (active) {\n      return;\n    }\n    mouseDownEvent.preventDefault();\n    mouseDownEvent.stopPropagation();\n    model.setFocus(false);\n    selectNode(mouseDownEvent);\n    playground.node.focus(); // 防止节点无法被删除\n\n    const startX = mouseDownEvent.clientX;\n    const startY = mouseDownEvent.clientY;\n\n    const handleMouseUp = (mouseMoveEvent: MouseEvent) => {\n      const deltaX = mouseMoveEvent.clientX - startX;\n      const deltaY = mouseMoveEvent.clientY - startY;\n      // 判断是拖拽还是点击\n      const delta = 5;\n      if (Math.abs(deltaX) < delta && Math.abs(deltaY) < delta) {\n        // 点击后隐藏\n        setActive(true);\n      }\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('click', handleMouseUp);\n    };\n\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('click', handleMouseUp);\n  };\n\n  return (\n    <div\n      className=\"workflow-comment-content-drag-area\"\n      onMouseDown={handleMouseDown}\n      onWheel={handleWheel}\n      style={{\n        display: active ? 'none' : undefined,\n      }}\n    >\n      <DragArea\n        style={{\n          position: 'relative',\n          width: '100%',\n          height: '100%',\n        }}\n        model={model}\n        stopEvent={false}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/drag-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, type FC } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { type CommentEditorModel } from '../model';\n\ninterface IDragArea {\n  model: CommentEditorModel;\n  stopEvent?: boolean;\n  style?: CSSProperties;\n}\n\nexport const DragArea: FC<IDragArea> = (props) => {\n  const { model, stopEvent = true, style } = props;\n\n  const playground = usePlayground();\n\n  const { startDrag: onStartDrag, onFocus, onBlur, selectNode } = useNodeRender();\n\n  return (\n    <div\n      className=\"workflow-comment-drag-area\"\n      data-flow-editor-selectable=\"false\"\n      draggable={true}\n      style={style}\n      onMouseDown={(e) => {\n        if (stopEvent) {\n          e.preventDefault();\n          e.stopPropagation();\n        }\n        model.setFocus(false);\n        onStartDrag(e);\n        selectNode(e);\n        playground.node.focus(); // 防止节点无法被删除\n      }}\n      onFocus={onFocus}\n      onBlur={onBlur}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type CSSProperties, type FC, useEffect, useRef } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorEvent } from '../constant';\n\ninterface ICommentEditor {\n  model: CommentEditorModel;\n  style?: CSSProperties;\n  value?: string;\n  onChange?: (value: string) => void;\n}\n\nexport const CommentEditor: FC<ICommentEditor> = (props) => {\n  const { model, style, onChange } = props;\n  const playground = usePlayground();\n  const editorRef = useRef<HTMLTextAreaElement | null>(null);\n  const placeholder = model.value || model.focused ? undefined : 'Enter a comment...';\n\n  // 同步编辑器内部值变化\n  useEffect(() => {\n    const disposer = model.on((params) => {\n      if (params.type !== CommentEditorEvent.Change) {\n        return;\n      }\n      onChange?.(model.value);\n    });\n    return () => disposer.dispose();\n  }, [model, onChange]);\n\n  useEffect(() => {\n    if (!editorRef.current) {\n      return;\n    }\n    model.element = editorRef.current;\n  }, [editorRef]);\n\n  return (\n    <div className=\"workflow-comment-editor\">\n      <p className=\"workflow-comment-editor-placeholder\">{placeholder}</p>\n      <textarea\n        className=\"workflow-comment-editor-textarea\"\n        ref={editorRef}\n        style={style}\n        readOnly={playground.config.readonly}\n        onChange={(e) => {\n          const { value } = e.target;\n          model.setValue(value);\n        }}\n        onFocus={() => {\n          model.setFocus(true);\n        }}\n        onBlur={() => {\n          model.setFocus(false);\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.workflow-comment {\n  width: auto;\n  height: auto;\n  min-width: 120px;\n  min-height: 80px;\n}\n\n.workflow-comment-container {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: flex-start;\n  width: 100%;\n  height: 100%;\n  border-radius: 8px;\n  outline: 1px solid;\n  padding: 6px 2px 6px 10px;\n  overflow: hidden;\n}\n\n.workflow-comment-drag-area {\n  position: absolute;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: move;\n}\n\n.workflow-comment-content-drag-area {\n  position: absolute;\n  height: 100%;\n  width: calc(100% - 22px);\n}\n\n.workflow-comment-resize-area {\n  position: absolute;\n  width: 10px;\n  height: 10px;\n}\n\n.workflow-comment-editor {\n  width: 100%;\n  height: 100%;\n}\n\n.workflow-comment-editor-placeholder {\n  margin: 0;\n  position: absolute;\n  pointer-events: none;\n  color: rgba(55, 67, 106, 0.38);\n  font-weight: 500;\n}\n\n.workflow-comment-editor-textarea {\n  width: 100%;\n  height: 100%;\n  box-sizing: border-box;\n  appearance: none;\n  border: none;\n  margin: 0;\n  padding: 0;\n  width: 100%;\n  background: none;\n  color: inherit;\n  font-family: inherit;\n  font-size: 16px;\n  resize: none;\n  outline: none;\n}\n\n.workflow-comment-more-button {\n  position: absolute;\n  right: 6px;\n}\n\n.workflow-comment-more-button > .ant-btn {\n  color: rgba(255, 255, 255, 0);\n  background: none;\n}\n\n.workflow-comment-more-button > .ant-btn:hover {\n  color: #ffa100;\n  background: #fbf2d2cc;\n  backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button-focused > .ant-btn:hover {\n  color: #ff811a;\n  background: #ffe3cecc;\n  backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button > .ant-btn:active {\n  color: #f2b600;\n  background: #ede5c7cc;\n  backdrop-filter: blur(1px);\n}\n\n.workflow-comment-more-button-focused > .ant-btn:active {\n  color: #ff811a;\n  background: #eed5c1cc;\n  backdrop-filter: blur(1px);\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.css';\n\nexport { CommentRender } from './render';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/more-button.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { FC } from 'react';\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeMenu } from '@editor/components/node-menu';\n\ninterface IMoreButton {\n  node: WorkflowNodeEntity;\n  focused: boolean;\n  deleteNode: () => void;\n}\n\nexport const MoreButton: FC<IMoreButton> = ({ node, focused, deleteNode }) => (\n  <div\n    className={`workflow-comment-more-button ${\n      focused ? 'workflow-comment-more-button-focused' : ''\n    }`}\n  >\n    <NodeMenu node={node} deleteNode={deleteNode} updateTitleEdit={() => {}} />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport {\n  Field,\n  FieldRenderProps,\n  FlowNodeFormData,\n  Form,\n  FormModelV2,\n  WorkflowNodeEntity,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { useOverflow } from '../hooks/use-overflow';\nimport { useModel } from '../hooks/use-model';\nimport { useSize } from '../hooks';\nimport { CommentEditorFormField } from '../constant';\nimport { MoreButton } from './more-button';\nimport { CommentEditor } from './editor';\nimport { ContentDragArea } from './content-drag-area';\nimport { CommentContainer } from './container';\nimport { BorderArea } from './border-area';\n\nexport const CommentRender: FC<{\n  node: WorkflowNodeEntity;\n}> = (props) => {\n  const { node } = props;\n  const model = useModel();\n\n  const { selected: focused, selectNode, nodeRef, deleteNode } = useNodeRender();\n\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formControl = formModel?.formControl;\n\n  const { width, height, onResize } = useSize();\n  const { overflow, updateOverflow } = useOverflow({ model, height });\n\n  return (\n    <div\n      className=\"workflow-comment\"\n      style={{\n        width,\n        height,\n      }}\n      ref={nodeRef}\n      data-node-selected={String(focused)}\n      onMouseEnter={updateOverflow}\n      onMouseDown={(e) => {\n        setTimeout(() => {\n          // 防止 selectNode 拦截事件，导致 slate 编辑器无法聚焦\n          selectNode(e);\n        }, 20);\n      }}\n    >\n      <Form control={formControl}>\n        <>\n          {/* 背景 */}\n          <CommentContainer focused={focused} style={{ height }}>\n            <Field name={CommentEditorFormField.Note}>\n              {({ field }: FieldRenderProps<string>) => (\n                <>\n                  {/** 编辑器 */}\n                  <CommentEditor model={model} value={field.value} onChange={field.onChange} />\n                  {/* 内容拖拽区域（点击后隐藏） */}\n                  <ContentDragArea model={model} focused={focused} overflow={overflow} />\n                  {/* 更多按钮 */}\n                  <MoreButton node={node} focused={focused} deleteNode={deleteNode} />\n                </>\n              )}\n            </Field>\n          </CommentContainer>\n          {/* 边框 */}\n          <BorderArea model={model} overflow={overflow} onResize={onResize} />\n        </>\n      </Form>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/components/resize-area.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, type FC } from 'react';\n\nimport { useNodeRender, usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport type { CommentEditorModel } from '../model';\n\ninterface IResizeArea {\n  model: CommentEditorModel;\n  onResize?: () => {\n    resizing: (delta: { top: number; right: number; bottom: number; left: number }) => void;\n    resizeEnd: () => void;\n  };\n  getDelta?: (delta: { x: number; y: number }) => {\n    top: number;\n    right: number;\n    bottom: number;\n    left: number;\n  };\n  style?: CSSProperties;\n}\n\nexport const ResizeArea: FC<IResizeArea> = (props) => {\n  const { model, onResize, getDelta, style } = props;\n\n  const playground = usePlayground();\n\n  const { selectNode } = useNodeRender();\n\n  const handleMouseDown = (mouseDownEvent: React.MouseEvent) => {\n    mouseDownEvent.preventDefault();\n    mouseDownEvent.stopPropagation();\n    if (!onResize) {\n      return;\n    }\n    const { resizing, resizeEnd } = onResize();\n    model.setFocus(false);\n    selectNode(mouseDownEvent);\n    playground.node.focus(); // 防止节点无法被删除\n\n    const startX = mouseDownEvent.clientX;\n    const startY = mouseDownEvent.clientY;\n\n    const handleMouseMove = (mouseMoveEvent: MouseEvent) => {\n      const deltaX = mouseMoveEvent.clientX - startX;\n      const deltaY = mouseMoveEvent.clientY - startY;\n      const delta = getDelta?.({ x: deltaX, y: deltaY });\n      if (!delta || !resizing) {\n        return;\n      }\n      resizing(delta);\n    };\n\n    const handleMouseUp = () => {\n      resizeEnd();\n      document.removeEventListener('mousemove', handleMouseMove);\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('click', handleMouseUp);\n    };\n\n    document.addEventListener('mousemove', handleMouseMove);\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('click', handleMouseUp);\n  };\n\n  return (\n    <div\n      className=\"workflow-comment-resize-area\"\n      style={style}\n      data-flow-editor-selectable=\"false\"\n      onMouseDown={handleMouseDown}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum CommentEditorFormField {\n  Size = 'size',\n  Note = 'note',\n}\n\n/** 编辑器事件 */\nexport enum CommentEditorEvent {\n  /** 内容变更事件 */\n  Change = 'change',\n  /** 多选事件 */\n  MultiSelect = 'multiSelect',\n  /** 单选事件 */\n  Select = 'select',\n  /** 失焦事件 */\n  Blur = 'blur',\n}\n\nexport const CommentEditorDefaultValue = '';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useSize } from './use-size';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/hooks/use-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo } from 'react';\n\nimport {\n  FlowNodeFormData,\n  FormModelV2,\n  WorkflowNodeEntity,\n  useEntityFromContext,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorFormField } from '../constant';\n\nexport const useModel = () => {\n  const node = useEntityFromContext<WorkflowNodeEntity>();\n  const { selected: focused } = useNodeRender();\n\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n\n  const model = useMemo(() => new CommentEditorModel(), []);\n\n  // 同步失焦状态\n  useEffect(() => {\n    if (focused) {\n      return;\n    }\n    model.setFocus(focused);\n  }, [focused, model]);\n\n  // 同步表单值初始化\n  useEffect(() => {\n    const value = formModel.getValueIn<string>(CommentEditorFormField.Note);\n    model.setValue(value); // 设置初始值\n    model.selectEnd(); // 设置初始化光标位置\n  }, [formModel, model]);\n\n  // 同步表单外部值变化：undo/redo/协同\n  useEffect(() => {\n    const disposer = formModel.onFormValuesChange(({ name }) => {\n      if (name !== CommentEditorFormField.Note) {\n        return;\n      }\n      const value = formModel.getValueIn<string>(CommentEditorFormField.Note);\n      model.setValue(value);\n    });\n    return () => disposer.dispose();\n  }, [formModel, model]);\n\n  return model;\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/hooks/use-overflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorModel } from '../model';\nimport { CommentEditorEvent } from '../constant';\n\nexport const useOverflow = (params: { model: CommentEditorModel; height: number }) => {\n  const { model, height } = params;\n  const playground = usePlayground();\n\n  const [overflow, setOverflow] = useState(false);\n\n  const isOverflow = useCallback((): boolean => {\n    if (!model.element) {\n      return false;\n    }\n    return model.element.scrollHeight > model.element.clientHeight;\n  }, [model, height, playground]);\n\n  // 更新 overflow\n  const updateOverflow = useCallback(() => {\n    setOverflow(isOverflow());\n  }, [isOverflow]);\n\n  // 监听高度变化\n  useEffect(() => {\n    updateOverflow();\n  }, [height, updateOverflow]);\n\n  // 监听 change 事件\n  useEffect(() => {\n    const changeDisposer = model.on((params) => {\n      if (params.type !== CommentEditorEvent.Change) {\n        return;\n      }\n      updateOverflow();\n    });\n    return () => {\n      changeDisposer.dispose();\n    };\n  }, [model, updateOverflow]);\n\n  return { overflow, updateOverflow };\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/hooks/use-size.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport {\n  FlowNodeFormData,\n  FormModelV2,\n  FreeOperationType,\n  HistoryService,\n  TransformData,\n  useCurrentEntity,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorFormField } from '../constant';\n\nexport const useSize = () => {\n  const node = useCurrentEntity();\n  const nodeMeta = node.getNodeMeta();\n  const playground = usePlayground();\n  const historyService = useService(HistoryService);\n  const { size = { width: 240, height: 150 } } = nodeMeta;\n  const transform = node.getData(TransformData);\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const formSize = formModel.getValueIn<{ width: number; height: number }>(\n    CommentEditorFormField.Size\n  );\n\n  const [width, setWidth] = useState(formSize?.width ?? size.width);\n  const [height, setHeight] = useState(formSize?.height ?? size.height);\n\n  // 初始化表单值\n  useEffect(() => {\n    const initSize = formModel.getValueIn<{ width: number; height: number }>(\n      CommentEditorFormField.Size\n    );\n    if (!initSize) {\n      formModel.setValueIn(CommentEditorFormField.Size, {\n        width,\n        height,\n      });\n    }\n  }, [formModel, width, height]);\n\n  // 同步表单外部值变化：初始化/undo/redo/协同\n  useEffect(() => {\n    const disposer = formModel.onFormValuesChange(({ name }) => {\n      if (name !== CommentEditorFormField.Size) {\n        return;\n      }\n      const newSize = formModel.getValueIn<{ width: number; height: number }>(\n        CommentEditorFormField.Size\n      );\n      if (!newSize) {\n        return;\n      }\n      setWidth(newSize.width);\n      setHeight(newSize.height);\n    });\n    return () => disposer.dispose();\n  }, [formModel]);\n\n  const onResize = useCallback(() => {\n    const resizeState = {\n      width,\n      height,\n      originalWidth: width,\n      originalHeight: height,\n      positionX: transform.position.x,\n      positionY: transform.position.y,\n      offsetX: 0,\n      offsetY: 0,\n    };\n    const resizing = (delta: { top: number; right: number; bottom: number; left: number }) => {\n      if (!resizeState) {\n        return;\n      }\n\n      const { zoom } = playground.config;\n\n      const top = delta.top / zoom;\n      const right = delta.right / zoom;\n      const bottom = delta.bottom / zoom;\n      const left = delta.left / zoom;\n\n      const minWidth = 120;\n      const minHeight = 80;\n\n      const newWidth = Math.max(minWidth, resizeState.originalWidth + right - left);\n      const newHeight = Math.max(minHeight, resizeState.originalHeight + bottom - top);\n\n      // 如果宽度或高度小于最小值，则不更新偏移量\n      const newOffsetX =\n        (left > 0 || right < 0) && newWidth <= minWidth\n          ? resizeState.offsetX\n          : left / 2 + right / 2;\n      const newOffsetY =\n        (top > 0 || bottom < 0) && newHeight <= minHeight ? resizeState.offsetY : top;\n\n      const newPositionX = resizeState.positionX + newOffsetX;\n      const newPositionY = resizeState.positionY + newOffsetY;\n\n      resizeState.width = newWidth;\n      resizeState.height = newHeight;\n      resizeState.offsetX = newOffsetX;\n      resizeState.offsetY = newOffsetY;\n\n      // 更新状态\n      setWidth(newWidth);\n      setHeight(newHeight);\n\n      // 更新偏移量\n      transform.update({\n        position: {\n          x: newPositionX,\n          y: newPositionY,\n        },\n      });\n    };\n\n    const resizeEnd = () => {\n      historyService.transact(() => {\n        historyService.pushOperation(\n          {\n            type: FreeOperationType.dragNodes,\n            value: {\n              ids: [node.id],\n              value: [\n                {\n                  x: resizeState.positionX + resizeState.offsetX,\n                  y: resizeState.positionY + resizeState.offsetY,\n                },\n              ],\n              oldValue: [\n                {\n                  x: resizeState.positionX,\n                  y: resizeState.positionY,\n                },\n              ],\n            },\n          },\n          {\n            noApply: true,\n          }\n        );\n        formModel.setValueIn(CommentEditorFormField.Size, {\n          width: resizeState.width,\n          height: resizeState.height,\n        });\n      });\n    };\n\n    return {\n      resizing,\n      resizeEnd,\n    };\n  }, [node, width, height, transform, playground, formModel, historyService]);\n\n  return {\n    width,\n    height,\n    onResize,\n  };\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { CommentRender } from './components';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter } from '@flowgram.ai/free-layout-editor';\n\nimport { CommentEditorEventParams } from './type';\nimport { CommentEditorDefaultValue, CommentEditorEvent } from './constant';\n\nexport class CommentEditorModel {\n  private innerValue: string = CommentEditorDefaultValue;\n\n  private emitter: Emitter<CommentEditorEventParams> = new Emitter();\n\n  private editor: HTMLTextAreaElement;\n\n  /** 注册事件 */\n  public on = this.emitter.event;\n\n  /** 获取当前值 */\n  public get value(): string {\n    return this.innerValue;\n  }\n\n  /** 外部设置模型值 */\n  public setValue(value: string = CommentEditorDefaultValue): void {\n    if (!this.initialized) {\n      return;\n    }\n    if (value === this.innerValue) {\n      return;\n    }\n    this.innerValue = value;\n    this.syncEditorValue();\n    this.emitter.fire({\n      type: CommentEditorEvent.Change,\n      value: this.innerValue,\n    });\n  }\n\n  public set element(el: HTMLTextAreaElement) {\n    if (this.initialized) {\n      return;\n    }\n    this.editor = el;\n  }\n\n  /** 获取编辑器 DOM 节点 */\n  public get element(): HTMLTextAreaElement {\n    return this.editor;\n  }\n\n  /** 编辑器聚焦/失焦 */\n  public setFocus(focused: boolean): void {\n    if (!this.initialized) {\n      return;\n    }\n    if (focused && !this.focused) {\n      this.editor.focus();\n    } else if (!focused && this.focused) {\n      this.editor.blur();\n      this.deselect();\n      this.emitter.fire({\n        type: CommentEditorEvent.Blur,\n      });\n    }\n  }\n\n  /** 选择末尾 */\n  public selectEnd(): void {\n    if (!this.initialized) {\n      return;\n    }\n    // 获取文本长度\n    const length = this.editor.value.length;\n    // 将选择范围设置为文本末尾(开始位置和结束位置都是文本长度)\n    this.editor.setSelectionRange(length, length);\n  }\n\n  /** 获取聚焦状态 */\n  public get focused(): boolean {\n    return document.activeElement === this.editor;\n  }\n\n  /** 取消选择文本 */\n  private deselect(): void {\n    const selection: Selection | null = window.getSelection();\n\n    // 清除所有选择区域\n    if (selection) {\n      selection.removeAllRanges();\n    }\n  }\n\n  /** 是否初始化 */\n  private get initialized(): boolean {\n    return Boolean(this.editor);\n  }\n\n  /**\n   * 同步编辑器实例内容\n   * > **NOTICE:** *为确保不影响性能，应仅在外部值变更导致编辑器值与模型值不一致时调用*\n   */\n  private syncEditorValue(): void {\n    if (!this.initialized) {\n      return;\n    }\n    this.editor.value = this.innerValue;\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-comment/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CommentEditorEvent } from './constant';\n\ninterface CommentEditorChangeEvent {\n  type: CommentEditorEvent.Change;\n  value: string;\n}\n\ninterface CommentEditorMultiSelectEvent {\n  type: CommentEditorEvent.MultiSelect;\n}\n\ninterface CommentEditorSelectEvent {\n  type: CommentEditorEvent.Select;\n}\n\ninterface CommentEditorBlurEvent {\n  type: CommentEditorEvent.Blur;\n}\n\nexport type CommentEditorEventParams =\n  | CommentEditorChangeEvent\n  | CommentEditorMultiSelectEvent\n  | CommentEditorSelectEvent\n  | CommentEditorBlurEvent;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-menu/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, type MouseEvent, useCallback, useState } from 'react';\n\nimport { Button, Dropdown } from 'antd';\nimport {\n  WorkflowDragService,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n  delay,\n  useClientContext,\n  useService,\n} from '@flowgram.ai/free-layout-editor';\nimport { NodeIntoContainerService } from '@flowgram.ai/free-container-plugin';\nimport { EllipsisOutlined } from '@ant-design/icons';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { PasteShortcut } from '@editor/shortcuts/paste';\nimport { CopyShortcut } from '@editor/shortcuts/copy';\n\ninterface NodeMenuProps {\n  node: WorkflowNodeEntity;\n  updateTitleEdit: (setEditing: boolean) => void;\n  deleteNode: () => void;\n}\n\nexport const NodeMenu: FC<NodeMenuProps> = ({ node, deleteNode, updateTitleEdit }) => {\n  const [visible, setVisible] = useState(true);\n  const clientContext = useClientContext();\n  const registry = node.getNodeRegistry<FlowNodeRegistry>();\n  const nodeIntoContainerService = useService(NodeIntoContainerService);\n  const selectService = useService(WorkflowSelectService);\n  const dragService = useService(WorkflowDragService);\n  const canMoveOut = nodeIntoContainerService.canMoveOutContainer(node);\n\n  const rerenderMenu = useCallback(() => {\n    // force destroy component - 强制销毁组件触发重新渲染\n    setVisible(false);\n    requestAnimationFrame(() => {\n      setVisible(true);\n    });\n  }, []);\n\n  const handleMoveOut = useCallback(\n    async (e: MouseEvent) => {\n      e.stopPropagation();\n      const sourceParent = node.parent;\n      // move out of container - 移出容器\n      await nodeIntoContainerService.moveOutContainer({ node });\n      // clear invalid lines - 清除非法线条\n      await nodeIntoContainerService.clearInvalidLines({\n        dragNode: node,\n        sourceParent,\n      });\n      rerenderMenu();\n      await delay(16);\n      // select node - 选中节点\n      selectService.selectNode(node);\n      // start drag node - 开始拖拽\n      dragService.startDragSelectedNodes(e);\n    },\n    [nodeIntoContainerService, node, rerenderMenu]\n  );\n\n  const handleCopy = useCallback(\n    (e: React.MouseEvent) => {\n      const copyShortcut = new CopyShortcut(clientContext);\n      const pasteShortcut = new PasteShortcut(clientContext);\n      const data = copyShortcut.toClipboardData([node]);\n      pasteShortcut.apply(data);\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n\n  const handleDelete = useCallback(\n    (e: React.MouseEvent) => {\n      deleteNode();\n      e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n    },\n    [clientContext, node]\n  );\n  const handleEditTitle = useCallback(() => {\n    updateTitleEdit(true);\n  }, [updateTitleEdit]);\n\n  if (!visible) {\n    return <></>;\n  }\n\n  return (\n    <Dropdown\n      trigger={['hover']}\n      placement=\"bottomRight\"\n      menu={{\n        items: [\n          {\n            label: <a onClick={handleEditTitle}>Edit Title</a>,\n            key: 'editTitle',\n          },\n          {\n            label: <a onClick={handleMoveOut}>Move out</a>,\n            key: 'moveOut',\n            disabled: !canMoveOut,\n          },\n          {\n            label: <a onClick={handleCopy}>Create Copy</a>,\n            key: 'createCopy',\n            disabled: registry.meta!.copyDisable === true,\n          },\n          {\n            label: <a onClick={handleDelete}>Delete</a>,\n            key: 'delete',\n            disabled: !!(registry.canDelete?.(clientContext, node) || registry.meta!.deleteDisable),\n          },\n        ],\n      }}\n    >\n      <Button\n        size=\"small\"\n        type=\"text\"\n        icon={<EllipsisOutlined />}\n        onClick={(e) => e.stopPropagation()}\n      />\n    </Dropdown>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-panel/index.scss",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-placeholder {\n  width: 360px;\n\n  background-color: rgba(252, 252, 255, 1);\n  border: 1px solid rgba(68, 83, 130, 0.25);\n  border-radius: 8px;\n  box-shadow:\n    0 4px 12px 0 rgba(0, 0, 0, 2%),\n    0 2px 6px 0 rgba(0, 0, 0, 4%);\n}\n\n.node-placeholder-skeleton {\n  width: 100%;\n  padding: 12px;\n  background-color: rgba(252, 252, 255, 1);\n  border-radius: 8px;\n}\n\n.node-placeholder-hd {\n  display: flex;\n  align-items: center;\n  margin-bottom: 12px;\n}\n\n.node-placeholder-avatar {\n  width: 24px;\n  height: 24px;\n  margin-right: 8px;\n  border-radius: 6px;\n}\n\n.node-placeholder-content {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: 3px;\n}\n\n.node-placeholder-footer {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  gap: 2.5px;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-panel/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './index.scss';\nimport { FC } from 'react';\n\nimport { Popover } from 'antd';\nimport { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';\n\nimport { NodePlaceholder } from './node-placeholder';\nimport { NodeList } from './node-list';\n\nexport const NodePanel: FC<NodePanelRenderProps> = (props) => {\n  const { onSelect, position, onClose, panelProps } = props;\n  // @ts-ignore\n  const { enableNodePlaceholder } = panelProps;\n\n  return (\n    <Popover\n      trigger=\"click\"\n      visible={true}\n      onVisibleChange={(v) => (v ? null : onClose())}\n      content={<NodeList onSelect={onSelect} visibleNodeRegistries={[]} />}\n      getPopupContainer={(triggerNode) => triggerNode.parentElement || document.body}\n      placement=\"right\"\n      // popupAlign={{ offset: [30, 0] }}\n      overlayStyle={{\n        padding: 0,\n      }}\n    >\n      <div\n        style={\n          enableNodePlaceholder\n            ? {\n                position: 'absolute',\n                top: position.y - 61.5,\n                left: position.x,\n                width: 360,\n                height: 100,\n              }\n            : {\n                position: 'absolute',\n                top: position.y,\n                left: position.x,\n                width: 0,\n                height: 0,\n              }\n        }\n      >\n        {enableNodePlaceholder && <NodePlaceholder />}\n      </div>\n    </Popover>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-panel/node-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { FC } from 'react';\n\nimport styled from 'styled-components';\nimport { NodePanelRenderProps } from '@flowgram.ai/free-node-panel-plugin';\nimport { useClientContext } from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeRegistry } from '@editor/typings';\n\n// import { visibleNodeRegistries } from '../../nodes';\n\nconst NodeWrap = styled.div`\n  width: 100%;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  font-size: 19px;\n  padding: 0 15px;\n  &:hover {\n    background-color: hsl(252deg 62% 55% / 9%);\n    color: hsl(252 62% 54.9%);\n  }\n`;\n\nconst NodeLabel = styled.div`\n  font-size: 12px;\n  margin-left: 10px;\n`;\n\ninterface NodeProps {\n  label: string;\n  icon: JSX.Element;\n  onClick: React.MouseEventHandler<HTMLDivElement>;\n  disabled: boolean;\n}\n\nfunction Node(props: NodeProps) {\n  return (\n    <NodeWrap\n      data-testid={`demo-free-node-list-${props.label}`}\n      onClick={props.disabled ? undefined : props.onClick}\n      style={props.disabled ? { opacity: 0.3 } : {}}\n    >\n      <div style={{ fontSize: 14 }}>{props.icon}</div>\n      <NodeLabel>{props.label}</NodeLabel>\n    </NodeWrap>\n  );\n}\n\nconst NodesWrap = styled.div`\n  max-height: 500px;\n  overflow: auto;\n  &::-webkit-scrollbar {\n    display: none;\n  }\n`;\n\ninterface NodeListProps {\n  onSelect: NodePanelRenderProps['onSelect'];\n  visibleNodeRegistries: FlowNodeRegistry[];\n}\n\nexport const NodeList: FC<NodeListProps> = (props) => {\n  const { onSelect } = props;\n  const context = useClientContext();\n  const handleClick = (e: React.MouseEvent, registry: FlowNodeRegistry) => {\n    const json = registry.onAdd?.(context);\n    onSelect({\n      nodeType: registry.type as string,\n      selectEvent: e,\n      nodeJSON: json,\n    });\n  };\n  return (\n    <NodesWrap style={{ width: 80 * 2 + 20 }}>\n      {props.visibleNodeRegistries.map((registry) => (\n        <Node\n          key={registry.type}\n          disabled={!(registry.canAdd?.(context) ?? true)}\n          icon={\n            <img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info?.icon.src} />\n          }\n          label={registry.type as string}\n          onClick={(e) => handleClick(e, registry)}\n        />\n      ))}\n    </NodesWrap>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-panel/node-placeholder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Skeleton } from 'antd';\n\nexport const NodePlaceholder = () => (\n  <div className=\"node-placeholder\" data-testid=\"workflow.detail.node-panel.placeholder\">\n    <Skeleton\n      className=\"node-placeholder-skeleton\"\n      loading={true}\n      active={true}\n      // placeholder={\n      //   <div className=\"\">\n      //     <div className=\"node-placeholder-hd\">\n      //       <Skeleton.Avatar shape=\"square\" className=\"node-placeholder-avatar\" />\n      //       <Skeleton.Title style={{ width: 141 }} />\n      //     </div>\n      //     <div className=\"node-placeholder-content\">\n      //       <div className=\"node-placeholder-footer\">\n      //         <Skeleton.Title style={{ width: 85 }} />\n      //         <Skeleton.Title style={{ width: 241 }} />\n      //       </div>\n      //       <Skeleton.Title style={{ width: 220 }} />\n      //     </div>\n      //   </div>\n      // }\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport classnames from 'classnames';\nimport {\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      className={classnames(\n        'workflow-node-render min-w-[320px] p-4 bg-node-bg rounded-node-radius shadow-[var(--node-shadow)] border border-solid border-node-border',\n        {\n          'border-node-selected ': selected,\n        }\n      )}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/selector-box-popover/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FunctionComponent } from 'react';\n\nimport { Button, Tooltip } from 'antd';\nimport { SelectorBoxPopoverProps } from '@flowgram.ai/free-layout-editor';\nimport { WorkflowGroupCommand } from '@flowgram.ai/free-group-plugin';\nimport { CopyOutlined, DeleteOutlined, ExpandAltOutlined, ShrinkOutlined } from '@ant-design/icons';\n\nimport { FlowCommandId } from '@editor/shortcuts/constants';\nimport { IconGroup } from '../group';\n\nconst BUTTON_HEIGHT = 24;\n\nexport const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({\n  bounds,\n  children,\n  flowSelectConfig,\n  commandRegistry,\n}) => (\n  <>\n    <div\n      style={{\n        position: 'absolute',\n        left: bounds.right,\n        top: bounds.top,\n        transform: 'translate(-100%, -100%)',\n      }}\n      onMouseDown={(e) => {\n        e.stopPropagation();\n      }}\n    >\n      {/* <ButtonGroup\n        size=\"small\"\n        style={{ display: 'flex', flexWrap: 'nowrap', height: BUTTON_HEIGHT }}\n      > */}\n      <Tooltip title={'Collapse'}>\n        <Button\n          icon={<ShrinkOutlined />}\n          style={{ height: BUTTON_HEIGHT }}\n          type=\"primary\"\n          // theme=\"solid\"\n          onMouseDown={(e) => {\n            commandRegistry.executeCommand(FlowCommandId.COLLAPSE);\n          }}\n        />\n      </Tooltip>\n\n      <Tooltip title={'Expand'}>\n        <Button\n          icon={<ExpandAltOutlined />}\n          style={{ height: BUTTON_HEIGHT }}\n          type=\"primary\"\n          // theme=\"solid\"\n          onMouseDown={(e) => {\n            commandRegistry.executeCommand(FlowCommandId.EXPAND);\n          }}\n        />\n      </Tooltip>\n\n      <Tooltip title={'Create Group'}>\n        <Button\n          icon={<IconGroup size={14} />}\n          style={{ height: BUTTON_HEIGHT }}\n          type=\"primary\"\n          // theme=\"solid\"\n          onClick={() => {\n            commandRegistry.executeCommand(WorkflowGroupCommand.Group);\n          }}\n        />\n      </Tooltip>\n\n      <Tooltip title={'Copy'}>\n        <Button\n          icon={<CopyOutlined />}\n          style={{ height: BUTTON_HEIGHT }}\n          type=\"primary\"\n          // theme=\"solid\"\n          onClick={() => {\n            commandRegistry.executeCommand(FlowCommandId.COPY);\n          }}\n        />\n      </Tooltip>\n\n      <Tooltip title={'Delete'}>\n        <Button\n          type=\"primary\"\n          // theme=\"solid\"\n          icon={<DeleteOutlined />}\n          style={{ height: BUTTON_HEIGHT }}\n          onClick={() => {\n            commandRegistry.executeCommand(FlowCommandId.DELETE);\n          }}\n        />\n      </Tooltip>\n      {/* </ButtonGroup> */}\n    </div>\n    <div>{children}</div>\n  </>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/sidebar/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { SidebarProvider } from './sidebar-provider';\nexport { SidebarRenderer } from './sidebar-renderer';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/sidebar/sidebar-node-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, useNodeRender } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRenderContext } from '@editor/context';\n\nexport function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {\n  const { node } = props;\n  const nodeRender = useNodeRender(node);\n\n  return (\n    <NodeRenderContext.Provider value={nodeRender}>\n      {nodeRender.form?.render()}\n    </NodeRenderContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/sidebar/sidebar-provider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport { SidebarContext } from '@editor/context';\n\nexport function SidebarProvider({ children }: { children: React.ReactNode }) {\n  const [nodeId, setNodeId] = useState<string | undefined>();\n  return (\n    <SidebarContext.Provider value={{ visible: !!nodeId, nodeId, setNodeId }}>\n      {children}\n    </SidebarContext.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/sidebar/sidebar-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useContext, useEffect, useMemo } from 'react';\n\nimport { Drawer } from 'antd';\nimport {\n  PlaygroundEntityContext,\n  useClientContext,\n  useRefresh,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeMeta } from '@editor/typings';\nimport { IsSidebarContext, SidebarContext } from '@editor/context';\nimport { SidebarNodeRenderer } from './sidebar-node-renderer';\n\nexport const SidebarRenderer = () => {\n  const { nodeId, setNodeId } = useContext(SidebarContext);\n  const { selection, playground, document } = useClientContext();\n  const refresh = useRefresh();\n  const handleClose = useCallback(() => {\n    setNodeId(undefined);\n  }, []);\n  const node = nodeId ? document.getNode(nodeId) : undefined;\n  /**\n   * Listen readonly\n   */\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => {\n      handleClose();\n      refresh();\n    });\n    return () => disposable.dispose();\n  }, [playground]);\n  /**\n   * Listen selection\n   */\n  useEffect(() => {\n    const toDispose = selection.onSelectionChanged(() => {\n      /**\n       * 如果没有选中任何节点，则自动关闭侧边栏\n       * If no node is selected, the sidebar is automatically closed\n       */\n      if (selection.selection.length === 0) {\n        handleClose();\n      } else if (selection.selection.length === 1 && selection.selection[0] !== node) {\n        handleClose();\n      }\n    });\n    return () => toDispose.dispose();\n  }, [selection, handleClose, node]);\n  /**\n   * Close when node disposed\n   */\n  useEffect(() => {\n    if (node) {\n      const toDispose = node.onDispose(() => {\n        setNodeId(undefined);\n      });\n      return () => toDispose.dispose();\n    }\n    return () => {};\n  }, [node]);\n\n  const visible = useMemo(() => {\n    if (!node) {\n      return false;\n    }\n    const { sidebarDisable = false } = node.getNodeMeta<FlowNodeMeta>();\n    return !sidebarDisable;\n  }, [node]);\n\n  if (playground.config.readonly) {\n    return null;\n  }\n  /**\n   * Add \"key\" to rerender the sidebar when the node changes\n   */\n  const content =\n    node && visible ? (\n      <PlaygroundEntityContext.Provider key={node.id} value={node}>\n        <SidebarNodeRenderer node={node} />\n      </PlaygroundEntityContext.Provider>\n    ) : null;\n\n  return (\n    <Drawer mask={false} open={visible} onClose={handleClose}>\n      <IsSidebarContext.Provider value={true}>{content}</IsSidebarContext.Provider>\n    </Drawer>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n{\n  /* TODO */\n}\n\n// import { WorkflowDocument, useService } from '@flowgram.ai/free-layout-editor';\n// import { useState } from 'react';\n\nexport const Tools = () => (\n  // const [isLoading, setIsLoading] = useState(false);\n  // const document = useService(WorkflowDocument);\n\n  // const handleRun = async () => {\n  //   try {\n  //     setIsLoading(true);\n  //     const response = await fetch('/api/runtime', {\n  //       method: 'POST',\n  //       headers: {\n  //         'Content-Type': 'application/json',\n  //       },\n  //       body: JSON.stringify({\n  //         json: document.toJSON(),\n  //       }),\n  //     });\n  //     const data = await response.json();\n\n  //     if (!data.success) {\n  //       throw new Error(data.error || 'process failed');\n  //     }\n\n  //     console.log('run success', data.data);\n  //   } catch (error) {\n  //     console.error(error instanceof Error ? error.message : 'run failed');\n  //   } finally {\n  //     setIsLoading(false);\n  //   }\n  // };\n\n  <div className=\"mastra-workflow-tools absolute z-[999] bottom-4 left-1/2\">\n    {/* <button\n        className=\"bg-blue-400 cursor-pointer active:bg-blue-500 p-2 rounded\"\n        onClick={handleRun}\n        disabled={isLoading}\n      >\n        <p className=\"text-white\">TEST RUN</p>\n      </button> */}\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeRenderContext } from './node-render-context';\nexport { SidebarContext, IsSidebarContext } from './sidebar-context';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/context/node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { NodeRenderReturnType } from '@flowgram.ai/free-layout-editor';\n\ninterface INodeRenderContext extends NodeRenderReturnType {}\n\n/** 业务自定义节点上下文 */\nexport const NodeRenderContext = React.createContext<INodeRenderContext>({} as INodeRenderContext);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/context/sidebar-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const SidebarContext = React.createContext<{\n  visible: boolean;\n  nodeId?: string;\n  setNodeId: (node: string | undefined) => void;\n}>({ visible: false, setNodeId: () => {} });\n\nexport const IsSidebarContext = React.createContext<boolean>(false);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/data/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 381.75,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            query: {\n              type: 'string',\n              default: 'Hello Flow.',\n            },\n            enable: {\n              type: 'boolean',\n              default: true,\n            },\n            array_obj: {\n              type: 'array',\n              items: {\n                type: 'object',\n                properties: {\n                  int: {\n                    type: 'number',\n                  },\n                  str: {\n                    type: 'string',\n                  },\n                },\n              },\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 640,\n          y: 363.25,\n        },\n      },\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            key: 'if_0',\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'query'],\n              },\n              operator: 'contains',\n              right: {\n                type: 'constant',\n                content: 'Hello Flow.',\n              },\n            },\n          },\n          {\n            key: 'if_f0rOAt',\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'enable'],\n              },\n              operator: 'is_true',\n            },\n          },\n        ],\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 2220,\n          y: 381.75,\n        },\n      },\n      data: {\n        title: 'End',\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'loop_H8M3U',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 1020,\n          y: 547.96875,\n        },\n      },\n      data: {\n        title: 'Loop_2',\n        batchFor: {\n          type: 'ref',\n          content: ['start_0', 'array_obj'],\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n      blocks: [\n        {\n          id: 'llm_CBdCg',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 180,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_4',\n            inputsValues: {\n              modelType: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'constant',\n                content: 'You are an AI assistant.',\n              },\n              prompt: {\n                type: 'constant',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelType', 'temperature', 'prompt'],\n              properties: {\n                modelType: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                },\n                prompt: {\n                  type: 'string',\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'llm_gZafu',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 640,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_5',\n            inputsValues: {\n              modelType: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'constant',\n                content: 'You are an AI assistant.',\n              },\n              prompt: {\n                type: 'constant',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelType', 'temperature', 'prompt'],\n              properties: {\n                modelType: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                },\n                prompt: {\n                  type: 'string',\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'llm_CBdCg',\n          targetNodeID: 'llm_gZafu',\n        },\n      ],\n    },\n    {\n      id: '159623',\n      type: 'comment',\n      meta: {\n        position: {\n          x: 640,\n          y: 522.46875,\n        },\n      },\n      data: {\n        size: {\n          width: 240,\n          height: 150,\n        },\n        note: 'hi ~\\n\\nthis is a comment node\\n\\n- flowgram.ai',\n      },\n    },\n    {\n      id: 'group_V-_st',\n      type: 'group',\n      meta: {\n        position: {\n          x: 1020,\n          y: 96.25,\n        },\n      },\n      data: {\n        title: 'LLM_Group',\n        color: 'Violet',\n      },\n      blocks: [\n        {\n          id: 'llm_0',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 640,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_0',\n            inputsValues: {\n              modelType: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'constant',\n                content: 'You are an AI assistant.',\n              },\n              prompt: {\n                type: 'constant',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelType', 'temperature', 'prompt'],\n              properties: {\n                modelType: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                },\n                prompt: {\n                  type: 'string',\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'llm_l_TcE',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 180,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_1',\n            inputsValues: {\n              modelType: {\n                type: 'constant',\n                content: 'gpt-3.5-turbo',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.5,\n              },\n              systemPrompt: {\n                type: 'constant',\n                content: 'You are an AI assistant.',\n              },\n              prompt: {\n                type: 'constant',\n                content: '',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelType', 'temperature', 'prompt'],\n              properties: {\n                modelType: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                },\n                prompt: {\n                  type: 'string',\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'llm_l_TcE',\n          targetNodeID: 'llm_0',\n        },\n        {\n          sourceNodeID: 'llm_0',\n          targetNodeID: 'end_0',\n        },\n        {\n          sourceNodeID: 'condition_0',\n          targetNodeID: 'llm_l_TcE',\n          sourcePortID: 'if_0',\n        },\n      ],\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_l_TcE',\n      sourcePortID: 'if_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'loop_H8M3U',\n      sourcePortID: 'if_f0rOAt',\n    },\n    {\n      sourceNodeID: 'llm_0',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'loop_H8M3U',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/data/node-registries.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { StartNodeRegistry } from '@editor/nodes/start';\nimport { LoopNodeRegistry } from '@editor/nodes/loop';\nimport { LLMNodeRegistry } from '@editor/nodes/llm';\nimport { EndNodeRegistry } from '@editor/nodes/end';\nimport { ConditionNodeRegistry } from '@editor/nodes/condition';\nimport { CommentNodeRegistry } from '@editor/nodes/comment';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,\n  StartNodeRegistry,\n  EndNodeRegistry,\n  LLMNodeRegistry,\n  LoopNodeRegistry,\n  CommentNodeRegistry,\n  {\n    type: 'start2',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/feedback.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { FieldError, FieldState, FieldWarning } from '@flowgram.ai/free-layout-editor';\n\ninterface StatePanelProps {\n  errors?: FieldState['errors'];\n  warnings?: FieldState['warnings'];\n  invalid?: boolean;\n}\n\nconst Error = styled.span`\n  font-size: 12px;\n  color: red;\n`;\n\nconst Warning = styled.span`\n  font-size: 12px;\n  color: orange;\n`;\n\nexport const Feedback = ({ errors, warnings, invalid }: StatePanelProps) => {\n  const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {\n    if (!fs) return null;\n    return fs.map((f) => <span key={f.name}>{f.message}</span>);\n  };\n  return (\n    <div>\n      <div>\n        <Error>{renderFeedbacks(errors)}</Error>\n      </div>\n      <div>\n        <Warning>{renderFeedbacks(warnings)}</Warning>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-content/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport React from 'react';\n\nimport { FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { useIsSidebar, useNodeRenderContext } from '@editor/hooks';\nimport { FormTitleDescription, FormWrapper } from './styles';\n\n/**\n * @param props\n * @constructor\n */\nexport function FormContent(props: { children?: React.ReactNode }) {\n  const { node, expanded } = useNodeRenderContext();\n  const isSidebar = useIsSidebar();\n  const registry = node?.getNodeRegistry<FlowNodeRegistry>();\n  return (\n    <FormWrapper>\n      <>\n        {isSidebar && <FormTitleDescription>{registry.info?.description}</FormTitleDescription>}\n        {(expanded || isSidebar) && props.children}\n      </>\n    </FormWrapper>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-content/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FormWrapper = styled.div`\n  box-sizing: border-box;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  background-color: rgba(0, 0, 0, 0.02);\n  padding: 0 12px 12px;\n`;\n\nexport const FormTitleDescription = styled.div`\n  font-size: 12px;\n  line-height: 20px;\n  padding: 0px 4px;\n  word-break: break-all;\n  white-space: break-spaces;\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-header/index.scss",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.node-form-header {\n  &-title {\n    font-size: 20px;\n    flex: 1;\n    width: 0;\n    .title-text {\n      display: flex;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-header/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport './index.scss';\nimport { useState } from 'react';\n\nimport { Button } from 'antd';\nimport { CommandService, useClientContext } from '@flowgram.ai/free-layout-editor';\nimport { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons';\n\nimport { FlowCommandId } from '@editor/shortcuts';\nimport { useIsSidebar, useNodeRenderContext } from '@editor/hooks';\nimport { NodeMenu } from '@editor/components/node-menu';\nimport { getIcon } from './utils';\nimport { TitleInput } from './title-input';\nimport { Header, Operators } from './styles';\n\nexport function FormHeader() {\n  const { node, expanded, toggleExpand, readonly } = useNodeRenderContext();\n  const [titleEdit, updateTitleEdit] = useState<boolean>(false);\n  const ctx = useClientContext();\n  const isSidebar = useIsSidebar();\n  const handleExpand = (e: React.MouseEvent) => {\n    toggleExpand();\n    e.stopPropagation(); // Disable clicking prevents the sidebar from opening\n  };\n  const handleDelete = () => {\n    ctx.get<CommandService>(CommandService).executeCommand(FlowCommandId.DELETE, [node]);\n  };\n\n  return (\n    <Header className=\"node-form-header\">\n      {getIcon(node)}\n      <TitleInput readonly={readonly} updateTitleEdit={updateTitleEdit} titleEdit={titleEdit} />\n      {node?.renderData.expandable && !isSidebar && (\n        <Button\n          type=\"text\"\n          icon={expanded ? <CaretDownOutlined /> : <CaretUpOutlined />}\n          size=\"small\"\n          onClick={handleExpand}\n        />\n      )}\n      {readonly ? undefined : (\n        <Operators>\n          <NodeMenu node={node} deleteNode={handleDelete} updateTitleEdit={updateTitleEdit} />\n        </Operators>\n      )}\n    </Header>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-header/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Header = styled.div`\n  box-sizing: border-box;\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  width: 100%;\n  column-gap: 8px;\n  border-radius: 8px 8px 0 0;\n  cursor: move;\n\n  background: linear-gradient(#f2f2ff 0%, rgba(0, 0, 0, 0.02) 100%);\n  overflow: hidden;\n\n  padding: 8px;\n`;\n\nexport const Icon = styled.img`\n  width: 24px;\n  height: 24px;\n  scale: 0.8;\n  border-radius: 4px;\n`;\n\nexport const Operators = styled.div`\n  display: flex;\n  align-items: center;\n  column-gap: 4px;\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-header/title-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useRef } from 'react';\n\nimport { Input, Typography } from 'antd';\nimport { Field, FieldRenderProps } from '@flowgram.ai/free-layout-editor';\n\nimport { Feedback } from '../feedback';\n// import { Title } from \"./styles\";\n\nconst { Text } = Typography;\n\nexport function TitleInput(props: {\n  readonly: boolean;\n  titleEdit: boolean;\n  updateTitleEdit: (setEdit: boolean) => void;\n}): JSX.Element {\n  const { readonly, titleEdit, updateTitleEdit } = props;\n  const ref = useRef<any>();\n  const titleEditing = titleEdit && !readonly;\n  useEffect(() => {\n    if (titleEditing) {\n      ref.current?.focus();\n    }\n  }, [titleEditing]);\n\n  return (\n    <div className=\"node-form-header-title\">\n      <Field name=\"title\">\n        {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n          <div className=\"title-text\">\n            {titleEditing ? (\n              <Input\n                value={value}\n                onChange={onChange}\n                ref={ref}\n                onBlur={() => updateTitleEdit(false)}\n              />\n            ) : (\n              <Text>{value}</Text>\n            )}\n            <Feedback errors={fieldState?.errors} />\n          </div>\n        )}\n      </Field>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-header/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeEntity } from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { Icon } from './styles';\n\nexport const getIcon = (node: FlowNodeEntity) => {\n  const icon = node?.getNodeRegistry<FlowNodeRegistry>().info?.icon;\n  if (!icon) return null;\n  return <Icon src={icon.src} />;\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { DynamicValueInput } from '@flowgram.ai/form-antd-materials';\n\nimport { JsonSchema } from '@editor/typings';\nimport { useNodeRenderContext } from '@editor/hooks';\nimport { FormItem } from '../form-item';\nimport { Feedback } from '../feedback';\n\nexport function FormInputs() {\n  const { readonly } = useNodeRenderContext();\n  return (\n    <Field<JsonSchema> name=\"inputs\">\n      {({ field: inputsField }) => {\n        const required = inputsField.value?.required || [];\n        const properties = inputsField.value?.properties;\n        if (!properties) {\n          return <></>;\n        }\n        const content = Object.keys(properties).map((key) => {\n          const property = properties[key];\n          return (\n            <Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>\n              {({ field, fieldState }) => (\n                <FormItem\n                  name={key}\n                  type={property.type as string}\n                  required={required.includes(key)}\n                >\n                  <DynamicValueInput\n                    value={field.value}\n                    onChange={field.onChange}\n                    readonly={readonly}\n                    hasError={Object.keys(fieldState?.errors || {}).length > 0}\n                    schema={property}\n                  />\n                  <Feedback errors={fieldState?.errors} />\n                </FormItem>\n              )}\n            </Field>\n          );\n        });\n        return <>{content}</>;\n      }}\n    </Field>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import styled from 'styled-components';\n\n// TODO\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-item/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.form-item-type-tag {\n  color: inherit;\n  padding: 0 2px;\n  height: 18px;\n  width: 18px;\n  vertical-align: middle;\n  flex-shrink: 0;\n  flex-grow: 0;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-item/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { Tooltip, Typography } from 'antd';\n\nimport { TypeTag } from '../type-tag';\n\nimport './index.css';\n\nconst { Text } = Typography;\n\ninterface FormItemProps {\n  children: React.ReactNode;\n  name: string;\n  type: string;\n  required?: boolean;\n  description?: string;\n  labelWidth?: number;\n}\nexport function FormItem({\n  children,\n  name,\n  required,\n  description,\n  type,\n  labelWidth,\n}: FormItemProps): JSX.Element {\n  const renderTitle = useCallback(\n    (showTooltip?: boolean) => (\n      <div style={{ width: '0', display: 'flex', flex: '1' }}>\n        <Text style={{ width: '100%' }}>{name}</Text>\n        {required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}\n      </div>\n    ),\n    []\n  );\n  return (\n    <div\n      style={{\n        fontSize: 12,\n        marginBottom: 6,\n        width: '100%',\n        position: 'relative',\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        gap: 8,\n      }}\n    >\n      <div\n        style={{\n          justifyContent: 'center',\n          alignItems: 'center',\n          width: labelWidth || 118,\n          position: 'relative',\n          display: 'flex',\n          columnGap: 4,\n          flexShrink: 0,\n        }}\n      >\n        <TypeTag className=\"form-item-type-tag\" type={type} />\n        {description ? <Tooltip title={description}>{renderTitle()}</Tooltip> : renderTitle(true)}\n      </div>\n\n      <div\n        style={{\n          flexGrow: 1,\n          minWidth: 0,\n        }}\n      >\n        {children}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-outputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { JsonSchema } from '@editor/typings';\nimport { useIsSidebar } from '@editor/hooks';\nimport { TypeTag } from '../type-tag';\nimport { FormOutputsContainer } from './styles';\n\nexport function FormOutputs() {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return null;\n  }\n  return (\n    <Field<JsonSchema> name={'outputs'}>\n      {({ field }) => {\n        const properties = field.value?.properties;\n        if (properties) {\n          const content = Object.keys(properties).map((key) => {\n            const property = properties[key];\n            return <TypeTag key={key} name={key} type={property.type as string} />;\n          });\n          return <FormOutputsContainer>{content}</FormOutputsContainer>;\n        }\n        return <></>;\n      }}\n    </Field>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/form-outputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FormOutputsContainer = styled.div`\n  display: flex;\n  gap: 6px;\n  flex-wrap: wrap;\n  padding: 8px 0 0;\n  width: 100%;\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './feedback';\nexport * from './form-content';\nexport * from './form-outputs';\nexport * from './form-inputs';\nexport * from './form-header';\nexport * from './form-item';\nexport * from './type-tag';\nexport * from './properties-edit';\nexport * from './value-display';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/properties-edit/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport { Button } from 'antd';\nimport { PlusCircleOutlined } from '@ant-design/icons';\n\nimport { JsonSchema } from '@editor/typings';\nimport { useNodeRenderContext } from '@editor/hooks';\nimport { PropertyEdit } from './property-edit';\n\nexport interface PropertiesEditProps {\n  value?: Record<string, JsonSchema>;\n  onChange: (value: Record<string, JsonSchema>) => void;\n  useFx?: boolean;\n}\n\nexport const PropertiesEdit: React.FC<PropertiesEditProps> = (props) => {\n  const value = (props.value || {}) as Record<string, JsonSchema>;\n  const { readonly } = useNodeRenderContext();\n  const [newProperty, updateNewPropertyFromCache] = useState<{\n    key: string;\n    value: JsonSchema;\n  }>({\n    key: '',\n    value: { type: 'string' },\n  });\n  const [newPropertyVisible, setNewPropertyVisible] = useState<boolean>();\n  const clearCache = () => {\n    updateNewPropertyFromCache({ key: '', value: { type: 'string' } });\n    setNewPropertyVisible(false);\n  };\n  // 替换对象的key时，保持顺序\n  const replaceKeyAtPosition = (\n    obj: Record<string, any>,\n    oldKey: string,\n    newKey: string,\n    newValue: any\n  ) => {\n    const keys = Object.keys(obj);\n    const index = keys.indexOf(oldKey);\n\n    if (index === -1) {\n      // 如果 oldKey 不存在，直接添加到末尾\n      return { ...obj, [newKey]: newValue };\n    }\n\n    // 在原位置替换\n    const newKeys = [...keys.slice(0, index), newKey, ...keys.slice(index + 1)];\n\n    return newKeys.reduce((acc, key) => {\n      if (key === newKey) {\n        acc[key] = newValue;\n      } else {\n        acc[key] = obj[key];\n      }\n      return acc;\n    }, {} as Record<string, any>);\n  };\n\n  const updateProperty = (\n    propertyValue: JsonSchema,\n    propertyKey: string,\n    newPropertyKey?: string\n  ) => {\n    if (newPropertyKey) {\n      const orderedValue = replaceKeyAtPosition(value, propertyKey, newPropertyKey, propertyValue);\n      props.onChange(orderedValue);\n    } else {\n      const newValue = { ...value };\n      newValue[propertyKey] = propertyValue;\n      props.onChange(newValue);\n    }\n  };\n  const updateNewProperty = (\n    propertyValue: JsonSchema,\n    propertyKey: string,\n    newPropertyKey?: string\n  ) => {\n    // const newValue = { ...value }\n    if (newPropertyKey) {\n      if (!(newPropertyKey in value)) {\n        updateProperty(propertyValue, propertyKey, newPropertyKey);\n      }\n      clearCache();\n    } else {\n      updateNewPropertyFromCache({\n        key: newPropertyKey || propertyKey,\n        value: propertyValue,\n      });\n    }\n  };\n  return (\n    <>\n      {Object.keys(props.value || {}).map((key) => {\n        const property = (value[key] || {}) as JsonSchema;\n        return (\n          <PropertyEdit\n            key={key}\n            propertyKey={key}\n            useFx={props.useFx}\n            value={property}\n            disabled={readonly}\n            onChange={updateProperty}\n            onDelete={() => {\n              const newValue = { ...value };\n              delete newValue[key];\n              props.onChange(newValue);\n            }}\n          />\n        );\n      })}\n      {newPropertyVisible && (\n        <PropertyEdit\n          propertyKey={newProperty.key}\n          value={newProperty.value}\n          useFx={props.useFx}\n          onChange={updateNewProperty}\n          onDelete={() => {\n            const key = newProperty.key;\n            // after onblur\n            setTimeout(() => {\n              const newValue = { ...value };\n              delete newValue[key];\n              props.onChange(newValue);\n              clearCache();\n            }, 10);\n          }}\n        />\n      )}\n      {!readonly && (\n        <div>\n          <Button\n            // theme=\"borderless\"\n            icon={<PlusCircleOutlined />}\n            onClick={() => setNewPropertyVisible(true)}\n          >\n            Add\n          </Button>\n        </div>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/properties-edit/property-edit.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useLayoutEffect, useState } from 'react';\n\nimport { Button, Input } from 'antd';\nimport { DynamicValueInput, TypeSelector } from '@flowgram.ai/form-antd-materials';\nimport { CloseCircleOutlined } from '@ant-design/icons';\n\nimport { JsonSchema } from '@editor/typings';\nimport { LeftColumn, Row } from './styles';\n\nexport interface PropertyEditProps {\n  propertyKey: string;\n  value: JsonSchema;\n  useFx?: boolean;\n  disabled?: boolean;\n  onChange: (value: JsonSchema, propertyKey: string, newPropertyKey?: string) => void;\n  onDelete?: () => void;\n}\n\nexport const PropertyEdit: React.FC<PropertyEditProps> = (props) => {\n  const { value, disabled } = props;\n  const [inputKey, updateKey] = useState(props.propertyKey);\n  const updateProperty = (key: keyof JsonSchema, val: any) => {\n    value[key] = val;\n    props.onChange(value, props.propertyKey);\n  };\n\n  const partialUpdateProperty = (val?: Partial<JsonSchema>) => {\n    props.onChange({ ...value, ...val }, props.propertyKey);\n  };\n\n  useLayoutEffect(() => {\n    updateKey(props.propertyKey);\n  }, [props.propertyKey]);\n  return (\n    <Row>\n      <LeftColumn>\n        <TypeSelector\n          value={value}\n          disabled={disabled}\n          style={{\n            position: 'absolute',\n            top: 2,\n            left: 4,\n            zIndex: 1,\n            padding: '0 5px',\n            height: 20,\n          }}\n          onChange={(val) => partialUpdateProperty(val)}\n        />\n        <Input\n          value={inputKey}\n          disabled={disabled}\n          size=\"small\"\n          // onChange={(v) => updateKey(v.trim())}\n          onBlur={() => {\n            if (inputKey !== '') {\n              props.onChange(value, props.propertyKey, inputKey);\n            } else {\n              updateKey(props.propertyKey);\n            }\n          }}\n          style={{ paddingLeft: 26 }}\n        />\n      </LeftColumn>\n      {\n        <DynamicValueInput\n          value={value.default}\n          onChange={(val) => updateProperty('default', val)}\n          schema={value}\n          style={{ flexGrow: 1 }}\n        />\n      }\n      {props.onDelete && !disabled && (\n        <Button\n          style={{ marginLeft: 5, position: 'relative', top: 2 }}\n          size=\"small\"\n          // theme=\"borderless\"\n          icon={<CloseCircleOutlined />}\n          onClick={props.onDelete}\n        />\n      )}\n    </Row>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/properties-edit/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Row = styled.div`\n  display: flex;\n  justify-content: flex-start;\n  align-items: center;\n  font-size: 12px;\n  margin-bottom: 6px;\n`;\n\nexport const LeftColumn = styled.div`\n  width: 120px;\n  margin-right: 5px;\n  position: relative;\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/type-tag.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { Tag, Tooltip } from 'antd';\nimport { ArrayIcons, VariableTypeIcons } from '@flowgram.ai/form-antd-materials';\n\ninterface PropsType {\n  name?: string | JSX.Element;\n  type: string;\n  className?: string;\n  isArray?: boolean;\n}\n\nconst TooltipContainer = styled.div`\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  column-gap: 6px;\n`;\n\nexport function TypeTag({ name, type, isArray, className }: PropsType) {\n  const icon = isArray ? ArrayIcons[type] : VariableTypeIcons[type];\n  return (\n    <Tooltip\n      title={\n        <TooltipContainer>\n          {icon} {type}\n        </TooltipContainer>\n      }\n    >\n      <Tag\n        className={className}\n        style={{\n          maxWidth: 450,\n          backgroundColor: '#fff',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          marginInlineEnd: 0,\n          width: name ? undefined : 18,\n          height: 18,\n          paddingInline: name ? undefined : 3,\n        }}\n      >\n        {icon}\n        {name && (\n          <span\n            style={{\n              display: 'inline-block',\n              marginLeft: 4,\n              marginTop: -1,\n              overflow: 'hidden',\n              textOverflow: 'ellipsis',\n            }}\n          >\n            {' '}\n            {name}\n          </span>\n        )}\n      </Tag>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/value-display/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import { TypeTag } from '../type-tag'\nimport { ValueDisplayStyle } from './styles';\n\nexport interface ValueDisplayProps {\n  value: string;\n  placeholder?: string;\n  hasError?: boolean;\n}\n\nexport const ValueDisplay: React.FC<ValueDisplayProps> = (props) => (\n  <ValueDisplayStyle className={props.hasError ? 'has-error' : ''}>\n    {props.value}\n    {props.value === undefined || props.value === '' ? (\n      <span>{props.placeholder || '--'}</span>\n    ) : null}\n  </ValueDisplayStyle>\n);\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/form-components/value-display/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const ValueDisplayStyle = styled.div`\n  padding-left: 12px;\n  width: 100%;\n  min-height: 24px;\n  line-height: 24px;\n  display: flex;\n  align-items: center;\n  &.has-error {\n    outline: red solid 1px;\n  }\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useEditorProps } from './use-editor-props';\nexport { useNodeRenderContext } from './use-node-render-context';\nexport { useIsSidebar } from './use-is-sidebar';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin';\nimport { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\nimport { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';\nimport { createFreeGroupPlugin } from '@flowgram.ai/free-group-plugin';\nimport { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\n\nimport { onDragLineEnd } from '@editor/utils';\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { createContextMenuPlugin } from '@editor/plugins';\nimport { defaultFormMeta } from '@editor/nodes/default-form-meta';\nimport { WorkflowNodeType } from '@editor/nodes';\nimport { BaseNode } from '@editor/components/base-node';\nimport {\n  CommentRender,\n  GroupNodeRender,\n  LineAddButton,\n  NodePanel,\n  SelectorBoxPopover,\n} from '@editor/components';\n\nexport const useEditorProps = (initialData: WorkflowJSON, nodeRegistries: FlowNodeRegistry[]) =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n            size: {\n              width: 360,\n              height: 70,\n            },\n          },\n          formMeta: defaultFormMeta,\n        };\n      },\n      onDragLineEnd,\n      selectBox: {\n        SelectorBoxPopover,\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: BaseNode,\n        renderNodes: {\n          [WorkflowNodeType.Comment]: CommentRender,\n        },\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        // console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Variable engine enable\n       */\n      variableEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Line render plugin\n         * 连线渲染插件\n         */\n        createFreeLinesPlugin({\n          renderInsideLine: LineAddButton,\n        }),\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n        /**\n         * NodeAddPanel render plugin\n         * 节点添加面板渲染插件\n         */\n        createFreeNodePanelPlugin({\n          renderer: NodePanel,\n        }),\n        /**\n         * This is used for the rendering of the loop node sub-canvas\n         * 这个用于 loop 节点子画布的渲染\n         */\n        createContainerNodePlugin({}),\n        /**\n         * Group plugin\n         */\n        createFreeGroupPlugin({\n          groupNodeRender: GroupNodeRender,\n        }),\n        createContextMenuPlugin({}),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/hooks/use-is-sidebar.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { IsSidebarContext } from '../context';\n\nexport function useIsSidebar() {\n  return useContext(IsSidebarContext);\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/hooks/use-node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { NodeRenderContext } from '../context';\n\nexport function useNodeRenderContext() {\n  return useContext(NodeRenderContext);\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport './style/index.css';\n\nexport { EditorClient } from './components/editor-client';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/comment/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { WorkflowNodeType } from '../constants';\n\nexport const CommentNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Comment,\n  meta: {\n    sidebarDisable: true,\n    defaultPorts: [],\n    renderKey: WorkflowNodeType.Comment,\n    size: {\n      width: 240,\n      height: 150,\n    },\n  },\n  formMeta: {\n    render: () => <></>,\n  },\n  getInputPoints: () => [], // Comment 节点没有输入\n  getOutputPoints: () => [], // Comment 节点没有输出\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/condition/condition-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { Button } from 'antd';\nimport { Field, FieldArray } from '@flowgram.ai/free-layout-editor';\nimport { ConditionRow, ConditionRowValueType } from '@flowgram.ai/form-antd-materials';\nimport { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';\n\nimport { useNodeRenderContext } from '@editor/hooks';\nimport { FormItem } from '@editor/form-components';\nimport { Feedback } from '@editor/form-components';\nimport { ConditionPort } from './styles';\n\n// TODO\n// interface ConditionRowValueType{}\n// function ConditionRow(params){\n//   return <>{params.children}</>\n// }\n\ninterface ConditionValue {\n  key: string;\n  value?: ConditionRowValueType;\n}\n\nexport function ConditionInputs() {\n  const { readonly } = useNodeRenderContext();\n  return (\n    <FieldArray name=\"conditions\">\n      {({ field }) => (\n        <>\n          {field.map((child, index) => (\n            <Field<ConditionValue> key={child.name} name={child.name}>\n              {({ field: childField, fieldState: childState }) => (\n                <FormItem name=\"if\" type=\"boolean\" required={true} labelWidth={40}>\n                  <div style={{ display: 'flex', alignItems: 'center' }}>\n                    <ConditionRow\n                      readonly={readonly}\n                      style={{ flexGrow: 1 }}\n                      value={childField.value.value}\n                      onChange={(v) =>\n                        childField.onChange({\n                          value: v,\n                          key: childField.value.key,\n                        })\n                      }\n                    />\n\n                    {!readonly && (\n                      <Button\n                        type=\"text\"\n                        icon={<CloseCircleOutlined />}\n                        onClick={() => field.delete(index)}\n                      />\n                    )}\n                  </div>\n\n                  <Feedback errors={childState?.errors} invalid={childState?.invalid} />\n                  <ConditionPort data-port-id={childField.value.key} data-port-type=\"output\" />\n                </FormItem>\n              )}\n            </Field>\n          ))}\n          {!readonly && (\n            <div>\n              <Button\n                type=\"text\"\n                icon={<PlusOutlined />}\n                onClick={() =>\n                  field.append({\n                    key: `if_${nanoid(6)}`,\n                    value: { type: 'expression', content: '' },\n                  })\n                }\n              >\n                Add\n              </Button>\n            </div>\n          )}\n        </>\n      )}\n    </FieldArray>\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/condition/condition-inputs/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const ConditionPort = styled.div`\n  position: absolute;\n  right: -12px;\n  top: 50%;\n`;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/condition/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\n\nimport { FlowNodeJSON } from '@editor/typings';\nimport { FormContent, FormHeader } from '@editor/form-components';\nimport { ConditionInputs } from './condition-inputs';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <ConditionInputs />\n    </FormContent>\n  </>\n);\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n    'conditions.*': ({ value }) => {\n      if (!value?.value) return 'Condition is required';\n      return undefined;\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { WorkflowNodeType } from '../constants';\nimport iconCondition from '../../assets/icon-condition.svg';\nimport { formMeta } from './form-meta';\n\nexport const ConditionNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Condition,\n  info: {\n    icon: iconCondition,\n    description:\n      'Connect multiple downstream branches. Only the corresponding branch will be executed if the set conditions are met.',\n  },\n  meta: {\n    defaultPorts: [{ type: 'input' }],\n    // Condition Outputs use dynamic port\n    useDynamicPort: true,\n    expandable: false, // disable expanded\n  },\n  formMeta,\n  onAdd() {\n    return {\n      id: `condition_${nanoid(5)}`,\n      type: 'condition',\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            key: `if_${nanoid(5)}`,\n            value: {},\n          },\n          {\n            key: `if_${nanoid(5)}`,\n            value: {},\n          },\n        ],\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowNodeType {\n  Start = 'start',\n  End = 'end',\n  LLM = 'llm',\n  Condition = 'condition',\n  Loop = 'loop',\n  Comment = 'comment',\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/default-form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormMeta, FormRenderProps, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\nimport {\n  autoRenameRefEffect,\n  syncVariableTitle,\n  provideJsonSchemaOutputs,\n} from '@flowgram.ai/form-antd-materials';\n\nimport { FormContent, FormHeader, FormInputs, FormOutputs } from '@editor/form-components';\nimport { FlowNodeJSON } from '../typings';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => (\n  <>\n    <FormHeader />\n    <FormContent>\n      <FormInputs />\n      <FormOutputs />\n    </FormContent>\n  </>\n);\n\nexport const defaultFormMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }) => (value ? undefined : 'Title is required'),\n    'inputsValues.*': ({ value, context, formValues, name }) => {\n      const valuePropetyKey = name.replace(/^inputsValues\\./, '');\n      const required = formValues.inputs?.required || [];\n      if (\n        required.includes(valuePropetyKey) &&\n        (value === '' || value === undefined || value?.content === '')\n      ) {\n        return `${valuePropetyKey} is required`;\n      }\n      return undefined;\n    },\n  },\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n    inputsValues: autoRenameRefEffect,\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/end/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { mapValues } from 'lodash-es';\nimport { Field, FieldRenderProps, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport { IFlowValue } from '@flowgram.ai/form-antd-materials';\n\nimport { JsonSchema } from '@editor/typings';\nimport { useIsSidebar } from '@editor/hooks';\nimport { FormContent, FormHeader, FormOutputs, PropertiesEdit } from '@editor/form-components';\nimport { defaultFormMeta } from '../default-form-meta';\n\nexport const renderForm = () => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field\n            name=\"outputs.properties\"\n            render={({\n              field: { value: propertiesSchemaValue, onChange: propertiesSchemaChange },\n            }: FieldRenderProps<Record<string, JsonSchema>>) => (\n              <Field<Record<string, IFlowValue>> name=\"inputsValues\">\n                {({ field: { value: propertiesValue, onChange: propertiesValueChange } }) => {\n                  const onChange = (newProperties: Record<string, JsonSchema>) => {\n                    const newPropertiesValue = mapValues(newProperties, (v) => v.default);\n                    const newPropetiesSchema = mapValues(newProperties, (v) => {\n                      delete v.default;\n                      return v;\n                    });\n                    propertiesValueChange(newPropertiesValue);\n                    propertiesSchemaChange(newPropetiesSchema);\n                  };\n                  const value = mapValues(propertiesSchemaValue, (v, key) => ({\n                    ...v,\n                    default: propertiesValue?.[key],\n                  }));\n                  return (\n                    <>\n                      <PropertiesEdit value={value} onChange={onChange} useFx={true} />\n                    </>\n                  );\n                }}\n              </Field>\n            )}\n          />\n          <FormOutputs />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <FormOutputs />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  ...defaultFormMeta,\n  render: renderForm,\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { WorkflowNodeType } from '../constants';\nimport iconEnd from '../../assets/icon-end.jpg';\nimport { formMeta } from './form-meta';\n\nexport const EndNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.End,\n  meta: {\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'input' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconEnd,\n    description:\n      'The final node of the workflow, used to return the result information after the workflow is run.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * End Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\n// import { FlowNodeRegistry } from '../typings';\n// import { ConditionNodeRegistry } from './condition';\n// import { CommentNodeRegistry } from './comment';\n// import { LoopNodeRegistry } from './loop';\n// import { LLMNodeRegistry } from './llm';\n// import { EndNodeRegistry } from './end';\n// import { WorkflowNodeType } from './constants';\n// import { StartNodeRegistry } from './start';\n\nexport { WorkflowNodeType } from './constants';\n\n// export const nodeRegistries: FlowNodeRegistry[] = [\n//   // ConditionNodeRegistry,\n//   StartNodeRegistry,\n//   // EndNodeRegistry,\n//   // LLMNodeRegistry,\n//   // LoopNodeRegistry,\n//   // CommentNodeRegistry,\n// ];\n\n// export const visibleNodeRegistries = nodeRegistries.filter(\n//   (r) => r.type !== WorkflowNodeType.Comment\n// );\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/llm/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { WorkflowNodeType } from '../constants';\nimport iconLLM from '../../assets/icon-llm.jpg';\n\nlet index = 0;\nexport const LLMNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.LLM,\n  info: {\n    icon: iconLLM,\n    description:\n      'Call the large language model and use variables and prompt words to generate responses.',\n  },\n  meta: {\n    size: {\n      width: 360,\n      height: 305,\n    },\n  },\n  onAdd() {\n    return {\n      id: `llm_${nanoid(5)}`,\n      type: 'llm',\n      data: {\n        title: `LLM_${++index}`,\n        inputsValues: {\n          modelType: {\n            type: 'constant',\n            content: 'gpt-3.5-turbo',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'constant',\n            content: 'You are an AI assistant.',\n          },\n          prompt: {\n            type: 'constant',\n            content: '',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelType', 'temperature', 'prompt'],\n          properties: {\n            modelType: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n            },\n            prompt: {\n              type: 'string',\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n          },\n        },\n      },\n    };\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport {\n  FlowNodeTransformData,\n  PositionSchema,\n  WorkflowNodeEntity,\n} from '@flowgram.ai/free-layout-editor';\nimport { provideBatchInputEffect } from '@flowgram.ai/form-antd-materials';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { defaultFormMeta } from '../default-form-meta';\nimport { WorkflowNodeType } from '../constants';\nimport iconLoop from '../../assets/icon-loop.jpg';\nimport { LoopFormRender } from './loop-form-render';\n\nlet index = 0;\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Loop,\n  info: {\n    icon: iconLoop,\n    description:\n      'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',\n  },\n  meta: {\n    /**\n     * Mark as subcanvas\n     * 子画布标记\n     */\n    isContainer: true,\n    /**\n     * The subcanvas default size setting\n     * 子画布默认大小设置\n     */\n    size: {\n      width: 560,\n      height: 400,\n    },\n    /**\n     * The subcanvas padding setting\n     * 子画布 padding 设置\n     */\n    padding: () => ({\n      top: 125,\n      bottom: 100,\n      left: 100,\n      right: 100,\n    }),\n    /**\n     * Controls the node selection status within the subcanvas\n     * 控制子画布内的节点选中状态\n     */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // 鼠标开始时所在位置不包括当前节点时才可选中\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    expandable: false, // disable expanded\n  },\n  onAdd() {\n    return {\n      id: `loop_${nanoid(5)}`,\n      type: 'loop',\n      data: {\n        title: `Loop_${++index}`,\n      },\n    };\n  },\n  formMeta: {\n    ...defaultFormMeta,\n    render: LoopFormRender,\n    effect: {\n      batchFor: provideBatchInputEffect,\n    },\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/loop/loop-form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field, FlowNodeJSON, FormRenderProps } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\nimport { BatchVariableSelector, IFlowRefValue } from '@flowgram.ai/form-antd-materials';\n\nimport { useIsSidebar, useNodeRenderContext } from '@editor/hooks';\nimport { Feedback, FormContent, FormHeader, FormItem, FormOutputs } from '@editor/form-components';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    batchFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n  const { readonly } = useNodeRenderContext();\n\n  const batchFor = (\n    <Field<IFlowRefValue> name={`batchFor`}>\n      {({ field, fieldState }) => (\n        <FormItem name={'batchFor'} type={'array'} required>\n          <BatchVariableSelector\n            style={{ width: '100%' }}\n            value={field.value?.content}\n            onChange={(val) => field.onChange({ type: 'ref', content: val })}\n            readonly={readonly}\n            hasError={Object.keys(fieldState?.errors || {}).length > 0}\n          />\n          <Feedback errors={fieldState?.errors} />\n        </FormItem>\n      )}\n    </Field>\n  );\n\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          {batchFor}\n          <FormOutputs />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        {batchFor}\n        <SubCanvasRender />\n        <FormOutputs />\n      </FormContent>\n    </>\n  );\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/start/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport {\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  FormRenderProps,\n  ValidateTrigger,\n} from '@flowgram.ai/free-layout-editor';\nimport {\n  JsonSchemaEditor,\n  syncVariableTitle,\n  provideJsonSchemaOutputs,\n} from '@flowgram.ai/form-antd-materials';\n\nimport { FlowNodeJSON, JsonSchema } from '@editor/typings';\nimport { useIsSidebar } from '@editor/hooks';\nimport { FormContent, FormHeader, FormOutputs } from '@editor/form-components';\n\nexport const renderForm = ({ form }: FormRenderProps<FlowNodeJSON>) => {\n  const isSidebar = useIsSidebar();\n  if (isSidebar) {\n    return (\n      <>\n        <FormHeader />\n        <FormContent>\n          <Field\n            name=\"outputs\"\n            render={({ field: { value, onChange } }: FieldRenderProps<JsonSchema>) => (\n              <>\n                <JsonSchemaEditor\n                  value={value}\n                  onChange={(value) => onChange(value as JsonSchema)}\n                />\n              </>\n            )}\n          />\n        </FormContent>\n      </>\n    );\n  }\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <FormOutputs />\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta<FlowNodeJSON> = {\n  render: renderForm,\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    title: ({ value }: { value: string }) => (value ? undefined : 'Title is required'),\n  },\n  effect: {\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/nodes/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { FlowNodeRegistry } from '@editor/typings';\nimport { WorkflowNodeType } from '../constants';\nimport iconStart from '../../assets/icon-start.jpg';\nimport { formMeta } from './form-meta';\n\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,\n    copyDisable: true,\n    defaultPorts: [{ type: 'output' }],\n    size: {\n      width: 360,\n      height: 211,\n    },\n  },\n  info: {\n    icon: iconStart,\n    description:\n      'The starting node of the workflow, used to set the information needed to initiate the workflow.',\n  },\n  /**\n   * Render node via formMeta\n   */\n  formMeta,\n  /**\n   * Start Node cannot be added\n   */\n  canAdd() {\n    return false;\n  },\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/plugins/context-menu-plugin/context-menu-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  Layer,\n  injectable,\n  inject,\n  FreeLayoutPluginContext,\n  WorkflowHoverService,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n} from '@flowgram.ai/free-layout-editor';\n\n@injectable()\nexport class ContextMenuLayer extends Layer {\n  @inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext;\n\n  @inject(WorkflowNodePanelService) nodePanelService: WorkflowNodePanelService;\n\n  @inject(WorkflowHoverService) hoverService: WorkflowHoverService;\n\n  onReady() {\n    this.listenPlaygroundEvent('contextmenu', (e) => {\n      this.openNodePanel(e);\n      e.preventDefault();\n      e.stopPropagation();\n    });\n  }\n\n  openNodePanel(e: MouseEvent) {\n    const pos = this.getPosFromMouseEvent(e);\n    this.nodePanelService.callNodePanel({\n      position: pos,\n      panelProps: {},\n      // handle node selection from panel - 处理从面板中选择节点\n      onSelect: async (panelParams?: NodePanelResult) => {\n        if (!panelParams) {\n          return;\n        }\n        const { nodeType, nodeJSON } = panelParams;\n        // create new workflow node based on selected type - 根据选择的类型创建新的工作流节点\n        const node: WorkflowNodeEntity = this.ctx.document.createWorkflowNodeByType(\n          nodeType,\n          pos,\n          nodeJSON ?? ({} as WorkflowNodeJSON)\n        );\n        // select the newly created node - 选择新创建的节点\n        this.ctx.selection.selection = [node];\n      },\n      // handle panel close - 处理面板关闭\n      onClose: () => {},\n    });\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/plugins/context-menu-plugin/context-menu-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  PluginCreator,\n  definePluginCreator,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { ContextMenuLayer } from './context-menu-layer';\n\nexport interface ContextMenuPluginOptions {}\n\n/**\n * Creates a plugin of contextmenu\n * @param ctx - The plugin context, containing the document and other relevant information.\n * @param options - Plugin options, currently an empty object.\n */\nexport const createContextMenuPlugin: PluginCreator<ContextMenuPluginOptions> = definePluginCreator<\n  ContextMenuPluginOptions,\n  FreeLayoutPluginContext\n>({\n  onInit(ctx, options) {\n    ctx.playground.registerLayer(ContextMenuLayer);\n  },\n});\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/plugins/context-menu-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createContextMenuPlugin } from './context-menu-plugin';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createContextMenuPlugin } from './context-menu-plugin';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/collapse/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  ShortcutsHandler,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class CollapseShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.COLLAPSE;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Collapse',\n  };\n\n  public shortcuts = ['meta alt openbracket', 'ctrl alt openbracket'];\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    this.selectService.selectedNodes.forEach((node) => {\n      node.renderData.expanded = false;\n    });\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const WorkflowClipboardDataID = 'flowgram-workflow-clipboard-data';\n\nexport enum FlowCommandId {\n  COPY = 'COPY',\n  PASTE = 'PASTE',\n  CUT = 'CUT',\n  GROUP = 'GROUP',\n  UNGROUP = 'UNGROUP',\n  COLLAPSE = 'COLLAPSE',\n  EXPAND = 'EXPAND',\n  DELETE = 'DELETE',\n  ZOOM_IN = 'ZOOM_IN',\n  ZOOM_OUT = 'ZOOM_OUT',\n  RESET_ZOOM = 'RESET_ZOOM',\n  SELECT_ALL = 'SELECT_ALL',\n  CANCEL_SELECT = 'CANCEL_SELECT',\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/copy/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { message as Toast } from 'antd';\nimport {\n  FreeLayoutPluginContext,\n  Rectangle,\n  ShortcutsHandler,\n  TransformData,\n  WorkflowDocument,\n  WorkflowEdgeJSON,\n  WorkflowJSON,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowNodeType } from '@editor/nodes';\nimport type {\n  WorkflowClipboardData,\n  WorkflowClipboardRect,\n  WorkflowClipboardSource,\n} from '../type';\nimport { FlowCommandId, WorkflowClipboardDataID } from '../constants';\n\nexport class CopyShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.COPY;\n\n  public shortcuts = ['meta c', 'ctrl c'];\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute copy operation - 执行复制操作\n   */\n  public async execute(): Promise<void> {\n    if (await this.hasSelectedText()) {\n      return;\n    }\n    if (!this.isValid(this.selectedNodes)) {\n      return;\n    }\n    const data = this.toClipboardData();\n    await this.write(data);\n  }\n\n  /**\n   *  has selected text - 是否有文字被选中\n   */\n  private async hasSelectedText(): Promise<boolean> {\n    if (!window.getSelection()?.toString()) {\n      return false;\n    }\n    await navigator.clipboard.writeText(window.getSelection()?.toString() ?? '');\n    Toast.success({\n      content: 'Text copied',\n    });\n    return true;\n  }\n\n  /**\n   * get selected nodes - 获取选中的节点\n   */\n  private get selectedNodes(): WorkflowNodeEntity[] {\n    return this.selectService.selection.filter(\n      (n) => n instanceof WorkflowNodeEntity\n    ) as WorkflowNodeEntity[];\n  }\n\n  /**\n   * validate selected nodes - 验证选中的节点\n   */\n  private isValid(nodes: WorkflowNodeEntity[]): boolean {\n    if (nodes.length === 0) {\n      Toast.warning({\n        content: 'No nodes selected',\n      });\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * create clipboard data - 转换为剪贴板数据\n   */\n  toClipboardData(nodes?: WorkflowNodeEntity[]): WorkflowClipboardData {\n    const validNodes = this.getValidNodes(nodes ? nodes : this.selectedNodes);\n    const source = this.toSource();\n    const json = this.toJSON(validNodes);\n    const bounds = this.getEntireBounds(validNodes);\n    return {\n      type: WorkflowClipboardDataID,\n      source,\n      json,\n      bounds,\n    };\n  }\n\n  /**\n   * get valid nodes - 获取有效的节点\n   */\n  private getValidNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {\n    return nodes.filter((n) => {\n      if (\n        [WorkflowNodeType.Start, WorkflowNodeType.End].includes(n.flowNodeType as WorkflowNodeType)\n      ) {\n        return false;\n      }\n      if (n.getNodeMeta<WorkflowNodeMeta>().copyDisable) {\n        return false;\n      }\n      return true;\n    });\n  }\n\n  /**\n   * get source data - 获取来源数据\n   */\n  private toSource(): WorkflowClipboardSource {\n    return {\n      host: window.location.host,\n    };\n  }\n\n  /**\n   * convert nodes to JSON - 将节点转换为JSON\n   */\n  private toJSON(nodes: WorkflowNodeEntity[]): WorkflowJSON {\n    const nodeJSONs = this.getNodeJSONs(nodes);\n    const edgeJSONs = this.getEdgeJSONs(nodes);\n    return {\n      nodes: nodeJSONs,\n      edges: edgeJSONs,\n    };\n  }\n\n  /**\n   * get JSON representation of nodes - 获取节点的JSON表示\n   */\n  private getNodeJSONs(nodes: WorkflowNodeEntity[]): WorkflowNodeJSON[] {\n    const nodeJSONs = nodes.map((node) => {\n      const nodeJSON = this.document.toNodeJSON(node);\n      if (!nodeJSON.meta?.position) {\n        return nodeJSON;\n      }\n      const { bounds } = node.getData(TransformData);\n      // Use absolute positioning as coordinates - 使用绝对定位作为坐标\n      nodeJSON.meta.position = {\n        x: bounds.x,\n        y: bounds.y,\n      };\n      return nodeJSON;\n    });\n    return nodeJSONs.filter(Boolean);\n  }\n\n  /**\n   * get edges of all nodes - 获取所有节点的边\n   */\n  private getEdgeJSONs(nodes: WorkflowNodeEntity[]): WorkflowEdgeJSON[] {\n    const lineSet = new Set<WorkflowLineEntity>();\n    const nodeIdSet = new Set(nodes.map((n) => n.id));\n    nodes.forEach((node) => {\n      const linesData = node.lines;\n      const lines = [...linesData.inputLines, ...linesData.outputLines];\n      lines.forEach((line) => {\n        if (\n          line.from?.id &&\n          nodeIdSet.has(line.from.id) &&\n          line.to?.id &&\n          nodeIdSet.has(line.to.id)\n        ) {\n          lineSet.add(line);\n        }\n      });\n    });\n    return Array.from(lineSet).map((line) => line.toJSON());\n  }\n\n  /**\n   * get bounding rectangle of all nodes - 获取所有节点的边界矩形\n   */\n  private getEntireBounds(nodes: WorkflowNodeEntity[]): WorkflowClipboardRect {\n    const bounds = nodes.map((node) => node.getData<TransformData>(TransformData).bounds);\n    const rect = Rectangle.enlarge(bounds);\n    return {\n      x: rect.x,\n      y: rect.y,\n      width: rect.width,\n      height: rect.height,\n    };\n  }\n\n  /**\n   * write data to clipboard - 将数据写入剪贴板\n   */\n  private async write(data: WorkflowClipboardData): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(JSON.stringify(data));\n      this.notifySuccess();\n    } catch (err) {\n      console.error('Failed to write text: ', err);\n    }\n  }\n\n  /**\n   * show success notification - 显示成功通知\n   */\n  private notifySuccess(): void {\n    const nodeTypes = this.selectedNodes.map((node) => node.flowNodeType);\n    if (nodeTypes.includes('start') || nodeTypes.includes('end')) {\n      Toast.warning({\n        content:\n          'The Start/End node cannot be duplicated, other nodes have been copied to the clipboard',\n        // showClose: false,\n      });\n      return;\n    }\n    Toast.success({\n      content: 'Nodes have been copied to the clipboard',\n      // showClose: false,\n    });\n    return;\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/delete/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { message as Toast } from 'antd';\nimport {\n  FreeLayoutPluginContext,\n  HistoryService,\n  ShortcutsHandler,\n  WorkflowDocument,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowNodeType } from '@editor/nodes';\nimport { FlowCommandId } from '../constants';\n\nexport class DeleteShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.DELETE;\n\n  public shortcuts = ['backspace', 'delete'];\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  private historyService: HistoryService;\n\n  /**\n   * initialize delete shortcut - 初始化删除快捷键\n   */\n  constructor(context: FreeLayoutPluginContext) {\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.historyService = context.get(HistoryService);\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute delete operation - 执行删除操作\n   */\n  public async execute(nodes?: WorkflowNodeEntity[]): Promise<void> {\n    const selection = Array.isArray(nodes) ? nodes : this.selectService.selection;\n    if (\n      !this.isValid(\n        selection.filter((n) => n instanceof WorkflowNodeEntity) as WorkflowNodeEntity[]\n      )\n    ) {\n      return;\n    }\n    // Merge actions to redo/undo\n    this.historyService.startTransaction();\n    // delete selected entities - 删除选中实体\n    selection.forEach((entity) => {\n      if (entity instanceof WorkflowNodeEntity) {\n        this.removeNode(entity);\n      } else if (entity instanceof WorkflowLineEntity) {\n        this.removeLine(entity);\n      } else {\n        entity.dispose();\n      }\n    });\n    // filter out disposed entities - 过滤掉已删除的实体\n    this.selectService.selection = this.selectService.selection.filter((s) => !s.disposed);\n    this.historyService.endTransaction();\n  }\n\n  /**\n   * validate if nodes can be deleted - 验证节点是否可以删除\n   */\n  private isValid(nodes: WorkflowNodeEntity[]): boolean {\n    const hasSystemNodes = nodes.some((n) =>\n      [WorkflowNodeType.Start, WorkflowNodeType.End].includes(n.flowNodeType as WorkflowNodeType)\n    );\n    if (hasSystemNodes) {\n      Toast.error({\n        content: 'Start or End node cannot be deleted',\n        // showClose: false,\n      });\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * remove node from workflow - 从工作流中删除节点\n   */\n  private removeNode(node: WorkflowNodeEntity): void {\n    if (!this.document.canRemove(node)) {\n      return;\n    }\n    const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(node);\n    if (subCanvas?.isCanvas) {\n      subCanvas.parentNode.dispose();\n      return;\n    }\n    node.dispose();\n  }\n\n  /**\n   * remove line from workflow - 从工作流中删除连线\n   */\n  private removeLine(line: WorkflowLineEntity): void {\n    if (!this.document.linesManager.canRemove(line)) {\n      return;\n    }\n    line.dispose();\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/expand/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  ShortcutsHandler,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ExpandShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.EXPAND;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Expand',\n  };\n\n  public shortcuts = ['meta alt closebracket', 'ctrl alt openbracket'];\n\n  private selectService: WorkflowSelectService;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    this.selectService.selectedNodes.forEach((node) => {\n      node.renderData.expanded = true;\n    });\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './constants';\nexport * from './shortcuts';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/paste/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { message as Toast } from 'antd';\nimport {\n  EntityManager,\n  FlowNodeTransformData,\n  FreeLayoutPluginContext,\n  IPoint,\n  Rectangle,\n  ShortcutsHandler,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowHoverService,\n  WorkflowJSON,\n  WorkflowNodeEntity,\n  WorkflowNodeMeta,\n  WorkflowSelectService,\n  delay,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { WorkflowClipboardData, WorkflowClipboardRect } from '../type';\nimport { FlowCommandId, WorkflowClipboardDataID } from '../constants';\nimport { generateUniqueWorkflow } from './unique-workflow';\n\nexport class PasteShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.PASTE;\n\n  public shortcuts = ['meta v', 'ctrl v'];\n\n  private document: WorkflowDocument;\n\n  private selectService: WorkflowSelectService;\n\n  private entityManager: EntityManager;\n\n  private hoverService: WorkflowHoverService;\n\n  private dragService: WorkflowDragService;\n\n  /**\n   * initialize paste shortcut handler - 初始化粘贴快捷键处理器\n   */\n  constructor(context: FreeLayoutPluginContext) {\n    this.document = context.get(WorkflowDocument);\n    this.selectService = context.get(WorkflowSelectService);\n    this.entityManager = context.get(EntityManager);\n    this.hoverService = context.get(WorkflowHoverService);\n    this.dragService = context.get(WorkflowDragService);\n    this.execute = this.execute.bind(this);\n  }\n\n  /**\n   * execute paste action - 执行粘贴操作\n   */\n  public async execute(): Promise<WorkflowNodeEntity[] | undefined> {\n    const data = await this.tryReadClipboard();\n    if (!data) {\n      return;\n    }\n    if (!this.isValidData(data)) {\n      return;\n    }\n    const nodes = this.apply(data);\n    if (nodes.length > 0) {\n      Toast.success({\n        content: 'Copy successfully',\n        // showClose: false,\n      });\n      // wait for nodes to render - 等待节点渲染\n      await this.nextTick();\n      // scroll to visible area - 滚动到可视区域\n      this.scrollNodesToView(nodes);\n    }\n    return nodes;\n  }\n\n  /** apply clipboard data - 应用剪切板数据 */\n  public apply(data: WorkflowClipboardData): WorkflowNodeEntity[] {\n    // extract raw json from clipboard data - 从剪贴板数据中提取原始JSON\n    const { json: rawJSON } = data;\n    const json = generateUniqueWorkflow({\n      json: rawJSON,\n      isUniqueId: (id: string) => !this.entityManager.getEntityById(id),\n    });\n\n    const offset = this.calcPasteOffset(data.bounds);\n    const parent = this.getSelectedContainer();\n    this.applyOffset({ json, offset, parent });\n    const { nodes } = this.document.batchAddFromJSON(json, {\n      parent,\n    });\n    this.selectNodes(nodes);\n    return nodes;\n  }\n\n  private isValidData(data?: WorkflowClipboardData): boolean {\n    if (data?.type !== WorkflowClipboardDataID) {\n      Toast.error({\n        content: 'Invalid clipboard data',\n      });\n      return false;\n    }\n    // 跨域名表示不同环境，上架插件不同，不能复制\n    if (data.source.host !== window.location.host) {\n      Toast.error({\n        content: 'Cannot paste nodes from different host',\n      });\n      return false;\n    }\n    return true;\n  }\n\n  /** try to read clipboard - 尝试读取剪贴板 */\n  private async tryReadClipboard(): Promise<WorkflowClipboardData | undefined> {\n    try {\n      // need user permission to access clipboard, may throw NotAllowedError - 需要用户授予网页剪贴板读取权限, 如果用户没有授予权限, 代码可能会抛出异常 NotAllowedError\n      const text: string = (await navigator.clipboard.readText()) || '';\n      const clipboardData: WorkflowClipboardData = JSON.parse(text);\n      return clipboardData;\n    } catch (e) {\n      // clipboard data is not fixed, no need to show error - 这里本身剪贴板里的数据就不固定，所以没必要报错\n      return;\n    }\n  }\n\n  /** calculate paste offset - 计算粘贴偏移 */\n  private calcPasteOffset(boundsData: WorkflowClipboardRect): IPoint {\n    // extract bounds data - 提取边界数据\n    const { x, y, width, height } = boundsData;\n    const rect = new Rectangle(x, y, width, height);\n    const { center } = rect;\n    const mousePos = this.hoverService.hoveredPos;\n    return {\n      x: mousePos.x - center.x,\n      y: mousePos.y - center.y,\n    };\n  }\n\n  /**\n   * apply offset to node positions - 应用偏移到节点位置\n   */\n  private applyOffset(params: {\n    json: WorkflowJSON;\n    offset: IPoint;\n    parent?: WorkflowNodeEntity;\n  }): void {\n    const { json, offset, parent } = params;\n    json.nodes.forEach((nodeJSON) => {\n      if (!nodeJSON.meta?.position) {\n        return;\n      }\n      // calculate new position - 计算新位置\n      let position = {\n        x: nodeJSON.meta.position.x + offset.x,\n        y: nodeJSON.meta.position.y + offset.y,\n      };\n      if (parent) {\n        position = this.dragService.adjustSubNodePosition(\n          nodeJSON.type as string,\n          parent,\n          position\n        );\n      }\n      nodeJSON.meta.position = position;\n    });\n  }\n\n  /** get selected container node - 获取鼠标选中的容器 */\n  private getSelectedContainer(): WorkflowNodeEntity | undefined {\n    const { activatedNode } = this.selectService;\n    return activatedNode?.getNodeMeta<WorkflowNodeMeta>().isContainer ? activatedNode : undefined;\n  }\n\n  /** select nodes - 选中节点 */\n  private selectNodes(nodes: WorkflowNodeEntity[]): void {\n    this.selectService.selection = nodes;\n  }\n\n  /** scroll to nodes - 滚动到节点 */\n  private async scrollNodesToView(nodes: WorkflowNodeEntity[]): Promise<void> {\n    const nodeBounds = nodes.map((node) => node.getData(FlowNodeTransformData).bounds);\n    await this.document.playgroundConfig.scrollToView({\n      bounds: Rectangle.enlarge(nodeBounds),\n    });\n  }\n\n  /** wait for next frame - 等待下一帧 */\n  private async nextTick(): Promise<void> {\n    // 16ms is one render frame - 16ms 为一个渲染帧\n    const frameTime = 16;\n    await delay(frameTime);\n    await new Promise((resolve) => requestAnimationFrame(resolve));\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/paste/traverse.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// traverse value type - 遍历值类型\nexport type TraverseValue = any;\n\n// traverse node interface - 遍历节点接口\nexport interface TraverseNode {\n  value: TraverseValue; // node value - 节点值\n  container?: TraverseValue; // parent container - 父容器\n  parent?: TraverseNode; // parent node - 父节点\n  key?: string; // object key - 对象键名\n  index?: number; // array index - 数组索引\n}\n\n// traverse context interface - 遍历上下文接口\nexport interface TraverseContext {\n  node: TraverseNode; // current node - 当前节点\n  setValue: (value: TraverseValue) => void; // set value function - 设置值函数\n  getParents: () => TraverseNode[]; // get parents function - 获取父节点函数\n  getPath: () => Array<string | number>; // get path function - 获取路径函数\n  getStringifyPath: () => string; // get string path function - 获取字符串路径函数\n  deleteSelf: () => void; // delete self function - 删除自身函数\n}\n\n// traverse handler type - 遍历处理器类型\nexport type TraverseHandler = (context: TraverseContext) => void;\n\n/**\n * traverse object deeply and handle each value - 深度遍历对象并处理每个值\n * @param value traverse target - 遍历目标\n * @param handle handler function - 处理函数\n */\nexport const traverse = <T extends TraverseValue = TraverseValue>(\n  value: T,\n  handler: TraverseHandler | TraverseHandler[]\n): T => {\n  const traverseHandler: TraverseHandler = Array.isArray(handler)\n    ? (context: TraverseContext) => {\n        handler.forEach((handlerFn) => handlerFn(context));\n      }\n    : handler;\n  TraverseUtils.traverseNodes({ value }, traverseHandler);\n  return value;\n};\n\nnamespace TraverseUtils {\n  /**\n   * traverse nodes deeply and handle each value - 深度遍历节点并处理每个值\n   * @param node traverse node - 遍历节点\n   * @param handle handler function - 处理函数\n   */\n  export const traverseNodes = (node: TraverseNode, handle: TraverseHandler): void => {\n    const { value } = node;\n    if (!value) {\n      // handle null value - 处理空值\n      return;\n    }\n    if (Object.prototype.toString.call(value) === '[object Object]') {\n      // traverse object properties - 遍历对象属性\n      Object.entries(value).forEach(([key, item]) =>\n        traverseNodes(\n          {\n            value: item,\n            container: value,\n            key,\n            parent: node,\n          },\n          handle\n        )\n      );\n    } else if (Array.isArray(value)) {\n      // traverse array elements from end to start - 从末尾开始遍历数组元素\n      for (let index = value.length - 1; index >= 0; index--) {\n        const item: string = value[index];\n        traverseNodes(\n          {\n            value: item,\n            container: value,\n            index,\n            parent: node,\n          },\n          handle\n        );\n      }\n    }\n    const context: TraverseContext = createContext({ node });\n    handle(context);\n  };\n\n  /**\n   * create traverse context - 创建遍历上下文\n   * @param node traverse node - 遍历节点\n   */\n  const createContext = ({ node }: { node: TraverseNode }): TraverseContext => ({\n    node,\n    setValue: (value: unknown) => setValue(node, value),\n    getParents: () => getParents(node),\n    getPath: () => getPath(node),\n    getStringifyPath: () => getStringifyPath(node),\n    deleteSelf: () => deleteSelf(node),\n  });\n\n  /**\n   * set node value - 设置节点值\n   * @param node traverse node - 遍历节点\n   * @param value new value - 新值\n   */\n  const setValue = (node: TraverseNode, value: unknown) => {\n    // handle empty value - 处理空值\n    if (!value || !node) {\n      return;\n    }\n    node.value = value;\n    // get container info from parent scope - 从父作用域获取容器信息\n    const { container, key, index } = node;\n    if (key && container) {\n      container[key] = value;\n    } else if (typeof index === 'number') {\n      container[index] = value;\n    }\n  };\n\n  /**\n   * get parent nodes - 获取父节点列表\n   * @param node traverse node - 遍历节点\n   */\n  const getParents = (node: TraverseNode): TraverseNode[] => {\n    const parents: TraverseNode[] = [];\n    let currentNode: TraverseNode | undefined = node;\n    while (currentNode) {\n      parents.unshift(currentNode);\n      currentNode = currentNode.parent;\n    }\n    return parents;\n  };\n\n  /**\n   * get node path - 获取节点路径\n   * @param node traverse node - 遍历节点\n   */\n  const getPath = (node: TraverseNode): Array<string | number> => {\n    const path: Array<string | number> = [];\n    const parents = getParents(node);\n    parents.forEach((parent) => {\n      if (parent.key) {\n        path.unshift(parent.key);\n      } else if (parent.index) {\n        path.unshift(parent.index);\n      }\n    });\n    return path;\n  };\n\n  /**\n   * get stringify path - 获取字符串路径\n   * @param node traverse node - 遍历节点\n   */\n  const getStringifyPath = (node: TraverseNode): string => {\n    const path = getPath(node);\n    return path.reduce((stringifyPath: string, pathItem: string | number) => {\n      if (typeof pathItem === 'string') {\n        const re = /\\W/g;\n        if (re.test(pathItem)) {\n          // handle special characters - 处理特殊字符\n          return `${stringifyPath}[\"${pathItem}\"]`;\n        }\n        return `${stringifyPath}.${pathItem}`;\n      } else {\n        return `${stringifyPath}[${pathItem}]`;\n      }\n    }, '');\n  };\n\n  /**\n   * delete current node - 删除当前节点\n   * @param node traverse node - 遍历节点\n   */\n  const deleteSelf = (node: TraverseNode): void => {\n    const { container, key, index } = node;\n    if (key && container) {\n      delete container[key];\n    } else if (typeof index === 'number') {\n      container.splice(index, 1);\n    }\n  };\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/paste/unique-workflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { customAlphabet } from 'nanoid';\nimport type { WorkflowJSON, WorkflowNodeJSON } from '@flowgram.ai/free-layout-editor';\n\nimport { TraverseContext, traverse } from './traverse';\n\nnamespace UniqueWorkflowUtils {\n  /** generate unique id - 生成唯一ID */\n  const generateUniqueId = customAlphabet('1234567890', 6); // create a function to generate 6-digit number - 创建一个生成6位数字的函数\n\n  /** get all node ids from workflow json - 从工作流JSON中获取所有节点ID */\n  export const getAllNodeIds = (json: WorkflowJSON): string[] => {\n    const nodeIds = new Set<string>(); // use set to store unique ids - 使用Set存储唯一ID\n    const addNodeId = (node: WorkflowNodeJSON) => {\n      nodeIds.add(node.id);\n      if (node.blocks?.length) {\n        node.blocks.forEach((child) => addNodeId(child)); // recursively add child node ids - 递归添加子节点ID\n      }\n    };\n    json.nodes.forEach((node) => addNodeId(node));\n    return Array.from(nodeIds);\n  };\n\n  /** generate node replacement mapping - 生成节点替换映射 */\n  export const generateNodeReplaceMap = (\n    nodeIds: string[],\n    isUniqueId: (id: string) => boolean\n  ): Map<string, string> => {\n    const nodeReplaceMap = new Map<string, string>(); // create map for id replacement - 创建ID替换映射\n    nodeIds.forEach((id) => {\n      if (isUniqueId(id)) {\n        nodeReplaceMap.set(id, id); // keep original id if unique - 如果ID唯一则保持不变\n      } else {\n        let newId: string;\n        do {\n          newId = generateUniqueId(); // generate new id until unique - 生成新ID直到唯一\n        } while (!isUniqueId(newId));\n        nodeReplaceMap.set(id, newId);\n      }\n    });\n    return nodeReplaceMap;\n  };\n\n  /** check if value exists - 检查值是否存在 */\n  const isExist = (value: unknown): boolean => value !== null && value !== undefined;\n\n  /** check if node should be handled - 检查节点是否需要处理 */\n  const shouldHandle = (context: TraverseContext): boolean => {\n    const { node } = context;\n    // check edge data - 检查边数据\n    if (\n      node?.key &&\n      ['sourceNodeID', 'targetNodeID'].includes(node.key) &&\n      node.parent?.parent?.key === 'edges'\n    ) {\n      return true;\n    }\n    // check node data - 检查节点数据\n    if (\n      node?.key === 'id' &&\n      isExist(node.container?.type) &&\n      isExist(node.container?.meta) &&\n      isExist(node.container?.data)\n    ) {\n      return true;\n    }\n    // check variable data - 检查变量数据\n    if (\n      node?.key === 'blockID' &&\n      isExist(node.container?.name) &&\n      node.container?.source === 'block-output'\n    ) {\n      return true;\n    }\n    return false;\n  };\n\n  /**\n   * replace node ids in workflow json - 替换工作流JSON中的节点ID\n   * notice: this method has side effects, it will modify the input json to avoid deep copy overhead\n   * - 注意：此方法有副作用，会修改输入的json以避免深拷贝开销\n   */\n  export const replaceNodeId = (\n    json: WorkflowJSON,\n    nodeReplaceMap: Map<string, string>\n  ): WorkflowJSON => {\n    traverse(json, (context) => {\n      if (!shouldHandle(context)) {\n        return;\n      }\n      const { node } = context;\n      if (nodeReplaceMap.has(node.value)) {\n        context.setValue(nodeReplaceMap.get(node.value)); // replace old id with new id - 用新ID替换旧ID\n      }\n    });\n    return json;\n  };\n}\n\n/** generate unique workflow json - 生成唯一工作流JSON */\nexport const generateUniqueWorkflow = (params: {\n  json: WorkflowJSON;\n  isUniqueId: (id: string) => boolean;\n}): WorkflowJSON => {\n  const { json, isUniqueId } = params;\n  const nodeIds = UniqueWorkflowUtils.getAllNodeIds(json); // get all existing node ids - 获取所有现有节点ID\n  const nodeReplaceMap = UniqueWorkflowUtils.generateNodeReplaceMap(nodeIds, isUniqueId); // generate id replacement map - 生成ID替换映射\n  return UniqueWorkflowUtils.replaceNodeId(json, nodeReplaceMap); // replace all node ids - 替换所有节点ID\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/select-all/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  Playground,\n  ShortcutsHandler,\n  WorkflowDocument,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class SelectAllShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.SELECT_ALL;\n\n  public shortcuts = ['meta a', 'ctrl a'];\n\n  private document: WorkflowDocument;\n\n  private playground: Playground;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.document = context.get(WorkflowDocument);\n    this.playground = context.playground;\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    const allNodes = this.document.getAllNodes();\n    this.playground.selectionService.selection = allNodes;\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/shortcuts.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FreeLayoutPluginContext, ShortcutsRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { ZoomOutShortcut } from './zoom-out';\nimport { ZoomInShortcut } from './zoom-in';\nimport { SelectAllShortcut } from './select-all';\nimport { PasteShortcut } from './paste';\nimport { ExpandShortcut } from './expand';\nimport { DeleteShortcut } from './delete';\nimport { CopyShortcut } from './copy';\nimport { CollapseShortcut } from './collapse';\n\nexport function shortcuts(shortcutsRegistry: ShortcutsRegistry, ctx: FreeLayoutPluginContext) {\n  shortcutsRegistry.addHandlers(\n    new CopyShortcut(ctx),\n    new PasteShortcut(ctx),\n    new SelectAllShortcut(ctx),\n    new CollapseShortcut(ctx),\n    new ExpandShortcut(ctx),\n    new DeleteShortcut(ctx),\n    new ZoomInShortcut(ctx),\n    new ZoomOutShortcut(ctx)\n  );\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nimport type { WorkflowClipboardDataID } from './constants';\n\nexport interface WorkflowClipboardSource {\n  host: string;\n  // more: id?, workspaceId? etc.\n}\n\nexport interface WorkflowClipboardRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface WorkflowClipboardData {\n  type: typeof WorkflowClipboardDataID;\n  json: WorkflowJSON;\n  source: WorkflowClipboardSource;\n  bounds: WorkflowClipboardRect;\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/zoom-in/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  PlaygroundConfigEntity,\n  ShortcutsHandler,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ZoomInShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.ZOOM_IN;\n\n  public shortcuts = ['meta =', 'ctrl ='];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.get(PlaygroundConfigEntity);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    if (this.playgroundConfig.zoom > 1.9) {\n      return;\n    }\n    this.playgroundConfig.zoomin();\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/shortcuts/zoom-out/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FreeLayoutPluginContext,\n  PlaygroundConfigEntity,\n  ShortcutsHandler,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { FlowCommandId } from '../constants';\n\nexport class ZoomOutShortcut implements ShortcutsHandler {\n  public commandId = FlowCommandId.ZOOM_OUT;\n\n  public shortcuts = ['meta -', 'ctrl -'];\n\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  constructor(context: FreeLayoutPluginContext) {\n    this.playgroundConfig = context.get(PlaygroundConfigEntity);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    if (this.playgroundConfig.zoom > 1.9) {\n      return;\n    }\n    this.playgroundConfig.zoomout();\n  }\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/style/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n@import './var.css';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/style/var.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  --node-shadow:\n    0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/flow-value/config.json",
    "content": "{\n  \"name\": \"flow-value\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/flow-value/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface IFlowConstantValue {\n  type: 'constant';\n  content?: string | number | boolean;\n}\n\nexport interface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n\nexport interface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n}\n\nexport interface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n}\n\nexport type IFlowValue =\n  | IFlowConstantValue\n  | IFlowRefValue\n  | IFlowExpressionValue\n  | IFlowTemplateValue;\n\nexport type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node';\nexport * from './json-schema';\nexport * from './flow-value';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/json-schema/config.json",
    "content": "{\n  \"name\": \"json-schema\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/json-schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IJsonSchema, IBasicJsonSchema } from '@flowgram.ai/form-antd-materials';\n\nexport type BasicType = IBasicJsonSchema;\nexport type JsonSchema = IJsonSchema;\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/typings/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { StaticImageData } from 'next/image';\nimport {\n  FlowNodeEntity,\n  WorkflowNodeJSON as FlowNodeJSONDefault,\n  WorkflowNodeRegistry as FlowNodeRegistryDefault,\n  FreeLayoutPluginContext,\n  type WorkflowEdgeJSON,\n  WorkflowNodeMeta,\n} from '@flowgram.ai/free-layout-editor';\nimport { IFlowValue } from '@flowgram.ai/form-antd-materials';\n\nimport { type JsonSchema } from './json-schema';\n\n/**\n * You can customize the data of the node, and here you can use JsonSchema to define the input and output of the node\n * 你可以自定义节点的 data 业务数据, 这里演示 通过 JsonSchema 来定义节点的输入/输出\n */\nexport interface FlowNodeJSON extends FlowNodeJSONDefault {\n  data: {\n    /**\n     * Node title\n     */\n    title?: string;\n    /**\n     * Inputs data values\n     */\n    inputsValues?: Record<string, IFlowValue>;\n    /**\n     * Define the inputs data of the node by JsonSchema\n     */\n    inputs?: JsonSchema;\n    /**\n     * Define the outputs data of the node by JsonSchema\n     */\n    outputs?: JsonSchema;\n    /**\n     * Rest properties\n     */\n    [key: string]: any;\n  };\n}\n\n/**\n * You can customize your own node meta\n * 你可以自定义节点的meta\n */\nexport interface FlowNodeMeta extends WorkflowNodeMeta {\n  sidebarDisable?: boolean;\n}\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport interface FlowNodeRegistry extends FlowNodeRegistryDefault {\n  meta: FlowNodeMeta;\n  info?: {\n    icon: StaticImageData;\n    description: string;\n  };\n  canAdd?: (ctx: FreeLayoutPluginContext) => boolean;\n  canDelete?: (ctx: FreeLayoutPluginContext, from: FlowNodeEntity) => boolean;\n  onAdd?: (ctx: FreeLayoutPluginContext) => FlowNodeJSON;\n}\n\nexport interface FlowDocumentJSON {\n  nodes: FlowNodeJSON[];\n  edges: WorkflowEdgeJSON[];\n}\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { onDragLineEnd } from './on-drag-line-end';\n"
  },
  {
    "path": "apps/demo-nextjs-antd/src/editor/utils/on-drag-line-end.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowNodePanelService,\n  WorkflowNodePanelUtils,\n} from '@flowgram.ai/free-node-panel-plugin';\nimport {\n  FreeLayoutPluginContext,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  delay,\n  onDragLineEndParams,\n} from '@flowgram.ai/free-layout-editor';\n\n/**\n * Drag the end of the line to create an add panel (feature optional)\n * 拖拽线条结束需要创建一个添加面板 （功能可选）\n */\nexport const onDragLineEnd = async (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => {\n  // get services from context - 从上下文获取服务\n  const nodePanelService = ctx.get(WorkflowNodePanelService);\n  const document = ctx.document;\n  const dragService = ctx.get(WorkflowDragService);\n  const linesManager = ctx.get(WorkflowLinesManager);\n\n  // get params from drag event - 从拖拽事件获取参数\n  const { fromPort, toPort, mousePos, line, originLine } = params;\n\n  // return if invalid line state - 如果线条状态无效则返回\n  if (originLine || !line) {\n    return;\n  }\n\n  // return if target port exists - 如果目标端口存在则返回\n  if (toPort) {\n    return;\n  }\n\n  // get container node for the new node - 获取新节点的容器节点\n  const containerNode = WorkflowNodePanelUtils.getContainerNode({\n    fromPort,\n  });\n\n  // open node selection panel - 打开节点选择面板\n  const result = await nodePanelService.singleSelectNodePanel({\n    position: mousePos,\n    containerNode,\n    panelProps: {\n      enableNodePlaceholder: true,\n      enableScrollClose: true,\n    },\n  });\n\n  // return if no node selected - 如果没有选择节点则返回\n  if (!result) {\n    return;\n  }\n\n  // get selected node type and data - 获取选择的节点类型和数据\n  const { nodeType, nodeJSON } = result;\n\n  // calculate position for the new node - 计算新节点的位置\n  const nodePosition = WorkflowNodePanelUtils.adjustNodePosition({\n    nodeType,\n    position: mousePos,\n    fromPort,\n    toPort,\n    containerNode,\n    document,\n    dragService,\n  });\n\n  // create new workflow node - 创建新的工作流节点\n  const node: WorkflowNodeEntity = document.createWorkflowNodeByType(\n    nodeType,\n    nodePosition,\n    nodeJSON ?? ({} as WorkflowNodeJSON),\n    containerNode?.id\n  );\n\n  // wait for node render - 等待节点渲染\n  await delay(20);\n\n  // build connection line - 构建连接线\n  WorkflowNodePanelUtils.buildLine({\n    fromPort,\n    node,\n    toPort,\n    linesManager,\n  });\n};\n"
  },
  {
    "path": "apps/demo-nextjs-antd/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"experimentalDecorators\": true,\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n          \"name\": \"next\"\n      }\n    ],\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@app/*\": [\"app/*\"],\n      \"@editor/*\": [\"editor/*\"],\n    }\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/demo-node-form/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-node-form/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-node-form/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-node-form\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-node-form/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n  },\n  html: {\n    title: 'demo-node-form',\n  },\n});\n"
  },
  {
    "path": "apps/demo-node-form/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { Editor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-node-form/src/components/field-title.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FieldTitle = styled.div`\n  padding-bottom: 4px;\n`;\n"
  },
  {
    "path": "apps/demo-node-form/src/components/field-wrapper.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.error-message {\n  color: #f5222d !important;\n}\n\n.required {\n  color: #f5222d !important;\n  padding-left: 4px\n}\n\n.field-wrapper {\n  width: 100%;\n  margin-bottom: 12px;\n}\n\n.field-title {\n  margin-bottom: 6px;\n}\n\n.field-note{\n  color: #a3a0a0 !important;\n  font-size: 12px;\n  margin: 6px 0;\n}\n\n"
  },
  {
    "path": "apps/demo-node-form/src/components/field-wrapper.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport './field-wrapper.css';\n\ninterface FieldWrapperProps {\n  required?: boolean;\n  title?: string;\n  children?: React.ReactNode;\n  error?: React.ReactNode;\n  note?: string;\n}\n\nexport const FieldWrapper = ({ required, title, children, error, note }: FieldWrapperProps) => (\n  <div className=\"field-wrapper\">\n    <div className=\"field-title\">\n      {title}\n      {note ? <p className=\"field-note\">{note}</p> : null}\n      {required ? <span className=\"required\">*</span> : null}\n    </div>\n    {children}\n    <p className=\"error-message\">{error}</p>\n    {note ? <br /> : null}\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-node-form/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FieldTitle } from './field-title';\nexport { FieldWrapper } from './field-wrapper';\n"
  },
  {
    "path": "apps/demo-node-form/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const fieldWrapperTs = `import React from 'react';\n\nimport './field-wrapper.css';\n\ninterface FieldWrapperProps {\n  required?: boolean;\n  title?: string;\n  children?: React.ReactNode;\n  error?: string;\n  note?: string;\n}\n\nexport const FieldWrapper = ({ required, title, children, error, note }: FieldWrapperProps) => (\n  <div className=\"field-wrapper\">\n    <div className=\"field-title\">\n      {title}\n      {note ? <p className=\"field-note\">{note}</p> : null}\n      {required ? <span className=\"required\">*</span> : null}\n    </div>\n    {children}\n    <p className=\"error-message\">{error}</p>\n    {note ? <br /> : null}\n  </div>\n);\n`;\n\nexport const fieldWrapperCss = `.error-message {\n  color: #f5222d !important;\n}\n\n.required {\n  color: #f5222d !important;\n  padding-left: 4px\n}\n\n.field-wrapper {\n  width: 100%;\n  margin-bottom: 12px;\n}\n\n.field-title {\n  margin-bottom: 6px;\n}\n\n.field-note{\n  color: #a3a0a0 !important;\n  font-size: 12px;\n  margin: 6px 0;\n}\n`;\n\nexport const defaultInitialDataTs = `import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const DEFAULT_INITIAL_DATA: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n    },\n  ],\n  edges: [],\n};`;\n"
  },
  {
    "path": "apps/demo-node-form/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  EditorRenderer,\n  FlowNodeRegistry,\n  FreeLayoutEditorProvider,\n  WorkflowJSON,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\ninterface EditorProps {\n  registries?: FlowNodeRegistry[];\n  initialData?: WorkflowJSON;\n}\n\nexport const Editor = ({ registries, initialData }: EditorProps) => {\n  const editorProps = useEditorProps({\n    registries,\n    initialData,\n  });\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-node-form/src/form-meta.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n} from '@flowgram.ai/free-layout-editor';\nimport { Input } from '@douyinfe/semi-ui';\n\n// FieldWrapper is not provided by sdk, and can be customized\nimport { FieldWrapper } from './components';\n\nconst render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Basic Node</div>\n    <Field name=\"name\">\n      {({ field, fieldState }: FieldRenderProps<string>) => (\n        <FieldWrapper required title=\"Name\" error={fieldState.errors?.[0]?.message}>\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"city\">\n      {({ field, fieldState }: FieldRenderProps<string>) => (\n        <FieldWrapper required title=\"City\" error={fieldState.errors?.[0]?.message}>\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n  </div>\n);\n\nconst formMeta: FormMeta = {\n  render,\n  defaultValues: { name: 'Tina', city: 'Hangzhou' },\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    name: ({ value }) => {\n      if (!value) {\n        return 'Name is required';\n      }\n    },\n    city: ({ value }) => {\n      if (!value) {\n        return 'City is required';\n      }\n    },\n  },\n};\n\nexport const DEFAULT_FORM_META = formMeta;\n"
  },
  {
    "path": "apps/demo-node-form/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n  FlowNodeRegistry,\n  WorkflowJSON,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { DEFAULT_DEMO_REGISTRY } from '../node-registries';\nimport { DEFAULT_INITIAL_DATA } from '../initial-data';\n\ninterface EditorProps {\n  registries?: FlowNodeRegistry[];\n  initialData?: WorkflowJSON;\n}\n\nexport const useEditorProps = ({\n  registries = [DEFAULT_DEMO_REGISTRY],\n  initialData = DEFAULT_INITIAL_DATA,\n}: EditorProps) =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries: registries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { form } = useNodeRender();\n          return (\n            <WorkflowNodeRenderer className=\"demo-free-node\" node={props.node}>\n              {form?.render()}\n            </WorkflowNodeRenderer>\n          );\n        },\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        // console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-node-form/src/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-node {\n    display: flex;\n    min-width: 300px;\n    min-height: 100px;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-node-content {\n    padding: 8px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n\n.demo-node-title {\n    font-weight: 500;\n    font-size: 14px;\n    width: 100%;\n    margin: 4px 0px 12px 0px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n"
  },
  {
    "path": "apps/demo-node-form/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor } from './editor';\nexport { FieldTitle, FieldWrapper } from './components';\nexport { DEFAULT_FORM_META } from './form-meta';\nexport { DEFAULT_DEMO_REGISTRY } from './node-registries';\nexport { DEFAULT_INITIAL_DATA } from './initial-data';\nexport { fieldWrapperTs, fieldWrapperCss, defaultInitialDataTs } from './constant';\n"
  },
  {
    "path": "apps/demo-node-form/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const DEFAULT_INITIAL_DATA: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n    },\n  ],\n  edges: [],\n};\n"
  },
  {
    "path": "apps/demo-node-form/src/node-registries.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nimport { DEFAULT_FORM_META } from './form-meta';\n\nexport const DEFAULT_DEMO_REGISTRY: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta: DEFAULT_FORM_META,\n};\n"
  },
  {
    "path": "apps/demo-node-form/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/demo-playground/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-playground/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-playground/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-playground\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/playground-react\": \"workspace:*\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-playground/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [pluginReact()],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n  },\n  html: {\n    title: 'demo-playground',\n  },\n});\n"
  },
  {
    "path": "apps/demo-playground/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { PlaygroundEditor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<PlaygroundEditor />);\n"
  },
  {
    "path": "apps/demo-playground/src/components/card.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useState } from 'react';\n\nimport { usePlayground, usePlaygroundDrag } from '@flowgram.ai/playground-react';\n\nexport function StaticCard() {\n  return (\n    <div\n      style={{\n        width: 200,\n        height: 100,\n        position: 'absolute',\n        color: 'white',\n        backgroundColor: 'gray',\n        left: 200,\n        top: 200,\n        alignItems: 'center',\n        justifyContent: 'center',\n        display: 'flex',\n      }}\n    >\n      {' '}\n      Static Card\n    </div>\n  );\n}\n\nexport function DragableCard() {\n  const [pos, setPos] = useState({ x: 100, y: 50 });\n  // Used for dragging, the canvas will automatically scroll when dragged to the edge\n  // 用于拖拽，拖拽到边缘时候会自动滚动画布\n  const dragger = usePlaygroundDrag();\n  const playground = usePlayground();\n  const handleMouseDown = useCallback(\n    (e: React.MouseEvent) => {\n      const startPos = { x: pos.x, y: pos.y };\n      dragger.start(e, {\n        // start Drag\n        onDragStart() {\n          playground.config.grabDisable = true;\n        },\n        onDrag(dragEvent) {\n          setPos({\n            x: startPos.x + (dragEvent.endPos.x - dragEvent.startPos.x) / dragEvent.scale,\n            y: startPos.y + (dragEvent.endPos.y - dragEvent.startPos.y) / dragEvent.scale,\n          });\n        },\n        // end drag\n        onDragEnd() {\n          playground.config.grabDisable = false;\n        },\n      });\n      // e.stopPropagation();\n      // e.preventDefault();\n    },\n    [pos]\n  );\n  return (\n    <div\n      onMouseDown={handleMouseDown}\n      style={{\n        cursor: 'move',\n        width: 200,\n        height: 100,\n        position: 'absolute',\n        color: 'white',\n        backgroundColor: '#0089ff',\n        left: pos.x,\n        top: pos.y,\n        alignItems: 'center',\n        justifyContent: 'center',\n        display: 'flex',\n      }}\n    >\n      {' '}\n      Draggable Card\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-playground/src/components/playground-tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { usePlaygroundTools } from '@flowgram.ai/playground-react';\n\nexport const PlaygroundTools: React.FC<{ minZoom?: number; maxZoom?: number }> = (props) => {\n  const tools = usePlaygroundTools(props);\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        zIndex: 100,\n        right: 40,\n        bottom: 40,\n        padding: 13,\n        border: '1px solid #ccc',\n        backgroundColor: 'white',\n        borderRadius: 8,\n        userSelect: 'none',\n        cursor: 'pointer',\n      }}\n    >\n      <button onClick={() => tools.toggleIneractiveType()}>{tools.interactiveType} Mode</button>\n      &nbsp;\n      <button onClick={() => tools.zoomout()}>Zoom Out</button>\n      &nbsp;\n      <button onClick={() => tools.zoomin()}>Zoom In</button>\n      &nbsp;\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-playground/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport {\n  Command,\n  PlaygroundReact,\n  PlaygroundReactContent,\n  PlaygroundReactProps,\n} from '@flowgram.ai/playground-react';\n\nimport { PlaygroundTools } from './components/playground-tools';\nimport { StaticCard, DragableCard } from './components/card';\n\n// Load style\nimport '@flowgram.ai/playground-react/index.css';\n\n/**\n * The ability to zoom to provide an infinite canvas\n */\nexport function PlaygroundEditor(props: { className?: string }) {\n  const playgroundProps = useMemo<PlaygroundReactProps>(\n    () => ({\n      background: true, // Background available\n      playground: {\n        ineractiveType: 'PAD', // MOUSE | PAD\n      },\n      // 自定义快捷键\n      shortcuts(registry, ctx) {\n        registry.addHandlers(\n          /**\n           * Zoom In\n           */\n          {\n            commandId: Command.Default.ZOOM_IN,\n            shortcuts: ['meta =', 'ctrl ='],\n            execute: () => {\n              ctx.playground.config.zoomin();\n            },\n          },\n          /**\n           * Zoom Out\n           */\n          {\n            commandId: Command.Default.ZOOM_OUT,\n            shortcuts: ['meta -', 'ctrl -'],\n            execute: () => {\n              ctx.playground.config.zoomout();\n            },\n          }\n        );\n      },\n    }),\n    []\n  );\n  /*\n   * PlaygroundReact: Canvas React containers 画布 react 容器\n   * PlaygroundReactContent: The canvas content will be scaled accordingly 画布内容，会跟着缩放\n   */\n  return (\n    <div className={props.className}>\n      <PlaygroundReact {...playgroundProps}>\n        <PlaygroundReactContent>\n          <StaticCard />\n          <DragableCard />\n        </PlaygroundReactContent>\n        <PlaygroundTools />\n      </PlaygroundReact>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-playground/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PlaygroundEditor } from './editor';\n"
  },
  {
    "path": "apps/demo-playground/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"],\n}\n"
  },
  {
    "path": "apps/demo-react-16/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n    'react/no-deprecated': 'off',\n  },\n  settings: {\n    react: {\n      version: '16.8.6',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-react-16/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-react-16/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-react-16\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"rsbuild.config.ts\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"exit 0\",\n    \"build:fast\": \"exit 0\",\n    \"build:watch\": \"exit 0\",\n    \"build:prod\": \"cross-env MODE=app NODE_ENV=production rsbuild build\",\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"cross-env  MODE=app NODE_ENV=development rsbuild dev --open\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"start\": \"cross-env NODE_ENV=development rsbuild dev --open\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/panel-manager-plugin\": \"workspace:*\",\n    \"react\": \"^16.8.6\",\n    \"react-dom\": \"^16.8.6\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@rsbuild/core\": \"^1.2.16\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^16.8.6\",\n    \"@types/react-dom\": \"^16.8.6\",\n    \"@types/styled-components\": \"^5\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"cross-env\": \"~7.0.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-react-16/rsbuild.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'node:path';\n\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { defineConfig } from '@rsbuild/core';\n\nexport default defineConfig({\n  plugins: [\n    pluginReact({\n      swcReactOptions: {\n        runtime: 'classic',\n      },\n    }),\n  ],\n  source: {\n    entry: {\n      index: './src/app.tsx',\n    },\n  },\n  resolve: {\n    alias: {\n      react: path.resolve(__dirname, './node_modules/react'),\n      'react-dom': path.resolve(__dirname, './node_modules/react-dom'),\n    },\n  },\n  html: {\n    title: 'demo-free-layout-simple',\n  },\n});\n"
  },
  {
    "path": "apps/demo-react-16/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React from 'react';\nconsole.log(React.version);\n\nimport { Editor } from './editor';\n\nReactDOM.render(<Editor />, document.getElementById('root'));\n"
  },
  {
    "path": "apps/demo-react-16/src/components/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport React from 'react';\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 226,\n      bottom: 51,\n      zIndex: 100,\n      width: 198,\n    }}\n  >\n    {/* @ts-ignore */}\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-react-16/src/components/node-add-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';\n\nconst cardkeys = ['Node1', 'Node2'];\n\nexport const NodeAddPanel: React.FC = (props) => {\n  const startDragSerivce = useService<WorkflowDragService>(WorkflowDragService);\n\n  return (\n    <div className=\"demo-free-sidebar\">\n      {cardkeys.map((nodeType) => (\n        <div\n          key={nodeType}\n          className=\"demo-free-card\"\n          onMouseDown={(e) =>\n            startDragSerivce.startDragCard(nodeType, e, {\n              data: {\n                title: `New ${nodeType}`,\n                content: 'xxxx',\n              },\n            })\n          }\n        >\n          {nodeType}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-react-16/src/components/node-form-panel/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { nodeFormPanelFactory } from './sidebar-renderer';\n"
  },
  {
    "path": "apps/demo-react-16/src/components/node-form-panel/sidebar-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useEffect } from 'react';\n\nimport { type PanelFactory, usePanelManager } from '@flowgram.ai/panel-manager-plugin';\nimport { useClientContext, useRefresh } from '@flowgram.ai/free-layout-editor';\n\nexport interface NodeFormPanelProps {\n  nodeId: string;\n}\n\nexport const SidebarRenderer: React.FC<NodeFormPanelProps> = ({ nodeId }) => {\n  const panelManager = usePanelManager();\n  const { selection, playground, document } = useClientContext();\n  const refresh = useRefresh();\n  const handleClose = useCallback(() => {\n    panelManager.close(nodeFormPanelFactory.key);\n  }, []);\n  const node = nodeId ? document.getNode(nodeId) : undefined;\n  /**\n   * Listen readonly\n   */\n  useEffect(() => {\n    const disposable = playground.config.onReadonlyOrDisabledChange(() => {\n      handleClose();\n      refresh();\n    });\n    return () => disposable.dispose();\n  }, [playground]);\n  /**\n   * Listen selection\n   */\n  useEffect(() => {\n    const toDispose = selection.onSelectionChanged(() => {\n      /**\n       * 如果没有选中任何节点，则自动关闭侧边栏\n       * If no node is selected, the sidebar is automatically closed\n       */\n      if (selection.selection.length === 0) {\n        handleClose();\n      } else if (selection.selection.length === 1 && selection.selection[0] !== node) {\n        handleClose();\n      }\n    });\n    return () => toDispose.dispose();\n  }, [selection, handleClose, node]);\n  /**\n   * Close when node disposed\n   */\n  useEffect(() => {\n    if (node) {\n      const toDispose = node.onDispose(() => {\n        panelManager.close(nodeFormPanelFactory.key);\n      });\n      return () => toDispose.dispose();\n    }\n    return () => {};\n  }, [node]);\n\n  if (!node) {\n    return null;\n  }\n\n  if (playground.config.readonly) {\n    return null;\n  }\n  return (\n    <div\n      style={{\n        background: 'rgb(251, 251, 251)',\n        height: '100%',\n        borderRadius: 8,\n        border: '1px solid rgba(82,100,154, 0.13)',\n        boxSizing: 'border-box',\n      }}\n    >\n      {node.form?.getValueIn('title')} Node Form Panel\n      <input\n        onBlur={() => {\n          console.log('onBlur is before then close panel');\n        }}\n      />\n    </div>\n  );\n};\n\nexport const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {\n  key: 'node-form-panel',\n  defaultSize: 400,\n  render: (props: NodeFormPanelProps) => <SidebarRenderer {...props} />,\n};\n"
  },
  {
    "path": "apps/demo-react-16/src/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-react-16/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport React from 'react';\n\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    /** @ts-ignore */\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <NodeAddPanel />\n          {/* @ts-ignore */}\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n        <Tools />\n        <Minimap />\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-react-16/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { createPanelManagerPlugin, usePanelManager } from '@flowgram.ai/panel-manager-plugin';\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { nodeRegistries } from '../node-registries';\nimport { initialData } from '../initial-data';\nimport { nodeFormPanelFactory } from '../components/node-form-panel';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { form } = useNodeRender();\n          const panelManager = usePanelManager();\n          return (\n            <div\n              onClick={() =>\n                panelManager.open(nodeFormPanelFactory.key, 'right', {\n                  props: { nodeId: props.node.id },\n                })\n              }\n            >\n              {/* @ts-ignore */}\n              <WorkflowNodeRenderer className=\"demo-free-node\" node={props.node}>\n                {form?.render()}\n              </WorkflowNodeRenderer>\n            </div>\n          );\n        },\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        // console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n        /**\n         * Panel manager plugin\n         * 面板管理插件\n         */\n        createPanelManagerPlugin({\n          factories: [nodeFormPanelFactory],\n        }),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-react-16/src/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-node {\n    display: flex;\n    min-width: 300px;\n    min-height: 100px;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-free-node-title {\n    background-color: #93bfe2;\n    width: 100%;\n    border-radius: 8px 8px 0 0;\n    padding: 4px 12px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n"
  },
  {
    "path": "apps/demo-react-16/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFreeLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-react-16/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content',\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom',\n        content: 'Custom node content',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End',\n        content: 'End content',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-react-16/src/node-registries.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/demo-react-16/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"jsx\": \"react\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./src\"]\n}\n"
  },
  {
    "path": "apps/demo-vite/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n    'react/no-deprecated': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n});\n"
  },
  {
    "path": "apps/demo-vite/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\" data-bundler=\"rspack\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Flow FreeLayoutEditor Demo</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/app.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/demo-vite/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/demo-vite\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"main\": \"./src/index.tsx\",\n  \"files\": [\n    \"src/\",\n    \"eslint.config.js\",\n    \".gitignore\",\n    \"index.html\",\n    \"package.json\",\n    \"vite.config.js\",\n    \"tsconfig.json\"\n  ],\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"start\": \"vite\",\n    \"build\": \"exit 0\",\n    \"clean\": \"rimraf dist\",\n    \"build:prod\": \"vite build\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\",\n    \"watch\": \"exit 0\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/form-materials\": \"workspace:*\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@vitejs/plugin-react\": \"^5.0.2\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"cross-env\": \"~7.0.3\",\n    \"globals\": \"^15.11.0\",\n    \"less\": \"^4.1.2\",\n    \"vite\": \"^6.3.6\",\n    \"vite-bundle-analyzer\": \"~1.2.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "apps/demo-vite/src/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createRoot } from 'react-dom/client';\n\nimport { Editor } from './editor';\n\nconst app = createRoot(document.getElementById('root')!);\n\napp.render(<Editor />);\n"
  },
  {
    "path": "apps/demo-vite/src/components/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 226,\n      bottom: 51,\n      zIndex: 100,\n      width: 198,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/demo-vite/src/components/node-add-panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';\n\nconst cardkeys = ['Node1', 'Node2'];\n\nexport const NodeAddPanel: React.FC = (props) => {\n  const startDragSerivce = useService<WorkflowDragService>(WorkflowDragService);\n\n  return (\n    <div className=\"demo-free-sidebar\">\n      {cardkeys.map((nodeType) => (\n        <div\n          key={nodeType}\n          className=\"demo-free-card\"\n          onMouseDown={(e) =>\n            startDragSerivce.startDragCard(nodeType, e, {\n              data: {\n                title: `New ${nodeType}`,\n                content: 'xxxx',\n              },\n            })\n          }\n        >\n          {nodeType}\n        </div>\n      ))}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/demo-vite/src/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/demo-vite/src/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <NodeAddPanel />\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n        <Tools />\n        <Minimap />\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n"
  },
  {
    "path": "apps/demo-vite/src/hooks/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { nodeRegistries } from '../node-registries';\nimport { initialData } from '../initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      /**\n       * Whether to enable the background\n       */\n      background: true,\n      /**\n       * Whether it is read-only or not, the node cannot be dragged in read-only mode\n       */\n      readonly: false,\n      /**\n       * Initial data\n       * 初始化数据\n       */\n      initialData,\n      /**\n       * Node registries\n       * 节点注册\n       */\n      nodeRegistries,\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      materials: {\n        /**\n         * Render Node\n         */\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { form } = useNodeRender();\n          return (\n            <WorkflowNodeRenderer className=\"demo-free-node\" node={props.node}>\n              {form?.render()}\n            </WorkflowNodeRenderer>\n          );\n        },\n      },\n      /**\n       * Content change\n       */\n      onContentChange(ctx, event) {\n        // console.log('Auto Save: ', event, ctx.document.toJSON());\n      },\n      // /**\n      //  * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n      //  */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      /**\n       * Playground init\n       */\n      onInit: (ctx) => {},\n      /**\n       * Playground render\n       */\n      onAllLayersRendered(ctx) {\n        //  Fitview\n        ctx.document.fitView(false);\n      },\n      /**\n       * Playground dispose\n       */\n      onDispose() {\n        console.log('---- Playground Dispose ----');\n      },\n      plugins: () => [\n        /**\n         * Minimap plugin\n         * 缩略图插件\n         */\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        /**\n         * Snap plugin\n         * 自动对齐及辅助线插件\n         */\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n      ],\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/demo-vite/src/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-node {\n    display: flex;\n    min-width: 300px;\n    min-height: 100px;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-free-node-title {\n    background-color: #93bfe2;\n    width: 100%;\n    border-radius: 8px 8px 0 0;\n    padding: 4px 12px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n"
  },
  {
    "path": "apps/demo-vite/src/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Editor as DemoFreeLayout } from './editor';\n"
  },
  {
    "path": "apps/demo-vite/src/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content',\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom',\n        content: 'Custom node content',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End',\n        content: 'End content',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/demo-vite/src/node-registries.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/demo-vite/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist\",\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"bundler\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"jsx\": \"react-jsx\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ]\n  },\n  \"include\": [\n    \"./src\"\n  ],\n}\n"
  },
  {
    "path": "apps/demo-vite/vite.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { analyzer } from 'vite-bundle-analyzer';\n\n// https://vite.dev/config/\nexport default defineConfig({\n  plugins: [react(), analyzer()],\n  server: {\n    fs: {\n      strict: false,\n    },\n  },\n});\n"
  },
  {
    "path": "apps/docs/.gitignore",
    "content": "# Local\n.DS_Store\n*.local\n*.log*\n\n# Dist\nnode_modules\ndist/\ndoc_build/\n\n# IDE\n.vscode/*\n!.vscode/extensions.json\n.idea\n\n# 自动构建生成的 API\nauto-docs\n"
  },
  {
    "path": "apps/docs/README.md",
    "content": "# FlowGram.AI WebSite\n\n## Setup\n\nInstall the dependencies:\n\n```bash\nrush update\n```\n\n## Get Started\n\nStart the dev server:\n\n```bash\ncd apps/docs\nrushx dev\n```\n\nBuild the website for production:\n\n```bash\nrushx build\n```\n\nPreview the production build locally:\n\n```bash\nrushx preview\n```\n"
  },
  {
    "path": "apps/docs/components/code-preview/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { useDark } from '@rspress/core/runtime';\nimport { Sandpack } from '@codesandbox/sandpack-react';\n\ninterface CodePreviewProps {\n  files: Record<string, string>;\n  activeFile?: string;\n}\n\nexport const CodePreview: FC<CodePreviewProps> = ({ files, activeFile }) => {\n  const dark = useDark();\n  return (\n    <Sandpack\n      files={files}\n      theme={dark ? 'dark' : 'light'}\n      template=\"react-ts\"\n      customSetup={{\n        dependencies: {\n          '@flowgram.ai/free-layout-editor': '0.5.5',\n          '@flowgram.ai/free-snap-plugin': '0.5.5',\n          '@flowgram.ai/minimap-plugin': '0.5.5',\n          'styled-components': '5.3.11',\n        },\n      }}\n      options={{\n        editorHeight: 350,\n        activeFile,\n      }}\n    />\n  );\n};\n\nexport const FixedLayoutCodePreview: FC<CodePreviewProps> = ({ files, activeFile }) => {\n  const dark = useDark();\n  return (\n    <Sandpack\n      files={files}\n      theme={dark ? 'dark' : 'light'}\n      template=\"react-ts\"\n      customSetup={{\n        dependencies: {\n          '@flowgram.ai/fixed-layout-editor': '0.1.0-alpha.19',\n          // 为了解决semi无法在sandpack使用的问题，单独发了包，将semi打包进@flowgram.ai/fixed-semi-materials中\n          '@flowgram.ai/fixed-semi-materials': '0.1.0-alpha.19',\n          '@flowgram.ai/minimap-plugin': '0.1.0-alpha.19',\n          'styled-components': '5.3.11',\n        },\n      }}\n      options={{\n        editorHeight: 350,\n        activeFile,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-1.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nconst FlowGramApp = () => (\n  <FixedLayoutEditorProvider\n    materials={{\n      components: defaultFixedSemiMaterials,\n    }}\n  >\n    <EditorRenderer />\n  </FixedLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-2.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport {\n  FixedLayoutEditorProvider,\n  EditorRenderer,\n  FlowNodeEntity,\n  useNodeRender,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n    </div>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FixedLayoutEditorProvider\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    initialData={{\n      nodes: [\n        {\n          id: 'custom_0',\n          type: 'custom',\n        },\n      ],\n    }}\n    materials={{\n      renderDefaultNode: NodeRender,\n      components: defaultFixedSemiMaterials,\n    }}\n  >\n    <EditorRenderer />\n  </FixedLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-3.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FC } from 'react';\n\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport {\n  FixedLayoutEditorProvider,\n  EditorRenderer,\n  FlowNodeEntity,\n  useNodeRender,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n  FlowRendererKey,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n  const ctx = useClientContext();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n      {/* 删除按钮 */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          ctx.operation.deleteNode(node);\n        }}\n        style={{\n          position: 'absolute',\n          top: 4,\n          right: 4,\n          width: 20,\n          height: 20,\n          border: 'none',\n          borderRadius: '50%',\n          background: '#fff',\n          color: '#666',\n          fontSize: 12,\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxShadow: '0 1px 3px rgba(0,0,0,0.12)',\n          transition: 'all 0.2s',\n        }}\n      >\n        ×\n      </button>\n    </div>\n  );\n};\n\nconst useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n\nconst Adder: FC<{\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}> = ({ from, hoverActivated }) => {\n  const playground = usePlayground();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <div\n      style={{\n        width: hoverActivated ? 15 : 6,\n        height: hoverActivated ? 15 : 6,\n        backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',\n        color: '#fff',\n        borderRadius: '50%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        cursor: 'pointer',\n      }}\n      onClick={() => {\n        handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);\n      }}\n    >\n      {hoverActivated ? (\n        <span\n          style={{\n            color: '#3370ff',\n            fontSize: 12,\n          }}\n        >\n          +\n        </span>\n      ) : null}\n    </div>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FixedLayoutEditorProvider\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    initialData={{\n      nodes: [\n        {\n          id: 'start_0',\n          type: 'start',\n        },\n        {\n          id: 'custom_1',\n          type: 'custom',\n        },\n        {\n          id: 'end_2',\n          type: 'end',\n        },\n      ],\n    }}\n    onAllLayersRendered={(ctx) => {\n      setTimeout(() => {\n        ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n      }, 10);\n    }}\n    materials={{\n      renderDefaultNode: NodeRender,\n      components: {\n        ...defaultFixedSemiMaterials,\n        [FlowRendererKey.ADDER]: Adder,\n      },\n    }}\n  >\n    <EditorRenderer />\n  </FixedLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-4.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FC } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport {\n  FixedLayoutEditorProvider,\n  EditorRenderer,\n  FlowNodeEntity,\n  useNodeRender,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n  FlowRendererKey,\n  useClientContext,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n  const ctx = useClientContext();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n      {/* 删除按钮 */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          ctx.operation.deleteNode(node);\n        }}\n        style={{\n          position: 'absolute',\n          top: 4,\n          right: 4,\n          width: 20,\n          height: 20,\n          border: 'none',\n          borderRadius: '50%',\n          background: '#fff',\n          color: '#666',\n          fontSize: 12,\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxShadow: '0 1px 3px rgba(0,0,0,0.12)',\n          transition: 'all 0.2s',\n        }}\n      >\n        ×\n      </button>\n    </div>\n  );\n};\n\nconst useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n\nconst Adder: FC<{\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}> = ({ from, hoverActivated }) => {\n  const playground = usePlayground();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <div\n      style={{\n        width: hoverActivated ? 15 : 6,\n        height: hoverActivated ? 15 : 6,\n        backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',\n        color: '#fff',\n        borderRadius: '50%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        cursor: 'pointer',\n      }}\n      onClick={() => {\n        handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);\n      }}\n    >\n      {hoverActivated ? (\n        <span\n          style={{\n            color: '#3370ff',\n            fontSize: 12,\n          }}\n        >\n          +\n        </span>\n      ) : null}\n    </div>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FixedLayoutEditorProvider\n    plugins={() => [\n      createMinimapPlugin({\n        enableDisplayAllNodes: true,\n      }),\n    ]}\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    initialData={{\n      nodes: [\n        {\n          id: 'start_0',\n          type: 'start',\n        },\n        {\n          id: 'custom_1',\n          type: 'custom',\n        },\n        {\n          id: 'end_2',\n          type: 'end',\n        },\n      ],\n    }}\n    onAllLayersRendered={(ctx) => {\n      setTimeout(() => {\n        ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n      }, 10);\n    }}\n    materials={{\n      renderDefaultNode: NodeRender,\n      components: {\n        ...defaultFixedSemiMaterials,\n        [FlowRendererKey.ADDER]: Adder,\n      },\n    }}\n  >\n    <EditorRenderer />\n  </FixedLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FC } from 'react';\n\nimport {\n  FlowNodeEntity,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nconst useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n\nexport const Adder: FC<{\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}> = ({ from, hoverActivated }) => {\n  const playground = usePlayground();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <div\n      style={{\n        width: hoverActivated ? 15 : 6,\n        height: hoverActivated ? 15 : 6,\n        backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',\n        color: '#fff',\n        borderRadius: '50%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        cursor: 'pointer',\n      }}\n      onClick={() => {\n        handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from);\n      }}\n    >\n      {hoverActivated ? (\n        <span\n          style={{\n            color: '#3370ff',\n            fontSize: 12,\n          }}\n        >\n          +\n        </span>\n      ) : null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n    </FixedLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'custom_1',\n      type: 'custom',\n    },\n    {\n      id: 'end_2',\n      type: 'end',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/node-registries.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nexport const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [\n  {\n    type: 'custom',\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n  const ctx = useClientContext();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n      {/* 删除按钮 */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          ctx.operation.deleteNode(node);\n        }}\n        style={{\n          position: 'absolute',\n          top: 4,\n          right: 4,\n          width: 20,\n          height: 20,\n          border: 'none',\n          borderRadius: '50%',\n          background: '#fff',\n          color: '#666',\n          fontSize: 12,\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxShadow: '0 1px 3px rgba(0,0,0,0.12)',\n          transition: 'all 0.2s',\n        }}\n      >\n        ×\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-5/use-editor-props.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport { FlowRendererKey, FixedLayoutProps } from '@flowgram.ai/fixed-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\nimport { Adder } from './adder';\n\nexport function useEditorProps(): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      plugins: () => [\n        createMinimapPlugin({\n          enableDisplayAllNodes: true,\n        }),\n      ],\n      nodeRegistries,\n      initialData,\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n        }, 10);\n      },\n      materials: {\n        renderDefaultNode: NodeRender,\n        components: {\n          ...defaultFixedSemiMaterials,\n          [FlowRendererKey.ADDER]: Adder,\n        },\n      },\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FC } from 'react';\n\nimport {\n  FlowNodeEntity,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nconst useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n\nexport const Adder: FC<{\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}> = ({ from, hoverActivated }) => {\n  const playground = usePlayground();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <div\n      style={{\n        width: hoverActivated ? 15 : 6,\n        height: hoverActivated ? 15 : 6,\n        backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',\n        color: '#fff',\n        borderRadius: '50%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        cursor: 'pointer',\n      }}\n      onClick={() => {\n        handleAdd(\n          {\n            type: 'custom',\n            id: `custom_${Date.now()}`,\n            data: {\n              title: 'New Custom Node',\n              content: 'Custom Node Content',\n            },\n          },\n          from\n        );\n      }}\n    >\n      {hoverActivated ? (\n        <span\n          style={{\n            color: '#3370ff',\n            fontSize: 12,\n          }}\n        >\n          +\n        </span>\n      ) : null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n    </FixedLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/initial-data.ts",
    "content": "import type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    // 开始节点\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content',\n      },\n      blocks: [],\n    },\n    // 分支节点\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: 'Condition',\n        content: 'condition content',\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'custom_0',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'break_0',\n              type: 'break',\n              data: {\n                title: 'Break',\n                content: 'Break content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_2',\n          type: 'block',\n          data: {\n            title: 'Branch 2',\n            content: 'branch 2 content',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    // 结束节点\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/node-registries.tsx",
    "content": "/**\n * Copyright (c) 202 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nconst randomID = () => Math.random().toString(36).slice(2, 7);\n\nexport const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [\n  {\n    /**\n     * 自定义节点类型\n     */\n    type: 'condition',\n    /**\n     * 自定义节点扩展:\n     *  - loop: 扩展为循环节点\n     *  - start: 扩展为开始节点\n     *  - dynamicSplit: 扩展为分支节点\n     *  - end: 扩展为结束节点\n     *  - tryCatch: 扩展为 tryCatch 节点\n     *  - break: 分支断开\n     *  - default: 扩展为普通节点 (默认)\n     */\n    extend: 'dynamicSplit',\n    /**\n     * 节点配置信息\n     */\n    meta: {\n      // isStart: false, // 是否为开始节点\n      // isNodeEnd: false, // 是否为结束节点，结束节点后边无法再添加节点\n      // draggable: false, // 是否可拖拽，如开始节点和结束节点无法拖拽\n      // selectable: false, // 触发器等开始节点不能被框选\n      // deleteDisable: true, // 禁止删除\n      // copyDisable: true, // 禁止copy\n      // addDisable: true, // 禁止添加\n    },\n    onAdd() {\n      return {\n        id: `condition_${randomID()}`,\n        type: 'condition',\n        data: {\n          title: 'Condition',\n        },\n        blocks: [\n          {\n            id: randomID(),\n            type: 'block',\n            data: {\n              title: 'If_0',\n            },\n          },\n          {\n            id: randomID(),\n            type: 'block',\n            data: {\n              title: 'If_1',\n            },\n          },\n        ],\n      };\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    onAdd() {\n      return {\n        id: `custom_${randomID()}`,\n        type: 'custom',\n        data: {\n          title: 'Custom',\n          content: 'this is custom content',\n        },\n      };\n    },\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n  const ctx = useClientContext();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n      {/* 删除按钮 */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          ctx.operation.deleteNode(node);\n        }}\n        style={{\n          position: 'absolute',\n          top: 4,\n          right: 4,\n          width: 20,\n          height: 20,\n          border: 'none',\n          borderRadius: '50%',\n          background: '#fff',\n          color: '#666',\n          fontSize: 12,\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxShadow: '0 1px 3px rgba(0,0,0,0.12)',\n          transition: 'all 0.2s',\n        }}\n      >\n        ×\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-6/use-editor-props.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport { FlowRendererKey, FixedLayoutProps, Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\nimport { Adder } from './adder';\n\nexport function useEditorProps(): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      plugins: () => [\n        createMinimapPlugin({\n          enableDisplayAllNodes: true,\n        }),\n      ],\n      nodeRegistries,\n      initialData,\n      materials: {\n        renderDefaultNode: NodeRender,\n        components: {\n          ...defaultFixedSemiMaterials,\n          [FlowRendererKey.ADDER]: Adder,\n        },\n      },\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n        }, 10);\n      },\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n                <Field<string> name=\"content\">\n                  <input />\n                </Field>\n              </>\n            ),\n          },\n        };\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n        onApply: (ctx) => {\n          if (ctx.document.disposed) return;\n          // Listen change to trigger auto save\n          console.log('auto save: ', ctx.document.toJSON());\n        },\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */ nodeEngine: {\n        enable: true,\n      },\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FC } from 'react';\n\nimport {\n  FlowNodeEntity,\n  FlowNodeJSON,\n  FlowOperationService,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nconst useAddNode = () => {\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const handleAdd = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const blocks = addProps.blocks ? addProps.blocks : undefined;\n    const entity = flowOperationService.addFromNode(dropNode, {\n      ...addProps,\n      blocks,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: entity.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n    return entity;\n  };\n\n  const handleAddBranch = (addProps: FlowNodeJSON, dropNode: FlowNodeEntity) => {\n    const index = dropNode.index + 1;\n    const entity = flowOperationService.addBlock(dropNode.originParent!, addProps, {\n      index,\n    });\n    return entity;\n  };\n\n  return {\n    handleAdd,\n    handleAddBranch,\n  };\n};\n\nexport const Adder: FC<{\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}> = ({ from, hoverActivated }) => {\n  const playground = usePlayground();\n\n  const { handleAdd } = useAddNode();\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <div\n      style={{\n        width: hoverActivated ? 15 : 6,\n        height: hoverActivated ? 15 : 6,\n        backgroundColor: hoverActivated ? '#fff' : 'rgb(143, 149, 158)',\n        color: '#fff',\n        borderRadius: '50%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        cursor: 'pointer',\n      }}\n      onClick={() => {\n        handleAdd(\n          {\n            type: 'custom',\n            id: `custom_${Date.now()}`,\n            data: {\n              title: 'New Custom Node',\n              content: 'Custom Node Content',\n            },\n          },\n          from\n        );\n      }}\n    >\n      {hoverActivated ? (\n        <span\n          style={{\n            color: '#3370ff',\n            fontSize: 12,\n          }}\n        >\n          +\n        </span>\n      ) : null}\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/app.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FixedLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/fixed-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\nimport { Tools } from './tools';\nimport { Minimap } from './minimap';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n      <Tools />\n      <Minimap />\n    </FixedLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/initial-data.ts",
    "content": "import type { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    // 开始节点\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content',\n      },\n      blocks: [],\n    },\n    // 分支节点\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: 'Condition',\n        content: 'condition content',\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'custom_0',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custom content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n            content: 'branch 1 content',\n          },\n          blocks: [\n            {\n              id: 'break_0',\n              type: 'break',\n              data: {\n                title: 'Break',\n                content: 'Break content',\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_2',\n          type: 'block',\n          data: {\n            title: 'Branch 2',\n            content: 'branch 2 content',\n          },\n          blocks: [],\n        },\n      ],\n    },\n    // 结束节点\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/minimap.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      left: 16,\n      bottom: 72,\n      zIndex: 100,\n      width: 118,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/node-registries.tsx",
    "content": "/**\n * Copyright (c) 202 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeMeta, FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';\n\nconst randomID = () => Math.random().toString(36).slice(2, 7);\n\nexport const nodeRegistries: FlowNodeRegistry<FlowNodeMeta>[] = [\n  {\n    /**\n     * 自定义节点类型\n     */\n    type: 'condition',\n    /**\n     * 自定义节点扩展:\n     *  - loop: 扩展为循环节点\n     *  - start: 扩展为开始节点\n     *  - dynamicSplit: 扩展为分支节点\n     *  - end: 扩展为结束节点\n     *  - tryCatch: 扩展为 tryCatch 节点\n     *  - break: 分支断开\n     *  - default: 扩展为普通节点 (默认)\n     */\n    extend: 'dynamicSplit',\n    /**\n     * 节点配置信息\n     */\n    meta: {\n      // isStart: false, // 是否为开始节点\n      // isNodeEnd: false, // 是否为结束节点，结束节点后边无法再添加节点\n      // draggable: false, // 是否可拖拽，如开始节点和结束节点无法拖拽\n      // selectable: false, // 触发器等开始节点不能被框选\n      // deleteDisable: true, // 禁止删除\n      // copyDisable: true, // 禁止copy\n      // addDisable: true, // 禁止添加\n    },\n    onAdd() {\n      return {\n        id: `condition_${randomID()}`,\n        type: 'condition',\n        data: {\n          title: 'Condition',\n        },\n        blocks: [\n          {\n            id: randomID(),\n            type: 'block',\n            data: {\n              title: 'If_0',\n            },\n          },\n          {\n            id: randomID(),\n            type: 'block',\n            data: {\n              title: 'If_1',\n            },\n          },\n        ],\n      };\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    onAdd() {\n      return {\n        id: `custom_${randomID()}`,\n        type: 'custom',\n        data: {\n          title: 'Custom',\n          content: 'this is custom content',\n        },\n      };\n    },\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { FlowNodeEntity, useNodeRender, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n  const {\n    onMouseEnter,\n    onMouseLeave,\n    startDrag,\n    form,\n    dragging,\n    isBlockOrderIcon,\n    isBlockIcon,\n    activated,\n  } = useNodeRender();\n  const ctx = useClientContext();\n\n  return (\n    <div\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      onMouseDown={(e) => {\n        startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: activated ? '#82a7fc' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n        opacity: dragging ? 0.3 : 1,\n        ...(isBlockOrderIcon || isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n      {/* 删除按钮 */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation();\n          ctx.operation.deleteNode(node);\n        }}\n        style={{\n          position: 'absolute',\n          top: 4,\n          right: 4,\n          width: 20,\n          height: 20,\n          border: 'none',\n          borderRadius: '50%',\n          background: '#fff',\n          color: '#666',\n          fontSize: 12,\n          cursor: 'pointer',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          boxShadow: '0 1px 3px rgba(0,0,0,0.12)',\n          transition: 'all 0.2s',\n        }}\n      >\n        ×\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties, useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport const Tools = () => {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  const buttonStyle: CSSProperties = {\n    border: '1px solid #e0e0e0',\n    borderRadius: '4px',\n    cursor: 'pointer',\n    padding: '4px',\n    color: '#141414',\n    background: '#e1e3e4',\n  };\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 34, left: 16, display: 'flex', gap: 8 }}\n    >\n      <button style={buttonStyle} onClick={() => tools.zoomin()}>\n        ZoomIn\n      </button>\n      <button style={buttonStyle} onClick={() => tools.zoomout()}>\n        ZoomOut\n      </button>\n      <span\n        style={{\n          ...buttonStyle,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          cursor: 'default',\n          width: 40,\n        }}\n      >\n        {Math.floor(tools.zoom * 100)}%\n      </span>\n      <button style={buttonStyle} onClick={() => tools.fitView()}>\n        FitView\n      </button>\n      <button style={buttonStyle} onClick={() => tools.changeLayout()}>\n        ChangeLayout\n      </button>\n      <button\n        style={{\n          ...buttonStyle,\n          cursor: canUndo ? 'pointer' : 'not-allowed',\n          color: canUndo ? '#141414' : '#b1b1b1',\n        }}\n        onClick={() => history.undo()}\n        disabled={!canUndo}\n      >\n        Undo\n      </button>\n      <button\n        style={{\n          ...buttonStyle,\n          cursor: canRedo ? 'pointer' : 'not-allowed',\n          color: canRedo ? '#141414' : '#b1b1b1',\n        }}\n        onClick={() => history.redo()}\n        disabled={!canRedo}\n      >\n        Redo\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/fixed-examples/step-7/use-editor-props.tsx",
    "content": "import '@flowgram.ai/fixed-layout-editor/index.css';\n\nimport { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';\nimport { FlowRendererKey, FixedLayoutProps, Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\nimport { Adder } from './adder';\n\nexport function useEditorProps(): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      plugins: () => [\n        createMinimapPlugin({\n          disableLayer: true,\n          enableDisplayAllNodes: true,\n          canvasStyle: {\n            canvasWidth: 100,\n            canvasHeight: 50,\n            canvasPadding: 50,\n          },\n        }),\n      ],\n      nodeRegistries,\n      initialData,\n      materials: {\n        renderDefaultNode: NodeRender,\n        components: {\n          ...defaultFixedSemiMaterials,\n          [FlowRendererKey.ADDER]: Adder,\n        },\n      },\n      onAllLayersRendered: (ctx) => {\n        setTimeout(() => {\n          ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n        }, 10);\n      },\n      /**\n       * Get the default node registry, which will be merged with the 'nodeRegistries'\n       * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n       */\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n                <Field<string> name=\"content\">\n                  <input />\n                </Field>\n              </>\n            ),\n          },\n        };\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n        onApply: (ctx) => {\n          if (ctx.document.disposed) return;\n          // Listen change to trigger auto save\n          console.log('auto save: ', ctx.document.toJSON());\n        },\n      },\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */ nodeEngine: {\n        enable: true,\n      },\n    }),\n    []\n  );\n}\n"
  },
  {
    "path": "apps/docs/components/fixed-feature-overview/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.doc-feature-overview {\n  position: relative;\n  z-index: 1;\n\n  .gedit-playground {\n    position: relative;\n    width: 100%;\n    height: 600px;\n  }\n\n  .fixed-demo-tools {\n    position: relative;\n    bottom: 40px;\n    color: black;\n  }\n}\n"
  },
  {
    "path": "apps/docs/components/fixed-feature-overview/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport './index.less';\n\n// https://github.com/web-infra-dev/rspress/issues/553\nconst FixedFeatureOverview = React.lazy(() =>\n  import('@flowgram.ai/demo-fixed-layout').then((module) => ({\n    default: module.DemoFixedLayout,\n  }))\n);\n\nexport { FixedFeatureOverview };\n"
  },
  {
    "path": "apps/docs/components/fixed-layout-simple/composite-nodes-preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-unresolved */\n\nimport tryCatch from '@flowgram.ai/demo-fixed-layout-simple/src/data/tryCatch.ts?raw';\nimport slot from '@flowgram.ai/demo-fixed-layout-simple/src/data/slot.ts?raw';\nimport multiOutputs from '@flowgram.ai/demo-fixed-layout-simple/src/data/multiOutputs.ts?raw';\nimport multiInputs from '@flowgram.ai/demo-fixed-layout-simple/src/data/multiInputs.ts?raw';\nimport loop from '@flowgram.ai/demo-fixed-layout-simple/src/data/loop.ts?raw';\nimport dynamicSplit from '@flowgram.ai/demo-fixed-layout-simple/src/data/dynamicSplit.ts?raw';\n\nimport { PreviewEditor } from '../preview-editor.tsx';\nimport { FixedLayoutSimple } from './fixed-layout-simple.tsx';\n\nexport function CompositeNodesPreview(props: { cellHeight?: number }) {\n  const previewWidth = '50%';\n  const editorWidth = '50%';\n  const cellHeight = props.cellHeight || 300;\n  return (\n    <table\n      className=\"\"\n      style={{\n        width: '100%',\n        border: '1px solid var(--rp-c-divider-light)',\n        zIndex: 1,\n        position: 'relative',\n      }}\n    >\n      <tr>\n        <td style={{ textAlign: 'center' }}>dynamicSplit</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: dynamicSplit,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              width: editorWidth,\n              height: cellHeight,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"dynamicSplit\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n      <tr>\n        <td style={{ textAlign: 'center' }}>loop</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: loop,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              height: cellHeight,\n              width: editorWidth,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"loop\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n      <tr>\n        <td style={{ textAlign: 'center' }}>tryCatch</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: tryCatch,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              height: cellHeight,\n              width: editorWidth,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"tryCatch\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n      <tr>\n        <td style={{ textAlign: 'center' }}>multiInputs</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: multiInputs,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              height: cellHeight,\n              width: editorWidth,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"multiInputs\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n      <tr>\n        <td style={{ textAlign: 'center' }}>multiOutputs</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: multiOutputs,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              height: cellHeight,\n              width: editorWidth,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"multiOutputs\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n      <tr>\n        <td style={{ textAlign: 'center' }}>slot</td>\n        <td>\n          <PreviewEditor\n            codeInRight\n            files={{\n              'index.tsx': {\n                code: slot,\n                active: true,\n              },\n            }}\n            previewStyle={{\n              width: previewWidth,\n              height: cellHeight,\n              position: 'relative',\n            }}\n            editorStyle={{\n              height: cellHeight,\n              width: editorWidth,\n            }}\n          >\n            <FixedLayoutSimple hideTools demo=\"slot\" />\n          </PreviewEditor>\n        </td>\n      </tr>\n    </table>\n  );\n}\n"
  },
  {
    "path": "apps/docs/components/fixed-layout-simple/fixed-layout-simple.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\n// https://github.com/web-infra-dev/rspress/issues/553\nconst FixedLayoutSimple = React.lazy(() =>\n  import('@flowgram.ai/demo-fixed-layout-simple').then((module) => ({\n    default: module.DemoFixedLayout,\n  }))\n);\n\nexport { FixedLayoutSimple };\n"
  },
  {
    "path": "apps/docs/components/fixed-layout-simple/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FixedLayoutSimple } from './fixed-layout-simple.tsx';\nexport { CompositeNodesPreview } from './composite-nodes-preview.tsx';\n"
  },
  {
    "path": "apps/docs/components/fixed-layout-simple/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-unresolved */\n\nimport nodeRegistriesCode from '@flowgram.ai/demo-fixed-layout-simple/src/node-registries.ts?raw';\nimport initialDataCode from '@flowgram.ai/demo-fixed-layout-simple/src/initial-data.ts?raw';\nimport indexCssCode from '@flowgram.ai/demo-fixed-layout-simple/src/index.css?raw';\nimport useEditorPropsCode from '@flowgram.ai/demo-fixed-layout-simple/src/hooks/use-editor-props.tsx?raw';\nimport editorCode from '@flowgram.ai/demo-fixed-layout-simple/src/editor.tsx?raw';\nimport toolsCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/tools.tsx?raw';\nimport nodeAdderCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/node-adder.tsx?raw';\nimport miniMapCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/minimap.tsx?raw';\nimport branchAdderCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/branch-adder.tsx?raw';\nimport baseNodeCode from '@flowgram.ai/demo-fixed-layout-simple/src/components/base-node.tsx?raw';\n\nimport { FixedLayoutSimple } from './index';\nimport { PreviewEditor } from '../preview-editor';\n\nconst indexCode = {\n  code: editorCode,\n  active: true,\n};\n\nexport const FixedLayoutSimplePreview = () => (\n  <div\n    style={{\n      zIndex: 1,\n      position: 'relative',\n    }}\n  >\n    <PreviewEditor\n      files={{\n        'editor.tsx': indexCode,\n        'index.css': indexCssCode,\n        'initial-data.ts': initialDataCode,\n        'node-registries.ts': nodeRegistriesCode,\n        'use-editor-props.tsx': useEditorPropsCode,\n        'base-node.tsx': baseNodeCode,\n        'branch-adder.tsx': branchAdderCode,\n        'minimap.tsx': miniMapCode,\n        'node-adder.tsx': nodeAdderCode,\n        'tools.tsx': toolsCode,\n      }}\n      previewStyle={{\n        height: 500,\n      }}\n      editorStyle={{\n        height: 500,\n      }}\n    >\n      <FixedLayoutSimple />\n    </PreviewEditor>\n  </div>\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/common/disable-declaration-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { createDisableDeclarationPlugin } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    plugins={() => [createDisableDeclarationPlugin()]}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/common/json-schema-preset.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.json-schema-color-picker-container {\n  .semi-colorPicker-popover-defaultChildren {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "apps/docs/components/form-materials/common/json-schema-preset.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport {\n  ConditionRow,\n  ConditionRowValueType,\n  createTypePresetPlugin,\n  DynamicValueInput,\n  IFlowConstantRefValue,\n  type IJsonSchema,\n} from '@flowgram.ai/form-materials';\nimport { ColorPicker } from '@douyinfe/semi-ui';\nimport { IconColorPalette } from '@douyinfe/semi-icons';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nimport './json-schema-preset.css';\n\nconst TypeSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.TypeSelector,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    transformInitialNode={{\n      start_0: (node) => {\n        node.data.outputs = {\n          type: 'object',\n          properties: {\n            color_output: {\n              type: 'color',\n            },\n          },\n        };\n        return node;\n      },\n    }}\n    plugins={() => [\n      createTypePresetPlugin({\n        types: [\n          {\n            type: 'color',\n            icon: <IconColorPalette />,\n            label: 'Color',\n            ConstantRenderer: ({ value, onChange }) => (\n              <div className=\"json-schema-color-picker-container \">\n                <ColorPicker\n                  alpha={true}\n                  usePopover={true}\n                  value={value ? ColorPicker.colorStringToValue(value) : undefined}\n                  onChange={(_value) => onChange?.(_value.hex)}\n                />\n              </div>\n            ),\n            conditionRule: {\n              eq: { type: 'color' },\n            },\n          },\n        ],\n      }),\n    ]}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <b>Type Selector: </b>\n          <Field<Partial<IJsonSchema> | undefined>\n            name=\"type_selector\"\n            defaultValue={{ type: 'color' }}\n          >\n            {({ field }) => (\n              <TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n\n          <b>DynamicValueInput: </b>\n          <Field<IFlowConstantRefValue | undefined>\n            name=\"dynamic_value_input\"\n            defaultValue={{ type: 'constant', schema: { type: 'color' }, content: '#b5ed0c' }}\n          >\n            {({ field }) => (\n              <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n\n          <b>Condition: </b>\n          <Field<ConditionRowValueType | undefined>\n            name=\"condition_row\"\n            defaultValue={{\n              left: { type: 'ref', content: ['start_0', 'color_output'] },\n              operator: 'eq',\n              right: { type: 'ref', content: ['start_0', 'color_output'] },\n            }}\n          >\n            {({ field }) => (\n              <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/assign-row.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { AssignValueType } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst AssignRow = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.AssignRow,\n  }))\n);\n\nexport const AssignModeStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<AssignValueType | undefined>\n            defaultValue={{\n              operator: 'assign',\n              left: { type: 'ref', content: ['start_0', 'str'] },\n              right: { type: 'constant', content: 'Hello World', schema: { type: 'string' } },\n            }}\n            name=\"assign_row\"\n          >\n            {({ field }) => (\n              <AssignRow value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const DeclareModeStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<AssignValueType | undefined> name=\"assign_row\">\n            {({ field }) => (\n              <AssignRow\n                value={{\n                  operator: 'declare',\n                  left: 'newVariable',\n                  right: {\n                    type: 'constant',\n                    content: 'Hello World',\n                    schema: { type: 'string' },\n                  },\n                }}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/assign-rows.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst AssignRows = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.AssignRows,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <AssignRows name=\"assign_rows\" />\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/batch-outputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\nimport {\n  provideBatchInputEffect,\n  createBatchOutputsFormPlugin,\n  type IFlowRefValue,\n  DisplayOutputs,\n} from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst BatchVariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchVariableSelector,\n  }))\n);\n\nconst BatchOutputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchOutputs,\n  }))\n);\n\ntype BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;\n\nconst createLoopRegistry = (): FlowNodeRegistry => ({\n  type: 'custom',\n  meta: {\n    isContainer: true,\n    size: {\n      width: 500,\n      height: 260,\n    },\n    padding: () => ({\n      top: 160,\n      bottom: 40,\n      left: 50,\n      right: 50,\n    }),\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <FormHeader />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop input (select array variable):\n          </div>\n          <Field<IFlowRefValue | undefined>\n            name=\"loopFor\"\n            defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}\n          >\n            {({ field }) => (\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n              />\n            )}\n          </Field>\n        </div>\n        <SubCanvasRender offsetY={-100} />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop outputs (key-value pairs collected into arrays):\n          </div>\n          <Field<BatchOutputsValueType | undefined>\n            name=\"loopOutputs\"\n            defaultValue={{\n              result: { type: 'ref', content: ['variable_0', 'result'] },\n              count: { type: 'ref', content: ['variable_0', 'count'] },\n            }}\n          >\n            {({ field }) => (\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n              />\n            )}\n          </Field>\n          <div>\n            <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n              Generated output variables:\n            </div>\n            <DisplayOutputs displayFromScope />\n          </div>\n        </div>\n      </>\n    ),\n    effect: {\n      loopFor: provideBatchInputEffect,\n    },\n    plugins: [\n      createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),\n    ],\n  },\n});\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    height={550}\n    initialData={{\n      nodes: [\n        {\n          id: 'custom_0',\n          type: 'custom',\n          data: {\n            title: 'Loop',\n            loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },\n          },\n          blocks: [\n            {\n              id: 'block_start_0',\n              type: 'block-start',\n              data: { title: 'Start' },\n              meta: { position: { x: 20, y: 0 } },\n            },\n            {\n              id: 'variable_0',\n              type: 'variable',\n              data: {\n                title: 'Variable',\n                assign: [\n                  {\n                    operator: 'declare',\n                    left: 'result',\n                    right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },\n                  },\n                  {\n                    operator: 'declare',\n                    left: 'count',\n                    right: { type: 'ref', content: ['custom_0_locals', 'index'] },\n                  },\n                ],\n              },\n              meta: { position: { x: 100, y: 0 } },\n            },\n            {\n              id: 'block_end_0',\n              type: 'block-end',\n              data: { title: 'End' },\n              meta: { position: { x: 360, y: 0 } },\n            },\n          ],\n          edges: [\n            { sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },\n            { sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },\n          ],\n        },\n      ],\n      edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],\n    }}\n    transformRegistry={(props) => ({\n      ...props,\n      nodeRegistries: [\n        ...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),\n        createLoopRegistry(),\n      ],\n    })}\n    plugins={(ctx) => [createContainerNodePlugin({})]}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/batch-variable-selector.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst BatchVariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchVariableSelector,\n  }))\n);\n\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <div style={{ marginBottom: 16 }}>\n            <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n              BatchVariableSelector (Array variables only):\n            </div>\n            <Field<string[] | undefined> name=\"batch_variable\">\n              {({ field }) => (\n                <BatchVariableSelector\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n              )}\n            </Field>\n          </div>\n          <div>\n            <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n              VariableSelector (All variables):\n            </div>\n            <Field<string[] | undefined> name=\"normal_variable\">\n              {({ field }) => (\n                <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n              )}\n            </Field>\n          </div>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/blur-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst BlurInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BlurInput,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"blur_input\" defaultValue=\"Initial text\">\n            {({ field }) => (\n              <>\n                <BlurInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  placeholder=\"Please enter text\"\n                />\n                <p className=\"mt-2\">Current value: {field.value}</p>\n                <p className=\"text-sm text-gray-500\">\n                  Note: Value updates after clicking outside the input\n                </p>\n              </>\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const PlaceholderStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"blur_input_placeholder\" defaultValue=\"\">\n            {({ field }) => (\n              <>\n                <BlurInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  placeholder=\"This is an input field with placeholder\"\n                />\n                <p className=\"mt-2\">Current value: {field.value || 'Empty'}</p>\n              </>\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const DisabledStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"blur_input_disabled\" defaultValue=\"Disabled state text\">\n            {({ field }) => (\n              <BlurInput value={field.value} onChange={(value) => field.onChange(value)} disabled />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/code-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst CodeEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.CodeEditor,\n  }))\n);\n\nconst defaultTsCode = `// Here, you can retrieve input variables from the node using 'params' and output results using 'ret'.\n// 'params' has been correctly injected into the environment.\n// Here's an example of getting the value of the parameter named 'input' from the node input:\n// const input = params.input;\n// Here's an example of outputting a 'ret' object containing multiple data types:\n// const ret = { \"name\": 'Xiaoming', \"hobbies\": [\"Reading\", \"Traveling\"] };\n\nasync function main({ params }) {\n  // Build the output object\n  const ret = {\n    key0: params.input + params.input, // Concatenate the input parameter 'input' twice\n    key1: [\"hello\", \"world\"], // Output an array\n    key2: { // Output an Object\n      key21: \"hi\"\n    },\n  };\n\n  return ret;\n}`;\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    height={600}\n    formMeta={{\n      render: () => (\n        <div style={{ width: 500 }}>\n          <FormHeader />\n          <Field<string | undefined> name=\"code_editor\" defaultValue={defaultTsCode}>\n            {({ field }) => (\n              <CodeEditor\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                languageId=\"typescript\"\n              />\n            )}\n          </Field>\n        </div>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/condition-context.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport type { ConditionOpConfigs, IConditionRule } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst ConditionRow = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.ConditionRow,\n  }))\n);\n\nconst DBConditionRow = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DBConditionRow,\n  }))\n);\n\nconst ConditionProvider = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.ConditionProvider,\n  }))\n);\n\nconst OPS: ConditionOpConfigs = {\n  cop: {\n    abbreviation: 'C',\n    label: 'Custom Operator',\n  },\n};\n\nconst RULES: Record<string, IConditionRule> = {\n  string: {\n    cop: { type: 'string' },\n  },\n};\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <ConditionProvider ops={OPS} rules={RULES}>\n            <Field<any | undefined> name=\"condition_row\">\n              {({ field }) => (\n                <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n              )}\n            </Field>\n            <Field<any | undefined> name=\"db_condition_row\">\n              {({ field }) => (\n                <DBConditionRow\n                  options={[\n                    {\n                      label: 'UserName',\n                      value: 'username',\n                      schema: { type: 'string' },\n                    },\n                  ]}\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n              )}\n            </Field>\n          </ConditionProvider>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/condition-row.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst ConditionRow = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.ConditionRow,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any | undefined>\n            name=\"condition_row\"\n            defaultValue={{\n              left: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n              operator: 'eq',\n              right: {\n                type: 'constant',\n                content: 'Hello World!',\n                schema: {\n                  type: 'string',\n                },\n              },\n            }}\n          >\n            {({ field }) => (\n              <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/constant-inputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst ConstantInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.ConstantInput,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>\n            <b>String</b>\n            <Field<string> name=\"constant_string\" defaultValue=\"Hello World\">\n              {({ field }) => (\n                <ConstantInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  schema={{ type: 'string' }}\n                />\n              )}\n            </Field>\n\n            <b>Number</b>\n            <Field<number> name=\"constant_number\" defaultValue={42}>\n              {({ field }) => (\n                <ConstantInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  schema={{ type: 'number' }}\n                />\n              )}\n            </Field>\n\n            <b>Boolean</b>\n            <Field<boolean> name=\"constant_boolean\" defaultValue={true}>\n              {({ field }) => (\n                <ConstantInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  schema={{ type: 'boolean' }}\n                />\n              )}\n            </Field>\n\n            <b>Object</b>\n            <Field<string>\n              name=\"constant_object\"\n              defaultValue={JSON.stringify({ key: 'value', nested: { data: 'test' } })}\n            >\n              {({ field }) => (\n                <ConstantInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  schema={{ type: 'object' }}\n                />\n              )}\n            </Field>\n\n            <b>Array</b>\n            <Field<string>\n              name=\"constant_array\"\n              defaultValue={JSON.stringify([1, 2, 3, 'four', true])}\n            >\n              {({ field }) => (\n                <ConstantInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                  schema={{ type: 'array' }}\n                />\n              )}\n            </Field>\n          </div>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const FallbackRendererStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any>\n            name=\"constant_fallback\"\n            defaultValue={{ custom: 'data', type: 'unsupported' }}\n          >\n            {({ field }) => (\n              <ConstantInput\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                schema={{ type: 'custom-unsupported-type' }}\n                fallbackRenderer={({ value, onChange, readonly }) => (\n                  <div style={{ padding: '8px', background: '#f0f0f0', border: '1px dashed #ccc' }}>\n                    <p>Fallback renderer for unsupported type</p>\n                  </div>\n                )}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const CustomStrategyStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string> name=\"constant_custom\" defaultValue=\"Custom Value\">\n            {({ field }) => (\n              <ConstantInput\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                schema={{ type: 'object' }}\n                strategies={[\n                  {\n                    hit: (schema) => schema.type === 'object',\n                    Renderer: ({ value, onChange, readonly }) => <p>Object is not supported now</p>,\n                  },\n                ]}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/db-condition-row.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DBConditionRow = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DBConditionRow,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any | undefined>\n            name=\"db_condition_row\"\n            defaultValue={{\n              left: 'amount',\n              operator: 'gt',\n              right: {\n                type: 'constant',\n                content: 1000,\n                schema: {\n                  type: 'number',\n                },\n              },\n            }}\n          >\n            {({ field }) => (\n              <DBConditionRow\n                options={[\n                  {\n                    label: 'TransactionID',\n                    value: 'transaction_id',\n                    schema: { type: 'integer' },\n                  },\n                  {\n                    label: 'Amount',\n                    value: 'amount',\n                    schema: { type: 'number' },\n                  },\n                  {\n                    label: 'Description',\n                    value: 'description',\n                    schema: { type: 'string' },\n                  },\n                  {\n                    label: 'Archived',\n                    value: 'archived',\n                    schema: { type: 'boolean' },\n                  },\n                  {\n                    label: 'CreateTime',\n                    value: 'create_time',\n                    schema: { type: 'date-time' },\n                  },\n                ]}\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/display-flow-value.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DynamicValueInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DynamicValueInput,\n  }))\n);\n\nconst DisplayFlowValue = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplayFlowValue,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode={true}\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.dynamic_value_input = {\n          type: 'constant',\n          content: 'Hello World',\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any> name=\"dynamic_value_input\">\n            {({ field }) => (\n              <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <div>\n            <Field<any> name=\"dynamic_value_input\">\n              {({ field }) => {\n                console.log('debugger field value', field);\n                return <DisplayFlowValue value={field.value} title=\"Display Flow Value\" />;\n              }}\n            </Field>\n          </div>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/display-inputs-values.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValuesTree = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValuesTree,\n  }))\n);\n\nconst DisplayInputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplayInputsValues,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode={true}\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.inputs_values = {\n          a: {\n            b: {\n              type: 'ref',\n              content: ['start_0', 'str'],\n            },\n            c: {\n              type: 'constant',\n              content: 'hello',\n            },\n          },\n          d: {\n            type: 'ref',\n            content: ['start_0', 'arr', 'arr_str'],\n          },\n          e: {\n            type: 'ref',\n            content: ['start_0', 'obj'],\n          },\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined> name=\"inputs_values\">\n            {({ field }) => (\n              <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <Field<Record<string, any> | undefined> name=\"inputs_values\">\n            {({ field }) => <DisplayInputsValues value={field.value} />}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/display-outputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { provideJsonSchemaOutputs, type IJsonSchema } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DisplayOutputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplayOutputs,\n  }))\n);\n\nconst JsonSchemaEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonSchemaEditor,\n  }))\n);\n\nexport const DisplayFromScopeStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.outputs = {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n            status: { type: 'number' },\n          },\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IJsonSchema | undefined> name=\"outputs\">\n            {({ field }) => (\n              <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <b>Display Outputs by Scope:</b>\n          <DisplayOutputs displayFromScope />\n        </>\n      ),\n      effect: {\n        outputs: provideJsonSchemaOutputs,\n      },\n    }}\n  />\n);\n\nexport const DisplayFromFieldStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.outputs = {\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n            status: { type: 'number' },\n          },\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IJsonSchema | undefined> name=\"outputs\">\n            {({ field }) => (\n              <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <b>Display Outputs By Schema</b>\n          <Field<IJsonSchema | undefined> name=\"outputs\">\n            {({ field }) => <DisplayOutputs value={field.value} />}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/display-schema-tag.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DisplaySchemaTag = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplaySchemaTag,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <div>\n            <DisplaySchemaTag\n              title=\"Transaction\"\n              value={{\n                type: 'object',\n                properties: {\n                  transaction_id: { type: 'integer' },\n                  amount: { type: 'number' },\n                  description: { type: 'string' },\n                  archived: { type: 'boolean' },\n                  owner: {\n                    type: 'object',\n                    properties: {\n                      id: { type: 'integer' },\n                      username: { type: 'string' },\n                      friends: {\n                        type: 'array',\n                        items: {\n                          type: 'object',\n                          properties: {\n                            id: { type: 'integer' },\n                            username: { type: 'string' },\n                          },\n                        },\n                      },\n                    },\n                  },\n                },\n              }}\n            />\n          </div>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/display-schema-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DisplaySchemaTree = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplaySchemaTree,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    filterStartNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <DisplaySchemaTree\n            value={{\n              type: 'object',\n              properties: {\n                transaction_id: { type: 'integer' },\n                amount: { type: 'number' },\n                description: { type: 'string' },\n                archived: { type: 'boolean' },\n                owner: {\n                  type: 'object',\n                  properties: {\n                    id: { type: 'integer' },\n                    username: { type: 'string' },\n                    friends: {\n                      type: 'array',\n                      items: {\n                        type: 'object',\n                        properties: {\n                          id: { type: 'integer' },\n                          username: { type: 'string' },\n                        },\n                      },\n                    },\n                  },\n                },\n              },\n            }}\n          />\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/dynamic-value-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DynamicValueInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DynamicValueInput,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any> name=\"dynamic_value_input\">\n            {({ field }) => (\n              <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const WithSchemaStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any> name=\"dynamic_value_input\">\n            {({ field }) => (\n              <DynamicValueInput\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                schema={{ type: 'string' }}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/inputs-values-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValuesTree = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValuesTree,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputs_values\"\n            defaultValue={{\n              a: {\n                b: {\n                  type: 'ref',\n                  content: ['start_0', 'str'],\n                },\n                c: {\n                  type: 'constant',\n                  content: 'hello',\n                },\n              },\n              d: {\n                type: 'constant',\n                content: '{ \"a\": \"b\"}',\n                schema: { type: 'object' },\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/inputs-values.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValues,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputs_values\"\n            defaultValue={{\n              a: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const WithSchemaStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputs_values\"\n            defaultValue={{\n              a: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValues\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                schema={{\n                  type: 'string',\n                }}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/json-editor-with-variables.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst JsonEditorWithVariables = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonEditorWithVariables,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <div style={{ width: 400 }}>\n          <FormHeader />\n          <Field<any> name=\"json_editor_with_variables\" defaultValue={`{ \"a\": {{start_0.str}}}`}>\n            {({ field }) => (\n              <JsonEditorWithVariables\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </div>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/json-schema-creator.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport type { IJsonSchema } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst JsonSchemaCreator = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonSchemaCreator,\n  }))\n);\n\nconst JsonSchemaEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonSchemaEditor,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IJsonSchema | undefined> name=\"json_schema\">\n            {({ field }) => (\n              <div>\n                <JsonSchemaCreator onSchemaCreate={(schema) => field.onChange(schema)} />\n                <div style={{ marginTop: 16 }}>\n                  <JsonSchemaEditor\n                    value={field.value}\n                    onChange={(value) => field.onChange(value)}\n                  />\n                </div>\n              </div>\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/json-schema-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport type { IJsonSchema } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst JsonSchemaEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonSchemaEditor,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IJsonSchema | undefined>\n            name=\"json_schema_editor\"\n            defaultValue={{\n              type: 'object',\n              properties: {\n                name: { type: 'string' },\n                age: { type: 'number' },\n              },\n            }}\n          >\n            {({ field }) => (\n              <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/prompt-editor-with-inputs.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { IFlowTemplateValue, IInputsValues, InputsValuesTree } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst PromptEditorWithInputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.PromptEditorWithInputs,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <div style={{ width: 400 }}>\n          <FormHeader />\n          <Field<IInputsValues | undefined>\n            name=\"inputsValues\"\n            defaultValue={{\n              a: { type: 'constant', content: '123' },\n              b: { type: 'ref', content: ['start_0', 'obj'] },\n            }}\n          >\n            {({ field }) => (\n              <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <Field<IInputsValues | undefined> name=\"inputsValues\">\n            {({ field: inputsField }) => (\n              <Field<IFlowTemplateValue | undefined>\n                name=\"prompt_editor_with_inputs\"\n                defaultValue={{\n                  type: 'template',\n                  content: '# Query \\n {{b.obj2.num}}',\n                }}\n              >\n                {({ field }) => (\n                  <PromptEditorWithInputs\n                    value={field.value}\n                    onChange={(value) => field.onChange(value)}\n                    inputsValues={inputsField.value || {}}\n                  />\n                )}\n              </Field>\n            )}\n          </Field>\n        </div>\n      ),\n    }}\n  />\n);\n\nexport const WithoutCanvas = () => {\n  const [value, setValue] = useState<IFlowTemplateValue | undefined>({\n    type: 'template',\n    content: '# Role \\nYou are a helpful assistant. \\n\\n# Query \\n{{b.obj2.num}} \\n\\n',\n  });\n\n  return (\n    <div>\n      <PromptEditorWithInputs\n        value={value}\n        onChange={(value) => setValue(value)}\n        inputsValues={{\n          a: { type: 'constant', content: '123' },\n          b: {\n            c: {\n              d: { type: 'constant', content: 456 },\n            },\n            e: { type: 'constant', content: 789 },\n          },\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/prompt-editor-with-variables.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst PromptEditorWithVariables = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.PromptEditorWithVariables,\n  }))\n);\n\nconst VariableSelectorProvider = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelectorProvider,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <div style={{ width: 400 }}>\n          <FormHeader />\n          <Field<any | undefined>\n            name=\"prompt_editor\"\n            defaultValue={{\n              type: 'template',\n              content: `# Role\nYou are a helpful assistant\n\n# Query\n{{start_0.str}}`,\n            }}\n          >\n            {({ field }) => (\n              <PromptEditorWithVariables\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </div>\n      ),\n    }}\n  />\n);\n\nconst STRING_ONLY_SCHEMA = { type: 'string' };\nexport const StringOnlyStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <div style={{ width: 400 }}>\n          <FormHeader />\n          <VariableSelectorProvider includeSchema={STRING_ONLY_SCHEMA}>\n            <Field<any | undefined>\n              name=\"prompt_editor\"\n              defaultValue={{\n                type: 'template',\n                content: `# Role\nYou are a helpful assistant`,\n              }}\n            >\n              {({ field }) => (\n                <PromptEditorWithVariables\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n              )}\n            </Field>\n          </VariableSelectorProvider>\n        </div>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/prompt-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst PromptEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.PromptEditor,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<any | undefined>\n            name=\"prompt_editor\"\n            defaultValue={{\n              type: 'template',\n              content: '# Role \\n You are a helpful assistant',\n            }}\n          >\n            {({ field }) => (\n              <PromptEditor value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/sql-editor-with-variables.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst SQLEditorWithVariables = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.SQLEditorWithVariables,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <div style={{ width: 400 }}>\n          <FormHeader />\n          <Field<string | undefined>\n            name=\"sql_editor_with_variables\"\n            defaultValue={'SELECT * FROM users \\n WHERE user_id = {{start_0.str}}'}\n          >\n            {({ field }) => (\n              <SQLEditorWithVariables\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            )}\n          </Field>\n        </div>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/type-selector.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport type { IJsonSchema } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst TypeSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.TypeSelector,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Partial<IJsonSchema> | undefined>\n            name=\"type_selector\"\n            defaultValue={{ type: 'string' }}\n          >\n            {({ field }) => (\n              <TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/components/variable-selector.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { useVariableTree } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\nconst VariableSelectorProvider = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelectorProvider,\n  }))\n);\n\nconst Tree = React.lazy(() =>\n  import('@douyinfe/semi-ui').then((module) => ({\n    default: module.Tree,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const FilterSchemaStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                includeSchema={{ type: 'string' }}\n              />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n\nexport const CustomFilterStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => (\n        <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n          <FormHeader />\n          <Field<string[] | undefined> name=\"variable_selector\">\n            {({ field }) => (\n              <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </VariableSelectorProvider>\n      ),\n    }}\n  />\n);\n\nexport const CustomVariableTreeStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      render: () => {\n        const treeData = useVariableTree({});\n\n        return (\n          <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n            <FormHeader />\n            <Tree treeData={treeData} defaultExpandAll />\n          </VariableSelectorProvider>\n        );\n      },\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/auto-rename-ref.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValues,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      effect: {\n        inputsValues: autoRenameRefEffect,\n      },\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputsValues\"\n            defaultValue={{\n              a: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/listen-ref-schema-change.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { listenRefSchemaChange } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValues,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      effect: {\n        'inputsValues.*': listenRefSchemaChange(({ name, schema, form, formValues }) => {\n          form.setValueIn(\n            `log`,\n            `${form.getValueIn(`log`) || ''}${name}: ${JSON.stringify(schema)}\\n`\n          );\n        }),\n      },\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputsValues\"\n            defaultValue={{\n              a: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <Field<any> name=\"log\" defaultValue={'When schema updated, log changes:\\n'}>\n            {({ field }) => (\n              <pre\n                style={{\n                  width: 500,\n                  padding: 4,\n                  background: '#f5f5f5',\n                  fontSize: 12,\n                  whiteSpace: 'pre-wrap',\n                }}\n              >\n                {field.value}\n              </pre>\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/listen-ref-value-change.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { listenRefValueChange } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/fixed-layout-editor';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValues,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    formMeta={{\n      effect: {\n        'inputsValues.*': listenRefValueChange(({ name, variable, form }) => {\n          form.setValueIn(\n            `log`,\n            `${form.getValueIn(`log`) || ''}${name}: ${JSON.stringify(variable?.toJSON() || {})} \\n`\n          );\n        }),\n      },\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<Record<string, any> | undefined>\n            name=\"inputsValues\"\n            defaultValue={{\n              a: {\n                type: 'ref',\n                content: ['start_0', 'str'],\n              },\n            }}\n          >\n            {({ field }) => (\n              <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n          <br />\n          <Field<any> name=\"log\" defaultValue={'When variable value updated, log changes:\\n'}>\n            {({ field }) => (\n              <pre\n                style={{\n                  width: 500,\n                  padding: 4,\n                  background: '#f5f5f5',\n                  fontSize: 12,\n                  whiteSpace: 'pre-wrap',\n                }}\n              >\n                {field.value}\n              </pre>\n            )}\n          </Field>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/provide-batch-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\nimport {\n  provideBatchInputEffect,\n  createBatchOutputsFormPlugin,\n  type IFlowRefValue,\n  DisplayOutputs,\n} from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst BatchVariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchVariableSelector,\n  }))\n);\n\nconst BatchOutputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchOutputs,\n  }))\n);\n\nconst VariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.VariableSelector,\n  }))\n);\n\ntype BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;\n\nconst createLoopRegistry = (): FlowNodeRegistry => ({\n  type: 'custom',\n  meta: {\n    isContainer: true,\n    size: {\n      width: 500,\n      height: 260,\n    },\n    padding: () => ({\n      top: 180,\n      bottom: 40,\n      left: 50,\n      right: 50,\n    }),\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <FormHeader />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop input (select array variable):\n          </div>\n          <Field<IFlowRefValue | undefined>\n            name=\"loopFor\"\n            defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}\n          >\n            {({ field }) => (\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n              />\n            )}\n          </Field>\n        </div>\n        <SubCanvasRender offsetY={-120} />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Available local variables (item & index generated by provideBatchInputEffect):\n          </div>\n          <Field<string[] | undefined> name=\"localVariable\">\n            {({ field }) => (\n              <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </div>\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop outputs (collected into arrays):\n          </div>\n          <Field<BatchOutputsValueType | undefined>\n            name=\"loopOutputs\"\n            defaultValue={{\n              names: { type: 'ref', content: ['variable_0', 'name'] },\n            }}\n          >\n            {({ field }) => (\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n              />\n            )}\n          </Field>\n        </div>\n        <DisplayOutputs displayFromScope />\n      </>\n    ),\n    effect: {\n      loopFor: provideBatchInputEffect,\n    },\n    plugins: [\n      createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),\n    ],\n  },\n});\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    height={550}\n    initialData={{\n      nodes: [\n        {\n          id: 'custom_0',\n          type: 'custom',\n          data: {\n            title: 'Loop',\n            loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },\n          },\n          blocks: [\n            {\n              id: 'block_start_0',\n              type: 'block-start',\n              data: { title: 'Start' },\n              meta: { position: { x: 20, y: 0 } },\n            },\n            {\n              id: 'variable_0',\n              type: 'variable',\n              data: {\n                title: 'Variable',\n                assign: [\n                  {\n                    operator: 'declare',\n                    left: 'name',\n                    right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },\n                  },\n                ],\n              },\n              meta: { position: { x: 100, y: 0 } },\n            },\n            {\n              id: 'block_end_0',\n              type: 'block-end',\n              data: { title: 'End' },\n              meta: { position: { x: 360, y: 0 } },\n            },\n          ],\n          edges: [\n            { sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },\n            { sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },\n          ],\n        },\n      ],\n      edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],\n    }}\n    transformRegistry={(props) => ({\n      ...props,\n      nodeRegistries: [\n        ...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),\n        createLoopRegistry(),\n      ],\n    })}\n    plugins={(ctx) => [createContainerNodePlugin({})]}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/provide-json-schema-output.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport {\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n  type IJsonSchema,\n} from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst JsonSchemaEditor = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.JsonSchemaEditor,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    transformInitialNode={{\n      end_0: (node) => {\n        node.data.inputsValues = {\n          success: { type: 'constant', content: true, schema: { type: 'boolean' } },\n          message: { type: 'ref', content: ['custom_0', 'name'] },\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <Field<IJsonSchema | undefined>\n            name=\"outputs\"\n            defaultValue={{\n              type: 'object',\n              properties: {\n                name: { type: 'string' },\n                age: { type: 'number' },\n              },\n            }}\n          >\n            {({ field }) => (\n              <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n      effect: {\n        // Sync the title to variables\n        title: syncVariableTitle,\n        outputs: provideJsonSchemaOutputs,\n      },\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/sync-variable-title.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst Input = React.lazy(() =>\n  import('@douyinfe/semi-ui').then((module) => ({\n    default: module.Input,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterStartNode\n    transformInitialNode={{\n      end_0: (node) => {\n        node.data.inputsValues = {\n          success: { type: 'constant', content: true, schema: { type: 'boolean' } },\n          message: { type: 'ref', content: ['custom_0', 'name'] },\n        };\n        return node;\n      },\n      custom_0: (node) => {\n        node.data.outputs = {\n          type: 'object',\n          properties: {\n            name: { type: 'string' },\n            age: { type: 'number' },\n          },\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <p>Please Edit Title below to sync to variables:</p>\n          <Field<string | undefined> name=\"title\">\n            {({ field }) => (\n              <Input value={field.value} onChange={(value) => field.onChange(value)} />\n            )}\n          </Field>\n        </>\n      ),\n      effect: {\n        // Sync the title to variables\n        title: syncVariableTitle,\n        outputs: provideJsonSchemaOutputs,\n      },\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/effects/validate-when-variable-sync.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect } from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { validateFlowValue, validateWhenVariableSync } from '@flowgram.ai/form-materials';\nimport { Button } from '@douyinfe/semi-ui';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DynamicValueInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DynamicValueInput,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.value = {\n          type: 'ref',\n          content: ['start_0', 'query'],\n        };\n\n        return node;\n      },\n    }}\n    formMeta={{\n      effect: {\n        value: validateWhenVariableSync(),\n      },\n      validate: {\n        value: ({ value, context }) =>\n          validateFlowValue(value, {\n            node: context.node,\n            errorMessages: {\n              unknownVariable: 'Unknown Variable',\n            },\n          }),\n      },\n      render: ({ form }) => {\n        useEffect(() => {\n          form.validate();\n        }, []);\n\n        return (\n          <>\n            <FormHeader />\n\n            <b>{\"Add 'query' in Start\"}</b>\n            <Field<any> name=\"value\">\n              {({ field, fieldState }) => (\n                <>\n                  <DynamicValueInput\n                    value={field.value}\n                    onChange={(value) => field.onChange(value)}\n                  />\n                  <span style={{ color: 'red' }}>\n                    {fieldState.errors?.map((e) => e.message).join('\\n')}\n                  </span>\n                </>\n              )}\n            </Field>\n            <br />\n\n            <Button onClick={() => form.validate()}>Trigger Validate</Button>\n          </>\n        );\n      },\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/form-plugins/batch-outputs-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field, FlowNodeRegistry } from '@flowgram.ai/free-layout-editor';\nimport { SubCanvasRender, createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\nimport {\n  provideBatchInputEffect,\n  createBatchOutputsFormPlugin,\n  type IFlowRefValue,\n  DisplayOutputs,\n} from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst BatchVariableSelector = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchVariableSelector,\n  }))\n);\n\nconst BatchOutputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.BatchOutputs,\n  }))\n);\n\ntype BatchOutputsValueType = Record<string, IFlowRefValue | undefined>;\n\nconst createLoopRegistry = (): FlowNodeRegistry => ({\n  type: 'custom',\n  meta: {\n    isContainer: true,\n    size: {\n      width: 500,\n      height: 260,\n    },\n    padding: () => ({\n      top: 160,\n      bottom: 40,\n      left: 50,\n      right: 50,\n    }),\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <FormHeader />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop input (select array variable):\n          </div>\n          <Field<IFlowRefValue | undefined>\n            name=\"loopFor\"\n            defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}\n          >\n            {({ field }) => (\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n              />\n            )}\n          </Field>\n        </div>\n        <SubCanvasRender offsetY={-100} />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop outputs (collected into arrays):\n          </div>\n          <Field<BatchOutputsValueType | undefined>\n            name=\"loopOutputs\"\n            defaultValue={{\n              names: { type: 'ref', content: ['variable_0', 'name'] },\n            }}\n          >\n            {({ field }) => (\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n              />\n            )}\n          </Field>\n        </div>\n        <div>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Generated output variables:\n          </div>\n          <DisplayOutputs displayFromScope />\n        </div>\n      </>\n    ),\n    effect: {\n      loopFor: provideBatchInputEffect,\n    },\n    plugins: [\n      createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),\n    ],\n  },\n});\n\nconst createLoopRegistryWithInfer = (): FlowNodeRegistry => ({\n  type: 'custom',\n  meta: {\n    isContainer: true,\n    size: {\n      width: 500,\n      height: 260,\n    },\n    padding: () => ({\n      top: 160,\n      bottom: 40,\n      left: 50,\n      right: 50,\n    }),\n  },\n  formMeta: {\n    render: () => (\n      <>\n        <FormHeader />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>Loop input:</div>\n          <Field<IFlowRefValue | undefined>\n            name=\"loopFor\"\n            defaultValue={{ type: 'ref', content: ['start_0', 'arr', 'arr_obj'] }}\n          >\n            {({ field }) => (\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n              />\n            )}\n          </Field>\n        </div>\n        <SubCanvasRender offsetY={-100} />\n        <div style={{ marginBottom: 16 }}>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Loop outputs (with schema inference):\n          </div>\n          <Field<BatchOutputsValueType | undefined>\n            name=\"loopOutputs\"\n            defaultValue={{\n              items: { type: 'ref', content: ['variable_0', 'item_name'] },\n              values: { type: 'ref', content: ['variable_0', 'item_index'] },\n            }}\n          >\n            {({ field }) => (\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n              />\n            )}\n          </Field>\n        </div>\n        <div>\n          <div style={{ marginBottom: 8, fontSize: 12, color: '#666' }}>\n            Output variables (check Debug panel for inferred schema):\n          </div>\n          <DisplayOutputs displayFromScope />\n        </div>\n      </>\n    ),\n    effect: {\n      loopFor: provideBatchInputEffect,\n    },\n    plugins: [\n      createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' }),\n    ],\n  },\n});\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    height={550}\n    initialData={{\n      nodes: [\n        {\n          id: 'custom_0',\n          type: 'custom',\n          data: {\n            title: 'Loop',\n            loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },\n          },\n          blocks: [\n            {\n              id: 'block_start_0',\n              type: 'block-start',\n              data: { title: 'Start' },\n              meta: { position: { x: 20, y: 0 } },\n            },\n            {\n              id: 'variable_0',\n              type: 'variable',\n              data: {\n                title: 'Variable',\n                assign: [\n                  {\n                    operator: 'declare',\n                    left: 'name',\n                    right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },\n                  },\n                ],\n              },\n              meta: { position: { x: 100, y: 0 } },\n            },\n            {\n              id: 'block_end_0',\n              type: 'block-end',\n              data: { title: 'End' },\n              meta: { position: { x: 360, y: 0 } },\n            },\n          ],\n          edges: [\n            { sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },\n            { sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },\n          ],\n        },\n      ],\n      edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],\n    }}\n    transformRegistry={(props) => ({\n      ...props,\n      nodeRegistries: [\n        ...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),\n        createLoopRegistry(),\n      ],\n    })}\n    plugins={(ctx) => [createContainerNodePlugin({})]}\n  />\n);\n\nexport const WithInferSchemaStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    height={550}\n    initialData={{\n      nodes: [\n        {\n          id: 'custom_0',\n          type: 'custom',\n          data: {\n            title: 'Loop',\n            loopFor: { type: 'ref', content: ['start_0', 'arr', 'arr_obj'] },\n          },\n          blocks: [\n            {\n              id: 'block_start_0',\n              type: 'block-start',\n              data: { title: 'Start' },\n              meta: { position: { x: 20, y: 0 } },\n            },\n            {\n              id: 'variable_0',\n              type: 'variable',\n              data: {\n                title: 'Variable',\n                assign: [\n                  {\n                    operator: 'declare',\n                    left: 'item_name',\n                    right: { type: 'ref', content: ['custom_0_locals', 'item', 'str'] },\n                  },\n                  {\n                    operator: 'declare',\n                    left: 'item_index',\n                    right: { type: 'ref', content: ['custom_0_locals', 'index'] },\n                  },\n                ],\n              },\n              meta: { position: { x: 100, y: 0 } },\n            },\n            {\n              id: 'block_end_0',\n              type: 'block-end',\n              data: { title: 'End' },\n              meta: { position: { x: 360, y: 0 } },\n            },\n          ],\n          edges: [\n            { sourceNodeID: 'block_start_0', targetNodeID: 'variable_0' },\n            { sourceNodeID: 'variable_0', targetNodeID: 'block_end_0' },\n          ],\n        },\n      ],\n      edges: [{ sourceNodeID: 'start_0', targetNodeID: 'custom_0' }],\n    }}\n    transformRegistry={(props) => ({\n      ...props,\n      nodeRegistries: [\n        ...(props.nodeRegistries || []).filter((r) => r.type !== 'custom'),\n        createLoopRegistryWithInfer(),\n      ],\n    })}\n    plugins={(ctx) => [createContainerNodePlugin({})]}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/form-plugins/infer-assign-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { createInferAssignPlugin } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst AssignRows = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.AssignRows,\n  }))\n);\n\nconst DisplayOutputs = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DisplayOutputs,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    transformInitialNode={{\n      end_0: (node) => {\n        node.data.inputsValues = {\n          info: {\n            type: 'ref',\n            content: ['custom_0', 'userInfo'],\n          },\n        };\n\n        return node;\n      },\n    }}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <AssignRows\n            name=\"assign\"\n            defaultValue={[\n              // 从常量声明变量\n              {\n                operator: 'declare',\n                left: 'userName',\n                right: {\n                  type: 'constant',\n                  content: 'John Doe',\n                  schema: { type: 'string' },\n                },\n              },\n              // 从变量声明变量\n              {\n                operator: 'declare',\n                left: 'userInfo',\n                right: {\n                  type: 'ref',\n                  content: ['start_0', 'obj'],\n                },\n              },\n              // 赋值现有变量\n              {\n                operator: 'assign',\n                left: {\n                  type: 'ref',\n                  content: ['start_0', 'str'],\n                },\n                right: {\n                  type: 'constant',\n                  content: 'Hello Flowgram',\n                  schema: { type: 'string' },\n                },\n              },\n            ]}\n          />\n          <DisplayOutputs displayFromScope style={{ marginTop: 10 }} />\n        </>\n      ),\n      plugins: [\n        createInferAssignPlugin({\n          assignKey: 'assign',\n          outputKey: 'outputs',\n        }),\n      ],\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/form-plugins/infer-inputs-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { createInferInputsPlugin } from '@flowgram.ai/form-materials';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst InputsValues = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValues,\n  }))\n);\n\nconst InputsValuesTree = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.InputsValuesTree,\n  }))\n);\n\n/**\n * Basic usage story - demonstrates automatic schema inference from InputsValues\n */\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    height={500}\n    formMeta={{\n      render: () => (\n        <>\n          <FormHeader />\n          <div>\n            <div>\n              <h4>Headers</h4>\n              <Field<Record<string, any> | undefined>\n                name=\"headersValues\"\n                defaultValue={{\n                  'Content-Type': {\n                    type: 'constant',\n                    content: 'application/json',\n                    schema: { type: 'string' },\n                  },\n                  Authorization: {\n                    type: 'ref',\n                    content: ['start_0', 'str'],\n                  },\n                }}\n              >\n                {({ field }) => (\n                  <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n                )}\n              </Field>\n            </div>\n\n            <div>\n              <h4>Body</h4>\n              <Field<Record<string, any> | undefined>\n                name=\"bodyValues\"\n                defaultValue={{\n                  page: {\n                    index: {\n                      type: 'ref',\n                      content: ['start_0', 'obj', 'obj2', 'num'],\n                    },\n                    size: {\n                      type: 'constant',\n                      content: 10,\n                      schema: { type: 'number' },\n                    },\n                  },\n                  query: {\n                    type: 'ref',\n                    content: ['start_0', 'obj'],\n                  },\n                }}\n              >\n                {({ field }) => (\n                  <InputsValuesTree\n                    value={field.value}\n                    onChange={(value) => field.onChange(value)}\n                  />\n                )}\n              </Field>\n            </div>\n          </div>\n        </>\n      ),\n      plugins: [\n        createInferInputsPlugin({\n          sourceKey: 'headersValues',\n          targetKey: 'headersSchema',\n        }),\n        createInferInputsPlugin({\n          sourceKey: 'bodyValues',\n          targetKey: 'bodySchema',\n        }),\n      ],\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/form-materials/validate/validate-flow-value.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport { validateFlowValue } from '@flowgram.ai/form-materials';\nimport { Button } from '@douyinfe/semi-ui';\n\nimport { FreeFormMetaStoryBuilder, FormHeader } from '../../free-form-meta-story-builder';\n\nconst DynamicValueInput = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.DynamicValueInput,\n  }))\n);\n\nconst PromptEditorWithVariables = React.lazy(() =>\n  import('@flowgram.ai/form-materials').then((module) => ({\n    default: module.PromptEditorWithVariables,\n  }))\n);\n\nexport const BasicStory = () => (\n  <FreeFormMetaStoryBuilder\n    filterEndNode\n    transformInitialNode={{\n      custom_0: (node) => {\n        node.data.dynamic_value_input = {\n          type: 'ref',\n          content: ['start_0', 'unknown_key'],\n        };\n        node.data.required_dynamic_value_input = {\n          type: 'constant',\n          content: '',\n        };\n        node.data.prompt_editor = {\n          type: 'template',\n          content: 'Hello {{start_0.unknown_key}}',\n        };\n        return node;\n      },\n    }}\n    formMeta={{\n      validate: {\n        dynamic_value_input: ({ value, context }) =>\n          validateFlowValue(value, {\n            node: context.node,\n            errorMessages: {\n              required: 'Value is required',\n              unknownVariable: 'Unknown Variable',\n            },\n          }),\n\n        required_dynamic_value_input: ({ value, context }) =>\n          validateFlowValue(value, {\n            node: context.node,\n            required: true,\n            errorMessages: {\n              required: 'Value is required',\n              unknownVariable: 'Unknown Variable',\n            },\n          }),\n\n        prompt_editor: ({ value, context }) =>\n          validateFlowValue(value, {\n            node: context.node,\n            required: true,\n            errorMessages: {\n              required: 'Prompt is required',\n              unknownVariable: 'Unknown Variable In Template',\n            },\n          }),\n      },\n      render: ({ form }) => (\n        <>\n          <FormHeader />\n\n          <b>Validate variable valid</b>\n          <Field<any> name=\"dynamic_value_input\">\n            {({ field, fieldState }) => (\n              <>\n                <DynamicValueInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n                <span style={{ color: 'red' }}>\n                  {fieldState.errors?.map((e) => e.message).join('\\n')}\n                </span>\n              </>\n            )}\n          </Field>\n          <br />\n\n          <b>Validate required value</b>\n          <Field<any> name=\"required_dynamic_value_input\">\n            {({ field, fieldState }) => (\n              <>\n                <DynamicValueInput\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n                <span style={{ color: 'red' }}>\n                  {fieldState.errors?.map((e) => e.message).join('\\n')}\n                </span>\n              </>\n            )}\n          </Field>\n          <br />\n\n          <b>Validate required and variables valid in prompt</b>\n          <Field<any> name=\"prompt_editor\">\n            {({ field, fieldState }) => (\n              <>\n                <PromptEditorWithVariables\n                  value={field.value}\n                  onChange={(value) => field.onChange(value)}\n                />\n                <span style={{ color: 'red' }}>\n                  {fieldState.errors?.map((e) => e.message).join('\\n')}\n                </span>\n              </>\n            )}\n          </Field>\n\n          <br />\n\n          <Button onClick={() => form.validate()}>Trigger Validate</Button>\n        </>\n      ),\n    }}\n  />\n);\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-1.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\nimport { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';\n\nconst FlowGramApp = () => (\n  <FreeLayoutEditorProvider>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-2.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  FreeLayoutEditorProvider,\n  EditorRenderer,\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nconst NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FreeLayoutEditorProvider\n    materials={{\n      renderDefaultNode: NodeRender,\n    }}\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    initialData={{\n      nodes: [\n        {\n          id: '1',\n          type: 'custom',\n          meta: {\n            position: { x: 250, y: 100 },\n          },\n        },\n      ],\n      edges: [],\n    }}\n  >\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-3.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  FreeLayoutEditorProvider,\n  EditorRenderer,\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nconst NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FreeLayoutEditorProvider\n    onAllLayersRendered={(ctx) => {\n      ctx.tools.fitView(false);\n    }}\n    materials={{\n      renderDefaultNode: NodeRender,\n    }}\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    canDeleteNode={() => true}\n    canDeleteLine={() => true}\n    initialData={{\n      nodes: [\n        {\n          id: '1',\n          type: 'custom',\n          meta: {\n            position: { x: 0, y: 0 },\n          },\n        },\n        {\n          id: '2',\n          type: 'custom',\n          meta: {\n            position: { x: 400, y: 0 },\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: '1',\n          targetNodeID: '2',\n        },\n      ],\n    }}\n  >\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-4.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport {\n  FreeLayoutEditorProvider,\n  EditorRenderer,\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nconst NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n\nconst FlowGramApp = () => (\n  <FreeLayoutEditorProvider\n    plugins={() => [createMinimapPlugin({}), createFreeSnapPlugin({})]}\n    onAllLayersRendered={(ctx) => {\n      ctx.tools.fitView(false);\n    }}\n    materials={{\n      renderDefaultNode: NodeRender,\n    }}\n    nodeRegistries={[\n      {\n        type: 'custom',\n      },\n    ]}\n    canDeleteNode={() => true}\n    canDeleteLine={() => true}\n    initialData={{\n      nodes: [\n        {\n          id: '1',\n          type: 'custom',\n          meta: {\n            position: { x: 0, y: 0 },\n          },\n        },\n        {\n          id: '2',\n          type: 'custom',\n          meta: {\n            position: { x: 400, y: -200 },\n          },\n        },\n        {\n          id: '3',\n          type: 'custom',\n          meta: {\n            position: { x: 400, y: 0 },\n          },\n        },\n        {\n          id: '4',\n          type: 'custom',\n          meta: {\n            position: { x: 400, y: 200 },\n          },\n        },\n        {\n          id: '5',\n          type: 'custom',\n          meta: {\n            position: { x: 800, y: 0 },\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: '1',\n          targetNodeID: '2',\n        },\n        {\n          sourceNodeID: '1',\n          targetNodeID: '3',\n        },\n        {\n          sourceNodeID: '1',\n          targetNodeID: '4',\n        },\n        {\n          sourceNodeID: '2',\n          targetNodeID: '5',\n        },\n        {\n          sourceNodeID: '3',\n          targetNodeID: '5',\n        },\n        {\n          sourceNodeID: '4',\n          targetNodeID: '5',\n        },\n      ],\n    }}\n  >\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n);\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-5/app.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n    </FreeLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-5/initial-data.ts",
    "content": "import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: '1',\n      type: 'custom',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n    },\n    {\n      id: '2',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: -200 },\n      },\n    },\n    {\n      id: '3',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n    },\n    {\n      id: '4',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 200 },\n      },\n    },\n    {\n      id: '5',\n      type: 'custom',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: '1',\n      targetNodeID: '2',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '3',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '4',\n    },\n    {\n      sourceNodeID: '2',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '3',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '4',\n      targetNodeID: '5',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-5/node-registries.tsx",
    "content": "import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'custom',\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-5/node-render.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-5/use-editor-props.tsx",
    "content": "import { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      plugins: () => [createMinimapPlugin({}), createFreeSnapPlugin({})],\n      onAllLayersRendered: (ctx) => {\n        ctx.tools.fitView(false);\n      },\n      materials: {\n        renderDefaultNode: NodeRender,\n      },\n      nodeRegistries,\n      canDeleteNode: () => true,\n      canDeleteLine: () => true,\n      initialData,\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-6/app.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n    </FreeLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-6/initial-data.ts",
    "content": "import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: '1',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start Node',\n      },\n    },\n    {\n      id: '2',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: -200 },\n      },\n      data: {\n        title: 'Custom Node A',\n      },\n    },\n    {\n      id: '3',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom Node B',\n      },\n    },\n    {\n      id: '4',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 200 },\n      },\n      data: {\n        title: 'Custom Node C',\n      },\n    },\n    {\n      id: '5',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End Node',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: '1',\n      targetNodeID: '2',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '3',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '4',\n    },\n    {\n      sourceNodeID: '2',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '3',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '4',\n      targetNodeID: '5',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-6/node-registries.tsx",
    "content": "import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-6/node-render.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-6/use-editor-props.tsx",
    "content": "import { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { Field, FreeLayoutProps } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      plugins: () => [createMinimapPlugin({}), createFreeSnapPlugin({})],\n      onAllLayersRendered: (ctx) => {\n        ctx.tools.fitView(false);\n      },\n      materials: {\n        renderDefaultNode: NodeRender,\n      },\n      nodeRegistries,\n      canDeleteNode: () => true,\n      canDeleteLine: () => true,\n      initialData,\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n              </>\n            ),\n          },\n        };\n      },\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/add-node.tsx",
    "content": "import {\n  useService,\n  WorkflowDocument,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const AddNode = () => {\n  const workflowDocument = useService(WorkflowDocument);\n  const selectService = useService(WorkflowSelectService);\n\n  return (\n    <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 8, display: 'flex', gap: 8 }}>\n      <button\n        style={{\n          border: '1px solid #e0e0e0',\n          borderRadius: '50%',\n          cursor: 'pointer',\n          padding: '4px',\n          color: '#ffffff',\n          background: '#7e72e8',\n          width: 70,\n          height: 70,\n          fontSize: 14,\n        }}\n        onClick={() => {\n          const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(\n            'custom',\n            undefined, // position undefined means create node in center of canvas - position undefined 可以在画布中间创建节点\n            {\n              data: {\n                title: 'New Node',\n              },\n            }\n          );\n          selectService.selectNode(node);\n        }}\n      >\n        + Node\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/app.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './use-editor-props';\nimport { Tools } from './tools';\nimport { Minimap } from './minimap';\nimport { AddNode } from './add-node';\n\nconst FlowGramApp = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer />\n      <Tools />\n      <Minimap />\n      <AddNode />\n    </FreeLayoutEditorProvider>\n  );\n};\n\nexport default FlowGramApp;\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/initial-data.ts",
    "content": "import { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: '1',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start Node',\n      },\n    },\n    {\n      id: '2',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: -200 },\n      },\n      data: {\n        title: 'Custom Node A',\n      },\n    },\n    {\n      id: '3',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom Node B',\n      },\n    },\n    {\n      id: '4',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 200 },\n      },\n      data: {\n        title: 'Custom Node C',\n      },\n    },\n    {\n      id: '5',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End Node',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: '1',\n      targetNodeID: '2',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '3',\n    },\n    {\n      sourceNodeID: '1',\n      targetNodeID: '4',\n    },\n    {\n      sourceNodeID: '2',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '3',\n      targetNodeID: '5',\n    },\n    {\n      sourceNodeID: '4',\n      targetNodeID: '5',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/minimap.tsx",
    "content": "import { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => (\n  <div\n    style={{\n      position: 'absolute',\n      right: 16,\n      bottom: 72,\n      zIndex: 100,\n      width: 118,\n    }}\n  >\n    <MinimapRender\n      containerStyles={{\n        pointerEvents: 'auto',\n        position: 'relative',\n        top: 'unset',\n        right: 'unset',\n        bottom: 'unset',\n        left: 'unset',\n      }}\n      inactiveStyle={{\n        opacity: 1,\n        scale: 1,\n        translateX: 0,\n        translateY: 0,\n      }}\n    />\n  </div>\n);\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/node-registries.tsx",
    "content": "import { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/node-render.tsx",
    "content": "import '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { form, selected } = useNodeRender();\n  return (\n    <WorkflowNodeRenderer\n      style={{\n        width: 280,\n        minHeight: 88,\n        height: 'auto',\n        background: '#fff',\n        border: '1px solid rgba(6, 7, 9, 0.15)',\n        borderColor: selected ? '#4e40e5' : 'rgba(6, 7, 9, 0.15)',\n        borderRadius: 8,\n        boxShadow: '0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02)',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        position: 'relative',\n        padding: 12,\n        cursor: 'move',\n      }}\n      node={props.node}\n    >\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/tools.tsx",
    "content": "import { CSSProperties, useEffect, useState } from 'react';\n\nimport { usePlaygroundTools, useClientContext, LineType } from '@flowgram.ai/free-layout-editor';\n\nexport const Tools = () => {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  const buttonStyle: CSSProperties = {\n    border: '1px solid #e0e0e0',\n    borderRadius: '4px',\n    cursor: 'pointer',\n    padding: '4px',\n    color: '#141414',\n    background: '#e1e3e4',\n  };\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 34, right: 16, display: 'flex', gap: 8 }}\n    >\n      <button style={buttonStyle} onClick={() => tools.zoomin()}>\n        ZoomIn\n      </button>\n      <button style={buttonStyle} onClick={() => tools.zoomout()}>\n        ZoomOut\n      </button>\n      <span\n        style={{\n          ...buttonStyle,\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          cursor: 'default',\n          width: 40,\n        }}\n      >\n        {Math.floor(tools.zoom * 100)}%\n      </span>\n      <button style={buttonStyle} onClick={() => tools.fitView()}>\n        FitView\n      </button>\n      <button style={buttonStyle} onClick={() => tools.autoLayout()}>\n        AutoLayout\n      </button>\n      <button\n        style={buttonStyle}\n        onClick={() =>\n          tools.switchLineType(\n            tools.lineType === LineType.BEZIER ? LineType.LINE_CHART : LineType.BEZIER\n          )\n        }\n      >\n        {tools.lineType === LineType.BEZIER ? 'Bezier' : 'Fold'}\n      </button>\n      <button\n        style={{\n          ...buttonStyle,\n          cursor: canUndo ? 'pointer' : 'not-allowed',\n          color: canUndo ? '#141414' : '#b1b1b1',\n        }}\n        onClick={() => history.undo()}\n        disabled={!canUndo}\n      >\n        Undo\n      </button>\n      <button\n        style={{\n          ...buttonStyle,\n          cursor: canRedo ? 'pointer' : 'not-allowed',\n          color: canRedo ? '#141414' : '#b1b1b1',\n        }}\n        onClick={() => history.redo()}\n        disabled={!canRedo}\n      >\n        Redo\n      </button>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/free-examples/step-7/use-editor-props.tsx",
    "content": "import { useMemo } from 'react';\n\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\nimport { Field, FreeLayoutProps } from '@flowgram.ai/free-layout-editor';\n\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { initialData } from './initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      plugins: () => [\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 100,\n            canvasHeight: 50,\n            canvasPadding: 50,\n          },\n        }),\n        createFreeSnapPlugin({}),\n      ],\n      onAllLayersRendered: (ctx) => {\n        ctx.tools.fitView(false);\n      },\n      materials: {\n        renderDefaultNode: NodeRender,\n      },\n      nodeRegistries,\n      canDeleteNode: () => true,\n      canDeleteLine: () => true,\n      initialData,\n      /**\n       * Node engine enable, you can configure formMeta in the FlowNodeRegistry\n       */\n      nodeEngine: {\n        enable: true,\n      },\n      /**\n       * Redo/Undo enable\n       */\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen Node engine data change\n      },\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n              </>\n            ),\n          },\n        };\n      },\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/docs/components/free-feature-overview/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.doc-free-feature-overview {\n  position: relative;\n  z-index: 1;\n\n  .demo-container {\n    position: relative;\n    width: 100%;\n    height: 600px;\n  }\n\n  .demo-free-layout-tools {\n    position: relative;\n    bottom: 40px;\n    color: black;\n  }\n}\n"
  },
  {
    "path": "apps/docs/components/free-feature-overview/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport './index.less';\n\n// https://github.com/web-infra-dev/rspress/issues/553\nconst FreeFeatureOverview = React.lazy(() =>\n  import('@flowgram.ai/demo-free-layout').then((module) => ({\n    default: module.DemoFreeLayout,\n  }))\n);\n\nexport { FreeFeatureOverview };\n"
  },
  {
    "path": "apps/docs/components/free-form-meta-story-builder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nconst OriginFreeFormMetaStoryBuilder = React.lazy(() =>\n  import('@flowgram.ai/demo-materials').then((module) => ({\n    default: module.FreeFormMetaStoryBuilder,\n  }))\n);\n\nconst FormHeader = React.lazy(() =>\n  import('@flowgram.ai/demo-materials').then((module) => ({\n    default: module.FormHeader,\n  }))\n);\n\nconst FreeFormMetaStoryBuilder = (\n  props: React.ComponentPropsWithoutRef<typeof OriginFreeFormMetaStoryBuilder> & {\n    height?: number | string;\n  }\n) => (\n  <div style={{ position: 'relative', height: props.height || 400 }}>\n    <OriginFreeFormMetaStoryBuilder {...props} />\n  </div>\n);\n\nexport { FreeFormMetaStoryBuilder, FormHeader };\n"
  },
  {
    "path": "apps/docs/components/free-layout-simple/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-free-node {\n  min-height: unset !important;\n  min-width: unset !important;\n}\n"
  },
  {
    "path": "apps/docs/components/free-layout-simple/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport './index.less';\n\n// https://github.com/web-infra-dev/rspress/issues/553\nconst FreeLayoutSimple = React.lazy(() =>\n  import('@flowgram.ai/demo-free-layout-simple').then((module) => ({\n    default: module.DemoFreeLayout,\n  }))\n);\n\nexport { FreeLayoutSimple };\n"
  },
  {
    "path": "apps/docs/components/free-layout-simple/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-unresolved */\n\nimport nodesCode from '@flowgram.ai/demo-free-layout-simple/src/nodes/index.ts?raw';\nimport dataCode from '@flowgram.ai/demo-free-layout-simple/src/initial-data.ts?raw';\nimport useEditorPropsCode from '@flowgram.ai/demo-free-layout-simple/src/hooks/use-editor-props.tsx?raw';\nimport editorCode from '@flowgram.ai/demo-free-layout-simple/src/editor.tsx?raw';\nimport toolsCode from '@flowgram.ai/demo-free-layout-simple/src/components/tools.tsx?raw';\nimport nodeAddPanelCode from '@flowgram.ai/demo-free-layout-simple/src/components/node-add-panel.tsx?raw';\nimport minimapCode from '@flowgram.ai/demo-free-layout-simple/src/components/minimap.tsx?raw';\n\nimport { PreviewEditor } from '../preview-editor';\nimport { FreeLayoutSimple } from '.';\n\nexport const FreeLayoutSimplePreview = () => {\n  const files = {\n    'editor.tsx': {\n      active: true,\n      code: editorCode,\n    },\n    'use-editor-props.tsx': useEditorPropsCode,\n    'initial-data.ts': dataCode,\n    'nodes/index.ts': nodesCode,\n    'node-add-panel.tsx': nodeAddPanelCode,\n    'tools.tsx': toolsCode,\n    'minimap.tsx': minimapCode,\n  };\n  return (\n    <div\n      style={{\n        zIndex: 1,\n        position: 'relative',\n      }}\n    >\n      <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>\n        <FreeLayoutSimple />\n      </PreviewEditor>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PreviewEditor } from './preview-editor';\nexport { FixedFeatureOverview } from './fixed-feature-overview';\nexport { FreeFeatureOverview } from './free-feature-overview';\nexport { FreeLayoutSimple } from './free-layout-simple';\nexport { FreeLayoutSimplePreview } from './free-layout-simple/preview';\nexport { FixedLayoutSimple, CompositeNodesPreview } from './fixed-layout-simple';\nexport { FixedLayoutSimplePreview } from './fixed-layout-simple/preview';\nexport { NodeFormBasicPreview, NodeFormEffectPreview, NodeFormDynamicPreview } from './node-form';\nexport { InfiniteCanvasPreview } from './infinite-canvas';\n"
  },
  {
    "path": "apps/docs/components/infinite-canvas/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.doc-infinite-canvas-preview {\n  position: absolute;\n  width: 100%;\n  height: 500px;\n  button {\n    border: 1px solid #ccc;\n    padding: 4px 8px;\n    border-radius: 4px;\n  }\n}\n"
  },
  {
    "path": "apps/docs/components/infinite-canvas/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { InfiniteCanvasPreview } from './preview';\n"
  },
  {
    "path": "apps/docs/components/infinite-canvas/infinite-canvas.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport './index.less';\n\nconst InfiniteCanvas = React.lazy(() =>\n  import('@flowgram.ai/demo-playground').then((module) => ({\n    default: module.PlaygroundEditor,\n  }))\n);\n\nexport { InfiniteCanvas };\n"
  },
  {
    "path": "apps/docs/components/infinite-canvas/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-unresolved */\n\nimport editorCode from '@flowgram.ai/demo-playground/src/editor.tsx?raw';\nimport toolCode from '@flowgram.ai/demo-playground/src/components/playground-tools.tsx?raw';\nimport cardCode from '@flowgram.ai/demo-playground/src/components/card.tsx?raw';\n\nimport { InfiniteCanvas } from './infinite-canvas.tsx';\nimport { PreviewEditor } from '../preview-editor';\n\nexport const InfiniteCanvasPreview = () => {\n  const files = {\n    'editor.tsx': {\n      active: true,\n      code: editorCode,\n    },\n    'playground-tools.tsx': {\n      code: toolCode,\n    },\n    'card.tsx': {\n      code: cardCode,\n    },\n  };\n  return (\n    <PreviewEditor\n      files={files}\n      previewStyle={{ height: 500, position: 'relative' }}\n      editorStyle={{ height: 500 }}\n    >\n      <InfiniteCanvas className=\"doc-infinite-canvas-preview\" />\n    </PreviewEditor>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/materials.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// @ts-expect-error\n// eslint-disable-next-line import/no-unresolved\nimport { PackageManagerTabs, SourceCode } from '@theme';\n\nexport function MaterialDisplay(props: any) {\n  return (\n    <div>\n      <br />\n      <PackageManagerTabs\n        command={{\n          'By Import': `import { ${props.exportName} } from '@flowgram.ai/form-materials'`,\n          // components/type-selector/index.tsx -> components/type-selector\n          'By CLI': `npx @flowgram.ai/form-materials@latest ${props.filePath\n            .split('/')\n            .slice(0, -1)\n            .join('/')}`,\n        }}\n      />\n      <br />\n      <div style={{ display: 'flex', justifyContent: 'space-between' }}>\n        <div style={{ width: '42%' }}>\n          {props.imgs.map((img: string | any, index: number) => (\n            <div key={index}>\n              <img\n                loading=\"lazy\"\n                src={typeof img === 'string' ? img : img.src}\n                style={{ width: '100%' }}\n              />\n              {img.caption && (\n                <div style={{ textAlign: 'center', fontSize: 12, color: '#666' }}>\n                  {img.caption}\n                </div>\n              )}\n            </div>\n          ))}\n        </div>\n        <div style={{ width: '55%' }}>\n          {props.children}\n          <SourceCode\n            href={`https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/${props.filePath}`}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/docs/components/node-form/array/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.array-item-wrapper {\n  display: flex;\n  align-items: center;\n}\n.icon-button-popover {\n  padding: 6px 8px;\n}\n"
  },
  {
    "path": "apps/docs/components/node-form/array/node-registry.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  EffectFuncProps,\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n  WorkflowNodeRegistry,\n  FieldArray,\n  FieldArrayRenderProps,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input, Button, Popover } from '@douyinfe/semi-ui';\nimport { IconPlus, IconCrossCircleStroked, IconArrowDown } from '@douyinfe/semi-icons';\nimport './index.css';\nimport '../index.css';\n\nexport const render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Array Examples</div>\n    <FieldArray name=\"array\">\n      {({ field, fieldState }: FieldArrayRenderProps<string>) => (\n        <FieldWrapper title={'My Array'}>\n          {field.map((child, index) => (\n            <Field name={child.name} key={child.key}>\n              {({ field: childField, fieldState: childState }: FieldRenderProps<string>) => (\n                <FieldWrapper error={childState.errors?.[0]?.message}>\n                  <div className=\"array-item-wrapper\">\n                    <Input {...childField} size={'small'} />\n                    {index < field.value!.length - 1 ? (\n                      <Popover\n                        content={'swap with next element'}\n                        className={'icon-button-popover'}\n                        showArrow\n                        position={'topLeft'}\n                      >\n                        <Button\n                          theme=\"borderless\"\n                          size={'small'}\n                          icon={<IconArrowDown />}\n                          onClick={() => field.swap(index, index + 1)}\n                        />\n                      </Popover>\n                    ) : null}\n                    <Popover\n                      content={'delete current element'}\n                      className={'icon-button-popover'}\n                      showArrow\n                      position={'topLeft'}\n                    >\n                      <Button\n                        theme=\"borderless\"\n                        size={'small'}\n                        icon={<IconCrossCircleStroked />}\n                        onClick={() => field.delete(index)}\n                      />\n                    </Popover>\n                  </div>\n                </FieldWrapper>\n              )}\n            </Field>\n          ))}\n          <div>\n            <Button\n              size={'small'}\n              theme=\"borderless\"\n              icon={<IconPlus />}\n              onClick={() => field.append('default')}\n            >\n              Add\n            </Button>\n          </div>\n        </FieldWrapper>\n      )}\n    </FieldArray>\n  </div>\n);\n\ninterface FormData {\n  array: string[];\n}\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n  validateTrigger: ValidateTrigger.onChange,\n  defaultValues: {\n    array: ['default'],\n  },\n  validate: {\n    'array.*': ({ value }) =>\n      value.length > 8 ? 'max length exceeded: current length is ' + value.length : undefined,\n  },\n  effect: {\n    'array.*': [\n      {\n        event: DataEvent.onValueInit,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value init to ', value);\n        },\n      },\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value changed to ', value);\n        },\n      },\n    ],\n  },\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/array/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_INITIAL_DATA,\n  defaultInitialDataTs,\n  fieldWrapperCss,\n  fieldWrapperTs,\n} from '@flowgram.ai/demo-node-form';\n\nimport { Editor } from '../editor.tsx';\nimport { PreviewEditor } from '../../preview-editor.tsx';\nimport { nodeRegistry } from './node-registry.tsx';\n\nconst nodeRegistryFile = {\n  code: `import {\n  DataEvent,\n  EffectFuncProps,\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n  WorkflowNodeRegistry,\n  FieldArray,\n  FieldArrayRenderProps,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input, Button, Popover } from '@douyinfe/semi-ui';\nimport { IconPlus, IconCrossCircleStroked, IconArrowDown } from '@douyinfe/semi-icons';\nimport './index.css';\nimport '../index.css';\n\nexport const render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Array Examples</div>\n    <FieldArray name=\"array\">\n      {({ field, fieldState }: FieldArrayRenderProps<string>) => (\n        <FieldWrapper title={'My Array'}>\n          {field.map((child, index) => (\n            <Field name={child.name} key={child.key}>\n              {({ field: childField, fieldState: childState }: FieldRenderProps<string>) => (\n                <FieldWrapper error={childState.errors?.[0]?.message}>\n                  <div className=\"array-item-wrapper\">\n                    <Input {...childField} size={'small'} />\n                    {index < field.value!.length - 1 ? (\n                      <Popover\n                        content={'swap with next element'}\n                        className={'icon-button-popover'}\n                        showArrow\n                        position={'topLeft'}\n                      >\n                        <Button\n                          theme=\"borderless\"\n                          size={'small'}\n                          icon={<IconArrowDown />}\n                          onClick={() => field.swap(index, index + 1)}\n                        />\n                      </Popover>\n                    ) : null}\n                    <Popover\n                      content={'delete current element'}\n                      className={'icon-button-popover'}\n                      showArrow\n                      position={'topLeft'}\n                    >\n                      <Button\n                        theme=\"borderless\"\n                        size={'small'}\n                        icon={<IconCrossCircleStroked />}\n                        onClick={() => field.delete(index)}\n                      />\n                    </Popover>\n                  </div>\n                </FieldWrapper>\n              )}\n            </Field>\n          ))}\n          <div>\n            <Button\n              size={'small'}\n              theme=\"borderless\"\n              icon={<IconPlus />}\n              onClick={() => field.append('default')}\n            >\n              Add\n            </Button>\n          </div>\n        </FieldWrapper>\n      )}\n    </FieldArray>\n  </div>\n);\n\ninterface FormData {\n  array: string[];\n}\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n  validateTrigger: ValidateTrigger.onChange,\n  defaultValues: {\n    array: ['default'],\n  },\n  validate: {\n    'array.*': ({ value }) =>\n      value.length > 8 ? 'max length exceeded: current length is ' + value.length : undefined,\n  },\n  effect: {\n    'array.*': [\n      {\n        event: DataEvent.onValueInit,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value init to ', value);\n        },\n      },\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value changed to ', value);\n        },\n      },\n    ],\n  },\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n\n`,\n  active: true,\n};\n\nexport const NodeFormArrayPreview = () => {\n  const files = {\n    'node-registry.tsx': nodeRegistryFile,\n    'initial-data.ts': { code: defaultInitialDataTs, active: true },\n    'field-wrapper.tsx': { code: fieldWrapperTs, active: true },\n    'field-wrapper.css': { code: fieldWrapperCss, active: true },\n  };\n  return (\n    <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>\n      <Editor registries={[nodeRegistry]} initialData={DEFAULT_INITIAL_DATA} />\n    </PreviewEditor>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/basic-preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_DEMO_REGISTRY,\n  DEFAULT_INITIAL_DATA,\n  defaultInitialDataTs,\n  fieldWrapperCss,\n  fieldWrapperTs,\n} from '@flowgram.ai/demo-node-form';\n\nimport { PreviewEditor } from '../preview-editor';\nimport { Editor } from './editor';\n\nconst registryCode = {\n  code: `import {\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n} from '@flowgram.ai/free-layout-editor';\nimport { Input } from '@douyinfe/semi-ui';\n\n// FieldWrapper is not provided by sdk, it can be customized\nimport { FieldWrapper } from './components';\n\nconst render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Basic Node</div>\n    <Field name=\"name\">\n      {({ field, fieldState }: FieldRenderProps<string>) => (\n        <FieldWrapper required title=\"Name\" error={fieldState.errors?.[0]?.message}>\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"city\">\n      {({ field, fieldState }: FieldRenderProps<string>) => (\n        <FieldWrapper required title=\"City\" error={fieldState.errors?.[0]?.message}>\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n  </div>\n);\n\nconst formMeta: FormMeta = {\n  render,\n  defaultValues: { name: 'Tina', city: 'Hangzhou' },\n  validateTrigger: ValidateTrigger.onChange,\n  validate: {\n    name: ({ value }) => {\n      if (!value) {\n        return 'Name is required';\n      }\n    },\n    city: ({ value }) => {\n      if (!value) {\n        return 'City is required';\n      }\n    }\n  }\n};\n\n\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta\n};\n`,\n  active: true,\n};\n\nexport const NodeFormBasicPreview = () => {\n  const files = {\n    'node-registry.tsx': registryCode,\n    'initial-data.ts': { code: defaultInitialDataTs, active: true },\n    'field-wrapper.tsx': { code: fieldWrapperTs, active: true },\n    'field-wrapper.css': { code: fieldWrapperCss, active: true },\n  };\n  return (\n    <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>\n      <Editor registry={DEFAULT_DEMO_REGISTRY} initialData={DEFAULT_INITIAL_DATA} />\n    </PreviewEditor>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/dynamic/node-registry.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  WorkflowNodeRegistry,\n  FormRenderProps,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input } from '@douyinfe/semi-ui';\nimport '../index.css';\n\ninterface FormData {\n  country: string;\n  city: string;\n}\n\nconst render = ({ form }: FormRenderProps<FormData>) => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Visibility Examples</div>\n    <Field name=\"country\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper title=\"Country\">\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"city\" deps={['country']}>\n      {({ field }: FieldRenderProps<string>) =>\n        form.getValueIn('country') ? (\n          <FieldWrapper title=\"City\">\n            <Input size={'small'} {...field} />\n          </FieldWrapper>\n        ) : (\n          <></>\n        )\n      }\n    </Field>\n  </div>\n);\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/dynamic/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_INITIAL_DATA,\n  defaultInitialDataTs,\n  fieldWrapperCss,\n  fieldWrapperTs,\n} from '@flowgram.ai/demo-node-form';\n\nimport { Editor } from '../editor.tsx';\nimport { PreviewEditor } from '../../preview-editor.tsx';\nimport { nodeRegistry } from './node-registry.tsx';\n\nconst nodeRegistryFile = {\n  code: `import {\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  WorkflowNodeRegistry,\n  FormRenderProps,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input } from '@douyinfe/semi-ui';\nimport '../index.css';\n\ninterface FormData {\n  country: string;\n  city: string;\n}\n\nconst render = ({ form }: FormRenderProps<FormData>) => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Visibility Examples</div>\n    <Field name=\"country\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper title=\"Country\">\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"city\" deps={['country']}>\n      {({ field }: FieldRenderProps<string>) =>\n        form.getValueIn('country') ? (\n          <FieldWrapper title=\"City\">\n            <Input size={'small'} {...field} />\n          </FieldWrapper>\n        ) : (\n          <></>\n        )\n      }\n    </Field>\n  </div>\n);\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n`,\n  active: true,\n};\n\nexport const NodeFormDynamicPreview = () => {\n  const files = {\n    'node-registry.tsx': nodeRegistryFile,\n    'initial-data.ts': { code: defaultInitialDataTs, active: true },\n    'field-wrapper.tsx': { code: fieldWrapperTs, active: true },\n    'field-wrapper.css': { code: fieldWrapperCss, active: true },\n  };\n  return (\n    <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>\n      <Editor registries={[nodeRegistry]} initialData={DEFAULT_INITIAL_DATA} />\n    </PreviewEditor>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\n// https://github.com/web-infra-dev/rspress/issues/553\nconst Editor = React.lazy(() =>\n  import('@flowgram.ai/demo-node-form').then((module) => ({\n    default: module.Editor,\n  }))\n);\n\nexport { Editor };\n"
  },
  {
    "path": "apps/docs/components/node-form/effect/node-registry.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  EffectFuncProps,\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n  WorkflowNodeRegistry,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input } from '@douyinfe/semi-ui';\nimport '../index.css';\n\nconst render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Effect Examples</div>\n    <Field name=\"field1\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper\n          title=\"Basic effect\"\n          note={'The following field will console.log field value on value change'}\n        >\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"field2\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper\n          title=\"Control other fields\"\n          note={'The following field will change Field 3 value on value change'}\n        >\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n    <Field name=\"field3\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper title=\"Field 3\">\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n  </div>\n);\n\ninterface FormData {\n  field1: string;\n  field2: string;\n  field3: string;\n}\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n  validateTrigger: ValidateTrigger.onChange,\n  effect: {\n    field1: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value }: EffectFuncProps<string, FormData>) => {\n          console.log('field1 value:', value);\n        },\n      },\n    ],\n    field2: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value, form }: EffectFuncProps<string, FormData>) => {\n          form.setValueIn('field3', 'field2 value is ' + value);\n        },\n      },\n    ],\n  },\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/effect/preview.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_INITIAL_DATA,\n  defaultInitialDataTs,\n  fieldWrapperCss,\n  fieldWrapperTs,\n} from '@flowgram.ai/demo-node-form';\n\nimport { Editor } from '../editor.tsx';\nimport { PreviewEditor } from '../../preview-editor.tsx';\nimport { nodeRegistry } from './node-registry.tsx';\n\nconst nodeRegistryFile = {\n  code: `import {\n  DataEvent,\n  EffectFuncProps,\n  Field,\n  FieldRenderProps,\n  FormMeta,\n  ValidateTrigger,\n  WorkflowNodeRegistry,\n} from '@flowgram.ai/free-layout-editor';\nimport { FieldWrapper } from '@flowgram.ai/demo-node-form';\nimport { Input } from '@douyinfe/semi-ui';\nimport '../index.css';\n\nconst render = () => (\n  <div className=\"demo-node-content\">\n    <div className=\"demo-node-title\">Effect Examples</div>\n    <Field name=\"field1\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper\n          title=\"Basic effect\"\n          note={'The following field will console.log field value on value change'}\n        >\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n\n    <Field name=\"field2\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper\n          title=\"Control other fields\"\n          note={'The following field will change Field 3 value on value change'}\n        >\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n    <Field name=\"field3\">\n      {({ field }: FieldRenderProps<string>) => (\n        <FieldWrapper title=\"Field 3\">\n          <Input size={'small'} {...field} />\n        </FieldWrapper>\n      )}\n    </Field>\n  </div>\n);\n\ninterface FormData {\n  field1: string;\n  field2: string;\n  field3: string;\n}\n\nconst formMeta: FormMeta<FormData> = {\n  render,\n  validateTrigger: ValidateTrigger.onChange,\n  effect: {\n    field1: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value }: EffectFuncProps<string, FormData>) => {\n          console.log('field1 value:', value);\n        },\n      },\n    ],\n    field2: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value, form }: EffectFuncProps<string, FormData>) => {\n          form.setValueIn('field3', 'field2 value is ' + value);\n        },\n      },\n    ],\n  },\n};\n\nexport const nodeRegistry: WorkflowNodeRegistry = {\n  type: 'custom',\n  meta: {},\n  defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  formMeta,\n};\n\n`,\n  active: true,\n};\n\nexport const NodeFormEffectPreview = () => {\n  const files = {\n    'node-registry.tsx': nodeRegistryFile,\n    'initial-data.ts': { code: defaultInitialDataTs, active: true },\n    'field-wrapper.tsx': { code: fieldWrapperTs, active: true },\n    'field-wrapper.css': { code: fieldWrapperCss, active: true },\n  };\n  return (\n    <PreviewEditor files={files} previewStyle={{ height: 500 }} editorStyle={{ height: 500 }}>\n      <Editor registries={[nodeRegistry]} initialData={DEFAULT_INITIAL_DATA} />\n    </PreviewEditor>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/node-form/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.demo-node-content {\n  padding: 8px 12px;\n  flex-grow: 1;\n  min-width: 300px;\n}\n\n.demo-node-title {\n  font-weight: 500;\n  font-size: 14px;\n  width: 100%;\n  margin: 4px 0px 12px 0px;\n}\n"
  },
  {
    "path": "apps/docs/components/node-form/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeFormBasicPreview } from './basic-preview';\nexport { NodeFormEffectPreview } from './effect/preview';\nexport { NodeFormDynamicPreview } from './dynamic/preview';\n"
  },
  {
    "path": "apps/docs/components/preview-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { useDark } from '@rspress/core/runtime';\nimport {\n  SandpackProvider,\n  SandpackLayout,\n  SandpackCodeEditor,\n  SandpackFiles,\n  // SandpackPreview,\n} from '@codesandbox/sandpack-react';\n\nexport const PreviewEditor = ({\n  files,\n  children,\n  previewStyle,\n  dependencies,\n  editorStyle,\n  codeInRight,\n}: {\n  files: SandpackFiles;\n  children: JSX.Element;\n  previewStyle?: React.CSSProperties;\n  dependencies?: Record<string, string>;\n  editorStyle?: React.CSSProperties;\n  codeInRight?: boolean;\n}) => {\n  const dark = useDark();\n  const theme = useMemo(() => (dark ? 'dark' : 'light'), [dark]);\n  const content = codeInRight ? (\n    <>\n      <SandpackLayout style={{ width: '100%', display: 'flex' }}>\n        <div className=\"light-mode preview-ediitor\" style={previewStyle}>\n          {children}\n        </div>\n        <SandpackCodeEditor style={editorStyle} readOnly />\n      </SandpackLayout>\n    </>\n  ) : (\n    <>\n      <SandpackLayout style={previewStyle}>\n        <div className=\"light-mode preview-ediitor\">{children}</div>\n        {/* <SandpackPreview /> */}\n      </SandpackLayout>\n      <SandpackLayout>\n        <SandpackCodeEditor style={editorStyle} readOnly />\n      </SandpackLayout>\n    </>\n  );\n\n  return (\n    <SandpackProvider\n      template=\"react\"\n      theme={theme}\n      customSetup={{\n        dependencies,\n      }}\n      files={{\n        ...files,\n        '/App.js': {\n          code: '',\n          hidden: true,\n        },\n      }}\n      onChange={(v) => {\n        console.log('debugger', v);\n      }}\n    >\n      {content}\n    </SandpackProvider>\n  );\n};\n"
  },
  {
    "path": "apps/docs/components/tsx-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { useDark } from '@rspress/core/runtime';\nimport {\n  SandpackProvider,\n  SandpackLayout,\n  SandpackCodeEditor,\n  SandpackPreview,\n} from '@codesandbox/sandpack-react';\n\nexport const TsxEditor = ({ value }: { value: string }) => {\n  const dark = useDark();\n  const theme = useMemo(() => (dark ? 'dark' : 'light'), [dark]);\n\n  return (\n    <SandpackProvider\n      template=\"react\"\n      theme={theme}\n      files={{\n        'App.js': value,\n      }}\n      onChange={(v) => {\n        console.log('debugger', v);\n      }}\n    >\n      <SandpackLayout>\n        <SandpackPreview />\n      </SandpackLayout>\n      <SandpackLayout>\n        <SandpackCodeEditor />\n      </SandpackLayout>\n    </SandpackProvider>\n  );\n};\n"
  },
  {
    "path": "apps/docs/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/prop-types': 'off',\n  },\n  settings: {\n    react: {\n      version: 'detect', // 自动检测 React 版本\n    },\n  },\n});\n"
  },
  {
    "path": "apps/docs/global.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.rspress-doc-container:not(:has(.aside-container_edeb4)) .rspress-doc {\n  width: auto;\n  max-width: 1400px;\n}\n\n.rspress-doc-container:not(:has(.aside-container_edeb4)) .rspress-doc-footer {\n  width: auto;\n  max-width: 1400px;\n}\n\n//.medium-zoom-image {\n//  width: 100%;\n//}\n\n.gedit-playground .medium-zoom-image {\n  pointer-events: none;\n}\n\n.rs-table {\n  width: 100%;\n}\n\n.rs-table td {\n  padding: 8px;\n  border: 1px solid var(--rp-c-divider-light);\n}\n\n.rs-table img {\n  max-height: 500px;\n  max-width: 500px;\n}\n\n.rs-table td:first-child {\n  width: 20%;\n}\n\n.rs-center {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.rs-tip {\n  color: var(--rp-c-text-2);\n  border-left: 2px solid #ccc;\n  padding: 0 8px;\n  margin: 16px 0;\n}\n\n.rs-highlight {\n  border: 1px solid var(--rp-container-warning-border);\n  background-color: var(--rp-container-warning-bg);\n  padding: 12px 24px;\n  font-size: 14px;\n  font-weight: 400;\n  line-height: 1.7;\n  border-radius: var(--rp-radius);\n  color: var(--rp-c-text-1);\n}\n\n.rs-link {\n  color: var(--rp-c-link);\n}\n\n.rs-red {\n  color: var(--rp-container-danger-text);\n}\n\n.light-mode {\n  color-scheme: light;\n  --background-color: #ffffff !important;\n  /* 强制使用浅色背景 */\n  --text-color: #000000 !important;\n  /* 强制使用深色文本 */\n  --rp-c-bg: #fff !important;\n  --rp-c-text-1: #000000 !important;\n  /* 强制使用深色文本 */\n\n  button {\n    background: transparent;\n  }\n}\n\n.preview-ediitor {\n  button {\n    color: #000000e6;\n    background: #e1e3e4;\n    border: 1px solid #e5e6eb;\n    border-radius: 8px;\n    justify-content: center;\n    align-items: center;\n    gap: 8px;\n    height: 30px;\n    padding: 0px 8px;\n    font-size: 14px;\n    font-style: normal;\n    font-weight: 400;\n    transition: background .3s, border .3s, opacity .3s;\n    display: inline-flex\n  }\n\n  input {\n    border: 1px solid #e5e6eb;\n    background-color: var(--rp-c-bg);\n    padding: 4px 8px;\n  }\n}\n\n.light-mode * {\n  color: var(--text-color) !important;\n}\n\n// .semi-tooltip-content {\n//   color: black;\n// }\n\n.dark {\n  .invert-img {\n    filter: invert(0.9);\n  }\n}\n"
  },
  {
    "path": "apps/docs/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/docs\",\n  \"version\": \"0.0.1\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"shx rm -rf ./doc_build && cross-env NODE_OPTIONS=--max-old-space-size=8192 RSPRESS_SSG_WORKER_THREAD_COUNT=6 rspress build\",\n    \"docs\": \"cross-env NODE_OPTIONS=--max-old-space-size=8192 tsx ./scripts/auto-generate.ts\",\n    \"dev\": \"rspress dev\",\n    \"lint\": \"eslint ./components --cache\",\n    \"preview\": \"rspress preview\"\n  },\n  \"dependencies\": {\n    \"@rspress/core\": \"2.0.0-rc.6\",\n    \"@rspress/plugin-llms\": \"2.0.0-rc.6\",\n    \"@rsbuild/plugin-less\": \"^1.1.1\",\n    \"@codesandbox/sandpack-react\": \"2.19.10\",\n    \"@monaco-editor/react\": \"^4.6.0\",\n    \"@flowgram.ai/demo-fixed-layout\": \"workspace:*\",\n    \"@flowgram.ai/demo-free-layout\": \"workspace:*\",\n    \"@flowgram.ai/demo-free-layout-simple\": \"workspace:*\",\n    \"@flowgram.ai/demo-fixed-layout-simple\": \"workspace:*\",\n    \"@flowgram.ai/demo-node-form\": \"workspace:*\",\n    \"@flowgram.ai/demo-materials\": \"workspace:*\",\n    \"@flowgram.ai/demo-playground\": \"workspace:*\",\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-semi-materials\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n    \"@flowgram.ai/group-plugin\": \"workspace:*\",\n    \"@flowgram.ai/form-materials\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/free-auto-layout-plugin\": \"workspace:*\",\n    \"@flowgram.ai/minimap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-stack-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-container-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-snap-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-node-panel-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-lines-plugin\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"styled-components\": \"^5\",\n    \"nanoid\": \"^5.0.9\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"typedoc\": \"0.24.8\",\n    \"typedoc-plugin-markdown\": \"3.17.1\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"lodash-es\": \"^4.17.21\",\n    \"@types/react\": \"^18\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@eslint/js\": \"^9.12.0\",\n    \"@types/node\": \"^18\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"typescript\": \"5.0.4\",\n    \"sucrase\": \"3.35.0\",\n    \"eslint\": \"^9.0.0\",\n    \"globals\": \"^15.11.0\",\n    \"typescript-eslint\": \"^8.8.1\",\n    \"dotenv\": \"~16.5.0\",\n    \"tsx\": \"~4.19.4\",\n    \"shx\": \"0.4.0\",\n    \"cross-env\": \"~7.0.3\",\n    \"rspress-plugin-mermaid\": \"~0.3.0\"\n  },\n  \"homepage\": \"https://github.com/bytedance/flowgram.ai/\"\n}\n"
  },
  {
    "path": "apps/docs/rspress.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as path from 'node:path';\n\nimport mermaid from 'rspress-plugin-mermaid';\nimport { pluginLlms } from '@rspress/plugin-llms';\nimport { transformerCompatibleMetaHighlight } from '@rspress/core/shiki-transformers';\nimport { defineConfig, RspressPlugin } from '@rspress/core';\nimport { pluginLess } from '@rsbuild/plugin-less';\n\nexport default defineConfig({\n  root: path.join(__dirname, 'src'),\n  base: '/',\n  title: 'FlowGram.AI',\n  description: 'FlowGram.AI',\n  globalStyles: path.join(__dirname, './global.less'),\n  head: ['<meta name=\"keywords\" content=\"FlowGram, flowgram\">'],\n  builderConfig: {\n    performance: {\n      buildCache: false,\n      // 4MB log file size limit in Vercel platform\n      printFileSize: {\n        compressed: false,\n        detail: false,\n        total: true,\n      },\n    },\n    source: {\n      decorators: {\n        version: 'legacy',\n      },\n    },\n    plugins: [pluginLess()],\n    tools: {\n      rspack(options, { mergeConfig }) {\n        return mergeConfig(options, {\n          module: {\n            rules: [\n              {\n                test: /\\.mdc$/,\n                type: 'asset/source',\n              },\n            ],\n          },\n          /**\n           * ignore warnings from @coze-editor/editor/language-typescript\n           */\n          ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/],\n        });\n      },\n    },\n  },\n  ssg: {\n    experimentalWorker: true,\n    experimentalExcludeRoutePaths: [\n      /\\/auto-docs\\//,\n      // these pages do not support SSR\n      // document is not defined\n      '/index',\n      '/en/examples/node-form/basic',\n      '/en/examples/node-form/array',\n      '/en/examples/node-form/dynamic',\n      '/en/guide/getting-started/free-layout',\n      '/en/guide/getting-started/fixed-layout',\n      '/en/examples/node-form/effect',\n      '/en/guide/fixed-layout/composite-nodes',\n      '/en/examples/playground',\n      '/en/examples/fixed-layout/fixed-composite-nodes',\n      '/en/examples/fixed-layout/fixed-layout-simple',\n      '/en/examples/free-layout/free-layout-simple',\n      '/en/examples/fixed-layout/fixed-feature-overview',\n      '/en/examples/free-layout/free-feature-overview',\n      /\\/en\\/materials\\/.*\\/.*/,\n      /\\/materials\\/.*\\/.*/,\n      '/examples/node-form/basic',\n      '/examples/node-form/array',\n      '/examples/node-form/dynamic',\n      '/guide/getting-started/free-layout',\n      '/guide/getting-started/fixed-layout',\n      '/examples/node-form/effect',\n      '/guide/fixed-layout/composite-nodes',\n      '/examples/playground',\n      '/examples/fixed-layout/fixed-composite-nodes',\n      '/examples/fixed-layout/fixed-layout-simple',\n      '/examples/free-layout/free-layout-simple',\n      '/examples/fixed-layout/fixed-feature-overview',\n      '/examples/free-layout/free-feature-overview',\n    ],\n  },\n  // locales 为一个对象数组\n  locales: [\n    {\n      lang: 'en',\n      // 导航栏切换语言的标签\n      label: 'English',\n      title: 'Rspress',\n      description: 'Static Site Generator',\n    },\n    {\n      lang: 'zh',\n      label: '简体中文',\n      title: 'Rspress',\n      description: '静态网站生成器',\n    },\n  ],\n  icon: '/flowgram-logo.svg',\n  logo: {\n    light: '/flowgram-logo.svg',\n    dark: '/flowgram-logo.svg',\n  },\n  lang: 'zh',\n  logoText: 'FlowGram.AI',\n  markdown: {\n    shiki: {\n      transformers: [transformerCompatibleMetaHighlight()],\n    },\n  },\n  plugins: [\n    pluginLlms([\n      {\n        llmsTxt: {\n          name: 'llms.txt',\n        },\n        llmsFullTxt: {\n          name: 'llms-full.txt',\n        },\n        include: ({ page }) => page.lang === 'zh',\n      },\n      {\n        llmsTxt: {\n          name: 'en/llms.txt',\n        },\n        llmsFullTxt: {\n          name: 'en/llms-full.txt',\n        },\n        include: ({ page }) => page.lang === 'en',\n      },\n    ]),\n    mermaid() as RspressPlugin,\n  ],\n  themeConfig: {\n    localeRedirect: 'auto',\n    footer: {\n      message: '© 2025 Bytedance Inc. All Rights Reserved.',\n    },\n    lastUpdated: true,\n    locales: [\n      {\n        lang: 'en',\n        label: 'en',\n        outlineTitle: 'ON THIS Page',\n      },\n      {\n        lang: 'zh',\n        label: 'zh',\n        outlineTitle: '大纲',\n        searchNoResultsText: '未搜索到相关结果',\n        searchPlaceholderText: '搜索文档',\n        searchSuggestedQueryText: '可更换不同的关键字后重试',\n        overview: {\n          filterNameText: '过滤',\n          filterPlaceholderText: '输入关键词',\n          filterNoResultText: '未找到匹配的 API',\n        },\n      },\n    ],\n    socialLinks: [\n      {\n        icon: 'github',\n        mode: 'link',\n        content: 'https://github.com/bytedance/flowgram.ai',\n      },\n      {\n        icon: 'discord',\n        mode: 'link',\n        content: 'https://discord.gg/SwDWdrgA9f',\n      },\n      {\n        icon: 'X',\n        mode: 'link',\n        content: 'https://x.com/FlowGramAI',\n      },\n    ],\n  },\n});\n"
  },
  {
    "path": "apps/docs/scripts/auto-generate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// @ts-ignore\nimport * as path from 'path';\n// @ts-ignore\nimport * as fs from 'fs/promises'; // 使用 fs.promises 处理异步操作\n\nimport { load } from 'typedoc-plugin-markdown';\nimport { Application, TSConfigReader, ProjectReflection } from 'typedoc';\n\nimport { patchGeneratedApiDocs } from './patch';\nimport { docLabelMap, overviewMetaJson } from './constants';\n\n// 只生成指定的包文档，本地调试用\nconst gen_pkgs = process.argv.slice(2) || [];\n\nasync function generateDocs() {\n  // @ts-ignore\n  const projectRoot = path.resolve(__dirname, '../../../'); // Rush 项目根目录\n  // @ts-ignore\n  const packagesDir = path.join(projectRoot, 'packages'); // packages 目录\n  // @ts-ignore\n  const outputDir = path.join(__dirname, '../src/zh/auto-docs'); // 输出目录\n\n  const packages: string[] = [];\n\n  // 读取 packages 目录\n  const firstLevelFiles = await fs.readdir(packagesDir); // 使用 fs.promises 读取目录\n\n  for (const firstLevel of firstLevelFiles) {\n    const firstLevelPath = path.resolve(packagesDir, firstLevel);\n\n    // check if it is a directory\n    if (!(await fs.stat(firstLevelPath)).isDirectory()) {\n      continue;\n    }\n\n    const packageNames = await fs.readdir(firstLevelPath); // 异步读取包目录\n\n    for (const packageName of packageNames) {\n      if (gen_pkgs.length > 0 && !gen_pkgs.includes(packageName)) {\n        continue;\n      }\n\n      const packagePath = path.join(firstLevelPath, packageName);\n      const packageJsonPath = path.join(packagePath, 'package.json');\n      const tsconfigPath = path.join(packagePath, 'tsconfig.json');\n\n      // 检查是否是有效的包\n      if (!(await fileExists(packageJsonPath)) || !(await fileExists(tsconfigPath))) {\n        // console.log(`Skipping ${packagePath}: Missing package.json or tsconfig.json`);\n      } else {\n        packages.push(packageName);\n        console.log(`Generating docs for package: ${packageName}`);\n\n        // 输出目录为 auto-docs/{packageName}\n        const packageOutputDir = path.join(outputDir, packageName);\n\n        // 创建 Typedoc 应用实例\n        const app = new Application();\n        app.options.addReader(new TSConfigReader());\n        load(app);\n\n        // 配置 Typedoc\n        app.bootstrap({\n          entryPoints: [path.join(packagePath, 'src')],\n          tsconfig: tsconfigPath,\n          out: packageOutputDir,\n          plugin: ['typedoc-plugin-markdown'], // 使用 Markdown 插件\n          theme: 'markdown', // Markdown 模式不依赖 HTML 主题\n          exclude: ['**/__tests__/**', 'vitest.config.ts', 'vitest.setup.ts', '**/.DS_Store'],\n          basePath: packagePath,\n          excludePrivate: true,\n          excludeProtected: true,\n          disableSources: true,\n          readme: 'none',\n          githubPages: true,\n          hideGenerator: true,\n          skipErrorChecking: true,\n          requiredToBeDocumented: ['Class', 'Function', 'Interface'],\n          // @ts-expect-error MarkdownTheme has no export\n          hideBreadcrumbs: true,\n          hideMembersSymbol: true,\n          allReflectionsHaveOwnDocument: true,\n        });\n\n        // 生成文档\n        const project: ProjectReflection | undefined = app.convert();\n\n        if (project) {\n          await app.generateDocs(project, packageOutputDir);\n          await patchGeneratedApiDocs(packageOutputDir);\n          const files = await fs.readdir(packageOutputDir);\n          const packageMetaJson: Record<string, string>[] = [];\n          for (const file of files) {\n            if (!['.nojekyll', 'README.md'].includes(file)) {\n              packageMetaJson.push({\n                type: 'dir',\n                name: file,\n                label: docLabelMap[file] || file,\n              });\n            }\n          }\n          await fs.writeFile(\n            path.join(packageOutputDir, '_meta.json'),\n            JSON.stringify(packageMetaJson),\n            'utf-8'\n          );\n          await fs.unlink(path.join(packageOutputDir, 'README.md')); // 删除 README.md 文件\n          console.log(`Docs generated for ${packageName} at ${packageOutputDir}`);\n        } else {\n          console.error(`Failed to generate docs for ${packageName}: Conversion failed`);\n          // @ts-ignore\n          process.exit();\n        }\n      }\n    }\n  }\n\n  // 写入 index.md 和 _meta.json\n  await fs.writeFile(\n    // @ts-ignore\n    path.resolve(__dirname, '../src/zh/auto-docs/index.md'),\n    overviewMetaJson,\n    'utf-8'\n  );\n\n  const metaJson: (string | Record<string, string>)[] = [];\n  metaJson.push('index');\n  packages.forEach((packageName) => {\n    metaJson.push({\n      type: 'dir',\n      label: `@flowgram.ai/${packageName}`,\n      name: packageName,\n    });\n  });\n\n  await fs.writeFile(\n    // @ts-ignore\n    path.resolve(__dirname, '../src/zh/auto-docs/_meta.json'),\n    JSON.stringify(metaJson),\n    'utf-8'\n  );\n}\n\n// 检查文件是否存在\nasync function fileExists(path: string): Promise<boolean> {\n  try {\n    await fs.access(path);\n    return true;\n  } catch {\n    return false;\n  }\n}\n\ngenerateDocs().catch((error) => {\n  console.error('Error generating docs:', error);\n});\n"
  },
  {
    "path": "apps/docs/scripts/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const docLabelMap = {\n  classes: 'Classes',\n  enums: 'Enums',\n  functions: 'Functions',\n  interfaces: 'Interfaces',\n  modules: 'Namespaces',\n  types: 'Types',\n  variables: 'Variables',\n};\n\nexport const overviewMetaJson = `---\n# API Overview\noverview: true\n---`;\n"
  },
  {
    "path": "apps/docs/scripts/patch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// @ts-ignore\nimport path from 'path';\n// @ts-ignore\nimport fs from 'fs/promises'; // 使用 fs/promises 简化异步操作\n\nasync function patchLinks(outputDir: string) {\n  /**\n   * 修复 Markdown 文件中的链接。\n   * 1. [foo](bar) -> [foo](./bar)\n   * 2. [foo](./bar) -> [foo](./bar) (保持不变)\n   * 3. [foo](http(s)://...) -> [foo](http(s)://...) (保持不变)\n   */\n  const normalizeLinksInFile = async (filePath: string) => {\n    try {\n      const content = await fs.readFile(filePath, 'utf-8');\n      const newContent = content.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (_match, p1, p2) => {\n        // 如果链接以 '/' 或 './' 开头，则保持不变\n        if (['/', '.'].includes(p2[0]) || p2.startsWith('http://') || p2.startsWith('https://')) {\n          return `[${p1}](${p2})`;\n        }\n        // 否则添加 './'\n        return `[${p1}](./${p2})`;\n      });\n\n      if (newContent !== content) {\n        await fs.writeFile(filePath, newContent);\n        // console.log(`Updated links in file: ${filePath}`);\n      }\n    } catch (error) {\n      console.error(`Error processing file ${filePath}:`, error);\n    }\n  };\n\n  const traverse = async (dir: string) => {\n    try {\n      const entries = await fs.readdir(dir, { withFileTypes: true });\n      await Promise.all(\n        entries.map(async (entry) => {\n          const fullPath = path.join(dir, entry.name);\n\n          if (entry.isDirectory()) {\n            await traverse(fullPath);\n          } else if (entry.isFile() && /\\.mdx?$/.test(entry.name)) {\n            await normalizeLinksInFile(fullPath);\n          }\n        })\n      );\n    } catch (error) {\n      console.error(`Error traversing directory ${dir}:`, error);\n    }\n  };\n\n  await traverse(outputDir);\n}\n\nexport async function patchGeneratedApiDocs(absoluteApiDir: string) {\n  console.log(`Patching links in API docs at: ${absoluteApiDir}`);\n  await patchLinks(absoluteApiDir);\n}\n"
  },
  {
    "path": "apps/docs/src/en/_nav.json",
    "content": "[\n  {\n    \"text\": \"Guide\",\n    \"link\": \"/guide/getting-started/introduction\",\n    \"activeMatch\": \"/guide/\"\n  },\n  {\n    \"text\": \"Materials\",\n    \"link\": \"/materials/introduction\",\n    \"activeMatch\": \"/materials/\"\n  },\n  {\n    \"text\": \"Examples\",\n    \"link\": \"/examples/\",\n    \"activeMatch\": \"/examples/\"\n  },\n  {\n    \"text\": \"API\",\n    \"link\": \"/api/\",\n    \"activeMatch\": \"/api/\"\n  },\n  {\n    \"text\": \"Blogs\",\n    \"link\": \"https://juejin.cn/column/7479814468601315362\"\n  },\n  {\n    \"text\": \"TypeDocs\",\n    \"link\": \"/auto-docs/\",\n    \"activeMatch\": \"/auto-docs/\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/en/api/_meta.json",
    "content": "[\n  {\n    \"type\": \"file\",\n    \"name\": \"index\",\n    \"label\": \"API Overview\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"core\",\n    \"label\": \"Core\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"hooks\",\n    \"label\": \"Hooks\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"components\",\n    \"label\": \"Components\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"services\",\n    \"label\": \"Services\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"utils\",\n    \"label\": \"Utils\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/en/api/common-apis.mdx",
    "content": "# Common APIs\n\n## FlowDocument (Automated Layout Document Data)\n\n```ts\n// Can be obtained through hook or ctx\nconst doc = useService<FlowDocument>(FlowDocument)\n\ndoc.fromJSON(data) // Load data\ndoc.getAllNodes() // Get all nodes\ndoc.traverseDFS(node => {}) // Depth-first traversal of nodes\ndoc.toJSON() // TODO This is the old version data, not yet optimized. Business logic should implement JSON conversion using traverseDFS\n\ndoc.addFromNode(targetNode, json) // Insert after the specified node\n\ndoc.onNodeCreate(({ node, json }) => {}) // Listen to node creation, data is the JSON data at creation time\ndoc.onNodeDispose(({ node }) => {}) // Listen to node deletion\n```\n\n## WorkflowDocument (Free Connection Layout Document Data) Inherits from FlowDocument\n\n```ts\nconst doc = useService<WorkflowDocument>(WorkflowDocument)\n\ndoc.fromJSON(data) // Load data\ndoc.toJSON() // Export data\ndoc.getAllNodes() // Get all nodes\ndoc.linesManager.getAllLines() // Get all lines\n\n// Create node\ndoc.createWorkflowNode({ id: nanoid(), type: 'xxx', data: {}, meta: { position: { x: 0, y: 0 } } })\n// Create line, from and to are the node IDs to connect, fromPort and toPort can be omitted for single ports\ndoc.linesManager.createLine({ from, to, fromPort, toPort })\n\n// Listen to changes, this will monitor events for lines and nodes\ndoc.onContentChange((e) => {\n\n})\n```\n\n## FlowNodeEntity (Node)\n\n```ts\nnode.flowNodeType // Current node type\nnode.transform.bounds // Get node's bounding rectangle, includes x,y,width,height\nnode.updateExtInfo({ title: 'xxx' }) // Set extension data, reactive will refresh node\nnode.getExtInfo<T>() // Get extension data\nnode.getNodeRegister() // Get current node definition\n\nnode.dispose() // Delete node\n\n// renderData is node UI-related data\nconst renderData = node.renderData\nrenderData.node // Current node's DOM node\nrenderData.expanded // Whether current node is expanded, can be set\n\n// Get all upstream input and output nodes (free connection layout)\nnode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData).allInputNodes\nnode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData).allOutputNodes\n```\n\n## Playground (Canvas)\n\n```ts\n// Can be obtained through hook or ctx\nconst playground = useService(Playground)\n\n// Scroll to specified node and center it\nctx.playground.config.scrollToView({\n   entities: [node]\n   scrollToCenter: true\n   easing: true // Easing animation\n})\n\n// Scroll canvas\nctx.playground.config.scroll({\n  scrollX: 0\n  scrollY: 0\n})\n\n// Fit to screen\nctx.playground.config.fitView(\n  doc.root.getData<FlowNodeTransformData>().bounds, // Rectangle to center, here using root node size to represent maximum frame\n  true, // Whether to use easing\n  20, // padding, leave blank spacing\n)\n\n// Zoom\nctx.playground.config.zoomin()\nctx.playground.config.zoomout()\nctx.playground.config.finalScale // Current zoom scale\n```\n\n## SelectionService (Selector)\n\n```ts\nconst selectionService = useService<SelectionService>()\n\nselection.selection // Returns currently selected node array, can also be modified, e.g., select node: selection.selection = [node]\n\nselection.onSelectionChanged(() => {}) // Listen to changes\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/editor-renderer.mdx",
    "content": "# EditorRenderer\n\nCanvas rendering component, needs to be used with `FixedLayoutEditorProvider` or `FreeLayoutEditorProvider`\n\n```tsx pure\nfunction App() {\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" style={{ /* style */}}>\n        {/* If you provide children, this content will be placed below the canvas div */}\n      </EditorRenderer>\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/fixed-layout-editor-provider.mdx",
    "content": "# FixedLayoutEditorProvider\n\nFixed layout canvas configuration, supports ref\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FixedLayoutEditorProvider {...editorProps} ref={ref}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/fixed-layout-editor.mdx",
    "content": "# FixedLayoutEditor\n\n\nFixed layout canvas, equivalent to the combination of `FixedLayoutEditorProvider` and `EditorRenderer`\n\n```tsx pure\nimport { FixedLayoutEditor, FixedLayoutPluginContext } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n\n  return (\n    <FixedLayoutEditor className=\"demo-editor\" {...editorProps} ref={ref} />\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/free-layout-editor-provider.mdx",
    "content": "# FreeLayoutEditorProvider\n\nFree layout canvas configuration, supports ref\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FreeLayoutEditorProvider {...editorProps} ref={ref}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/free-layout-editor.mdx",
    "content": "# FreeLayoutEditor\n\nFree layout canvas, equivalent to the combination of `FreeLayoutEditorProvider` and `EditorRenderer`\n\n```tsx pure\nimport { FreeLayoutEditor, FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FreeLayoutEditor className=\"demo-editor\" {...editorProps} ref={ref} />\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/components/workflow-node-renderer.mdx",
    "content": "# WorkflowNodeRenderer(free)\n\nFree layout node container\n\n## Usage\n\n```tsx pure\nimport { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';\n\nexport const BaseNode = () => {\n  /**\n   * Provide methods related to node rendering\n   */\n  const { form } = useNodeRender()\n  /**\n   * WorkflowNodeRenderer will add node drag events and port rendering, if you want to deeply customize it, you can refer to the source code of the component:\n   * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx\n   */\n  return (\n    <WorkflowNodeRenderer\n      className=\"demo-free-node\"\n      node={props.node}\n      // Optional port color customization\n      portPrimaryColor=\"#4d53e8\"        // Active state color (linked/hovered)\n      portSecondaryColor=\"#9197f1\"      // Default state color\n      portErrorColor=\"#ff4444\"          // Error state color\n      portBackgroundColor=\"#ffffff\"     // Background color\n    >\n      {\n        // Form rendering through formMeta generation\n        form?.render()\n      }\n    </WorkflowNodeRenderer>\n  )\n};\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/core/_meta.json",
    "content": "[\n  \"flow-document\",\n  \"flow-node-entity\",\n  \"workflow-document\",\n  \"workflow-lines-manager\",\n  \"workflow-line-entity\",\n  \"playground\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/api/core/flow-document.mdx",
    "content": "# FlowDocument\n\nFlow document (fixed layout), stores all node data of the process\n\n[> API Detail](https://flowgram.ai/auto-docs/document/classes/FlowDocument.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document)\n```\n\n:::danger\nThe best way to operate nodes is through [ctx.operation](/api/services/flow-operation-service.html), so that it can be bound to redo/undo\n:::\n\n\n## root\n\nGet the root node of the canvas, all nodes are attached to the root node\n\n```ts pure\nconsole.log(ctx.document.root);\n```\n## getAllNodes\n\nGet all node data\n\n```ts pure\nconst nodes = ctx.document.getAllNodes();\n```\n\n## getNode\n\nGet node by specified id\n\n```ts pure\nctx.document.getNode('start')\n```\n\n## getNodeRegistry\n\nGet node definition, node definition can be extended according to business\n\n```ts pure\nconst startNodeRegistry = ctx.document.getNodeRegistry<FlowNodeRegistry>('start')\n```\n\n## fromJSON/toJSON\n\nImport and export data\n\n```ts pure\nconst json = ctx.document.toJSON();\nctx.document.fromJSON(json);\n```\n\n## registerFlowNodes\n\nRegister node configuration items, supports inheritance\n\n```ts pure\nconst node1: FlowNodeRegistry = {\n  type: 'node1',\n  meta: {}\n}\n\nconst node2: FlowNodeRegistry = {\n  type: 'node2',\n  extend: 'node1' // Inherit the configuration of node1\n}\nctx.document.registerFlowNodes(node1, node2)\n```\n\n## addNode\n\nAdd node\n\n```ts pure\nctx.document.addNode({\n  id: 'node1',\n  type: 'start',\n  meta: {},\n  data: {},\n  parent: ctx.document.root // Can specify a parent node\n});\n\n```\n\n## addFromNode\n\nAdd to the node after the specified node\n\n```ts pure\nctx.document.addFromNode(\n ctx.document.getNode('start'),\n { id: 'node1', type: 'custom', data: {} }\n);\n\n```\n\n## addBlock\n\nAdd a branch node to the specified node\n\n```ts pure\n\nctx.document.addBlock(ctx.document.getNode('condition'), { id: 'if_1', type: 'block', data: {} })\n```\n\n## removeNode\n\nDelete node\n\n```ts pure\nctx.document.removeNode('node1');\n```\n\n## onNodeCreate/onNodeUpdate/onNodeDispose\n\nNode creation/update/destruction event, returns the event's disposal function\n\n```tsx pure\n\nuseEffect(() => {\n  const toDispose1 = ctx.document.onNodeCreate((node) => {\n    console.log('onNodeCreate', node);\n  });\n  const toDispose2 = ctx.document.onNodeUpdate((node) => {\n    console.log('onNodeUpdate', node);\n  });\n  const toDispose3 = ctx.document.onNodeDispose((node) => {\n    console.log('onNodeDispose', node);\n  });\n  return () => {\n    toDispose1.dispose()\n    toDispose2.dispose()\n    toDispose3.dispose()\n  }\n}, []);\n```\n## traverse\n\nTraverse all child nodes from the specified node, default root node\n\n```ts pure\n/**\n *\n * traverse all nodes, O(n)\n *   R\n *   |\n *   +---1\n *   |   |\n *   |   +---1.1\n *   |   |\n *   |   +---1.2\n *   |   |\n *   |   +---1.3\n *   |   |    |\n *   |   |    +---1.3.1\n *   |   |    |\n *   |   |    +---1.3.2\n *   |   |\n *   |   +---1.4\n *   |\n *   +---2\n *       |\n *       +---2.1\n *\n *  sort: [1, 1.1, 1.2, 1.3, 1.3.1, 1.3.2, 1.4, 2, 2.1]\n */\nctx.document.traverse((node, depth, index) => {\n  console.log(node.id);\n}, ctx.document.root);\n```\n\n## toString\n\nReturn a string snapshot of the node structure\n\n```ts pure\nconsole.log(ctx.document.toString())\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/core/flow-node-entity.mdx",
    "content": "# FlowNodeEntity/WorkflowNodeEntity\n\nNode entity, `WorkflowNodeEntity` is the alias for the node used for free layout nodes, the node entity uses the [ECS](/guide/concepts/ecs.html) architecture, is `Entity`\n\n[> API Detail](https://flowgram.ai/auto-docs/document/classes/FlowNodeEntity-1.html)\n\n## Properties\n\n- id: `string` Node id\n- flowNodeType: `string` | `number` Node type\n- version `number` Node version, can be used to determine if the node state has been updated\n\n## Accessors\n\n- document: `FlowDocument | WorkflowDocument` Document link\n- bounds: `Rectangle` Get the node's x, y, width, height, equivalent to `transform.bounds`\n- blocks: `FlowNodeEntity[]` Get child nodes, including collapsed child nodes, equivalent to `collapsedChildren`\n- collapsedChildren: `FlowNodeEntity[]` Get child nodes, including collapsed child nodes\n- allCollapsedChildren: `FlowNodeEntity[]` Get all child nodes, including all collapsed child nodes\n- children: `FlowNodeEntity[]` Get child nodes, not including collapsed child nodes\n- pre: `FlowNodeEntity | undefined` Get the previous node\n- next: `FlowNodeEntity | undefined` Get the next node\n- parent: `FlowNodeEntity | undefined` Get the parent node\n- originParent: `FlowNodeEntity | undefined` Get the original parent node, this is used to find the entire virtual branch for the first node of the fixed layout branch (orderIcon)\n- allChildren: `FlowNodeEntity[]` Get all child nodes, not including collapsed child nodes\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) Get the node's transform matrix data\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) Get the node's render data, including render status\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) Get the node's form data, like [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable public scope\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable private scope\n- lines: [WorkflowNodeLinesData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodeLinesData.html) Get the node's lines data (Only FreeLayout)\n- ports: [WorkflowNodePortsData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodePortsData.html) Get the node's ports data (Only FreeLayout)\n\n\n## Methods\n\n### getExtInfo\n\nGet the node's extended information, can be updated through `updateExtInfo`\n\n```\nnode.getExtInfo<{ test: string }>()\n```\n\n### updateExtInfo\n\nUpdate extended data, update will not be recorded in `redo/undo`, if you need to record, please implement the [history](/guide/advanced/history.html) service\n\n```\nnode.updateExtInfo<{ test: string }>({\n  test: 'test'\n})\n```\n\n### getNodeRegistry\n\nGet the node registry, equivalent to `ctx.document.getNodeRegistry(node.flowNodeType)`\n\n```ts pure\nconst nodeRegistry = node.getNodeRegistry<FlowNodeRegistry>()\n```\n\n### getData\n\nEquivalent to getting the Component of Entity in the [ECS](/guide/concepts/ecs.html) architecture, currently built-in two core Components\n\n```ts pure\nnode.getData(FlowNodeTransformData) // transform matrix data, including the node's x, y, width, height, etc.\nnode.getData(FlowNodeRenderData) // node render data, including render status\n\n```\n\n### addData\n\nEquivalent to adding the Component of Entity in the [ECS](/guide/concepts/ecs.html) architecture\n\n```ts pure\n\n// Custom EntityData\nclass CustomEntityData extends EntityData<{ key0: string }> {\n  static type = 'CustomEntityData';\n  getDefaultData() {\n    return {\n      key0: 'test'\n    }\n  }\n}\n\n// Add Entity Component\nnode.addData(CustomEntityData)\n\n\n// Update Entity Component data\nnode.getData(CustomEntityData).update({ key0: 'new value' })\n\n```\n\n### getService\n\nNode access [IOC](/guide/concepts/ioc.html) service\n\n```ts pure\nnode.getService(SelectionService)\n```\n\n### dispose\n\nNode destruction from canvas\n\n### onDispose\n\nNode destruction event\n\n```ts pure\nuseEffect(() => {\n  const toDispose = node.onDispose(() => {\n    console.log('Dispose node')\n  })\n  return () => toDispose.dispose()\n}, [node])\n```\n\n### toJSON\n\nExport node data\n\n:::note Node data basic structure:\n\n- id: `string` Node unique identifier, must be unique\n- meta: `object` Node ui configuration information, such as `position` information for free layout\n- type: `string | number` Node type, will correspond to `type` in `nodeRegistries`\n- data: `object` Node form data, business can customize\n- blocks: `array` Node branches, using `block` is closer to `Gramming`\n\n:::\n"
  },
  {
    "path": "apps/docs/src/en/api/core/playground.mdx",
    "content": "# Playground\n\nCanvas instance\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/Playground.html)\n\n```ts pure\nconst ctx = useClientContext()\n\nconsole.log(ctx.playground)\n\n```\n## config\n\nCanvas configuration, provides zoom, scroll, etc.\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/PlaygroundConfigEntity.html)\n\n### updateConfig\n- zoom `number` Current zoom ratio\n- scrollX\n- scrollY\n- minZoom\n- maxZoom\n- readonly\n- disabled\n- width\n- height\n\n```ts pure\n// get current config state\nctx.playground.config.config.zoom\nctx.playground.config.config.readonly\n\n// updateConfig\nctx.playground.config.updateConfig({\n  zoom: 0.8,\n  minZoom: 0.1,\n  maxZoom: 2,\n  readonly: true\n})\n```\n\n### fitView\n\nNode fit canvas window, need to pass in the node's bounds\n\n```ts pure\n/**\n * Fit size\n * @param bounds {Rectangle} Target size\n * @param easing {number} Whether to start animation, default is true\n * @param padding {number} Boundary padding\n */\nctx.playground.config.fitView(node.bounds, true, 10)\n```\n\n### scrollToView\n\nSpecify the node position and scroll to the canvas visible area, if the position is already in the visible area, it will not scroll unless `scrollToCenter` is forced to scroll\n\n```ts pure\n\n/**\n * Detailed parameter description\n * @param opts {PlaygroundConfigRevealOpts}\n**/\ninterface PlaygroundConfigRevealOpts {\n  entities?: Entity[]\n  position?: PositionSchema // Scroll to the specified position and center\n  bounds?: Rectangle // Scroll bounds\n  scrollDelta?: PositionSchema\n  zoom?: number // Need to scale the ratio\n  easing?: boolean // Whether to start animation, default is true\n  easingDuration?: number // Default 500 ms\n  scrollToCenter?: boolean // Whether to force scroll to center\n}\n\nctx.playground.config.scrollToView({\n  bounds: ctx.document.getNode('start').bounds,\n})\n```\n\n### zoomin\n\nZoom In\n\n### zoomout\n\nZoom Out\n\n### getPoseFromMouseEvent\n\nConvert browser mouse position to canvas coordinate system\n\n```ts pure\n\nconst pos: { x: number, y: number } = ctx.playground.config.getPoseFromMouseEvent(domMouseEvent)\n\n```\n\n### scroll\n\nScroll canvas, need to pass in the scroll position, and whether to smooth scroll, scroll time\n\n```ts pure\nctx.playground.config.scroll({ scrollX: 100, scrollY: 100 }, true, 300)\n```\n\n### isViewportVisible\n\nDetermine whether the current node is within the viewport\n\n```ts pure\nctx.playground.config.isViewportVisible(node.bounds)\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/core/workflow-document.mdx",
    "content": "# WorkflowDocument (free)\n\nFree layout document data, inherited from [FlowDocument](/api/core/flow-document.html)\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowDocument.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document)\n```\n\n:::tip\nDue to historical reasons, all names with the `Workflow` prefix represent free layout\n:::\n\n## linesManager\n\nFree layout line management, see [WorkflowLinesManager](/api/core/workflow-lines-manager.html)\n\n## createWorkflowNodeByType\n\nCreate a free layout node by node type\n\n```ts pure\nconst node = ctx.document.createWorkflowNodeByType(\n 'custom',\n  { x: 100, y: 100 },\n  {\n    id: 'xxxx',\n    data: {}\n  }\n)\n```\n\n## onContentChange\n\nListen to the free layout canvas data change\n\n```ts pure\n\nexport enum WorkflowContentChangeType {\n  /**\n   * Add node\n   */\n  ADD_NODE = 'ADD_NODE',\n  /**\n   * Delete node\n   */\n  DELETE_NODE = 'DELETE_NODE',\n  /**\n   * Move node\n   */\n  MOVE_NODE = 'MOVE_NODE',\n  /**\n   * Node data update (form engine data or extInfo data)\n   */\n  NODE_DATA_CHANGE = 'NODE_DATA_CHANGE',\n  /**\n   * Add line\n   */\n  ADD_LINE = 'ADD_LINE',\n  /**\n   * Delete line\n   */\n  DELETE_LINE = 'DELETE_LINE',\n  /**\n   * Node meta information change\n   */\n  META_CHANGE = 'META_CHANGE',\n}\n\nexport interface WorkflowContentChangeEvent {\n  type: WorkflowContentChangeType;\n  /**\n   * The json data of the currently triggered element, toJSON needs to be triggered actively\n   */\n  toJSON: () => any;\n  /*\n   * The entity of the currently triggered event\n   */\n  entity: WorkflowNodeEntity | WorkflowLineEntity;\n}\n\n``\n"
  },
  {
    "path": "apps/docs/src/en/api/core/workflow-line-entity.mdx",
    "content": "# WorkflowLineEntity (free)\n\nFree layout line entity\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowLineEntity.html)\n\n"
  },
  {
    "path": "apps/docs/src/en/api/core/workflow-lines-manager.mdx",
    "content": "# WorkflowLinesManager (free)\n\nFree layout line management, currently attached to the free layout document\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowLinesManager.html)\n\n```\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document.linesManager)\n```\n\n## getAllLines\n\nGet all line entities\n\n```ts pure\nconst allLines = ctx.document.linesManager.getAllLines()\n\n```\n\n## toJSON\n\nExport line data\n\n```ts pure\nconst json = ctx.document.linesManager.toJSON()\n```\n\n## Custom Arrow Renderer\n\nWorkflowLinesManager supports customizing arrow styles through the renderer registry. For detailed usage, please refer to the [Line Configuration Guide](/en/guide/free-layout/line#4-custom-arrow-renderer) documentation.\n\n```tsx\n// Simple example: Register custom arrow\nconst editorProps = {\n  materials: {\n    components: {\n      'arrow-renderer': MyCustomArrow,\n    },\n  },\n};\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/hooks/use-client-context.mdx",
    "content": "# useClientContext\n\nProvides access to the canvas context within React. Currently, there are some differences between fixed layout and free layout.\n\n## Fixed Layout\n\n- Return: [FixedLayoutPluginContext](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/FixedLayoutPluginContext.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor'\nconst ctx = useClientContext()\n```\n\n## Free Layout\n\n- Return: [FreeLayoutPluginContext](https://flowgram.ai/auto-docs/free-layout-editor/interfaces/FreeLayoutPluginContext.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\nconst ctx = useClientContext()\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/hooks/use-node-render.mdx",
    "content": "# useNodeRender\n\nProvides methods related to node rendering, and the form returned is equivalent to [getNodeForm](/api/utils/get-node-form.html)\n\n## Fixed Layout\n\n- Return: [NodeRenderReturnType](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/NodeRenderReturnType.html)\n\n```tsx pure\n\nimport { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  /**\n   * Provides methods related to node rendering\n   */\n  const nodeRender = useNodeRender();\n  /**\n   * Only available when the node engine is enabled\n   */\n  const form = nodeRender.form;\n\n  return (\n    <div\n      className=\"demo-fixed-node\"\n      /*\n       * Adding onMouseEnter to the fixed layout node is mainly to monitor the hover highlight of the branch line\n       **/\n      onMouseEnter={nodeRender.onMouseEnter}\n      onMouseLeave={nodeRender.onMouseLeave}\n      onMouseDown={e => {\n        // trigger drag node\n        nodeRender.startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        /**\n         * Used to precisely control the style of the branch node\n         * isBlockIcon: The header node of the entire condition branch\n         * isBlockOrderIcon: The first node of the branch\n         */\n        ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n    </div>\n  );\n};\n\n```\n\n## Free Layout\n\n- Return: [NodeRenderReturnType](https://flowgram.ai/auto-docs/free-layout-core/interfaces/NodeRenderReturnType.html)\n\n```tsx pure\nimport { WorkflowNodeRenderer, useNodeRender } from '@flowgram.ai/free-layout-editor';\nexport const BaseNode = () => {\n  const { form, node } = useNodeRender()\n  return (\n    <WorkflowNodeRenderer className=\"demo-free-node\" node={node}>\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/hooks/use-playground-tools.mdx",
    "content": "# usePlaygroundTools\n\nCanvas tool methods\n\n## Fixed Layout\n\n- Return: [PlaygroundTools](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/PlaygroundTools.html)\n```tsx pure\nimport { useEffect, useState } from 'react'\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}>\n    <button onClick={() => tools.zoomin()}>ZoomIn</button>\n    <button onClick={() => tools.zoomout()}>ZoomOut</button>\n    <button onClick={() => tools.fitView()}>Fitview</button>\n    <button onClick={() => tools.changeLayout()}>ChangeLayout</button>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n    <span>{Math.floor(tools.zoom * 100)}%</span>\n  </div>\n}\n```\n\n\n## Free Layout\n\n- Return: [PlaygroundTools](https://flowgram.ai/auto-docs/free-layout-editor/interfaces/PlaygroundTools.html)\n\n```tsx pure\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}>\n    <button onClick={() => tools.zoomin()}>ZoomIn</button>\n    <button onClick={() => tools.zoomout()}>ZoomOut</button>\n    <button onClick={() => tools.fitView()}>Fitview</button>\n    <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n    <span>{Math.floor(tools.zoom * 100)}%</span>\n  </div>\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/hooks/use-refresh.mdx",
    "content": "# useRefresh\n\n## Source Code\n\n```ts\nimport { useCallback, useState } from 'react';\n\nexport function useRefresh(defaultValue?: any): (v?: any) => void {\n  const [, update] = useState<any>(defaultValue);\n  return useCallback((v?: any) => update(v !== undefined ? v : {}), []);\n}\n```\n\n## Usage\n\n```tsx pure\nimport { useRefresh } from '@flowgram.ai/fixed-layout-editor';\n\nfunction Demo() {\n  const refresh = useRefresh();\n  return (\n    <div>\n      <button onClick={() => refresh()}>Refresh</button>\n    </div>\n  )\n}\n\n```\n\n"
  },
  {
    "path": "apps/docs/src/en/api/hooks/use-service.mdx",
    "content": "# useService\n\nGet all singleton modules of the underlying [IOC](/guide/concepts/ioc.html)\n\n```ts pure\n\nconst playground = useService<Playground>(Playground)\nconst flowDocument = useService<FlowDocument>(FlowDocument)\nconst historyService = useService<HistoryService>(HistoryService)\n\n// Equivalent to\nconst playground1 = useClientContext().playground\n\n// Equivalent to\nconst playground3 = useClientContext().get<Playground>(Playground)\n\n```\n\n\n\n## Custom Service\n\n```tsx pure\n/**\n *  inversify: https://github.com/inversify/InversifyJS\n */\nimport { injectable } from 'inversify'\n\n@injectable()\nclass MyService {\n  // ...\n}\n\nimport { useMemo } from 'react';\nimport { type FixedLayoutProps } from '@flowgram.ai/fixed-layout-editor';\n\nfunction BaseNode() {\n  const mySerivce = useService<MyService>(MyService)\n}\n\nexport function useEditorProps(\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      // ....other props\n      onBind: ({ bind }) => {\n        bind(MyService).toSelf().inSingletonScope()\n      },\n      materials: {\n        renderDefaultNode: BaseNode\n      }\n    }),\n    [],\n  );\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/index.mdx",
    "content": "---\n# API Overview\noverview: true\n---\n"
  },
  {
    "path": "apps/docs/src/en/api/plugins.mdx",
    "content": "---\noverview: true\noverviewHeaders: [2]\n---\nThis is the official website api configuration, demo use.\n"
  },
  {
    "path": "apps/docs/src/en/api/services/clipboard-service.mdx",
    "content": "# ClipboardService\n\nClipboard Service\n\n[> API Detail](https://flowgram.ai/auto-docs/core/interfaces/ClipboardService.html)\n"
  },
  {
    "path": "apps/docs/src/en/api/services/command-service.mdx",
    "content": "# CommandService\n\nCommand Service, needs to be used with [Shortcuts](/guide/advanced/shortcuts.html)\n\n[> API Detail](https://flowgram.ai/auto-docs/command/interfaces/CommandService.html)\n\n\n```typescript pure\n\nctx.get(CommandService).execCommand('selectAll')\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/services/flow-operation-service.mdx",
    "content": "# FlowOperationService\n\nNode operation service, currently used for fixed layout, free layout can currently be operated directly through WorkflowDocument, and will be abstracted out as operation in the future\n\n[> API Detail](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/FlowOperationService.html)\n\n```typescript pure\nconst operationService = useService<FlowOperationService>(FlowOperationService)\noperationService.addNode({ id: 'xxx', type: 'custom', data: {} })\n\n// or\nconst ctx = useClientContext();\nctx.operation.addNode({ id: 'xxx', type: 'custom', data: {} })\n\n\n```\n\n## Interface\n\n```typescript pure\n\nexport interface FlowOperationBaseService extends Disposable {\n  /**\n   * Execute operation\n   * @param operation Serializable operation\n   * @returns Operation return\n   */\n  apply(operation: FlowOperation): any;\n\n  /**\n   * Add node, if the node already exists, it will not be created repeatedly\n   * @param nodeJSON Node data\n   * @param config Configuration\n   * @returns Successfully added node\n   */\n  addNode(nodeJSON: FlowNodeJSON, config?: AddNodeConfig): FlowNodeEntity;\n\n  /**\n   * Add node based on a starting node\n   * @param fromNode Starting node\n   * @param nodeJSON Added node JSON\n   */\n  addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity;\n\n  /**\n   * Delete node\n   * @param node Node\n   * @returns\n   */\n  deleteNode(node: FlowNodeEntityOrId): void;\n\n  /**\n   * Batch delete nodes\n   * @param nodes\n   */\n  deleteNodes(nodes: FlowNodeEntityOrId[]): void;\n\n  /**\n   * Add block (branch)\n   * @param target Target\n   * @param blockJSON Block data\n   * @param config Configuration\n   * @returns\n   */\n  addBlock(\n    target: FlowNodeEntityOrId,\n    blockJSON: FlowNodeJSON,\n    config?: AddBlockConfig,\n  ): FlowNodeEntity;\n\n  /**\n   * Move node\n   * @param node The node to be moved\n   * @param config Move node configuration\n   */\n  moveNode(node: FlowNodeEntityOrId, config?: MoveNodeConfig): void;\n\n  /**\n   * Drag node\n   * @param param0\n   * @returns\n   */\n  dragNodes({ dropNode, nodes }: { dropNode: FlowNodeEntity; nodes: FlowNodeEntity[] }): void;\n\n  /**\n   * Add node callback\n   */\n  onNodeAdd: Event<OnNodeAddEvent>;\n}\n\nexport interface FlowOperationService extends FlowOperationBaseService {\n  /**\n   * Create group\n   * @param nodes Node list\n   */\n  createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined;\n  /**\n   * Ungroup\n   * @param groupNode\n   */\n  ungroup(groupNode: FlowNodeEntity): void;\n  /**\n   * Start transaction\n   */\n  startTransaction(): void;\n  /**\n   * End transaction\n   */\n  endTransaction(): void;\n  /**\n   * Modify form data\n   * @param node Node\n   * @param path Property path\n   * @param value Value\n   */\n  setFormValue(node: FlowNodeEntityOrId, path: string, value: unknown): void;\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/services/history-service.mdx",
    "content": "## HistoryService\n\n[> API Detail](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryService.html)\n\n## Redo/Undo\n\n```tsx pure\nimport { useEffect, useState } from 'react'\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n  </div>\n}\n```\n\n## Render History\n\n```tsx pure\nimport { useEffect } from 'react'\nimport { useRefresh, useClientContext } from '@flowgram.ai/fixed-layout-editor'\n\nfunction HistoryListRender() {\n  const refresh = useRefresh()\n  const ctx = useClientContext()\n  useEffect(() => {\n    ctx.history.onApply(() => refresh())\n  }, [ctx])\n  return (\n    <div>\n      {ctx.history.historyManager.historyStack.items.map((record) => <HistoryOperations key={record.id} operations={record.operations} />)}\n    </div>\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/services/selection-service.mdx",
    "content": "# SelectionService\n\nUsed to control selected nodes\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/SelectionService.html)\n\n## Usage\n```tsx pure\n// Listen Selection Change\nctx.selection.onSelectionChanged((nodes) => {\n})\n// Select All Nodes\nctx.selection.selection = ctx.document.getAllNodes()\n```\n"
  },
  {
    "path": "apps/docs/src/en/api/utils/disposable-collection.mdx",
    "content": "# DisposableCollection\n\n## Usage\n\n\n```ts pure\n\nimport { DisposableCollection, Disposable } from '@flowgram.ai/utils'\nconst disposable1: Disposable = {\n  dispose() {\n    console.log(1)\n  },\n};\nconst disposable2: Disposable = {\n  dispose() {\n    console.log(2)\n  },\n};\nconst dc = new DisposableCollection();\ndc.onDispose(() => {\n  console.log('end')\n});\n\ndc.pushAll([disposable1, disposable2]);\ndc.dispose(); // Log: 1, 2, dispose end\n\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/disposable.ts\n\n\n"
  },
  {
    "path": "apps/docs/src/en/api/utils/disposable.mdx",
    "content": "# Disposable\n\n## Interface\n\n```ts\n/**\n * An object that performs a cleanup operation when `.dispose()` is called.\n *\n * Some examples of how disposables are used:\n *\n * - An event listener that removes itself when `.dispose()` is called.\n * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.\n */\nexport interface Disposable {\n  dispose(): void;\n}\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/disposable.ts\n"
  },
  {
    "path": "apps/docs/src/en/api/utils/emitter.mdx",
    "content": "# Emitter\n\nEvent module\n\n\n## Usage\n\n```tsx pure\nimport { Emitter } from '@flowgram.ai/utils'\n\nclass Doc {\n  private _content = ''\n  private _onContentChangeEmitter = new Emitter<string>()\n  readonly onContentChange = this._onContentChangeEmitter.event\n  setContent(content: string) {\n    this._content = content\n    this._onContentChangeEmitter.fire(content)\n  }\n  get content() {\n    return this._content\n  }\n}\n\nfunction App() {\n  const doc1 = useMemo(() => new Doc(), [])\n  const [content, updateContent] = useState(doc1.content)\n  useEffect(() => {\n    const toDispose = doc1.onContentChange((content) => {\n      updateContent(content)\n    })\n    return () => toDispose.dispose()\n  }, [doc1])\n  return <div>{content}</div>\n}\n\n\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/event.ts\n\n"
  },
  {
    "path": "apps/docs/src/en/api/utils/get-node-form.mdx",
    "content": "# getNodeForm\n\nGet the form capabilities of the node, needs to be enabled node engine\n\n[> API Detail](https://flowgram.ai/auto-docs/editor/functions/getNodeForm.html)\n\n\n## Usage\n\n```tsx pure\n\n// 1. BaseNode\nfunction BaseNode({ node }) {\n  const form = getNodeForm(node);\n  console.log(form.getValueIn('title'))\n  return <div>{form?.render()}</div>\n}\n\n// 2. useNodeRender\nfunction BaseNode() {\n  const { form } = useNodeRender();\n  console.log(form.getValueIn('title'))\n  return <div>{form?.render()}</div>\n}\n\n```\n\n## Return Inteface\n\n```ts pure\n\nexport interface NodeFormProps<TValues> {\n  /**\n   * The initialValues of the form.\n   */\n  initialValues: TValues;\n  /**\n   * Form values. Returns a deep copy of the data in the store.\n   */\n  values: TValues;\n  /**\n   * Form state\n   */\n  state: FormState;\n  /**\n   * Get value in certain path\n   * @param name path\n   */\n  getValueIn<TValue = FieldValue>(name: FieldName): TValue;\n\n  /**\n   * Set value in certain path.\n   * It will trigger the re-rendering of the Field Component if a Field is related to this path\n   * @param name path\n   */\n  setValueIn<TValue>(name: FieldName, value: TValue): void;\n  /**\n   * set form values\n   */\n  updateFormValues(values: any): void;\n  /**\n   * Render form\n   */\n  render: () => React.ReactNode;\n  /**\n   * Form value change event\n   */\n  onFormValuesChange: Event<OnFormValuesChangePayload>;\n  /**\n   * Trigger form validate\n   */\n  validate: () => Promise<boolean>;\n  /**\n   * Form validate event\n   */\n  onValidate: Event<FormState>;\n  /**\n   * Form field value change event\n   */\n  onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(\n    name: FieldName,\n    callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void\n  ): Disposable;\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/en/examples/_meta.json",
    "content": "[\n  {\n    \"type\": \"file\",\n    \"name\": \"index\",\n    \"label\": \"Experience Environment\"\n  },\n  \"playground\",\n  {\n    \"type\": \"dir\",\n    \"name\": \"fixed-layout\",\n    \"label\": \"Fixed Layout\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"free-layout\",\n    \"label\": \"Free Layout\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"node-form\",\n    \"label\": \"Node Form\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/en/examples/fixed-layout/_meta.json",
    "content": "[\n  \"fixed-layout-simple\",\n  \"fixed-composite-nodes\",\n  \"fixed-feature-overview\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/examples/fixed-layout/fixed-composite-nodes.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Composite Nodes\n\nimport { CompositeNodesPreview } from '../../../../components';\n\n<CompositeNodesPreview cellHeight={500}/>\n\n## Installation\n\n```bash\nnpx @flowgram.ai/create-app@latest fixed-layout-simple\n```\n\n## Source Code\n\n- jsonData: https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout-simple/src/data\n- nodeRegistries: https://github.com/bytedance/flowgram.ai/tree/main/packages/canvas-engine/fixed-layout-core/src/activities\n"
  },
  {
    "path": "apps/docs/src/en/examples/fixed-layout/fixed-feature-overview.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Best Practices\n\nimport { FixedFeatureOverview } from '../../../../components';\n\n<FixedFeatureOverview />\n\n## Installation\n\n```bash\nnpx @flowgram.ai/create-app@latest fixed-layout\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout\n\n## Project Overview\n\n### Core Tech Stack\n- Frontend Framework: React 18 + TypeScript\n- Build Tool: Rsbuild (a modern build tool based on Rspack)\n- Styling: Less + Styled Components + CSS Variables\n- UI Component Library: Semi Design (@douyinfe/semi-ui)\n- State Management: Editor framework developed in-house by Flowgram\n- Dependency Injection: Inversify\n\n\n### Core Dependencies\n\n- @flowgram.ai/fixed-layout-editor: Core dependency for the fixed-layout editor\n- @flowgram.ai/fixed-semi-materials: Semi Design materials library\n- @flowgram.ai/form-materials: Form materials library\n- @flowgram.ai/group-plugin: Group plugin\n- @flowgram.ai/minimap-plugin: Minimap plugin\n\n## Code Overview\n\n```\nsrc/\n├── app.tsx                    # Application entry component\n├── editor.tsx                 # Main editor component\n├── index.ts                   # Module export entry\n├── initial-data.ts            # Initial data configuration\n├── type.d.ts                  # Global type declarations\n│\n├── assets/                    # Static assets\n│   ├── icon-mouse.tsx         # Mouse icon component\n│   └── icon-pad.tsx           # Trackpad icon component\n│\n├── components/                # Common components library\n│   ├── index.ts               # Components export entry\n│   ├── node-list.tsx          # Node list component\n│   │\n│   ├── agent-adder/           # Agent adder component\n│   │   └── index.tsx\n│   ├── agent-label/           # Agent label component\n│   │   └── index.tsx\n│   ├── base-node/             # Base node component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── branch-adder/          # Branch adder component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── drag-node/             # Draggable node component\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── node-adder/            # Node adder component\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   └── utils.ts\n│   ├── selector-box-popover/  # Selection box popover component\n│   │   └── index.tsx\n│   ├── sidebar/               # Sidebar components\n│   │   ├── index.tsx\n│   │   ├── sidebar-node-renderer.tsx\n│   │   ├── sidebar-provider.tsx\n│   │   └── sidebar-renderer.tsx\n│   └── tools/                 # Toolbar components\n│       ├── index.tsx\n│       ├── styles.tsx\n│       ├── fit-view.tsx       # Fit view tool\n│       ├── minimap-switch.tsx # Minimap toggle\n│       ├── minimap.tsx        # Minimap component\n│       ├── readonly.tsx       # Readonly mode toggle\n│       ├── run.tsx            # Run tool\n│       ├── save.tsx           # Save tool\n│       ├── switch-vertical.tsx # Vertical layout toggle\n│       └── zoom-select.tsx    # Zoom selector\n│\n├── context/                   # React Context state management\n│   ├── index.ts               # Context export entry\n│   ├── node-render-context.ts # Node render context\n│   └── sidebar-context.ts     # Sidebar context\n│\n├── form-components/           # Form components library\n│   ├── index.ts               # Export entry for form components\n│   ├── feedback.tsx           # Feedback component\n│   │\n│   ├── form-content/          # Form content components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-header/           # Form header components\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   ├── title-input.tsx\n│   │   └── utils.tsx\n│   ├── form-inputs/           # Form input components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-item/             # Form item component\n│   │   ├── index.css\n│   │   └── index.tsx\n│   ├── form-outputs/          # Form output components\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   └── properties-edit/       # Property editing components\n│       ├── index.tsx\n│       ├── property-edit.tsx\n│       └── styles.tsx\n│\n├── hooks/                     # Custom React hooks\n│   ├── index.ts               # Hooks export entry\n│   ├── use-editor-props.ts    # Hook for editor properties\n│   ├── use-is-sidebar.ts      # Hook for sidebar state\n│   └── use-node-render-context.ts # Hook for node render context\n│\n├── nodes/                     # Flow node definitions\n│   ├── index.ts               # Node registry\n│   ├── default-form-meta.tsx  # Default form metadata\n│   │\n│   ├── agent/                 # Agent node type\n│   │   ├── index.ts\n│   │   ├── agent.ts\n│   │   ├── agent-llm.ts\n│   │   ├── agent-memory.ts\n│   │   ├── agent-tools.ts\n│   │   ├── memory.ts\n│   │   └── tool.ts\n│   ├── break-loop/            # Break loop node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case/                  # Case branch node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case-default/          # Default case node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── catch-block/           # Exception catch block node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── end/                   # End node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── if/                    # Conditional node\n│   │   └── index.ts\n│   ├── if-block/              # Conditional block node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── llm/                   # LLM node\n│   │   └── index.ts\n│   ├── loop/                  # Loop node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── start/                 # Start node\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── switch/                # Switch branch node\n│   │   └── index.ts\n│   └── trycatch/              # Try-Catch node\n│       ├── index.ts\n│       └── form-meta.tsx\n│\n├── plugins/                   # Plugin system\n│   ├── index.ts               # Plugins export entry\n│   │\n│   ├── clipboard-plugin/      # Clipboard plugin\n│   │   └── create-clipboard-plugin.ts\n│   ├── group-plugin/          # Group plugin\n│   │   ├── index.ts\n│   │   ├── group-box-header.tsx\n│   │   ├── group-node.tsx\n│   │   ├── group-note.tsx\n│   │   ├── group-tools.tsx\n│   │   ├── icons/\n│   │   │   └── index.tsx\n│   │   └── multilang-textarea-editor/ # Multi-language textarea editor\n│   │       ├── index.css\n│   │       ├── index.tsx\n│   │       └── base-textarea.tsx\n│   └── variable-panel-plugin/ # Variable panel plugin\n│       ├── index.ts\n│       ├── variable-panel-layer.tsx\n│       ├── variable-panel-plugin.ts\n│       └── components/\n│           ├── full-variable-list.tsx\n│           ├── global-variable-editor.tsx\n│           └── variable-panel.tsx\n│\n├── services/                  # Services layer\n│   ├── index.ts\n│   └── custom-service.ts      # Custom service\n│\n├── shortcuts/                 # Shortcuts system\n│   ├── index.ts\n│   ├── constants.ts           # Shortcut constants\n│   └── utils.ts               # Shortcut utilities\n│\n└── typings/                   # Type definitions\n    ├── index.ts               # Types export entry\n    ├── json-schema.ts         # JSON Schema types\n    └── node.ts                # Node type definitions\n```\n\n## Architecture Design Analysis\n\n### Overall Architecture Pattern\n\nThis project adopts a layered architecture combined with modular design:\n\n1. Presentation Layer\n - Component layer: responsible for UI rendering and user interactions\n - Tools layer: provides editor tool features\n\n2. Business Logic Layer\n - Node system: defines the behavior and properties of various flow nodes\n - Plugin system: provides extensible functional modules\n - Services layer: handles business logic and data operations\n\n3. Data Layer\n - Context state management: manages global application state\n - Type system: ensures consistency of data structures\n\n### Key Design Patterns\n\n#### 1. Provider Pattern\n```typescript\n// The main editor component uses multiple nested Providers\n<FixedLayoutEditorProvider {...editorProps}>\n  <SidebarProvider>\n    <EditorRenderer />\n    <DemoTools />\n    <SidebarRenderer />\n  </SidebarProvider>\n  </FixedLayoutEditorProvider>\n```\n\nUse cases:\n- `FixedLayoutEditorProvider`: provides core editor features and state\n- `SidebarProvider`: manages sidebar visibility and the selected node\n\n#### 2. Registry Pattern\n```typescript\nexport const FlowNodeRegistries: FlowNodeRegistry[] = [\n  StartNodeRegistry,\n  EndNodeRegistry,\n  SwitchNodeRegistry,\n  LLMNodeRegistry,\n  // ... more node types\n];\n```\n\nAdvantages:\n- Supports dynamic registration of node types\n- Easy to extend with new node types\n- Decouples node type definitions\n\n#### 3. Plugin Pattern\n```typescript\nplugins: () => [\n  createMinimapPlugin({...}),\n  createGroupPlugin({...}),\n  createClipboardPlugin(),\n  createVariablePanelPlugin({}),\n]\n```\n\nPlugin system highlights:\n- Minimap plugin: provides a canvas minimap\n- Group plugin: supports node grouping and management\n- Clipboard plugin: enables copy and paste\n- Variable panel plugin: provides a UI for variable management\n\n#### 4. Factory Pattern\nWidely used in node creation and configuration:\n```typescript\ngetNodeDefaultRegistry(type) {\n  return {\n    type,\n    meta: {\n      defaultExpanded: true,\n    },\n  };\n}\n```\n\n#### 5. Observer Pattern\nImplemented via the history system:\n```typescript\nhistory: {\n  enable: true,\n  enableChangeNode: true,\n  onApply: debounce((ctx, opt) => {\n    console.log('auto save: ', ctx.document.toJSON());\n  }, 100),\n}\n```\n\n#### 6. Strategy Pattern\nReflected in the materials system:\n```typescript\nmaterials: {\n  components: {\n    ...defaultFixedSemiMaterials,\n    [FlowRendererKey.ADDER]: NodeAdder,\n    [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n    // Different render strategies can be swapped by key\n  }\n}\n```\n\n### State Management Architecture\n\n#### Context System Design\nThe project uses multiple dedicated Contexts to manage different domains of state:\n\n1. SidebarContext: manages sidebar state\n```typescript\nexport const SidebarContext = React.createContext<{\n  visible: boolean;\n  nodeId?: string;\n  setNodeId: (node: string | undefined) => void;\n}>({ visible: false, setNodeId: () => {} });\n```\n\n2. NodeRenderContext: manages state related to node rendering\n3. IsSidebarContext: simple boolean state\n\n#### Custom Hooks\n- `useEditorProps`: centralizes all editor configuration props\n- `useIsSidebar`: determines whether the current environment is the sidebar\n- `useNodeRenderContext`: gets the node render context\n\n### Component Architecture\n\n#### Component Layering\n1. Base components\n - `BaseNode`: base rendering component for all nodes\n - `DragNode`: node component in drag state\n\n2. Functional components\n - Adders: `NodeAdder`, `BranchAdder`, `AgentAdder`\n - Tools: zoom, save, run, and other utilities\n\n3. Container components\n - `Sidebar`: sidebar container and its subcomponents\n - `Tools`: toolbar container\n\n### Data Flow Architecture\n\n#### Initial Data Structure\nThe project defines a complete initial flow dataset, including examples of multiple node types:\n- Start node: entry point of the flow, defines output parameters\n- Agent node: contains LLM, Memory, and Tools subcomponents\n- LLM node: large language model processing node\n- Switch node: conditional branch node\n- Loop node: loop processing node\n- TryCatch node: exception handling node\n- End node: end of the flow\n\n#### Data Transformation Mechanism\n```typescript\nfromNodeJSON(node, json) {\n  return json; // Transform logic on data import\n},\ntoNodeJSON(node, json) {\n  return json; // Transform logic on data export\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/examples/fixed-layout/fixed-layout-simple.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Basic Usage\n\nimport { FixedLayoutSimplePreview } from '../../../../components';\n\n<FixedLayoutSimplePreview />\n\n## Installation\n\n```bash\nnpx @flowgram.ai/create-app@latest fixed-layout-simple\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout-simple\n"
  },
  {
    "path": "apps/docs/src/en/examples/free-layout/_meta.json",
    "content": "[\n  \"free-layout-simple\",\n  \"free-feature-overview\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/examples/free-layout/free-feature-overview.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Best Practices\n\nimport { FreeFeatureOverview } from '../../../../components';\n\n<FreeFeatureOverview />\n\n## Installation\n\n```bash\nnpx @flowgram.ai/create-app@latest free-layout\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout\n\n## Project Overview\n\n### Core Tech Stack\n- **Frontend framework**: React 18 + TypeScript\n- **Build tool**: Rsbuild (a modern build tool based on Rspack)\n- **Styling**: Less + Styled Components + CSS Variables\n- **UI library**: Semi Design (@douyinfe/semi-ui)\n- **State management**: Flowgram’s in-house editor framework\n- **Dependency injection**: Inversify\n\n### Core Dependencies\n\n- **@flowgram.ai/free-layout-editor**: Core dependency for the free layout editor\n- **@flowgram.ai/free-snap-plugin**: Auto-alignment and guide-lines plugin\n- **@flowgram.ai/free-lines-plugin**: Connection line rendering plugin\n- **@flowgram.ai/free-node-panel-plugin**: Node add-panel rendering plugin\n- **@flowgram.ai/minimap-plugin**: Minimap plugin\n- **@flowgram.ai/free-container-plugin**: Sub-canvas plugin\n- **@flowgram.ai/free-group-plugin**: Grouping plugin\n- **@flowgram.ai/form-materials**: Form materials\n- **@flowgram.ai/runtime-interface**: Runtime interfaces\n- **@flowgram.ai/runtime-js**: JS runtime module\n- **@flowgram.ai/panel-manager-plugin**:  Sidebar panel management\n\n## Code Guide\n\n### Directory Structure\n```\nsrc/\n├── app.tsx                  # Application entry file\n├── editor.tsx               # Main editor component\n├── initial-data.ts          # Initial data configuration\n├── assets/                  # Static assets\n├── components/              # Component library\n│   ├── index.ts\n│   ├── add-node/            # Add-node component\n│   ├── base-node/           # Base node components\n│   ├── comment/             # Comment components\n│   ├── group/               # Group components\n│   ├── line-add-button/     # Connection add button\n│   ├── node-menu/           # Node menu\n│   ├── node-panel/          # Node add panel\n│   ├── selector-box-popover/ # Selection box popover\n│   ├── sidebar/             # Sidebar\n│   ├── testrun/             # Test-run module\n│   │   ├── hooks/           # Test-run hooks\n│   │   ├── node-status-bar/ # Node status bar\n│   │   ├── testrun-button/  # Test-run button\n│   │   ├── testrun-form/    # Test-run form\n│   │   ├── testrun-json-input/ # JSON input component\n│   │   └── testrun-panel/   # Test-run panel\n│   └── tools/               # Utility components\n├── context/                 # React Context\n│   ├── node-render-context.ts # Current rendering node context\n│   ├── sidebar-context        # Sidebar context\n├── form-components/         # Form component library\n│   ├── form-content/        # Form content\n│   ├── form-header/         # Form header\n│   ├── form-inputs/         # Form inputs\n│   └── form-item/           # Form item\n│   └── feedback.tsx         # Validation error rendering\n├── hooks/\n│   ├── index.ts\n│   ├── use-editor-props.tsx # Editor props hook\n│   ├── use-is-sidebar.ts    # Sidebar state hook\n│   ├── use-node-render-context.ts # Node render context hook\n│   └── use-port-click.ts    # Port click hook\n├── nodes/                    # Node definitions\n│   ├── index.ts\n│   ├── constants.ts         # Node constants\n│   ├── default-form-meta.ts # Default form metadata\n│   ├── block-end/           # Block end node\n│   ├── block-start/         # Block start node\n│   ├── break/               # Break node\n│   ├── code/                # Code node\n│   ├── comment/             # Comment node\n│   ├── condition/           # Condition node\n│   ├── continue/            # Continue node\n│   ├── end/                 # End node\n│   ├── group/               # Group node\n│   ├── http/                # HTTP node\n│   ├── llm/                 # LLM node\n│   ├── loop/                # Loop node\n│   ├── start/               # Start node\n│   └── variable/            # Variable node\n├── plugins/                 # Plugin system\n│   ├── index.ts\n│   ├── context-menu-plugin/ # Right-click context menu plugin\n│   ├── runtime-plugin/      # Runtime plugin\n│   │   ├── client/          # Client\n│   │   │   ├── browser-client/ # Browser client\n│   │   │   └── server-client/  # Server client\n│   │   └── runtime-service/ # Runtime service\n│   └── variable-panel-plugin/ # Variable panel plugin\n│       └── components/      # Variable panel components\n├── services/                 # Service layer\n│   ├── index.ts\n│   └── custom-service.ts    # Custom service\n├── shortcuts/                # Shortcuts system\n│   ├── index.ts\n│   ├── constants.ts         # Shortcut constants\n│   ├── shortcuts.ts         # Shortcut definitions\n│   ├── type.ts              # Type definitions\n│   ├── collapse/            # Collapse shortcut\n│   ├── copy/                # Copy shortcut\n│   ├── delete/              # Delete shortcut\n│   ├── expand/              # Expand shortcut\n│   ├── paste/               # Paste shortcut\n│   ├── select-all/          # Select-all shortcut\n│   ├── zoom-in/             # Zoom-in shortcut\n│   └── zoom-out/            # Zoom-out shortcut\n├── styles/                   # Styles\n├── typings/                  # Type definitions\n│   ├── index.ts\n│   ├── json-schema.ts       # JSON Schema types\n│   └── node.ts              # Node type definitions\n└── utils/                    # Utility functions\n    ├── index.ts\n    └── on-drag-line-end.ts  # Handle end of drag line\n```\n\n### Key Directory Functions\n\n#### 1. `/components` - Component Library\n- **base-node**: Base rendering components for all nodes\n- **testrun**: Complete test-run module, including status bar, form, and panel\n- **sidebar**: Sidebar components providing tools and property panels\n- **node-panel**: Node add panel with drag-to-add capability\n\n#### 2. `/nodes` - Node System\nEach node type has its own directory, including:\n- Node registration (`index.ts`)\n- Form metadata (`form-meta.ts`)\n- Node-specific components and logic\n\n#### 3. `/plugins` - Plugin System\n- **runtime-plugin**: Supports both browser and server modes\n- **context-menu-plugin**: Right-click context menu\n- **variable-panel-plugin**: Variable management panel\n\n#### 4. `/shortcuts` - Shortcuts System\nComplete keyboard shortcut support, including:\n- Basic actions: copy, paste, delete, select-all\n- View actions: zoom-in, zoom-out, collapse, expand\n- Each shortcut has its own implementation module\n\n## Application Architecture\n\n### Core Design Patterns\n\n#### 1. Plugin Architecture\nHighly modular plugin system; each feature is an independent plugin:\n\n```typescript\nplugins: () => [\n  createFreeLinesPlugin({ renderInsideLine: LineAddButton }),\n  createMinimapPlugin({ /* config */ }),\n  createFreeSnapPlugin({ /* alignment config */ }),\n  createFreeNodePanelPlugin({ renderer: NodePanel }),\n  createContainerNodePlugin({}),\n  createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),\n  createContextMenuPlugin({}),\n  createRuntimePlugin({ mode: 'browser' }),\n  createVariablePanelPlugin({})\n]\n```\n\n#### 2. Node Registry Pattern\nManage different workflow node types via a registry:\n\n```typescript\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,    // Condition node\n  StartNodeRegistry,        // Start node\n  EndNodeRegistry,          // End node\n  LLMNodeRegistry,          // LLM node\n  LoopNodeRegistry,         // Loop node\n  CommentNodeRegistry,      // Comment node\n  HTTPNodeRegistry,         // HTTP node\n  CodeNodeRegistry,         // Code node\n  // ... more node types\n];\n```\n\n#### 3. Dependency Injection\nUse Inversify for service DI:\n\n```typescript\nonBind: ({ bind }) => {\n  bind(CustomService).toSelf().inSingletonScope();\n}\n```\n\n## Core Features\n\n### 1. Editor Configuration System\n\n`useEditorProps` is the configuration center of the editor:\n\n```typescript\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FreeLayoutProps {\n  return useMemo<FreeLayoutProps>(() => ({\n    background: true,                    // Background grid\n    readonly: false,                     // Readonly mode\n    initialData,                         // Initial data\n    nodeRegistries,                      // Node registries\n\n    // Core feature configs\n    playground: { preventGlobalGesture: true /* Prevent Mac browser swipe gestures */ },\n    nodeEngine: { enable: true },\n    variableEngine: { enable: true },\n    history: { enable: true, enableChangeNode: true },\n\n    // Business rules\n    canAddLine: (ctx, fromPort, toPort) => { /* Connection rules */ },\n    canDeleteLine: (ctx, line) => { /* Line deletion rules */ },\n    canDeleteNode: (ctx, node) => { /* Node deletion rules */ },\n    canDropToNode: (ctx, params) => { /* Drag-and-drop rules */ },\n\n    // Plugins\n    plugins: () => [/* Plugin list */],\n\n    // Events\n    onContentChange: debounce((ctx, event) => { /* Auto save */ }, 1000),\n    onInit: (ctx) => { /* Initialization */ },\n    onAllLayersRendered: (ctx) => { /* After render */ }\n  }), []);\n}\n```\n\n### 2. Node Type System\n\nThe app supports multiple workflow node types:\n\n```typescript\nexport enum WorkflowNodeType {\n  Start = 'start',           // Start node\n  End = 'end',               // End node\n  LLM = 'llm',               // Large language model node\n  HTTP = 'http',             // HTTP request node\n  Code = 'code',             // Code execution node\n  Variable = 'variable',     // Variable node\n  Condition = 'condition',   // Conditional node\n  Loop = 'loop',             // Loop node\n  BlockStart = 'block-start', // Sub-canvas start node\n  BlockEnd = 'block-end',    // Sub-canvas end node\n  Comment = 'comment',       // Comment node\n  Continue = 'continue',     // Continue node\n  Break = 'break',           // Break node\n}\n```\n\nEach node follows a unified registration pattern:\n\n```typescript\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,        // Not deletable\n    copyDisable: true,          // Not copyable\n    nodePanelVisible: false,    // Hidden in node panel\n    defaultPorts: [{ type: 'output' }],\n    size: { width: 360, height: 211 }\n  },\n  info: {\n    icon: iconStart,\n    description: 'The starting node of the workflow, used to set up information needed to launch the workflow.'\n  },\n  formMeta,                     // Form configuration\n  canAdd() { return false; }    // Disallow multiple start nodes\n};\n```\n\n### 3. Plugin Architecture\n\nApp features are modularized via the plugin system:\n\n#### Core Plugin List\n1. **FreeLinesPlugin** - Connection rendering and interaction\n2. **MinimapPlugin** - Minimap navigation\n3. **FreeSnapPlugin** - Auto-alignment and guide-lines\n4. **FreeNodePanelPlugin** - Node add panel\n5. **ContainerNodePlugin** - Container nodes (e.g., loop nodes)\n6. **FreeGroupPlugin** - Node grouping\n7. **ContextMenuPlugin** - Right-click context menu\n8. **RuntimePlugin** - Workflow runtime\n9. **VariablePanelPlugin** - Variable management panel\n\n### 4. Runtime System\n\nTwo run modes are supported:\n\n```typescript\ncreateRuntimePlugin({\n  mode: 'browser',              // Browser mode\n  // mode: 'server',            // Server mode\n  // serverConfig: {\n  //   domain: 'localhost',\n  //   port: 4000,\n  //   protocol: 'http',\n  // },\n})\n```\n\n## Design Philosophy and Advantages\n\n### 1. Highly Modular\n- **Plugin architecture**: Each feature is an independent plugin, easy to extend and maintain\n- **Node registry system**: Add new node types without changing core code\n- **Componentized UI**: Highly reusable components with clear responsibilities\n\n### 2. Type Safety\n- **Full TypeScript support**: End-to-end type safety from configuration to runtime\n- **JSON Schema integration**: Node data validated by schemas\n- **Strongly typed plugin interfaces**: Clear type constraints for plugin development\n\n### 3. User Experience\n- **Real-time preview**: Run and debug workflows live\n- **Rich interactions**: Dragging, zooming, snapping, shortcuts for a complete editing experience\n- **Visual feedback**: Minimap, status indicators, line animations\n\n### 4. Extensibility\n- **Open plugin system**: Third parties can easily develop custom plugins\n- **Flexible node system**: Custom node types and form configurations supported\n- **Multiple runtimes**: Both browser and server modes\n\n### 5. Performance\n- **On-demand loading**: Components and plugins support lazy loading\n- **Debounce**: Performance optimizations for high-frequency operations like auto-save\n\n## Technical Highlights\n\n### 1. In-house Editor Framework\nBased on `@flowgram.ai/free-layout-editor`, providing:\n- Free-layout canvas system\n- Full undo/redo functionality\n- Lifecycle management for nodes and connections\n- Variable engine and expression system\n\n### 2. Advanced Build Configuration\nUsing Rsbuild as the build tool:\n\n```typescript\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: { index: './src/app.tsx' },\n    decorators: { version: 'legacy' }  // Enable decorators\n  },\n  tools: {\n    rspack: {\n      ignoreWarnings: [/Critical dependency/]  // Ignore specific warnings\n    }\n  }\n});\n```\n\n### 3. Internationalization\nBuilt-in multilingual support:\n\n```typescript\ni18n: {\n  locale: navigator.language,\n  languages: {\n    'zh-CN': {\n      'Never Remind': '不再提示',\n      'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',\n    },\n    'en-US': {},\n  }\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/en/examples/free-layout/free-layout-simple.mdx",
    "content": "---\noutline: true\n---\n\n# Basic Usage\n\nimport { FreeLayoutSimplePreview } from '../../../../components';\n\n<FreeLayoutSimplePreview />\n\n## Feature Overview\n\nFree Layout is a layout editor component provided by Flowgram.ai that allows users to create and edit flowcharts, workflows, and various node connection diagrams. Core features include:\n\n- Free drag-and-drop node positioning\n- Node connection and edge management\n- Configurable node registration and custom rendering\n- Built-in undo/redo history\n- Plugin extension support (e.g., minimap, auto-alignment)\n\n## Building Free Layout Editor from Scratch\n\nThis section will guide you through building a free layout editor application from scratch, demonstrating how to use the @flowgram.ai/free-layout-editor package to build an interactive workflow editor.\n\n### 1. Environment Setup\n\nFirst, we need to create a new project:\n\n```bash\n# Use the scaffolding tool to quickly create a project\nnpx @flowgram.ai/create-app@latest free-layout-simple\n\n# Enter the project directory\ncd free-layout-simple\n\n# Install dependencies\nnpm install\n```\n\n### 2. Project Structure\n\nAfter creation, the project structure is as follows:\n\n```\nfree-layout-simple/\n├── src/\n│   ├── components/            # Components directory\n│   │   ├── node-add-panel.tsx # Node addition panel\n│   │   ├── tools.tsx          # Toolbar component\n│   │   └── minimap.tsx        # Minimap component\n│   ├── hooks/\n│   │   └── use-editor-props.tsx # Editor configuration\n│   ├── initial-data.ts        # Initial data definition\n│   ├── node-registries.ts     # Node type registration\n│   ├── editor.tsx             # Editor main component\n│   ├── app.tsx                # Application entry\n│   ├── index.tsx              # Render entry\n│   └── index.css              # Style file\n├── package.json\n└── ...other configuration files\n```\n\n### 3. Development Process\n\n#### Step 1: Define Initial Data\n\nFirst, we need to define the canvas's initial data structure, including nodes and connections:\n\n```tsx\n// src/initial-data.ts\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start Node',\n        content: 'This is a start node'\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom Node',\n        content: 'This is a custom node'\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End Node',\n        content: 'This is an end node'\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n```\n\n#### Step 2: Register Node Types\n\nNext, we need to define the behavior and appearance of different types of nodes:\n\n```tsx\n// src/node-registries.ts\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // A normal node has two ports\n  },\n];\n```\n\n#### Step 3: Create Editor Configuration\n\nUse React hook to encapsulate editor configuration:\n\n```tsx\n// src/hooks/use-editor-props.tsx\nimport { useMemo } from 'react';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\n\nimport { nodeRegistries } from '../node-registries';\nimport { initialData } from '../initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      // Enable background grid\n      background: true,\n      // Non-readonly mode\n      readonly: false,\n      // Initial data\n      initialData,\n      // Node type registration\n      nodeRegistries,\n      // Default node registration\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            // Node form rendering\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      // Node rendering\n      materials: {\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { form } = useNodeRender();\n          return (\n            <WorkflowNodeRenderer className=\"demo-free-node\" node={props.node}>\n              {form?.render()}\n            </WorkflowNodeRenderer>\n          );\n        },\n      },\n      // Content change callback\n      onContentChange(ctx, event) {\n        console.log('Data Change: ', event, ctx.document.toJSON());\n      },\n      // Enable node form engine\n      nodeEngine: {\n        enable: true,\n      },\n      // Enable history record\n      history: {\n        enable: true,\n        enableChangeNode: true, // Listen for node engine data changes\n      },\n      // Initialization callback\n      onInit: (ctx) => {},\n      // Render completion callback\n      onAllLayersRendered(ctx) {\n        ctx.document.fitView(false); // Fit view\n      },\n      // Destruction callback\n      onDispose() {\n        console.log('Editor has been destroyed');\n      },\n      // Plugin configuration\n      plugins: () => [\n        // Minimap plugin\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        // Auto-alignment plugin\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n      ],\n    }),\n    []\n  );\n```\n\n#### Step 4: Create Node Addition Panel\n\n```tsx\n// src/components/node-add-panel.tsx\nimport React from 'react';\nimport { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';\n\nconst nodeTypes = ['Custom Node 1', 'Custom Node 2'];\n\nexport const NodeAddPanel: React.FC = () => {\n  const dragService = useService<WorkflowDragService>(WorkflowDragService);\n\n  return (\n    <div className=\"demo-free-sidebar\">\n      {nodeTypes.map(nodeType => (\n        <div\n          key={nodeType}\n          className=\"demo-free-card\"\n          onMouseDown={e => dragService.startDragCard('custom', e, {\n            data: {\n              title: nodeType,\n              content: 'Node created by dragging'\n            }\n          })}\n        >\n          {nodeType}\n        </div>\n      ))}\n    </div>\n  );\n};\n```\n\n#### Step 5: Create Toolbar and Minimap\n\n```tsx\nimport React from 'react';\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport const Tools: React.FC = () => {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n};\n\n// src/components/minimap.tsx\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 226,\n        bottom: 51,\n        zIndex: 100,\n        width: 198,\n      }}\n    >\n      <MinimapRender\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </div>\n  );\n};\n```\n\n#### Step 6: Assemble Editor Main Component\n\n```tsx\n// src/editor.tsx\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <NodeAddPanel />\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n        <Tools />\n        <Minimap />\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n```\n\n#### Step 7: Create Application Entry\n\n```tsx\n// src/app.tsx\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { Editor } from './editor';\n\nReactDOM.render(<Editor />, document.getElementById('root'))\n```\n\n#### Step 8: Add Styles\n\n```css\n/* src/index.css */\n.demo-free-node {\n    display: flex;\n    min-width: 300px;\n    min-height: 100px;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-free-node-title {\n    background-color: #93bfe2;\n    width: 100%;\n    border-radius: 8px 8px 0 0;\n    padding: 4px 12px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n```\n\n### 4. Run the Project\n\nAfter completing the above steps, you can run the project to see the effect:\n\n```bash\nnpm run dev\n```\n\nThe project will start locally, usually accessible at http://localhost:3000.\n\n## Core Concepts\n\n### 1. Data Structure\n\nFree Layout uses a standardized data structure to describe nodes and connections:\n\n```tsx\n// Workflow data structure\nconst initialData: WorkflowJSON = {\n  // Node definitions\n  nodes: [\n    {\n      id: 'start_0',        // Unique node ID\n      type: 'start',        // Node type (corresponding to registration in nodeRegistries)\n      meta: {\n        position: { x: 0, y: 0 }, // Node position\n      },\n      data: {\n        title: 'Start',     // Node data (customizable)\n        content: 'Start content'\n      },\n    },\n    // More nodes...\n  ],\n  // Edge definitions\n  edges: [\n    {\n      sourceNodeID: 'start_0', // Source node ID\n      targetNodeID: 'node_0',  // Target node ID\n    },\n    // More edges...\n  ],\n};\n```\n\n### 2. Node Registration\n\nUse `nodeRegistries` to define the behavior and appearance of different types of nodes:\n\n```tsx\n// Node registration\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  // Start node definition\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  // More node types...\n];\n```\n\n### 3. Editor Components\n\n```tsx\n// Core editor container and renderer\nimport {\n  FreeLayoutEditorProvider,\n  EditorRenderer\n} from '@flowgram.ai/free-layout-editor';\n\n// Editor configuration example\nconst editorProps = {\n  background: true,       // Enable background grid\n  readonly: false,        // Non-readonly mode, allow editing\n  initialData: {...},     // Initial data: definition of nodes and edges\n  nodeRegistries: [...],  // Node type registration\n  nodeEngine: {\n    enable: true,         // Enable node form engine\n  },\n  history: {\n    enable: true,         // Enable history record\n    enableChangeNode: true, // Listen for node data changes\n  }\n};\n\n// Complete editor rendering\n<FreeLayoutEditorProvider {...editorProps}>\n  <div className=\"container\">\n    <NodeAddPanel />              {/* Node addition panel */}\n    <EditorRenderer />            {/* Core editor rendering area */}\n    <Tools />                     {/* Toolbar */}\n    <Minimap />                   {/* Minimap */}\n  </div>\n</FreeLayoutEditorProvider>\n```\n\n### 4. Core Hook Functions\n\nIn components, you can use various hook functions to get and manipulate the editor:\n\n```tsx\n// Get drag service\nconst dragService = useService<WorkflowDragService>(WorkflowDragService);\n// Start dragging node\ndragService.startDragCard('nodeType', event, { data: {...} });\n\n// Get editor context\nconst { document, playground } = useClientContext();\n// Manipulate canvas\ndocument.fitView();                 // Fit view\nplayground.config.zoomin();               // Zoom canvas\ndocument.fromJSON(newData);         // Update data\n```\n\n### 5. Plugin Extensions\n\nFree Layout supports extending functionality through the plugin mechanism:\n\n```tsx\nplugins: () => [\n  // Minimap plugin\n  createMinimapPlugin({\n    canvasStyle: {\n      canvasWidth: 180,\n      canvasHeight: 100,\n      canvasBackground: 'rgba(245, 245, 245, 1)',\n    }\n  }),\n  // Auto-alignment plugin\n  createFreeSnapPlugin({\n    edgeColor: '#00B2B2',     // Alignment line color\n    alignColor: '#00B2B2',    // Guide line color\n    edgeLineWidth: 1,         // Line width\n  }),\n],\n```\n\n## Installation\n\n```bash\nnpx @flowgram.ai/create-app@latest free-layout-simple\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout-simple\n"
  },
  {
    "path": "apps/docs/src/en/examples/index.mdx",
    "content": "---\noverview: true\n---\n"
  },
  {
    "path": "apps/docs/src/en/examples/node-form/_meta.json",
    "content": "[\n  \"basic\",\n  \"effect\",\n  \"array\",\n  \"dynamic\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/examples/node-form/array.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Array\n\nimport { NodeFormArrayPreview } from '../../../../components/node-form/array/preview';\n\nThe following example demonstrates the basic usage of arrays, including:\n- Basic implementation (rendering, adding and removing items).\n- How to configure validation logic for each array item. In this case, the validation rule is that each item should not exceed 8 English characters.\n- How to configure side effects for each array item. Here, the side effect outputs `${name} value init to ${value}` to the console during initialization, and `${name} value changed to ${value}` when the value changes.\n- How to swap array items.\n\n<NodeFormArrayPreview />\n"
  },
  {
    "path": "apps/docs/src/en/examples/node-form/basic.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Basic Usage\n\nimport { NodeFormBasicPreview } from '../../../../components';\n\n<div>\n  This example demonstrates several basic form usages:\n   - Form component rendering\n   - Required field validation\n   - Default value setting\n</div>\n<NodeFormBasicPreview />\n"
  },
  {
    "path": "apps/docs/src/en/examples/node-form/dynamic.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Dynamic Field\n\nimport { NodeFormDynamicPreview } from '../../../../components';\n\nThis example demonstrates how to declare dependencies between form fields using the `deps` property.\n\nExample explanation: The `City` field will only be displayed when `Country` has a value.\n\nYou can also use `form.getValueIn('country')` as an input parameter for the component under the city `Field` to control the component's behavior, such as filtering cities based on the selected country.\n\n<NodeFormDynamicPreview />\n"
  },
  {
    "path": "apps/docs/src/en/examples/node-form/effect.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# Effect\n\nimport { NodeFormEffectPreview } from '../../../../components';\n\nThe following examples demonstrate how to configure form side effects. Two examples are provided, with behaviors described below:\n1. Basic effect: When a form field value changes, the current form values will be printed to the console.\n2. Control other fields: When the current form field data changes, it will simultaneously change the value of another form field.\n\n\n<NodeFormEffectPreview />\n"
  },
  {
    "path": "apps/docs/src/en/examples/playground.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# PlaygroundReact\n\n\nPlaygroundReact is the underlying module of fixed-layout and free-layout, which can be used separately, Features:\n\n- Infinite drag on the canvas\n- Mouse or touchpad gesture zooming\n- Built-in background plugin\n- Built-in shortcut plugin\n\nimport { InfiniteCanvasPreview } from '../../../components';\n\n<InfiniteCanvasPreview />\n\n## Install\n\n```bash\nnpx @flowgram.ai/create-app@latest playground\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-playground\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/_meta.json",
    "content": "[\n  {\n    \"type\": \"dir\",\n    \"name\": \"getting-started\",\n    \"label\": \"Getting Started\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"free-layout\",\n    \"label\": \"Free Layout\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"fixed-layout\",\n    \"label\": \"Fixed Layout\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"form\",\n    \"label\": \"Form\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"variable\",\n    \"label\": \"Variable\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"plugin\",\n    \"label\": \"Plugin\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"advanced\",\n    \"label\": \"Advanced\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"concepts\",\n    \"label\": \"Concepts\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"runtime\",\n    \"label\": \"Runtime\"\n  },\n  \"contributing\",\n  \"contact-us\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/_meta.json",
    "content": "[\n  \"zoom-scroll\",\n  \"history\",\n  \"shortcuts\",\n  \"custom-service\",\n  \"custom-layer\",\n  \"custom-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/custom-layer.mdx",
    "content": "# Custom Layer\n\nWe split the canvas into multiple Layers, implementing the concept of interaction layering for better plugin management. For more details, see [Canvas Engine](/guide/concepts/canvas-engine.html)\n\n1. Use `observeEntityDatas`, `observeEntities`, and `observeEntity` to monitor updates to any data module of canvas nodes\n2. Use `onZoom`, `onScroll`, `onViewportChange`, etc. to monitor canvas zooming or scrolling\n3. Use `render` to insert React elements into the canvas, such as drawing SVG lines\n\n![Aspect-oriented programming](@/public/en-layer-uml.jpg)\n\n## Creating a Layer\n\n```tsx pure\nimport { FreeLayoutPluginContext, inject, injectable, FlowNodeEntity, FlowNodeTransformData, FlowNodeFormData } from '@flowgram.ai/free-layout-editor'\n\n@injectable()\nexport class MyLayer extends Layer {\n  @inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext\n  // Can monitor node width, height, and position changes\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) transformDatas: FlowNodeTransformData[];\n  // Can monitor form data changes, connection data can be stored in forms\n  @observeEntityDatas(FlowNodeEntity, FlowNodeFormData) formDatas: FlowNodeFormData[];\n  onReady() {\n    // Can also add styles\n    // zIndex controls whether to overlay nodes, nodes default to 10, greater than 10 will be above nodes\n    this.node.style.zIndex = 11;\n  }\n  onZoom(scale) {\n    // Scale with canvas\n    this.node.style.transform = `scale(${scale})`;\n  }\n  render() {\n    return <div>{...}</div>\n  }\n}\n\n```\n\n## Adding to Canvas\n\n- Through use-editor-props\n\n```ts pure\n{\n  onInit: (ctx) => {\n    ctx.playground.registerLayer(MyLayer)\n  }\n}\n```\n\n- Through plugin\n\n```tsx pure\nimport { FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'\n\nexport const createMyPlugin = definePluginCreator<{}, FreeLayoutPluginContext>({\n  onInit: (ctx, opts) => {\n    ctx.playground.registerLayer(MyLayer)\n  },\n});\n```\n\n## Layer Lifecycle Description\n\n```ts\ninterface Layer {\n    /**\n     * Triggered during initialization\n     */\n    onReady?(): void;\n\n    /**\n     * Triggered when playground size changes\n     */\n    onResize?(size: PipelineDimension): void;\n\n    /**\n     * Triggered when playground is focused\n     */\n    onFocus?(): void;\n\n    /**\n     * Triggered when playground loses focus\n     */\n    onBlur?(): void;\n\n    /**\n     * Monitor zoom\n     */\n    onZoom?(scale: number): void;\n\n    /**\n     * Monitor scroll\n     */\n    onScroll?(scroll: { scrollX: number; scrollY: number }): void;\n\n    /**\n     * Triggered when viewport updates\n     */\n    onViewportChange?(): void;\n\n    /**\n     * Triggered when readonly or disabled state changes\n     * @param state\n     */\n    onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;\n\n    /**\n     * Data updates automatically trigger React render, if not provided React rendering won't be called\n     */\n    render?(): JSX.Element\n }\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/custom-plugin.mdx",
    "content": "# Custom Plugin\n\n## Plugin Lifecycle Explanation\n\n```tsx pure\n/**\n * from: https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/core/src/plugin/plugin.ts\n */\nimport { ContainerModule, interfaces } from 'inversify';\n\nexport interface PluginBindConfig {\n  bind: interfaces.Bind;\n  unbind: interfaces.Unbind;\n  isBound: interfaces.IsBound;\n  rebind: interfaces.Rebind;\n}\nexport interface PluginConfig<Opts, CTX extends PluginContext = PluginContext> {\n  /**\n   * Plugin IOC registration, equivalent to containerModule\n   * @param ctx\n   */\n  onBind?: (bindConfig: PluginBindConfig, opts: Opts) => void;\n  /**\n   * Canvas registration phase\n   */\n  onInit?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * Canvas preparation phase, generally used for DOM event registration, etc.\n   */\n  onReady?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * Canvas destruction phase\n   */\n  onDispose?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * After all layers of the canvas are rendered\n   */\n  onAllLayersRendered?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * IOC module, used for more low - level plugin extensions\n   */\n  containerModules?: interfaces.ContainerModule[];\n}\n\n```\n\n## Create a Plugin\n\n```tsx pure\n/**\n * If you want the plugin to be usable in both fixed and free layouts, please use\n *  import { definePluginCreator } from '@flowgram.ai/core'\n */\nimport { definePluginCreator, FixedLayoutPluginContext } from '@flowgram.ai/fixed-layout-editor'\n\nexport interface MyPluginOptions {\n  opt1: string;\n}\n\nexport const createMyPlugin = definePluginCreator<MyPluginOptions, FixedLayoutPluginContext>({\n  onBind: (bindConfig, opts) => {\n    // Register the IOC module. See Custom Service for how to define a Service.\n    bindConfig.bind(MyService).toSelf().inSingletonScope()\n  },\n  onInit: (ctx, opts) => {\n    // Plugin configuration\n    console.log(opts.opt1)\n    // ctx corresponds to FixedLayoutPluginContext or FreeLayoutPluginContext\n    console.log(ctx.document)\n    console.log(ctx.playground)\n    console.log(ctx.get<MyService>(MyService)) // Get the IOC module\n  },\n});\n```\n\n## Add a Plugin\n\n```tsx pure title=\"use-editor-props.ts\"\n\n// EditorProps\n{\n  plugins: () => [\n    createMyPlugin({\n      opt1: 'xxx'\n    })\n  ]\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/custom-service.mdx",
    "content": "# Custom Service\n\nIn business, it is necessary to abstract singleton services for easy plug-in management.\n\n```tsx pure\nimport { useMemo } from 'react';\nimport { FlowDocument, type FixedLayoutProps, inject, injectable } from '@flowgram.ai/fixed-layout-editor'\n\n/**\n * Docs: https://inversify.io/docs/introduction/getting-started/\n * Warning: Use decorator legacy\n *   // rsbuild.config.ts\n *   {\n *     source: {\n *       decorators: {\n *         version: 'legacy'\n *       }\n *     }\n *   }\n * Usage:\n *  1.\n *    const myService = useService(MyService)\n *    myService.save()\n *  2.\n *    const myService = useClientContext().get(MyService)\n *  3.\n *    const myService = node.getService(MyService)\n */\n@injectable()\nclass MyService {\n  // Dependency injection of singleton module\n  @inject(FlowDocument) flowDocument: FlowDocument\n  // ...\n}\n\nfunction BaseNode() {\n  const mySerivce = useService<MyService>(MyService)\n}\n\nexport function useEditorProps(\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      // ....other props\n      onBind: ({ bind }) => {\n        bind(MyService).toSelf().inSingletonScope()\n      },\n      materials: {\n        renderDefaultNode: BaseNode\n      }\n    }),\n    [],\n  );\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/history.mdx",
    "content": "# History\n\nUndo/Redo is a plugin of FlowGram.AI, which is provided in both @flowgram.ai/fixed-layout-editor and @flowgram.ai/free-layout-editor.\n\n\n## 1. Quick Start\n\n[> Demo Detail](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout/src/hooks/use-editor-props.ts#L125)\n\n### 1.1. Enable history\nBefore using the Undo/Redo feature, you need to introduce the editor, using the fixed layout editor as an example.\n\n1. Add dependencies in package.json\n```tsx pure title=\"use-editor-props.tsx\" {4}\nexport function useEditorProps() {\n  return useMemo(\n    () => ({\n      history: {\n        enable: true,\n        enableChangeNode: true // Listen Node engine data change\n      }\n    })\n  )\n}\n```\n\nAfter enabling, you will get the following capabilities:\n\n<table className=\"rs-table\">\n  <tr>\n    <td>Introduction</td>\n    <td>Description</td>\n    <td>Free Layout</td>\n    <td>Fixed Layout</td>\n  </tr>\n  <tr>\n    <td rowSpan={2}>Undo/Redo Shortcut</td>\n    <td>Use Cmd/Ctrl + Z to trigger Undo</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Use Cmd/Ctrl + Shift + Z to trigger Redo</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td rowSpan={7}>Canvas node operation supports undo/redo</td>\n    <td>Add/Delete node</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Add/Delete line</td>\n    <td>✅</td>\n    <td>❌</td>\n  </tr>\n  <tr>\n    <td>Move node</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Add/Delete branch</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Move branch</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Add group</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Cancel group</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td rowSpan={2}>Canvas batch operation</td>\n    <td>Delete node</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>Move node</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n\n</table>\n\n### 1.2. Disable history\nIf some data changes triggered by the system do not want to be monitored by undo/redo, you can actively stop the history service and restart it after the data operation is completed\n\n```tsx pure\nconst { history } = useClientContext();\n\nhistory.stop()\n// Do some operations that do not want to be captured, these changes will not be recorded in the operation stack\n...\nhistory.start()\n```\n\n### 1.3. History Undo/Redo merge\n\n```tsx pure\n\nconst { history } = useClientContext();\n\nhistory.startTransaction();\n\n// Any operations here will be merged into one\n...\n\nhistory.endTransaction();\n\n```\n\n### 1.4. Undo/Redo Call\nUndo/Redo is generally provided with two button entries on the interface, clicking which can trigger Undo and Redo, and the buttons themselves need to have the status of whether Undo/Redo is possible.\n\n```tsx pure\nexport function useUndoRedo(): UndoRedo {\n  const { history } = useClientContext();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const toDispose = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => {\n      toDispose.dispose();\n    };\n  }, []);\n\n  return {\n    canUndo,\n    canRedo,\n    undo: () => history.undo(),\n    redo: () => history.redo(),\n  };\n}\n```\n\n## 2. Extension Function\n### 2.1. Operation Registration\nOperations are registered through operationMetas\n\n```tsx pure title=\"use-editor-props.tsx\"\n...\nhistory={{\n  enable: true,\n  operationMetas: [\n    {\n        type: 'addNode',\n        apply: () => { console.log('addNode')},\n        inverse: (op) => ({ type: 'deleteNode', value: op.value })\n    }\n  ]\n}}\n```\n`OperationMeta` Core Definition:\n  - `type` is the unique identifier of the operation\n  - `inverse` is a function, which returns the inverse operation of the current operation\n  - `apply` is the logic executed when the operation is triggered\n\n```tsx pure\nexport interface OperationMeta {\n  /**\n   * Operation type, needs to be unique\n   */\n  type: string;\n  /**\n   * Convert an operation to another inverse operation, such as insert to delete\n   * @param op Operation\n   * @returns Inverse operation\n   */\n  inverse: (op: Operation) => Operation;\n  /**\n   * Execute operation\n   * @param operation Operation\n   */\n  apply(operation: Operation, source: any): void | Promise<void>;\n}\n```\n\nSuppose I want to add a function to support Undo/Redo for adding and deleting nodes, I need to add two operations\n\n<div style={{marginTop: 16, display: 'flex', gap: 8 }}>\n  <div>\n    <div>\n      ```tsx pure\n      {\n        type: 'addNode',\n        inverse: op => ({ ...op, type: 'deleteNode' }),\n        apply(op, ctx) {\n          document = ctx.get(Document)\n          document.addNode(op.value)\n        },\n      }\n      ```\n    </div>\n  </div>\n  <div>\n    <div>\n      ```tsx pure\n      {\n        type: 'deleteNode',\n        inverse: op => ({ ...op, type: 'addNode' }),\n        apply(op, ctx) {\n          document = ctx.get(Document)\n          document.deleteNode(op.value.id)\n        },\n      }\n      ```\n    </div>\n  </div>\n</div>\n\n### 2.2. Operation Merge\noperationMeta supports shouldMerge to customize the merge strategy, if frequent operations can be merged\n\n:::warning shouldMerge returns\n- Return false means not merged\n- Return true means merged into one operation stack element\n- Return Operation means merged into one operation\n\n:::\n\nThe following example is a merge of operations that edit the same field within 500ms\n\n```tsx pure\n{\n  type: 'changeData',\n  inverse: op => ({ ...op, type: 'changeData' }),\n  apply(op, ctx) {},\n  shouldMerge: (op, prev, element) => {\n    // Merge operations within 500ms\n    if (Date.now() - element.getTimestamp() < 500) {\n      if (\n        op.type === prev.type && // Same type\n        op.value.id === prev.value.id && // Same node\n        op.value?.path === prev.value?.path // Same path\n      ) {\n        return {\n          type: op.type,\n          value: {\n            ...op.value,\n            value: op.value.value,\n            oldValue: prev.value.oldValue,\n          },\n        };\n      }\n    }\n    return false;\n  }\n}\n```\n\n### 2.3. Operation Execution\n1. Single operation execution\n\nTrigger through pushOperation, the following example uses the operation defined in the business\n\n```tsx pure\nfunction handleAddNode () {\n   const { history } = useClientContext()\n   history.pushOperation({\n       type: 'addNode',\n       value: {\n          name: 'xx'\n          id: 'xxx'\n       }\n   })\n}\n```\n\n2. Batch execution\nAll operations executed in the function called by transact will be merged into one stack element, and will be executed together when undo/redo\nThe following is an example of implementing a batch delete:\n\n```tsx pure\nfunction deleteNodes(nodes: FlowNodeEntity[]) {\n  const { history } = useClientContext()\n  history.transact(() => {\n    nodes.forEach(node => {\n      history.pushOperation({\n        type: OperationType.deleteNode,\n        value: {\n          fromId: fromNode.id,\n          data: node.data,\n        },\n      });\n    });\n  });\n}\n```\n\n### 2.4. Undo/Redo\n1. Undo/Redo\nUndo execution history.undo method\nRedo execution history.redo method\n\n```tsx pure\nfunction undo() {\n    const { history } = useClientContext();\n    history.undo();\n}\n\nfunction redo() {\n    const { history } = useClientContext();\n    history.redo();\n}\n```\n\n2. Listen Undo/Redo\nListen to the onChange event of undoRedoService.onChange\nThe following is an example of triggering the uri of the corresponding operation after undo/redo (selecting the corresponding node or form item)\n```tsx pure\nfunction listenHistoryChange() {\n  const { history } = useClientContext();\n  history.undoRedoService.onChange(\n    ({ type, element }) => {\n      if (type === UndoRedoChangeType.PUSH) {\n        return;\n      }\n      const op = element.getLastOperation();\n      if (!op) {\n        return;\n      }\n      if (op.uri) {\n        // goto somewhere\n      }\n    },\n  )\n}\n```\n\n### 2.5. Operation History\n1. View refresh\nYou can get the history record through HistoryStack.items, and refresh the interface by listening to HistoryStack.onChange\n\n```tsx pure\nimport React from 'react';\n\nexport function HistoryList() {\n  const { historyStack } = useService<HistoryManager>(HistoryManager)\n  const { refresh } = useRefresh()\n  let items = historyManager.historyStack.items;\n\n  useEffect(() => {\n      const disposable = historyStack.onChange(() => {\n          refresh()\n      ])\n\n      return () => {\n          disposable.dispose()\n      }\n  }, [])\n\n  return (\n      <ul>\n        {items.map((item, index) => (\n          <li key={index}>\n            <div>\n              {item.type}({item.id}):\n              {item.operations.map((o, index) => (\n                <Tooltip\n                  key={index}\n                  title={(o.description || '') + `----uri: ${o.uri?.displayName}`}\n                >\n                  {o.label || o.type}\n                </Tooltip>\n              ))}\n            </div>\n\n          </li>\n        ))}\n      </ul>\n  );\n}\n```\n2. Persistence\nPersistence is implemented through the history-storage plugin\n- databaseName: database name\n- resourceStorageLimit: resource storage limit number\n\nAfter introducing the @flowgram.ai/history-storage package, the plugin can be used\n\n```tsx pure\nimport { createHistoryStoragePlugin } from '@flowgram.ai/history-storage';\n\ncreateHistoryStoragePlugin({\n    databaseName: 'your-history',\n    resourceStorageLimit: 50,\n}),\n```\n\nQuery the database list through useStorageHistoryItems\n\n```tsx pure\nimport {\n  useStorageHistoryItems,\n} from '@flowgram.ai/history-storage';\n\nexport const HistoryList = () => {\n  const { uri } = useCurrentWidget();\n\n  const { items } = useStorageHistoryItems(\n    storage,\n    uri.withoutQuery().toString(),\n  );\n\n  return <>\n    { JSON.stringify(items) }\n  </>\n}\n```\n\n## 3. API List\n### 3.1. [OperationMeta](https://flowgram.ai/auto-docs/fixed-history-plugin/interfaces/OperationMeta.html)\nOperationMeta, used to define an operation\n\n### 3.2. [Operation](https://flowgram.ai/auto-docs/fixed-history-plugin/interfaces/Operation.html)\nOperation data, associated with OperationMeta through type\n\n### 3.3. [OperationService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/OperationService.html)\n\n[onApply](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/OperationService.html#onapply)\nUse onApply to listen to a triggered operation\n\n```tsx pure\nuseService(OperationService).onApply((op: Operation) => {\n    console.log(op)\n    // Here you can execute your own business logic according to type\n})\n```\n\n### 3.4. [HistoryService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryService.html)\nThe core API of the History module exposed Service\n\n### 3.5. [UndoRedoService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/UndoRedoService.html)\nThe service that manages the UndoRedo stack\n\n### 3.6. [HistoryStack](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryStack.html)\nHistory stack, listen to all push undo redo operations, and record them in the stack\n\n### 3.7. [HistoryDatabase](https://flowgram.ai/auto-docs/history-storage/classes/HistoryDatabase.html)\nPersistence database operations\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/lines.mdx",
    "content": "# Free Layout Lines\n\nThe lines in the free layout are managed by [WorkflowLinesManager](/api/core/workflow-lines-manager.html).\n\n## Get the Input/Output Nodes of the Current Node\n\n```ts pure\n// Get the input nodes of the current node (calculated through connection lines)\nnode.lines.inputNodes;\n// Get all input nodes (recursively get all upward)\nnode.lines.allInputNodes;\n// Get the output nodes\nnode.lines.outputNodes;\n// Get all output nodes\nnode.lines.allOutputNodes;\n```\n\n## Node listens to its own connection changes and refreshes\n\n```tsx pure\n\nimport {\n  useRefresh,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-editor';\n\nfunction NodeRender({ node }) {\n  const refresh = useRefresh()\n  const linesData = node.get(WorkflowNodeLinesData)\n  useEffect(() => {\n    const dispose = linesData.onDataChange(() => refresh())\n    return () => dispose.dispose()\n  }, [])\n  return <div>xxxx</div>\n}\n\n```\n\n## Listen for connection changes of all lines\n\n```ts pure\nimport { useEffect } from 'react'\nimport { useClientContext, useRefresh } from '@flowgram.ai/free-layout-editor'\n\n\nfunction SomeReact() {\n  const refresh = useRefresh()\n  const linesManager = useClientContext().document.linesManager\n  useEffect(() => {\n      const dispose = linesManager.onAvailableLinesChange(() => refresh())\n      return () => dispose.dispose()\n  }, [])\n  console.log(ctx.document.linesManager.getAllLines())\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/shortcuts.mdx",
    "content": "# Shortcuts\n\n## Customize Shortcuts\n\n```ts pure\n// Add to EditorProps\n{\n  shortcuts(shortcutsRegistry, ctx) {\n      // Press command + a to select all nodes\n      shortcutsRegistry.addHandlers({\n        commandId: 'selectAll',\n        shortcuts: ['meta a', 'ctrl a'],\n        isEnabled: (...args) => true,\n        execute(...args) {\n          const allNodes = ctx.document.getAllNodes();\n          ctx.playground.selectionService.selection = allNodes;\n        },\n      });\n  },\n}\n\n```\n\n## Call Shortcuts by CommandService\n\n```ts pure\nconst commandService = useService(CommandService)\n/**\n * Call command service, args will be passed to execute and isEnabled\n */\ncommandService.executeCommand('selectAll', ...args)\n\n// OR\nctx.get(CommandService).executeCommand('selectAll', ...args)\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/advanced/zoom-scroll.mdx",
    "content": "# Scroll And Zoom\n\n[> See Playground](/api/core/playground.html)\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/_meta.json",
    "content": "[\n  \"canvas-engine\",\n  \"node-engine\",\n  \"ecs\",\n  \"ioc\",\n  \"reactflow\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/canvas-engine.mdx",
    "content": "# Canvas Engine\n\n## Playground\nThe underlying canvas engine provides its own coordinate system, which is mainly driven by the Playground.\n\n```ts\ninterface Playground {\n   node: HTMLDivElement // The DOM node where the canvas is mounted\n   toReactComponent() // Render as a React node\n   readonly: boolean // Read-only mode\n   config: PlaygroundConfigEntity // Contains canvas data such as zoom and scroll\n}\n// Quick access hook\nconst { playground } = useClientContext()\n```\n\n## Layer\n\n:::warning P.S.\n- The rendering layer establishes its own coordinate system at the underlying level. Based on this coordinate system, logics such as simulated scrolling and zooming are implemented. When calculating the viewport, nodes also need to be converted to this coordinate system.\n- The rendering is split into multiple layers (Layer) according to the canvas. The layered design is based on the data segmentation concept of ECS. Different Layers only listen to the data they want and render independently without interference. A Layer can be understood as an ECS System, which is the final consumption place for Entity data.\n- The Layer implements observer-style reactive dynamic dependency collection similar to MobX. Data updates will trigger autorun or render.\n:::\n\n![Aspect-oriented programming](@/public/en-layer-uml.jpg)\n\n- Layer Lifecycle\n\n```ts\ninterface Layer {\n    /**\n     * Triggered during initialization\n     */\n    onReady?(): void;\n\n    /**\n     * Triggered when the size of the playground changes\n     */\n    onResize?(size: PipelineDimension): void;\n\n    /**\n     * Triggered when the playground gets focus\n     */\n    onFocus?(): void;\n\n    /**\n     * Triggered when the playground loses focus\n     */\n    onBlur?(): void;\n\n    /**\n     * Listen for zoom events\n     */\n    onZoom?(scale: number): void;\n\n    /**\n     * Listen for scroll events\n     */\n    onScroll?(scroll: { scrollX: number; scrollY: number }): void;\n\n    /**\n     * Triggered when the viewport is updated\n     */\n    onViewportChange?(): void;\n\n    /**\n     * Triggered when the readonly or disable state changes\n     * @param state\n     */\n    onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;\n\n    /**\n     * Automatically trigger React rendering when data is updated. If not provided, React rendering will not be called.\n     */\n    render?(): JSX.Element\n }\n```\n\nThe positioning of the Layer is actually similar to the [MonoBehaviour](https://docs.unity3d.com/ScriptReference/MonoBehaviour.html) provided by the Unity game engine. The script extensions of the Unity game engine are all based on this. It can be considered the core design, and the underlying layer also relies on the dependency injection capability provided by C#'s reflection.\n\n```c#\nusing System.Collections;\nusing System.Collections.Generic;\nusing UnityEngine;\npublic class MyMonoBehavior : MonoBehaviour\n{\n    void Awake()\n    {\n        Debug.Log(\"Awake method is always called before application starts.\");\n    }\n    void Start()\n    {\n        Debug.Log(\"Start method is always called after Awake().\");\n    }\n    void Update()\n    {\n        Debug.Log(\"Update method is called in every frame.\");\n    }\n}\n```\n\n- Reactive updates of the Layer\n\n```ts\nexport class DemoLayer extends Layer {\n    // Injection of any Inversify module\n    @inject(FlowDocument) document: FlowDocument\n    // Listen to a single Entity\n    @observeEntity(SomeEntity) entity: SomeEntity\n    // Listen to multiple Entities\n    @observeEntities(SomeEntity) entities: SomeEntity[]\n    // Listen to changes in the data block (ECS - Component) of an Entity\n    @observeEntityDatas(SomeEntity, SomeEntityData) transforms: SomeEntityData[]\n    autorun() {}\n    render() {\n      return <div></div>\n    }\n}\n```\n\n## FlowNodeEntity\n\n- The node is a tree, containing child nodes (blocks) and a parent node. The node uses the ECS architecture.\n```ts\ninterface FlowNodeEntity {\n    id: string\n    blocks: FlowNodeEntity[]\n    pre?: FlowNodeEntity\n    next?: FlowNodeEntity\n    parent?: FlowNodeEntity\n    collapsed: boolean // Whether it is expanded\n    getData(dataRegistry): NodeEntityData\n    addData(dataRegistry)\n}\n```\n\n## FlowNodeTransformData: Position and Size Data of the Node\n\n```ts\nclass FlowNodeTransformData {\n    localTransform: Matrix, // Relative offset, only relative to the previous sibling node in the same block\n    worldTransform: Matrix, // Absolute offset, relative to the superposition of the parent and sibling nodes\n    delta: Point // Centering and left-alignment offset, independent of the matrix, controlled by each node itself\n    getSize(): Size, // Calculated by the width, height, and spacing of the node itself (independent node) or its child branch nodes\n    getBounds(): Rectangle // Calculated by the world matrix and size, used for final rendering. This range can also be used to determine the highlighted selection area\n    inputPoint(): Point // Input point position, usually the middle-top position of the first node in the block (centered layout)\n    outputPoint(): Point // Output point position, default is the middle-bottom position of the node. For conditional branches, it is determined by specific logic such as the built-in end node\n   // ...others\n}\n```\n\n## FlowNodeRenderData: Node Content Rendering Data\n\n```ts\nclass FlowNodeRenderData {\n  node: HTMLDivElement // The DOM of the current node\n  expanded: boolean // Whether it is expanded\n  activated: boolean // Whether it is activated\n  hidden: boolean // Whether it is hidden\n  // ...others\n}\n```\n\n## FlowDocument\n\n```ts\ninterface FlowDocument {\n    root: FlowNodeEntity // The root node of the canvas\n    fromJSON(data): void // Import data\n    toJSON(): FlowDocumentJSON // Export data\n    addNode(type: string, meta: any): FlowNodeEntity // Add a node\n    traverse(fn: (node: FlowNodeEntity) => void, startNode = this.root) // Traverse\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/ecs.mdx",
    "content": "# ECS\n\n## Why ECS?\n\n:::warning ECS (Entity-Component-System)\nECS is suitable for decoupling large data objects, commonly used in games where each character (Entity) has extensive data that needs to be split into physics engine-related data, skin-related data, character attributes, etc. (multiple Components) for consumption by different subsystems (Systems). When workflow data structures are complex, ECS is very suitable for decomposition.\n:::\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/ecs.png\"/>\n\n## Solution Comparison\n\nLet's compare two data solutions:\n\n### 1. ReduxStore Solution\n\n```jsx pure\nconst store = () => ({\n  nodes: [{\n    position: any\n    form: any\n    data3: any\n\n  }],\n  edges: []\n})\n\nfunction Playground() {\n  const { nodes } = useStore(store)\n\n  return nodes.map(node => <Node data={node} />)\n}\n```\n\nAdvantages:\n- Simple to use with centralized data management\n\nDisadvantages:\n- Centralized data management cannot update precisely, leading to performance bottlenecks\n- Poor extensibility, adding new node data couples everything into one large JSON\n\n### 2. ECS Solution\n\nNotes:\n- NodeData corresponds to ECS - Component\n- Layer corresponds to ECS - System\n\n```jsx pure\n/**\n * Canvas document data\n */\nclass FlowDocument {\n  /**\n   * Node data definitions, node data will be instantiated when created\n   */\n  nodeDefines: [\n    NodePositionData,\n    NodeFormData,\n    NodeLineData\n  ]\n  nodeEntities: Entity[] = []\n}\n\n/**\n * Node\n */\nclass FlowNodeEntity {\n  id: string // Only has id, no data\n  getData: (dataId: string) => EntityData\n}\n\n// Render lines\nclass LinesLayer {\n  /**\n   * Internally gets corresponding data via node.getData(NodeLineData), same below\n   */\n  @observeEntityData(FlowNodeEntity, NodeLineData) lines: NodeLineData[]\n  render() {\n    // Render lines\n    return this.lines.map(line => <Line data={line} />)\n  }\n}\n\n// Render node positions\nclass NodePositionsLayer {\n  @observeEntityData(FlowNodeEntity, NodePositionData) positions: NodePositionData[]\n  render() {\n    // Render positions and layout\n  }\n}\n\n// Render node forms\nclass NodeFormsLayer {\n  @observeEntityData(FlowNodeEntity, NodeFormData) contents: NodeFormData[]\n  render() {\n    // Render node content\n  }\n}\n\n/**\n * Canvas instance, renders in layers via Layer\n */\nclass Playground {\n  layers: [\n    LinesLayer, // Line rendering\n    NodePositionsLayer, // Position rendering\n    NodeFormsLayer // Content rendering\n  ],\n  render() {\n    // Canvas layer rendering\n    return this.layers.map(layer => layer.render())\n  }\n}\n```\n\nAdvantages:\n- Node data is split for individual rendering control, enabling precise performance updates\n- Strong extensibility - adding new node data just requires adding a new XXXData + XXXLayer\n\nDisadvantages:\n- Has a certain learning curve\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/index.mdx",
    "content": "# 概念\n\n![FlowGramAI Architecture](@/public/canvas-engine.png)\n\n- CanvasEngine: Canvas engine is responsible for drawing the \"point-line\" diagram, ensuring the smoothness of large-scale nodes\n- NodeEngine: Node engine provides rendering, verification, data modification, etc. form capabilities\n- VariableEngine: Variable engine introduces a scope model, abstracts variables in various business scenarios\n- Material: Material library includes default ICONs, etc. UI, and can be extended after business access\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/ioc.mdx",
    "content": "# IOC\n\n## Why is IOC needed?\n\n:::warning Several concepts\n\n- Inversion of Control: Inversion of Control, a design principle in object - oriented programming, can be used to reduce the coupling between code modules. The most common way is called Dependency Injection (DI for short).\n- Domain Logic: Domain Logic, also known as Business Logic, is related to specific product features.\n- Aspect - Oriented Programming: AOP (Aspect - Oriented Programming). Its core design principle is to split the software system into multiple aspects (Aspect) of common logic (cross - cutting, with the meaning of penetration) and domain logic (vertical cutting). The cross - cutting part can be \"consumed on demand\" by all vertical - cutting parts.\n\n:::\n\nBefore answering this question, let's first understand aspect - oriented programming. The purpose of aspect - oriented programming is to split the granularity of domain logic into smaller parts. The cross - cutting part can be \"consumed on demand\" by the vertical - cutting part. The connection between the cross - cutting and vertical - cutting parts is also called weaving. And IOC plays the role of weaving and is injected into the vertical - cutting part.\n\n![Aspect - Oriented Programming](@/public/en-weaving.png)\n\nIdeal Aspect - Oriented Programming\n\n```ts\n- myAppliation provides business logic\n  - service Specific business logic services\n     - customDomainLogicService\n  - contributionImplement Instantiation of hook registrations\n    - MyApplicationContributionImpl\n  - component Business components\n\n- core provides common logic\n  - model Common models\n  - contribution Hook interfaces\n     - LifecycleContribution Application lifecycle\n     - CommandContribution\n  - service Common service services\n     - CommandService\n     - ClipboardService\n  - component Common components\n  ```\n\n  ```ts\n  // IOC injection\n@injectable()\nexport class CustomDomainLogicService {\n  @inject(FlowContextService) protected flowContextService: FlowContextService;\n  @inject(CommandService) protected commandService: CommandService;\n  @inject(SelectionService) protected selectionService: SelectionService;\n}\n// IOC interface declaration\ninterface LifecycleContribution {\n   onInit(): void\n   onStart(): void\n   onDispose(): void\n}\n// IOC interface implementation\n@injectable()\nexport class MyApplicationContributionImpl implements LifecycleContribution {\n    onStart(): void {\n      // Specific business logic code\n    }\n}\n\n// Manually attach to the lifecycle hook\nbind(LifecycleContribution).toService(MyApplicationContributionImpl)\n```\n\n\n:::warning IOC is an aspect-oriented programming technique, after the introduction, the underlying module can be exposed to the interface in the form of the external registration, which brings the following benefits:\n- Implement a micro - kernel + plug - in design to achieve plug - and - play and on - demand consumption of plug - ins.\n- Allow the package to be split more cleanly and achieve feature - based package splitting.\n\n:::\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/node-engine.mdx",
    "content": "# Node Engine\n\nThe Node Engine is a framework for writing the logic of process nodes. It allows businesses to focus on their own rendering and data logic without having to worry about the underlying APIs of the canvas and the interaction between nodes. At the same time, the Node Engine has precipitated the best practices for writing nodes, which helps businesses solve various problems that may arise in process - related business, such as the coupling of data logic and rendering.\n\nThe Node Engine is optional. If you don't have the following complex node logic, you can choose not to enable the Node Engine and maintain node data and rendering on your own. Examples of complex node logic include: 1) The ability to validate or trigger data side - effects even when nodes are not rendered; 2) Rich interaction between nodes; 3) Redo/undo functionality; and so on.\n\n## Basic Concepts\n\n### FlowNodeEntity\nThe process node model.\n\n### FlowNodeRegistry\nThe static configuration of process nodes.\n\n### FormMeta\nThe static configuration of the Node Engine. It is configured in the `formMeta` field of the `FlowNodeRegistry`.\n\n### Form\nThe form in the Node Engine. It maintains the data of nodes and provides capabilities such as rendering, validation, and side - effects. Its model, `FormModel`, provides the ability to access and modify node data and trigger validations.\n\n### Field\nA rendering field in the node form. Note that the `Form` has already provided the data - layer logic, and the `Field` is more of a rendering - layer model. It only exists after the form field is rendered.\n\n### validate\nForm validation. Usually, there is validation for individual fields as well as overall form validation.\n\n### effect\nSide - effects of form data. Usually, it refers to triggering specific logic when certain events occur to the form data. For example, synchronizing some information to a certain store when the data of a certain field changes can be called an effect.\n\n### FormPlugin\nForm plugins. They can be configured in the `formMeta`. Plugins can perform a series of in - depth operations on the form, such as variable plugins.\n"
  },
  {
    "path": "apps/docs/src/en/guide/concepts/reactflow.mdx",
    "content": "# Comparison with ReactFlow\n\n[Reactflow](https://reactflow.dev/) is an excellent open-source project with clear architecture and code. However, it focuses on low-level flow rendering engine architecture (Node, Edge, Handle), requiring extensive development at the upper layer to adapt to complex scenarios (such as fixed layouts, which need data modeling and layout algorithms). Advanced features are paid.\n\nCompared to ReactFlow, FlowGram aims to provide a complete out-of-the-box solution for flow editing.\n\n- Below are the pro paid features officially provided by ReactFlow\n\n| Paid Features                    | Supported by FlowGram | Future Plan |\n|----------------------------------|------------------------|--------------|\n| Grouping                         | Supported             |              |\n| redo/undo                        | Supported             |              |\n| copy/paste                       | Supported             |              |\n| HelpLines                        | Supported             |              |\n| Custom nodes and shapes          | Supported             |              |\n| Custom edges                     | Supported             |              |\n| AutoLayout                       | Supported             |              |\n| ForceLayout                      | Not Supported         | No           |\n| Expand/Collapse                  | Supported             |              |\n| Collaborative                    | Not Supported         | Yes          |\n| WorkflowBuilder (Fixed Layout Example) | Supported             |              |\n\n- ReactFlow events are bound to atomized DOM nodes and built-in, making interaction customization costly. Deep development requires understanding its source code. For example, it's difficult to select points when the canvas is zoomed out:\n\n<table>\n  <tr>\n    <td>\n      <div className=\"rs-tip\">Since events are bound to SVG, it's difficult to click on elements when zoomed out</div>\n      <img loading=\"lazy\" src=\"/reactflow/reactflow-render.gif\"/>\n    </td>\n    <td>\n      <div className=\"rs-tip\">FlowGram's events use global mousemove monitoring and calculate positions with Threshold, allowing clicks even when zoomed out, while also supporting edge reconnection</div>\n      <img loading=\"lazy\" src=\"/reactflow/reactflow-interaction.gif\"/>\n    </td>\n  </tr>\n</table>\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/contact-us.mdx",
    "content": "# Contact US\n\n- Issues: [Issues](https://github.com/bytedance/flowgram.ai/issues)\n- Discord: https://discord.gg/SwDWdrgA9f\n"
  },
  {
    "path": "apps/docs/src/en/guide/contributing.mdx",
    "content": "# Contribution Guide\n\nThis document helps you quickly develop, test, and submit PRs in this repository. The repository uses a Monorepo managed by Rush + pnpm, and the documentation site is built with Rspress.\n\n## Setting Up the Development Environment\n\n1.  **Install Node.js 18+** (LTS/Hydrogen recommended)\n\n    ```bash\n    nvm install lts/hydrogen\n    nvm alias default lts/hydrogen # Set as the default Node version\n    nvm use lts/hydrogen\n    ```\n\n2.  **Clone the repository locally**\n\n    ```bash\n    git clone git@github.com:bytedance/flowgram.ai.git\n    ```\n\n3.  **Install global dependencies**\n\n    ```bash\n    npm i -g pnpm@10.6.5 @microsoft/rush@5.150.0\n    ```\n\n4.  **Install project dependencies**\n\n    ```bash\n    rush install\n    ```\n\n5.  **Build the project**\n\n    ```bash\n    rush build\n    ```\n\n6.  **Run the documentation or examples**\n\n    ```bash\n    rush dev:docs                  # Start the documentation site in apps/docs (with incremental build)\n    rush dev:demo-fixed-layout     # Run the fixed layout example\n    rush dev:demo-free-layout      # Run the free layout example\n    ```\n\n## Common Commands (Rush Custom)\n\n```bash\nrush build            # Build all packages\nrush build:watch      # Incrementally build and watch\nrush lint             # Run ESLint checks\nrush lint:fix         # Automatically fix ESLint issues\nrush ts-check         # TypeScript type checking\nrush test             # Run test scripts for each package (aggregated by package)\nrush e2e:test         # Run all e2e tests\nrush e2e:update-screenshot # Update e2e snapshots\nrush dep-check        # Automatically check dependency health\n```\n\n## Branch and Commit Conventions\n\n-   Branch naming:\n    -   `feat/description` (new feature)\n    -   `fix/description` (bug fix)\n    -   `docs/description` (documentation changes)\n    -   `chore/description` (maintenance/miscellaneous)\n-   Commit messages (Conventional Commits):\n    -   Format: `type(scope): subject`, for example:\n\n        ```text\n        feat(editor): Support batch alignment of nodes\n        ```\n\n    -   Common types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`\n    -   The repository has commitlint validation enabled (commit-msg hook), so commit messages will be automatically checked; pre-commit will also run lint-staged (automatically update license headers, fix eslint issues) and `rush check`.\n\n## Development and Quality Assurance\n\n-   Local development recommendations:\n    -   First, run `rush build:watch`, then run the development command in the corresponding demo or docs directory (e.g., `rush dev:docs`).\n    -   After modifying the code, ensure it passes: `rush lint`, `rush ts-check`, `rush build`, `rush test`.\n-   Testing instructions:\n    -   e2e test cases are located in the `e2e/` directory and can be run with `rush e2e:test`, or you can update snapshots with `rush e2e:update-screenshot`.\n\n## Pull Request Process\n\n1.  Create your working branch from `main` (following the branch naming conventions).\n2.  Code and add tests/documentation.\n3.  Pass local quality checks (lint, ts-check, build, test).\n4.  Submit a PR: fill in the description, link the Issue, and use the template.\n5.  Review and CI: Maintainers will review the code, and it can be merged after the CI passes.\n\n## Documentation Contribution\n\n-   Documentation location: `apps/docs/src/zh/**` (Chinese) and `apps/docs/src/en/**` (English).\n-   Local preview: Run `rush dev:docs` to start the Rspress documentation site.\n-   To automatically generate API documentation, you can run `rushx docs` in the `apps/docs` directory (which calls a script to generate it).\n\n## Common Issues\n\n-   pnpm-lock merge conflicts: The repository has a merge strategy configured in the `post-checkout` hook, which usually avoids lock file conflicts.\n-   Node version: Please ensure you are using Node 18+, otherwise you may encounter dependency or build failures.\n\n## Reporting Issues\n\n-   Please submit an Issue on GitHub: https://github.com/bytedance/flowgram.ai/issues/new/choose\n    -   Describe the problem, reproduction steps, expected and actual behavior, and provide a code example if necessary.\n\n## License\n\n-   This project is licensed under the MIT License. By submitting code, you agree to the relevant terms."
  },
  {
    "path": "apps/docs/src/en/guide/fixed-layout/_meta.json",
    "content": "[\n  \"load\",\n  \"node\",\n  \"composite-nodes\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/fixed-layout/composite-nodes.mdx",
    "content": "# Composite nodes\n\nComposite nodes are composed of multiple nodes and support custom lines, such as condition, loop, and TryCatch.\n\n## Usage\n\n```ts pure title=\"node-registries.ts\"\n\nimport { FlowNodeRegistry  } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Node registration\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    type: 'yourCustomNodeType',\n    extend: 'dynamicSplit',\n  },\n];\n\n```\n\n## Built-in composite nodes\n\n<div className=\"rs-tip\">\n  <a className=\"rs-link\" target=\"_blank\" href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/canvas-engine/fixed-layout-core/src/activities\">\n    Source Code\n  </a>\n</div>\n\nimport { CompositeNodesPreview } from '../../../../components';\n\n<CompositeNodesPreview />\n"
  },
  {
    "path": "apps/docs/src/en/guide/fixed-layout/load.mdx",
    "content": "# Loading and Saving\n\nCanvas data is stored through [FlowDocument](/api/core/flow-document.html)\n\n## Canvas Data Format\n\nCanvas document data uses a tree structure that supports nesting\n\n:::note Document Data Basic Structure:\n\n- nodes `array` Node list, supports nesting\n\n:::\n\n:::note Node Data Basic Structure:\n\n- id: `string` Node unique identifier, must be unique\n- meta: `object` Node UI configuration information, such as free layout `position` information is stored here\n- type: `string | number` Node type, corresponds to the `type` in `nodeRegistries`\n- data: `object` Node form data\n- blocks: `array` Node branches, using `block` is closer to `Gramming`\n\n:::\n\n```tsx pure title=\"initial-data.tsx\"\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Configure flow data, data is in blocks nested format\n */\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    // Start node\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content'\n      },\n      blocks: [],\n    },\n    // Condition node\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: 'Condition'\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n            content: 'branch 1 content'\n          },\n          blocks: [\n            {\n              id: 'custom_0',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custrom content'\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n            content: 'branch 1 content'\n          },\n          blocks: [],\n        },\n      ],\n    },\n    // End node\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content'\n      },\n    },\n  ],\n};\n```\n\n## Loading\n\n- Loading through initialData\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App({ data }) {\n  return (\n    <FixedLayoutEditorProvider initialData={data} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n- Dynamic loading through ref\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    const data = await request('https://xxxx/getJSON')\n    ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // Trigger canvas fitview after loading to center nodes automatically\n      ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));\n    }, 100)\n  }, [])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n- Dynamically reload data\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App({ data }) {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    // Reload canvas data when data changes\n    await ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // Trigger canvas fitview after loading to center nodes automatically\n      ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));\n    }, 100)\n  }, [data])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n## Monitor Changes and Auto-save\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\nimport { debounce } from 'lodash-es'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    // Monitor canvas changes, delay 1 second to save data, avoid frequent canvas updates\n    const toDispose = ref.current.history.onApply(debounce(() => {\n        // Get the latest canvas data through toJSON\n        request('https://xxxx/save', {\n          data: ref.current.document.toJSON()\n        })\n    }, 1000))\n    return () => toDispose.dispose()\n  }, [])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n"
  },
  {
    "path": "apps/docs/src/en/guide/fixed-layout/node.mdx",
    "content": "# Nodes\n\nNodes are defined through [FlowNodeEntity](/api/core/flow-node-entity.html)\n\n## Node Core API\n\n- id: `string` Node id\n- flowNodeType: `string` | `number` Node type\n- bounds: `Rectangle` Get the node's x, y, width, height, equivalent to `transform.bounds`\n- blocks: `FlowNodeEntity[]` Get child nodes, including collapsed child nodes, equivalent to `collapsedChildren`\n- collapsedChildren: `FlowNodeEntity[]` Get child nodes, including collapsed child nodes\n- allCollapsedChildren: `FlowNodeEntity[]` Get all child nodes, including all collapsed child nodes\n- children: `FlowNodeEntity[]` Get child nodes, not including collapsed child nodes\n- pre: `FlowNodeEntity | undefined` Get the previous node\n- next: `FlowNodeEntity | undefined` Get the next node\n- parent: `FlowNodeEntity | undefined` Get the parent node\n- originParent: `FlowNodeEntity | undefined` Get the original parent node, this is used to find the entire virtual branch for the first node of the fixed layout branch (orderIcon)\n- allChildren: `FlowNodeEntity[]` Get all child nodes, not including collapsed child nodes\n- document: [FlowDocument](/api/core/flow-document.html) Document link\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) Get the node's transform data\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) Get the node's render data, including render status\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) Get the node's form data, like [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable public scope\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable private scope\n- getNodeRegistry(): [FlowNodeRegistry](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1) Get the node's registry\n- getService(): Get the [IOC](/guide/concepts/ioc.html) Service, such as`node.getService(HistoryService)`\n- getExtInfo(): Get the node's ext info, such as `node.getExtInfo<{ test: string }>()`\n- updateExtInfo(): Update the node's ext info, such as `node.updateExtInfo<{ test: string }>({ test: 'test' })`\n- dispose(): Destroy node\n- toJSON(): Get the node's json data\n\n## Node Data\n\nCan be obtained through `node.toJSON()`\n\n:::note Basic Structure:\n\n- id: `string` Node unique identifier, must be unique\n- meta: `object` Node UI configuration information, such as free layout `position` information is stored here\n- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`\n- data: `object` Node form data, can be customized by business\n- blocks: `array` Node branches, using `block` is closer to `Gramming`\n\n:::\n\n```ts pure\nconst nodeData: FlowNodeJSON = {\n  id: 'xxxx',\n  type: 'condition',\n  data: {\n    title: 'MyCondition',\n    desc: 'xxxxx'\n  },\n}\n```\n\n## Node Definition\n\nNode declaration can be used to determine node type and rendering method\n\n```tsx pure\nimport { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Custom node registration\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    /**\n     * Custom node type\n     */\n    type: 'condition',\n    /**\n     * Custom node extension:\n     *  - loop: Extend as loop node\n     *  - start: Extend as start node\n     *  - dynamicSplit: Extend as branch node\n     *  - end: Extend as end node\n     *  - tryCatch: Extend as tryCatch node\n     *  - default: Extend as normal node (default)\n     */\n    extend: 'dynamicSplit',\n    /**\n     * Node configuration information\n     */\n    meta: {\n      // isStart: false, // Whether it's a start node\n      // isNodeEnd: false, // Whether it's an end node, no nodes can be added after end node\n      // draggable: false, // Whether draggable, start and end nodes cannot be dragged\n      // selectable: false, // Triggers and start nodes cannot be box-selected\n      // deleteDisable: true, // Disable deletion\n      // copyDisable: true, // Disable copying\n      // addDisable: true, // Disable adding\n    },\n    /**\n     * Configure node form validation and rendering,\n     * Note: validate uses data and rendering separation to ensure nodes can validate data even without rendering\n     */\n    formMeta: {\n      validateTrigger: ValidateTrigger.onChange,\n      validate: {\n        title: ({ value }) => (value ? undefined : 'Title is required'),\n      },\n      /**\n       * Render form\n       */\n      render: () => (\n       <>\n          <Field name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field name=\"content\">\n            {({ field }) => <input onChange={field.onChange} value={field.value}/>}\n          </Field>\n        </>\n      )\n    },\n  },\n];\n```\n\n## Getting Current Rendered Node\n\nGet node-related methods through [useNodeRender](/api/hooks/use-node-render.html)\n\n```tsx pure\nfunction BaseNode() {\n  const { id, type, data, updateData, node } = useNodeRender()\n}\n```\n\n## Creating Nodes\n\nCreate through [FlowOperationService](/api/services/flow-operation-service.html)\n\n- Add node\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addNode({\n  id: 'xxx', // Must be unique within canvas\n  type: 'custom',\n  meta: {},\n  data: {}, // Form-related data\n  blocks: [], // Child nodes\n  parent: someParent // Parent node, used for branches\n})\n```\n\n- Add after specified node\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addFromNode(targetNode, {\n  id: 'xxx', // Must be unique within canvas\n  type: 'custom',\n  meta: {},\n  data: {}, // Form-related data\n  blocks: [], // Child nodes\n})\n```\n\n- Add branch node (used for conditional branches)\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addBlock(parentNode, {\n  id: 'xxx', // Must be unique within canvas\n  type: 'block',\n  meta: {},\n  data: {}, // Form-related data\n  blocks: [], // Child nodes\n})\n```\n\n## Deleting Nodes\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const ctx = useClientContext()\n  function onClick() {\n    ctx.operation.deleteNode(node)\n  }\n  return (\n    <button onClick={onClick}>Delete</button>\n  )\n}\n```\n\n## Updating Node Data\n\n- Get node data through [useNodeRender](/api/hooks/use-node-render.html) or [node.form](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html)\n\n```tsx pure\nfunction BaseNode() {\n  const { form, node } = useNodeRender();\n  // Corresponds to node's data\n  // 1. form.values: Corresponds to node's data\n  // 2. form.setValueIn('title', 'xxxx'): Update data.title\n  // 3. form.getValueIn('title'): Get data.title\n  // 4. form.updateFormValues({ ... }) Update all form values\n\n  function onChange(e) {\n    form.setValueIn('title', e.target.value)\n  }\n  return <input value={form.values.title} onChange={onChange}/>\n}\n\n```\n\n- Update form data through Field, see [Form Usage](/guide/form/form.html) for details\n\n```tsx pure\nfunction FormRender() {\n  return (\n    <Field name=\"title\">\n      <Input />\n    </Field>\n  )\n}\n```\n\n## Updating Node ExtInfo Data\n\nExtInfo is used to store some UI states. If node engine is not enabled, node data will be stored in extInfo by default\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const times = node.getExtInfo()?.times || 0\n  function onClick() {\n    node.updateExtInfo({ times: times ++ })\n  }\n  return (\n    <div>\n      <span>Click Times: {times}</span>\n      <button onClick={onClick}>Click</button>\n    </div>\n  )\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/form/_meta.json",
    "content": "[\n  \"form\",\n  \"without-form\",\n  \"form-materials\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/form/form-materials.mdx",
    "content": "# Official Form Materials\n\nTransferred to [Introduction](/en/materials/introduction)\n"
  },
  {
    "path": "apps/docs/src/en/guide/form/form.mdx",
    "content": "# Node Form\n\n## Terminology\n\n<table className=\"rs-table\">\n  <tr>\n    <td>Node Form</td>\n    <td>Specifically refers to forms within flow nodes or forms that expand when clicking nodes, associated with node data.</td>\n  </tr>\n  <tr>\n    <td>Node Engine</td>\n    <td>One of FlowGram.ai's built-in engines, which primarily maintains node data CRUD operations and provides capabilities for rendering, validation, side effects, canvas/variable linkage, etc. Additionally, it provides capabilities for node error capture rendering, placeholder rendering when there's no content, as shown in the following chapter examples.</td>\n  </tr>\n</table>\n\n## Quick Start\n\n### Enable Node Engine\n\n[> API Detail](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/editor/src/preset/editor-props.ts#L54)\n\n```tsx pure title=\"use-editor-props.ts\" {3}\n\n// EditorProps\n{\n  nodeEngine: {\n    /**\n     * Node engine must be enabled to use\n     */\n    enable: true;\n    materials: {\n      /**\n       * Component for rendering node errors\n       */\n      nodeErrorRender?: NodeErrorRender;\n      /**\n       * Component for rendering when node has no content\n       */\n      nodePlaceholderRender?: NodePlaceholderRender;\n    }\n  }\n}\n```\n\n### Configure Form\n\n`formMeta` is the only configuration entry point for node forms, configured on each node's NodeRegistry.\n\n[> node-registries.ts](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout-simple/src/node-registries.ts)\n\n```tsx pure title=\"node-registries.ts\"\nimport { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    type: 'start',\n    /**\n     * Configure form validation and rendering\n     */\n    formMeta: {\n      /**\n       * Configure validation to trigger on data change\n       */\n      validateTrigger: ValidateTrigger.onChange,\n      /**\n       * Configure validation rules, 'content' is the field path, the following configuration values validate data under this path\n       * Use Dynamic function  to generate a validator based on values:\n       *  validate: (values, ctx) => ({ content: () => {}, })\n       */\n      validate: {\n        content: ({ value }) => (value ? undefined : 'Content is required'),\n      },\n      /**\n       * Configure form rendering\n       */\n      render: () => (\n       <>\n          <Field<string> name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field<string> name=\"content\">\n            {({ field, fieldState }) => (\n              <>\n                <input onChange={field.onChange} value={field.value}/>\n                {fieldState?.invalid && <Feedback errors={fieldState?.errors}/>}\n              </>\n            )}\n          </Field>\n        </>\n      )\n    },\n  }\n]\n\n```\n\n[> Basic form example](/examples/node-form/basic.html)\n\n### Render Form\n\n[> base-node.tsx](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout-simple/src/components/base-node.tsx)\n\n```tsx pure title=\"base-node.tsx\"\n\nexport const BaseNode = () => {\n  /**\n   * Provides node rendering related methods\n   */\n  const { form } = useNodeRender()\n  return (\n    <div className=\"demo-free-node\" className={form?.state.invalid && \"error\"}>\n      {\n        // Form rendering is generated through formMeta\n        form?.render()\n      }\n    </div>\n  )\n};\n\n```\n\n## Core Concepts\n\n### FormMeta\n\nIn `NodeRegistry`, we configure node forms through `formMeta`, which follows the following API.\n\n[> FormMeta API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/node/src/types.ts#L89)\n\nIt's important to note that node forms differ significantly from general forms in that their data logic (such as validation, side effects after data changes, etc.) needs to remain effective even when the form is not rendered - we call this <span className=\"rs-red\">separation of data and rendering</span>. Therefore, this data logic needs to be configured in non-render fields within formMeta, ensuring the node engine can call these logics even when not rendering. General form engines (like react-hook-form) don't have this restriction, and validation can be written directly in React components.\n\n### FormMeta.render (Rendering)\nThe `render` field is used to configure form rendering logic\n\n`render: (props: FormRenderProps<any>) => React.ReactElement;`\n\n[> FormRenderProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/form.ts#L91)\n\nThe returned React component can use the following form components and models:\n\n#### Field (Component)\n\n`Field` is a React higher-order component for form fields, encapsulating common form field logic such as data and state injection, component refresh, etc. Its core required parameter is `name`, used to declare the form item's path, which must be unique within a form.\n\n[> Field Props API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L106)\n\nThe rendering part of Field supports three writing methods, as follows:\n\n```tsx pure\nconst render = () => (\n  <div>\n    <Label> 1. Through children </Label>\n    {/* This method is suitable for simple scenarios, Field will directly inject properties like value onChange into the first layer children component */}\n    <Field name=\"c\">\n      <Input />\n    </Field>\n    <Label> 2. Through Render Props </Label>\n    {/* This method is suitable for complex scenarios, when the returned component has multiple nested layers, users can actively inject field properties into desired components */}\n    <Field name=\"a\">\n        {({ field, fieldState, formState }: FieldRenderProps<string>) => <div><Input {...field} /><Feedbacks errors={fieldState.errors}/></div>}\n    </Field>\n\n    <Label> 3. Through passing render function</Label>\n    {/* This method is similar to method 2, but passed through props */}\n    <Field name=\"b\" render={({ field }: FieldRenderProps<string>) => <Input {...field} />} />\n  </div>\n);\n```\n\n```ts pure\ninterface FieldRenderProps<TValue> {\n  // Field instance\n  field: Field<TValue>;\n  // Field state (reactive)\n  fieldState: Readonly<FieldState>;\n  // Form state\n  formState: Readonly<FormState>;\n}\n```\n[> FieldRenderProps API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L125)\n\n#### Field (Model)\n\n`Field` instance is usually passed through render props (as in above example), or obtained through `useCurrentField` hook. It contains common APIs for form fields at the rendering level.\nNote: `Field` is a rendering model, only providing APIs that general components need, such as `value` `onChange` `onFocus` `onBlur`. For data-related APIs, please use the `Form` model instance, such as `form.setValueIn(name, value)` to set a field's value.\n\n[> Field Model API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L34)\n\n#### FieldArray (Component)\n\n`FieldArray` is a React higher-order component for array type fields, encapsulating common logic for array type fields, such as data and state injection, component refresh, and array item iteration. Its core required parameter is `name`, used to declare the form item's path, which must be unique within a form.\n\nBasic usage of `FieldArray` can be found in the following example:\n\n[> Array example](/examples/node-form/array.html)\n\n#### FieldArray (Model)\n\n`FieldArray` inherits from `Field`, it's the rendering level model for array type fields. Besides common rendering level APIs, it also includes basic array operations like `FieldArray.map`, `FieldArray.remove`, `FieldArray.append`, etc. API usage can also be found in the above [array example](/examples/node-form/array.html).\n\n[> FieldArray Model API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L69)\n\n#### Form (Component)\n\n`Form` component is the outermost higher-order component for forms. The above capabilities like `Field` `FieldArray` can only be used under this higher-order component. Node form rendering has already encapsulated `<Form />` inside the engine, so users don't need to care about it and can directly use `Field` in the React component returned by `render`. However, if users need to use the form engine independently or render a form independently outside the node, they need to wrap the form content with the `Form` component themselves.\n\n#### Form (Model)\n\n`Form` instance can be obtained through the input parameters of the `render` function, or through the `useForm` hook, see [example](#useform). It is the core model facade of the form, through which users can manipulate form data, listen to changes, trigger validation, etc.\n\n[> Form Model API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/form.ts#L58)\n\n### Validation\nBased on the \"separation of data and rendering\" concept mentioned in the [FormMeta](#formmeta) section, validation logic needs to be configured globally in `FormMeta`, and declared through path matching to act on form items, as shown in the following example.\n\nPaths support fuzzy matching, see [Paths](#paths) section.\n\n<div className=\"rs-center\" >\n  <img loading=\"lazy\" src=\"/form-validate.gif\"  style={{ maxWidth: 600 }}/>\n</div>\n\n```tsx pure\nexport const renderValidateExample = ({ form }: FormRenderProps<FormData>) => (\n  <>\n    <Label> a (max length is 5)</Label>\n    <Field name=\"a\">\n      {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n        <>\n          <Input value={value} onChange={onChange} />\n          <Feedback errors={fieldState?.errors} />\n        </>\n      )}\n    </Field>\n    <Label> b (if a exists, b can be optional) </Label>\n    <Field\n      name=\"b\"\n      render={({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n        <>\n          <Input value={value} onChange={onChange} />\n          <Feedback errors={fieldState?.errors} />\n        </>\n  )}\n/>\n  </>\n);\n\nexport const VALIDATE_EXAMPLE: FormMeta = {\n  render: renderValidateExample,\n  // Validation timing configuration\n  validateTrigger: ValidateTrigger.onChange,\n  /*\n   * Use Dynamic function to generate a validator based on values:\n   *  validate: (values, ctx) => ({ a: () => '', b: () => '', c, () => '' })\n  */\n  validate: {\n    // Simply validate value\n    a: ({ value }) => (value.length > 5 ? 'Max length is 5' : undefined),\n    // Validation depends on other form item values\n    b: ({ value, formValues }) => {\n      if (formValues.a) {\n        return undefined;\n      } else {\n        return value ? 'undefined' : 'b is required when a exists';\n      }\n    },\n    // Validation depends on node or canvas information\n    c: ({ value, formValues, context }) => {\n      const { node， playgroundContext } = context;\n      // Logic omitted here\n    },\n  },\n};\n```\n\n#### Validation Timing\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`ValidateTrigger.onChange`</td>\n    <td>Validate when form data changes (not including initialization data)</td>\n  </tr>\n  <tr>\n    <td>`ValidateTrigger.onBlur`</td>\n    <td>Validate when form item input control onBlur.<br/>Note, there are two prerequisites: first, the form item's input control needs to support the `onBlur` parameter, second, `Field.onBlur` needs to be passed to that control: <br/>```<Field>{({field})=><Input ... onBlur={field.onBlur}>}</Field>```</td>\n  </tr>\n</table>\n\nIt's recommended to configure `validateTrigger` as `ValidateTrigger.onChange` i.e., validate when data changes. If configured as `ValidateTrigger.onBlur`, validation will only trigger when the component blur event triggers. When the node form is not rendering, even if the data changes, validation won't trigger.\n\n#### Actively Trigger Validation\n\n1. Actively trigger validation for the entire form\n\n```tsx pure\nconst form = useForm()\nform.validate()\n```\n\n2. Actively trigger validation for a single form item\n\n```tsx pure\nconst validate = useFieldValidate(name)\nvalidate()\n```\nIf `name` is not passed, it defaults to getting the `validate` of the `Field` under the current `<Field />` tag. By passing `name`, you can get any `Field`'s validate under `<Form />`.\n\n### Paths\n\n1. Form paths use `.` as level separator, e.g., `a.b.c` points to `1` under data `{a:{b:{c:1}}}`\n2. Paths support fuzzy matching, used in validation and side effect configuration. As shown in the following example. Usually used more in array scenarios.\n\n<div className=\"rs-red\">\n  Note: * only represents drilling down one level\n</div>\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`arr.*`</td>\n    <td>All first-level sub-items of `arr` field</td>\n  </tr>\n  <tr>\n    <td>`arr.x.*`</td>\n    <td>All first-level sub-items of `arr.x`</td>\n  </tr>\n  <tr>\n    <td>`arr.*.x`</td>\n    <td>`x` under all first-level sub-items of `arr`</td>\n  </tr>\n</table>\n\n### Side Effects (effect)\n\nSide effects are a concept unique to node forms, referring to side effects that need to be executed when node data changes. Similarly, following the principle of \"separation of data and rendering\", side effects, like validation, are also configured globally in `FormMeta`.\n- Configured in key-value form, where key represents form item path matching rules (supports fuzzy matching) and value is the effect acting on that path.\n- Value is an array, meaning one form item can have multiple effects.\n\n```tsx pur\n\nexport const EFFECT_EXAMPLE: FormMeta = {\n  ...\n  effect: {\n    ['a.b']: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value }: EffectFuncProps<string, FormData>) => {\n          console.log('a.b value changed:', value);\n        },\n      },\n    ],\n    ['arr.*']:[\n      {\n        event: DataEvent.onValueInit,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value init:', value);\n        },\n      },\n    ]\n  }\n};\n```\n\n```tsx pur\ninterface EffectFuncProps<TFieldValue = any, TFormValues = any> {\n  name: FieldName;\n  value: TFieldValue;\n  prevValue?: TFieldValue;\n  formValues: TFormValues;\n  form: IForm;\n  context: NodeContext;\n}\n```\n\n[Effect Related API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/node/src/types.ts#L54)\n\n#### Side Effect Timing\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`DataEvent.onValueChange`</td>\n    <td>Triggered when data changes</td>\n  </tr>\n  <tr>\n    <td>`DataEvent.onValueInit`</td>\n    <td>Triggered when data initializes</td>\n  </tr>\n  <tr>\n    <td>`DataEvent.onValueInitOrChange`</td>\n    <td>Triggered both during data initialization and changes</td>\n  </tr>\n</table>\n\n### Dynamic Field\n\n[> Dynamic Field example](/examples/node-form/dynamic.html)\n\n## Hooks\n\n### Inside Node Form\nThe following hooks can be used inside node forms\n\n#### useCurrentField\n`() => Field`\n\nThis hook needs to be used inside Field tags\n\n```tsx pur\nconst field = useCurrentField()\n```\n[> Field Model API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L34)\n\n#### useCurrentFieldState\n`() => FieldState`\n\nThis hook needs to be used inside Field tags\n\n```tsx pur\nconst fieldState = useCurrentFieldState()\n```\n[> FieldState API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L158)\n\n#### useFieldValidate\n`(name?: FieldName) => () => Promise<void>`\n\nIf you need to actively trigger field validation, you can use this hook to get the Field's validate function.\n\n`name` is the Field's path, if not passed it defaults to getting the validate of the current `<Field />`\n\n```tsx pur\nconst validate = useFieldValidate()\nvalidate()\n```\n\n#### useForm\n`() => Form`\n\nUsed to get Form instance.\n\nNote, this hook doesn't work in the first layer of the `render` function, it can only be used inside React components within the `render` function. The `render` function's input parameters already include `form: Form`, which can be used directly.\n\n1. Directly use `props.form` in render function's first layer\n```tsx pur\nconst formMeta = {\n  render: ({form}) =>\n  <div>\n    {form.getValueIn('my.path')}\n  </div>\n}\n```\n\n2. Can use `useForm` inside components\n\n```tsx pur\n\nconst formMeta = {\n  render: () =>\n    <div>\n      <Field name={'my.path'}>\n        <MySelect />\n      </Field>\n    </div>\n}\n\n// MySelect.tsx\n...\nconst form = useForm()\nconst valueNeeded = form.getValueIn('my.other.path')\n...\n```\n\n<span className=\"rs-red\">Note: Form's api doesn't have any reactive capabilities, if you need to monitor a field's value, use [useWatch](#usewatch)</span>\n\n#### useWatch\n`<TValue = FieldValue>(name: FieldName) => TValue`\n\nThis hook is similar to the above `useForm`, it doesn't work in the first layer of the `render` function, only usable inside wrapped components. If you need to use it at the `render` root level, you can wrap the returned content in a component layer.\n\n```tsx pur\n{\n  render: () =>\n    <div>\n      <Field name={'a'}><A /></Field>\n      <Field name={'b'}><B /></Field>\n    </div>\n}\n\n// A.tsx\n...\nconst b = useWatch('b')\n// do something with b\n...\n```\n\n### Outside Node Form\nThe following hooks are used outside node forms, such as on the canvas globally or on adjacent nodes to monitor a node form's data or state. Usually needs to pass `node: FlowNodeEntity` as a parameter\n\n#### useWatchFormValues\nMonitor the values of the entire form inside the node\n\n`<TFormValues = any>(node: FlowNodeEntity) => TFormValues | undefined`\n\n```tsx pur\nconst values = useWatchFormValues(node)\n```\n\n#### useWatchFormValueIn\n\nMonitor the value of a specific form item inside the node\n\n`<TValue = any>(node: FlowNodeEntity，name: string) => TFormValues | undefined`\n\n```tsx pur\nconst value = useWatchFormValueIn(node, name)\n```\n\n#### useWatchFormState\n\nMonitor the form state inside the node\n\n`(node: FlowNodeEntity) => FormState | undefined`\n\n```tsx pur\nconst formState = useWatchFormState(node)\n```\n\n#### useWatchFormErrors\n\nMonitor the form Errors inside the node\n\n`(node: FlowNodeEntity) => Errors | undefined`\n\n```tsx pur\nconst errors = useWatchFormErrors(node)\n```\n\n#### useWatchFormWarnings\n\nMonitor the form Warnings inside the node\n\n`(node: FlowNodeEntity) => Warnings | undefined`\n\n```tsx pur\nconst warnings = useWatchFormErrors(node)\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/form/without-form.mdx",
    "content": "# Without Form\n\nWhen the node engine is disabled, the node's data will be stored in `node.getExtInfo`, as shown below\n\n```tsx pure\n\nexport const useEditorProps = () => {\n  return {\n    // ...\n    nodeEngine: {\n      enable: false, // Node engine disabled, form cannot be used\n    },\n    history: {\n      enable: true,\n      enableChangeNode: false // No longer monitor form data changes\n    },\n    materials: {\n      /**\n       * Render Node\n       */\n      renderDefaultNode: ({ node }: WorkflowNodeProps) => {\n        return (\n          <WorkflowNodeRenderer className=\"demo-free-node\" node={node}>\n            <input value={node.getExtInfo()?.title} onChange={e => node.updateExtInfo({ title: e.target.value})}/>\n          </WorkflowNodeRenderer>\n        );\n      },\n    },\n    // /...\n  }\n}\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/_meta.json",
    "content": "[\n  \"load\",\n  \"node\",\n  \"line\",\n  \"port\",\n  \"sub-canvas\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/line.mdx",
    "content": "# Lines\n\n- [WorkflowLinesManager](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/workflow-lines-manager.ts) manages all lines\n- [WorkflowNodeLinesData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-lines-data.ts) manages lines connected to nodes\n- [WorkflowLineEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts) line entity\n\n## Get All Line Entities\n\n```ts pure\nconst allLines = ctx.document.linesManager.getAllLines() // line entities containing from/to representing connected nodes\n```\n\n## Create/Delete Lines\n\n```ts pure\n// from and to are the node IDs to connect, fromPort, toPort are port IDs, if node has single port can be omitted\nconst line = ctx.document.linesManager.createLine({ from, to, fromPort, toPort })\n\n// delete line\nline.dispose()\n```\n\n## Export Line Data\n\n:::note Basic line structure:\n\n- sourceNodeID: `string` source node id\n- targetNodeID: `string` target node id\n- sourcePortID?: `string | number` source port id, defaults to node's default port if omitted\n- targetPortID?: `string | number` target port id, defaults to node's default port if omitted\n\n:::\n```ts pure\nconst json = ctx.document.linesManager.toJSON()\n```\n\n## Get Input/Output Nodes or Lines for Current Node\n\n```ts pure\n// get input nodes (calculated through connection lines)\nnode.lines.inputNodes\n// get all input nodes (recursively gets all upstream nodes)\nnode.lines.allInputNodes\n// get output nodes\nnode.lines.outputNodes\n// get all output nodes\nnode.lines.allOutputNodes\n// input lines (contains the line that isDrawing or isHidden)\nnode.lines.inputLines\n// output lines (contains the line that isDrawing or isHidden)\nnode.lines.outputLines\n// all availableLines (Doesn't contain the lines that isDrawing or isHidden)\nnode.lines.availableLines\n```\n\n## Line Configuration\n\nWe provide rich line configuration parameters for `FreeLayoutEditorProvider`, see details in [FreeLayoutProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/preset/free-layout-props.ts)\n\n```tsx pure\ninterface FreeLayoutProps {\n    /**\n     * Line color configuration\n     */\n    lineColor?: LineColor;\n    /**\n     * Determine if line should be marked red\n     * @param ctx\n     * @param fromPort\n     * @param toPort\n     * @param lines\n     */\n    isErrorLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity | undefined, lines: WorkflowLinesManager) => boolean;\n    /**\n     * Determine if port should be marked red\n     * @param ctx\n     * @param port\n     */\n    isErrorPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n    /**\n     * Determine if port should be disabled\n     * @param ctx\n     * @param port\n     */\n    isDisabledPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n    /**\n     * Determine if line arrow should be reversed\n     * @param ctx\n     * @param line\n     */\n    isReverseLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * Determine if line arrow should be hidden\n     * @param ctx\n     * @param line\n     */\n    isHideArrowLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * Determine if line should show flowing effect\n     * @param ctx\n     * @param line\n     */\n    isFlowingLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * Determine if line should be disabled\n     * @param ctx\n     * @param line\n     */\n    isDisabledLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * Line drag end\n     * @param ctx\n     * @param params\n     */\n    onDragLineEnd?: (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => Promise<void>;\n    /**\n     * Set line renderer type\n     * @param ctx\n     * @param line\n     */\n    setLineRenderType?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => LineRenderType | undefined;\n    /**\n     * Set line style\n     * @param ctx\n     * @param line\n     */\n    setLineClassName?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => string | undefined;\n    /**\n     * Whether to allow line creation\n     * @param ctx\n     * @param fromPort - start point\n     * @param toPort - target point\n     */\n    canAddLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, lines: WorkflowLinesManager, silent?: boolean) => boolean;\n    /**\n     * Whether to allow line deletion\n     * @param ctx\n     * @param line - target line\n     * @param newLineInfo - new line info\n     * @param silent - if false, can show toast\n     */\n    canDeleteLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity, newLineInfo?: Required<WorkflowLinePortInfo>, silent?: boolean) => boolean;\n    /**\n     * Whether to allow line reset\n     * @param ctx\n     * @param oldLine - old line\n     * @param newLineInfo - new line info\n     * @param lines - line manager\n     */\n    canResetLine?: (ctx: FreeLayoutPluginContext, oldLine: WorkflowLineEntity, newLineInfo: WorkflowLinePortInfo, lines: WorkflowLinesManager ) => boolean;\n}\n```\n\n### 1. Line ui state configuration\n\n- update UI state\n\n```tsx pure\n/**\n *  more: https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts#L41\n */\nline.updateUIState({\n  lockedColor: 'blue',\n  strokeWidth: 2,\n  strokeWidthSelected: 3,\n  className: 'xxx',\n  style: {}\n})\n```\n\n- Different lines specify a specific color (highest priority)\n\n```tsx pure\n\nctx.document.linesManager.getAllLines().forEach(line => {\n  if (line.from.flowNodeType === 'start') {\n    line.lockedColor = 'blue'\n  } else if (line.to.flowNodeType === 'end') {\n    line.lockedColor = 'yellow'\n  }\n})\n\n```\n- Global color configuration\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      lineColor: {\n        hidden: 'transparent',\n        default: '#4d53e8',\n        drawing: '#5DD6E3',\n        hovered: '#37d0ff',\n        selected: '#37d0ff',\n        error: 'red',\n        flowing: '#ff6b35', // Color for flowing lines (e.g., during workflow execution)\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n### 2. Limit Single Output Port to One Line\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }} className=\"invert-img\" src=\"/free-layout/line-limit.gif\"/>\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      /*\n       * Check whether the line can be added\n       */\n      canAddLine(ctx, fromPort, toPort) {\n        // not the same node\n        if (fromPort.node === toPort.node) {\n          return false;\n        }\n        // control number of lines\n        if (fromPort.availableLines.length >= 1) {\n          return false\n        }\n        return true;\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n### 3. Connect to blank area to add node\n\nSee free layout best practices for code\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }}  className=\"invert-img\" src=\"/free-layout/line-add-panel.gif\"/>\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      /**\n       * Drag the end of the line to create an add panel (feature optional)\n       * 拖拽线条结束需要创建一个添加面板 （功能可选）\n       */\n      async onDragLineEnd(ctx, params) {\n        const { fromPort, toPort, mousePos, line, originLine } = params;\n        if (originLine || !line) {\n          return;\n        }\n        if (toPort) {\n          return;\n        }\n        // Here you can open the add panel based on mousePos\n        await ctx.get(WorkflowNodePanelService).call({\n          fromPort,\n          toPort: undefined,\n          panelPosition: mousePos,\n          enableBuildLine: true,\n          panelProps: {\n            enableNodePlaceholder: true,\n            enableScrollClose: true,\n          },\n        });\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n### 4. Custom Arrow Renderer\n\nYou can completely customize the line arrow styles by registering custom arrow renderers.\n\n```tsx pure\nimport {\n  FlowRendererKey,\n  type ArrowRendererProps,\n  useEditorProps\n} from '@flowgram.ai/free-layout-editor';\n\n// 1. Create custom arrow component\nfunction CustomArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide }: ArrowRendererProps) {\n  if (hide) return null;\n\n  const size = 8;\n  const rotation = reverseArrow\n    ? (vertical ? 270 : 180)\n    : (vertical ? 90 : 0);\n\n  return (\n    <g\n      id={id}\n      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}\n    >\n      <path\n        d={`M0,0 L${-size},-${size/2} L${-size},${size/2} Z`}\n        fill=\"currentColor\"\n        strokeWidth={strokeWidth}\n        stroke=\"currentColor\"\n      />\n    </g>\n  );\n}\n\n// 2. Register custom arrow in editor\nfunction App() {\n  const materials = {\n    components: {\n      [FlowRendererKey.ARROW_RENDERER]: CustomArrowRenderer,\n    },\n  };\n\n  const editorProps = useEditorProps({\n    materials,\n    // ...other configs\n  });\n\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  );\n}\n```\n\n**Advanced Usage**: Dynamically render different arrow styles based on line state:\n\n```tsx pure\nfunction AdvancedArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide, line }: ArrowRendererProps) {\n  if (hide) return null;\n\n  const size = 8;\n  const rotation = reverseArrow\n    ? (vertical ? 270 : 180)\n    : (vertical ? 90 : 0);\n\n  // Choose different arrow styles based on line state\n  let arrowPath: string;\n  let fillColor: string;\n\n  if (line?.hasError) {\n    // Error state: red exclamation arrow\n    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;\n    fillColor = '#ff4d4f';\n  } else if (line?.processing) {\n    // Processing state: blue circular arrow\n    arrowPath = `M0,0 m-${size/2},0 a${size/2},${size/2} 0 1,0 ${size},0 a${size/2},${size/2} 0 1,0 -${size},0`;\n    fillColor = '#1890ff';\n  } else {\n    // Default state: standard triangle arrow\n    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;\n    fillColor = 'currentColor';\n  }\n\n  return (\n    <g\n      id={id}\n      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}\n    >\n      <path\n        d={arrowPath}\n        fill={fillColor}\n        strokeWidth={strokeWidth}\n        stroke={fillColor}\n      />\n    </g>\n  );\n}\n```\n\n**ArrowRendererProps Interface**:\n\n```ts pure\ninterface ArrowRendererProps {\n  id: string;                    // Arrow unique identifier\n  pos: { x: number; y: number }; // Arrow position\n  reverseArrow: boolean;         // Whether to reverse arrow direction\n  strokeWidth: number;           // Line thickness\n  vertical: boolean;             // Whether it's a vertical line\n  hide: boolean;                 // Whether to hide the arrow\n  line?: WorkflowLineEntity;     // Line entity (can be used to get state)\n}\n```\n\n## Add Label to Line\n\nSee code in free layout best practices\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }}  className=\"invert-img\" src=\"/free-layout/line-add-button.gif\"/>\n\n```ts pure\n\nimport { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';\n\nconst editorProps = {\n  plugins: () => [\n      /**\n       * Line render plugin\n       */\n      createFreeLinesPlugin({\n        renderInsideLine: LineAddButton,\n      }),\n  ]\n}\n```\n\n## Node Listening to Its Own Line Changes and Refresh\n\n```tsx pure\n\nimport {\n  useRefresh,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-editor';\n\nfunction NodeRender({ node }) {\n  const refresh = useRefresh()\n  const linesData = node.get(WorkflowNodeLinesData)\n  useEffect(() => {\n    const dispose = linesData.onDataChange(() => refresh())\n    return () => dispose.dispose()\n  }, [])\n  return <div>xxxx</div>\n}\n```\n\n## Listen to All Line Connection Changes\n\nThis scenario is used when you want to monitor line connections in external components\n\n```ts pure\nimport { useEffect } from 'react'\nimport { WorkflowLinesManager, useRefresh } from '@flowgram.ai/free-layout-editor'\n\n\nfunction SomeReact() {\n  const refresh = useRefresh()\n  const linesManager = useService(WorkflowLinesManager)\n  useEffect(() => {\n      const dispose = linesManager.onAvailableLinesChange(() => refresh())\n      return () => dispose.dispose()\n  }, [])\n  console.log(ctx.document.linesManager.getAllLines())\n}\n```\n\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/load.mdx",
    "content": "# Loading and Saving\n\nCanvas data is stored through [WorkflowDocument](/api/core/workflow-document.html)\n\n## Canvas Data\n\n:::note Basic Document Structure:\n\n- nodes `array` List of nodes, supports nesting\n- edges `array` List of edges\n\n:::\n\n:::note Basic Node Structure:\n\n- id: `string` Unique node identifier, must be unique\n- meta: `object` Node UI configuration information, such as `position` information for free layout\n- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`\n- data: `object` Node form data, customizable by business\n- blocks: `array` Node branches, using `block` is closer to `Gramming`, currently stores nodes of sub-canvas\n- edges: `array` Edge data of sub-canvas\n\n:::\n\n:::note Basic Edge Structure:\n\n- sourceNodeID: `string` Starting node id\n- targetNodeID: `string` Target node id\n- sourcePortID?: `string | number` Starting port id, defaults to the default port of the starting node if omitted\n- targetPortID?: `string | number` Target port id, defaults to the default port of the target node if omitted\n\n:::\n\n```tsx pure title=\"initial-data.ts\"\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content'\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom',\n        content: 'Custom node content'\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End',\n        content: 'End content'\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n```\n\n## Loading\n\n- Load through initialData\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App({ data }) {\n  return (\n    <FreeLayoutEditorProvider initialData={data} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n- Dynamic loading through ref\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    const data = await request('https://xxxx/getJSON')\n    ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // Trigger canvas fitview after loading to center nodes automatically\n      ref.current.document.fitView()\n    }, 100)\n  }, [])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n- Dynamic reload of all data (will produce redo/undo/onContentChange)\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App({ data }) {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n   // ctx.operation supports diff reloading of canvas data and can be undone via undo\n    ref.current.operation.fromJSON(data)\n    setTimeout(() => {\n      // Trigger canvas fitview after loading to center nodes automatically\n      ref.current.document.fitView()\n    }, 10)\n  }, [data])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n- Batch add nodes and lines\n\n```tsx pure\nctx.document.batchAddFromJSON({\n  nodes: [...],\n  edges: [...]\n}, {\n  // Optionally, use this if you want to add to a subcanvas\n  parent: loopNode\n})\n```\n\n## Listen for Changes and Auto-Save\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\nimport { debounce } from 'lodash-es'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    // Listen for canvas changes and save data after 1 second delay to avoid frequent updates\n    const toDispose = ref.current.document.onContentChange(debounce(() => {\n        // Get the latest canvas data through toJSON\n        request('https://xxxx/save', {\n          data: ref.current.document.toJSON()\n        })\n    }, 1000))\n    return () => toDispose.dispose()\n  }, [])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/node.mdx",
    "content": "# Nodes\n\nNodes are defined through [FlowNodeEntity](/api/core/flow-node-entity.html)\n\n## Node Core API\n\n- id: `string` Node id\n- flowNodeType: `string` | `number` Node type\n- bounds: `Rectangle` Get the node's x, y, width, height, equivalent to `transform.bounds`\n- blocks: `FlowNodeEntity[]` Get child nodes, including collapsed child nodes\n- parent: `FlowNodeEntity | undefined` Get the parent node (Such as loop)\n- document: [WorkflowDocument](/api/core/workflow-document.html) Document link\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) Get the node's transform data\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) Get the node's render data, including render status\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) Get the node's form data, like [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable public scope\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) Get the node's variable private scope\n- lines: [WorkflowNodeLinesData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodeLinesData.html) Get the node's lines data\n- ports: [WorkflowNodePortsData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodePortsData.html) Get the node's ports data\n- getNodeRegistry(): [WorkflowNodeRegistry](https://flowgram.ai/auto-docs/free-layout-core/interfaces/WorkflowNodeRegistry) Get the node's registry\n- getService(): Get the [IOC](/guide/concepts/ioc.html) Service, such as`node.getService(HistoryService)`\n- getExtInfo(): Get the node's ext info, such as `node.getExtInfo<{ test: string }>()`\n- updateExtInfo(): Update the node's ext info, such as `node.updateExtInfo<{ test: string }>({ test: 'test' })`\n- dispose(): Destroy node\n- toJSON(): Get the node's json data\n\n## Node Data\n\nCan be obtained through `node.toJSON()`\n\n:::note Basic Structure:\n\n- id: `string` Unique identifier for the node, must be unique\n- meta: `object` Node's UI configuration information, such as `position` information for free layout\n- type: `string | number` Node type, corresponds to `type` in `nodeRegistries`\n- data: `object` Node form data, can be customized by business\n- blocks: `array` Node branches, using `block` is more suitable for `Gramming` free layout scenarios, used in sub-nodes of sub-canvas\n- edges: `array` Edge data of sub-canvas\n\n:::\n\n```ts pure\nconst nodeData: FlowNodeJSON = {\n  id: 'xxxx',\n  type: 'condition',\n  data: {\n    title: 'MyCondition',\n    desc: 'xxxxx'\n  },\n}\n```\n\n## Node Definition\n\nIn free layout scenarios, node definition is used to declare node's initial position/size, ports, form rendering, etc.\n\n```tsx pure title=\"node-registries.tsx\"\nimport { WorkflowNodeRegistry, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start node\n      deleteDisable: true, // Start node cannot be deleted\n      copyDisable: true, // Start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Define node input and output ports, start node only has output port\n      // useDynamicPort: true, // For dynamic ports, will look for DOM with data-port-id and data-port-type attributes as ports\n    },\n    /**\n     * Configure node form validation and rendering\n     * Note: validate uses data and rendering separation to ensure node validation even without rendering\n     */\n    formMeta: {\n      validateTrigger: ValidateTrigger.onChange,\n      validate: {\n        title: ({ value }) => (value ? undefined : 'Title is required'),\n      },\n      /**\n       * Render form\n       */\n      render: () => (\n       <>\n          <Field name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field name=\"content\">\n            {({ field }) => <input onChange={field.onChange} value={field.value}/>}\n          </Field>\n        </>\n      )\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n    formMeta: {\n      // ...\n    }\n  },\n  {\n    type: 'custom',\n    meta: {\n    },\n    formMeta: {\n      // ...\n    },\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // Normal nodes have two ports\n  },\n];\n```\n\n## Get Current Rendering Node\n\nGet node-related methods through [useNodeRender](/api/hooks/use-node-render.html)\n\n```tsx pure\nfunction BaseNode() {\n  const { id, type, data, updateData, node } = useNodeRender()\n}\n```\n\n## Get Input/Output Nodes or Lines for Current Node\n\n```ts pure\n// get input nodes (calculated through connection lines)\nnode.lines.inputNodes\n// get all input nodes (recursively gets all upstream nodes)\nnode.lines.allInputNodes\n// get output nodes\nnode.lines.outputNodes\n// get all output nodes\nnode.lines.allOutputNodes\n// input lines (contains the line that isDrawing or isHidden)\nnode.lines.inputLines\n// output lines (contains the line that isDrawing or isHidden)\nnode.lines.outputLines\n// all availableLines (Doesn't contain the lines that isDrawing or isHidden)\nnode.lines.availableLines\n```\n\n## Create Node\n\n- Through [WorkflowDocument](/api/core/workflow-document.html)\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.document.createWorkflowNode({\n  id: 'xxx', // Must be unique within the canvas\n  type: 'custom',\n  meta: {\n    /**\n     * If not provided, defaults to creating in the center of the canvas\n     * To get position from mouse position (e.g., creating node by clicking anywhere on canvas),\n     * convert using `ctx.playground.config.getPosFromMouseEvent(mouseEvent)`\n     */\n    position: { x: 100, y: 100 } //\n  },\n  data: {}, // Form-related data\n  blocks: [], // Sub-canvas nodes\n  edges: [] // Sub-canvas edges\n})\n\n```\n- Through WorkflowDragService, see [Free Layout Basic Usage](/examples/free-layout/free-layout-simple.html)\n\n```ts pure\nconst dragService = useService<WorkflowDragService>(WorkflowDragService);\n\n// mouseEvent here will automatically convert to canvas position\ndragService.startDragCard(nodeType, mouseEvent, {\n  id: 'xxxx',\n  data: {}, // Form-related data\n  blocks: [], // Sub-canvas nodes\n  edges: [] // Sub-canvas edges\n})\n\n```\n\n## Delete Node\n\nDelete node through `node.dispose`\n\n```tsx pure\nfunction BaseNode({ node }) {\n  function onClick() {\n    node.dispose()\n  }\n  return (\n    <button onClick={onClick}>Delete</button>\n  )\n}\n```\n\n## Update Node Data\n\n- Get node's data through [useNodeRender](/api/hooks/use-node-render.html) or [node.form](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html)\n\n```tsx pure\nfunction BaseNode() {\n  const { form, node } = useNodeRender();\n  // Corresponds to node's data\n  // 1. form.values: Corresponds to node's data\n  // 2. form.setValueIn('title', 'xxxx'): Update data.title\n  // 3. form.getValueIn('title'): Get data.title\n  // 4. form.updateFormValues({ ... }) Update all form values\n\n  function onChange(e) {\n    form.setValueIn('title', e.target.value)\n  }\n  return <input value={form.values.title} onChange={onChange}/>\n}\n\n```\n- Update form data through Field, see details in [Form Usage](/guide/form/form.html)\n\n```tsx pure\n\nfunction FormRender() {\n  return (\n    <Field name=\"title\">\n      <Input />\n    </Field>\n  )\n}\n```\n\n## Update Node's extInfo Data\n\nextInfo is used to store UI states, if node engine is not enabled, node's data will be stored in extInfo by default\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const times = node.getExtInfo()?.times || 0\n  function onClick() {\n    node.updateExtInfo({ times: times ++ })\n  }\n  return (\n    <div>\n      <span>Click Times: {times}</span>\n      <button onClick={onClick}>Click</button>\n    </div>\n  )\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/port.mdx",
    "content": "# Ports\n\n- [WorkflowNodePortsData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-ports-data.ts) manages all port information for nodes\n- [WorkflowPortEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts) port instance\n- [WorkflowPortRender](https://github.com/bytedance/flowgram.ai/blob/main/packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx) port rendering component\n\n\n## Define Ports\n\nAdd `defaultPorts` to node declaration, such as `{ type: 'input', location: 'left' }`, which will add an input port on the left side of the node\n\n\n```ts pure\n// Port interface\nexport interface WorkflowPort {\n  /**\n   * If not specified, represents default connection point, default input type is left center, output type is right center\n   */\n  portID?: string | number;\n  /**\n   * Input or output point\n   */\n  type: 'input' | 'output';\n  /**\n   * Port location\n   */\n  location?: 'left' | 'top' | 'right' | 'bottom';\n  /**\n   * Port location config\n   * @example\n   *  // bottom-center\n   *  {\n   *    left: '50%',\n   *    bottom: 0\n   *  }\n   *  // right-center\n   *  {\n   *    right: 0,\n   *    top: '50%'\n   *  }\n   */\n  locationConfig?: { left?: string | number, top?: string | number, right?: string | number, bottom?: string | number}\n  /**\n   * Offset relative to location\n   */\n  offset?: IPoint;\n  /**\n   * Port hot zone size\n   */\n  size?: { width: number; height: number };\n  /**\n   * Disable port\n   */\n  disabled?: boolean;\n}\n```\n\n```ts pure title=\"node-registries.ts\"\n{\n  type: 'start',\n  meta: {\n    defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left' }]\n  },\n}\n```\n\n## Dynamic Ports\n\nAdd `useDynamicPort` to node declaration, when set to true it will look for DOM elements with `data-port-id` and `data-port-type` attributes on the node DOM as ports\n\n\n```ts pure title=\"node-registries.ts\"\n{\n  type: 'condition',\n  meta: {\n    defaultPorts: [{ type: 'input'}]\n    useDynamicPort: true\n  },\n}\n\n```\n\n```tsx pure\n\n/**\n*  Dynamic ports find port positions through querySelectorAll('[data-port-id]')\n */\nfunction BaseNode() {\n  return (\n    <div>\n      <div data-port-id=\"condition-if-0\" data-port-type=\"output\" data-port-location=\"right\"></div>\n      <div data-port-id=\"condition-if-1\" data-port-type=\"output\" data-port-location=\"righ\" ></div>\n      {/* others */}\n    </div>\n  )\n}\n```\n\n## Vertical Ports\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/vertical-ports.png\"/>\n\n```typescript pure\nexport const nodeRegsistries = [\n  {\n    type: 'chain',\n    meta: {\n      defaultPorts: [\n        { type: 'input' },\n        { type: 'output' },\n        {\n          portID: 'p4',\n          location: 'bottom',\n          locationConfig: { left: '33%', bottom: 0 },\n          type: 'output',\n        },\n        {\n          portID: 'p5',\n          location: 'bottom',\n          locationConfig: { left: '66%', bottom: 0 },\n          type: 'output',\n        },\n      ],\n    },\n  },\n  {\n    type: 'tool',\n    meta: {\n      defaultPorts: [{ location: 'top', type: 'input' }],\n    },\n  },\n]\n```\n\n## Update Ports Data\n\n- Static Ports Update\n```ts pure\n// You can call this method to update static ports data based on form data\nnode.ports.updateAllPorts([\n    { type: 'output', location: 'right', locationConfig: { left: '33%', bottom: 0 }},\n    { type: 'input', location: 'left', locationConfig: { left: '66%', bottom: 0 }}\n])\n```\n- Dynamic Ports Update\n\n```ts pure\n// Refresh and sync ports data from node dom content\nnode.ports.updateDynamicPorts()\n```\n\n## Update Ports Data Via Form Values Changed\n\nBelow, the `condition` node listens to `portKeys` data and updates ports data via [Form Effect](/guide/form/form.html), details see [Demo](/examples/free-layout/free-layout-simple.html)\n\n<img loading=\"lazy\" className=\"invert-img\" height=\"200\" src=\"/free-layout/auto-update-ports.gif\"/>\n\n```tsx pure title=\"node-registries.ts\"\nimport {\n  Field,\n  DataEvent,\n  EffectFuncProps,\n  WorkflowPorts\n} from '@flowgram.ai/free-layout-editor';\n\nconst CONDITION_ITEM_HEIGHT = 30\nconst conditionNodeRegistry =  {\n    type: 'condition',\n    meta: {\n      defaultPorts: [{ type: 'input' }],\n    },\n    formMeta: {\n      effect: {\n        /**\n         * Listen for \"portsKeys\" changes and update ports\n         */\n        portKeys: [{\n          event: DataEvent.onValueInitOrChange,\n          effect: ({ value, context }: EffectFuncProps<Array<string>, FormData>) => {\n            const { node } = context\n            const defaultPorts: WorkflowPorts = [{ type: 'input'}]\n            const newPorts: WorkflowPorts = value.map((portID: string, i: number) => ({\n              type: 'output',\n              portID,\n              location: 'right',\n              locationConfig: {\n                right: 0,\n                top: (i + 1) * CONDITION_ITEM_HEIGHT\n              }\n            }))\n            node.ports.updateAllPorts([...defaultPorts, ...newPorts])\n          },\n        }],\n      },\n      render: () => (\n        <>\n          <Field<string> name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field<Array<string>> name=\"portKeys\">\n            {({ field: { value, onChange }, }) => {\n              return (\n                <div className=\"demo-free-node-content\" style={{\n                  width: 160,\n                  height: value.length * CONDITION_ITEM_HEIGHT,\n                  minHeight: 2 * CONDITION_ITEM_HEIGHT\n                }}>\n                  <div>\n                    <button onClick={() => onChange(value.concat(`if_${value.length}`))}>Add Port</button>\n                  </div>\n                  <div style={{ marginTop: 8 }}>\n                    <button onClick={() => onChange(value.filter((v, i, arr) => i !== arr.length - 1))}>Delete Port\n                    </button>\n                  </div>\n                </div>\n              )\n            }}\n          </Field>\n        </>\n      ),\n    },\n  }\n```\n## Port Rendering\n\nPorts are ultimately rendered through the `WorkflowPortRender` component, supporting custom styles, or you can reimplement this component based on the source code. Refer to [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)\n\n## Custom Port Colors\n\nYou can customize port colors by passing color props to `WorkflowPortRender`:\n\n- `primaryColor` - Active state color (linked/hovered)\n- `secondaryColor` - Default state color\n- `errorColor` - Error state color\n- `backgroundColor` - Background color\n\n```tsx pure\n\nimport { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';\n\nfunction BaseNode() {\n  const { ports } = useNodeRender();\n  return (\n    <div>\n      <div data-port-id=\"condition-if-0\" data-port-type=\"output\"></div>\n      <div data-port-id=\"condition-if-1\" data-port-type=\"output\"></div>\n      {ports.map((p) => (\n        <WorkflowPortRender\n          key={p.id}\n          entity={p}\n          className=\"xxx\"\n          style={{ /* custom style */}}\n          // Custom port colors\n          primaryColor=\"#4d53e8\"        // Active state color (linked/hovered)\n          secondaryColor=\"#9197f1\"      // Default state color\n          errorColor=\"#ff4444\"          // Error state color\n          backgroundColor=\"#ffffff\"     // Background color\n        />\n      ))}\n    </div>\n  )\n}\n```\n\n## Get Ports Data\n\n```ts pure\nconst { ports } = node\n\nconsole.log(ports.inputPorts) // Get all input ports of current node\nconsole.log(ports.outputPorts) // Get all output ports of current node\n\nconsole.log(ports.inputPorts.map(port => port.availableLines)) // Find connected lines through ports\n\nports.updateDynamicPorts() // When dynamic ports modify DOM structure or position, you can manually refresh port positions through this method (DOM rendering has delay, best executed in useEffect or setTimeout)\n```\n\n## Two-way Port Connection\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/two-way-connection.gif\"/>\n\n```ts pure title=\"node-registries.ts\"\n  {\n    type: 'twoway',\n    meta: {\n      defaultPorts: [\n        // input and output ports can overlap\n        { type: 'input', portID: 'input-left', location: 'left' },\n        { type: 'output', portID: 'output-left', location: 'left' },\n        { type: 'input', portID: 'input-right', location: 'right' },\n        { type: 'output', portID: 'output-right', location: 'right' },\n      ],\n    },\n  },\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/free-layout/sub-canvas.mdx",
    "content": "# Sub-canvas\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/loop2.png\"/>\n\nFor detailed code, see [Free Layout Best Practices](/examples/free-layout/free-feature-overview.html)\n\n## Add Sub-canvas Plugin\n\n```tsx pure\n\nimport { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\n\nfunction App() {\n  const editorProps = {\n    plugins: () => [\n      createContainerNodePlugin({}),\n    ]\n    // ..others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n## Define Sub-canvas Node\n\n```tsx pure\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\n\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: 'loop',\n  info: {\n    icon: iconLoop,\n    description:\n      'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',\n  },\n  meta: {\n    /**\n     * Sub-canvas marker\n     */\n    isContainer: true,\n    /**\n    * Sub-canvas default size settings\n     */\n    size: {\n      width: 560,\n      height: 400,\n    },\n    /**\n    * Sub-canvas padding settings\n     */\n    padding: () => ({ // Container padding settings\n      top: 150,\n      bottom: 100,\n      left: 100,\n      right: 100,\n    }),\n    /**\n      * Control node selection state within sub-canvas\n      */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // Only selectable when mouse start position does not include current node\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n  },\n  formMeta: {\n    render: () => (\n      <div>\n        { /* others */ }\n        <SubCanvasRender />\n      </div>\n    )\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/getting-started/_meta.json",
    "content": "[\n  \"introduction\",\n  \"quick-start\",\n  \"free-layout\",\n  \"fixed-layout\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/getting-started/fixed-layout.mdx",
    "content": "# Fixed Layout\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\nimport { FixedLayoutCodePreview } from '@components/code-preview';\nimport step1 from '@components/fixed-examples/step-1.tsx?raw';\nimport step2 from '@components/fixed-examples/step-2.tsx?raw';\nimport step3 from '@components/fixed-examples/step-3.tsx?raw';\nimport step4 from '@components/fixed-examples/step-4.tsx?raw';\nimport step5App from '@components/fixed-examples/step-5/app.tsx?raw';\nimport step5UseEditorProps from '@components/fixed-examples/step-5/use-editor-props.tsx?raw';\nimport step5InitialData from '@components/fixed-examples/step-5/initial-data.ts?raw';\nimport step5NodeRegistries from '@components/fixed-examples/step-5/node-registries.tsx?raw';\nimport step5NodeRender from '@components/fixed-examples/step-5/node-render.tsx?raw';\nimport step5Adder from '@components/fixed-examples/step-5/adder.tsx?raw';\nimport step6App from '@components/fixed-examples/step-6/app.tsx?raw';\nimport step6UseEditorProps from '@components/fixed-examples/step-6/use-editor-props.tsx?raw';\nimport step6InitialData from '@components/fixed-examples/step-6/initial-data.ts?raw';\nimport step6NodeRegistries from '@components/fixed-examples/step-6/node-registries.tsx?raw';\nimport step6NodeRender from '@components/fixed-examples/step-6/node-render.tsx?raw';\nimport step6Adder from '@components/fixed-examples/step-6/adder.tsx?raw';\nimport step7App from '@components/fixed-examples/step-7/app.tsx?raw';\nimport step7UseEditorProps from '@components/fixed-examples/step-7/use-editor-props.tsx?raw';\nimport step7InitialData from '@components/fixed-examples/step-7/initial-data.ts?raw';\nimport step7NodeRegistries from '@components/fixed-examples/step-7/node-registries.tsx?raw';\nimport step7NodeRender from '@components/fixed-examples/step-7/node-render.tsx?raw';\nimport step7Adder from '@components/fixed-examples/step-7/adder.tsx?raw';\nimport step7Tools from '@components/fixed-examples/step-7/tools.tsx?raw';\nimport step7Minimap from '@components/fixed-examples/step-7/minimap.tsx?raw';\n\n\n\n## Step 0: Install Dependencies\n\n1.  Install the editor packages:\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"pnpm\": \"pnpm add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"yarn\": \"yarn add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"bun\": \"bun add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n}} />\n\n2.  Install `styled-components` (if you haven't already):\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install styled-components\",\n  \"pnpm\": \"pnpm add styled-components\",\n  \"yarn\": \"yarn add styled-components\",\n  \"bun\": \"bun add styled-components\",\n}} />\n\n## Step 1: Import the Canvas Components\n\n1.  Import the stylesheet to ensure basic styles are applied:\n    ```tsx\n    import '@flowgram.ai/fixed-layout-editor/index.css';\n    ```\n\n2.  Use `FixedLayoutEditorProvider` to provide the editor context and `EditorRenderer` to render the canvas. Import the default Fixed Layout Semi component set via `materials.components`:\n    ```tsx\n    const FlowGramApp = () => (\n      <FixedLayoutEditorProvider\n        materials={{ components: defaultFixedSemiMaterials }}\n      >\n        <EditorRenderer />\n      </FixedLayoutEditorProvider>\n    );\n    ```\n\n3.  Keep the default exports for the remaining files.\n\n> **Expected Result:** After the page loads, only a blank canvas will be displayed, with no nodes or connections.\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step1\n}} />\n\n\n## Step 2: Implement the Node Component\n\n1.  Import the necessary APIs for node rendering:\n    *   `useNodeRender`: A hook to get the node context (e.g., drag, hover, highlight, form).\n    *   `FlowNodeEntity`: The type for a node entity, used to declare the props for `NodeRender`.\n\n2.  Create the `NodeRender` component, customize the node's size and style, and integrate drag-and-drop and form rendering:\n    ```tsx\n    import { useNodeRender, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\n\n    export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n      const { onMouseEnter, onMouseLeave, startDrag, form, dragging, activated } = useNodeRender();\n      return (\n        <div\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          onMouseDown={(e) => { startDrag(e); e.stopPropagation(); }}\n          style={{ width: 280, minHeight: 88, background: '#fff', borderRadius: 8, opacity: dragging ? 0.3 : 1, /* ... */ }}\n        >\n          {form?.render()}\n        </div>\n      );\n    };\n    ```\n\n3.  Register the components in `FixedLayoutEditorProvider`:\n    *   `materials.renderDefaultNode`: Specifies `NodeRender` as the default node renderer.\n    *   `nodeRegistries`: Declares the available node types (e.g., `custom`).\n    *   `initialData`: Provides an initial node of type `custom`.\n\n> **Expected Result:** A draggable, custom-styled node will appear on the canvas.\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step2\n}} />\n\n\n## Step 3: Customize the \"Add Node\" Component and \"Delete Node\" Button\n\n1.  Add a delete button to the node:\n    *   In `NodeRender`, get the `ctx` using `useClientContext()`. Call `ctx.operation.deleteNode(node)` when the button is clicked to delete the current node.\n    *   Remember to stop event propagation with `e.stopPropagation()` to avoid interfering with canvas selection/drag behavior.\n    ```tsx\n    const ctx = useClientContext();\n    <button onClick={(e) => { e.stopPropagation(); ctx.operation.deleteNode(node); }}>×</button>\n    ```\n\n2.  Customize the \"Add Node\" component, `Adder`:\n    *   Use `useService(FlowOperationService)` and `usePlayground()` to encapsulate the `handleAdd` method: insert a new node after the specified node and scroll it into the center of the view.\n    *   Switch the UI based on `hoverActivated`: display a plus sign and a larger clickable area on hover; do not display in read-only mode.\n    ```tsx\n    const { handleAdd } = useAddNode();\n    const Adder = ({ from, hoverActivated }) => (\n      <div onClick={() => handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from)}>\n        {hoverActivated ? <span>+</span> : null}\n      </div>\n    );\n    ```\n\n3.  Register `Adder` in `materials.components`:\n    *   Override the default \"Add Node\" renderer using `FlowRendererKey.ADDER`.\n    ```tsx\n    materials={{\n      renderDefaultNode: NodeRender,\n      components: { ...defaultFixedSemiMaterials, [FlowRendererKey.ADDER]: Adder },\n    }}\n    ```\n\n4.  Initialize data and adapt the view:\n    *   `initialData` provides a basic flow: `start -> custom -> end`.\n    *   Call `fitView` in `onAllLayersRendered` to automatically fit the canvas content:\n    ```tsx\n    onAllLayersRendered={(ctx) => {\n      setTimeout(() => {\n        ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n      }, 10);\n    }}\n    ```\n\n> **Expected Result:**\n>\n> *   The canvas will display a basic flow consisting of `start`, `custom`, and `end` nodes, with the view automatically centered/zoomed to a suitable range.\n> *   When hovering over an addable position, a circular plus button will appear. Clicking it will add a new `custom` node after the current node and automatically scroll to the new node.\n> *   A \"×\" delete button will be displayed in the top-right corner of each node. Clicking it will delete the node (the add component is hidden in read-only mode).\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step3\n}} />\n\n## Step 4: Introduce Plugins\n\n:::info\n*   `@flowgram.ai/minimap-plugin`: A minimap plugin that provides a small map view of the canvas.\n:::\n\n1.  Install the plugin dependency:\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/minimap-plugin\",\n  \"pnpm\": \"pnpm add @flowgram.ai/minimap-plugin\",\n  \"yarn\": \"yarn add @flowgram.ai/minimap-plugin\",\n  \"bun\": \"bun add @flowgram.ai/minimap-plugin\",\n}} />\n\n2.  Import the plugin creation function from the corresponding package:\n    *   `createMinimapPlugin` is used to generate the canvas thumbnail.\n\n3.  Register the plugin in the `plugins` prop of `FixedLayoutEditorProvider`:\n    ```tsx\n    plugins={() => [\n      createMinimapPlugin({\n        enableDisplayAllNodes: true,\n      })\n    ]}\n    ```\n\n> **Expected Result:**\n>\n> *   A draggable/zoomable minimap will appear in the top-right corner of the canvas. Clicking or dragging the thumbnail allows for quick navigation of the main canvas.\n> *   With `enableDisplayAllNodes: true`, the minimap will display all nodes, making it easy to navigate long flows.\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step4\n}} />\n\n\n## Step 5: Split Files\n\nTo prevent a single file from becoming too long, we need to split the editor configuration, node rendering, initial data, etc., which were originally in one component, into separate files. This facilitates maintenance, reuse, and collaboration.\n\n```sh\n- use-editor-props.tsx # Canvas configuration (centralized management of Provider's props)\n- node-render.tsx      # Node rendering (including delete button)\n- initial-data.ts      # Initial data (start/custom/end)\n- node-registries.tsx  # Node registration (example only registers 'custom')\n- adder.tsx            # Custom \"Add Node\" component (adds a custom node on click)\n- App.tsx              # Canvas entry point (mounts EditorRenderer)\n```\n\n**File Responsibilities:**\n\n*   `use-editor-props.tsx`: Centralizes all props for `FixedLayoutEditorProvider` (plugins, view adaptation, materials, node registration, and initial data):\n    *   `plugins`: Registers the minimap plugin `createMinimapPlugin({ enableDisplayAllNodes: true })`.\n    *   `onAllLayersRendered`: Calls `fitView` after rendering is complete to automatically fit the canvas content.\n    *   `materials`:\n        *   `renderDefaultNode`: Specifies `NodeRender` as the default node renderer.\n        *   `components`: Merges `defaultFixedSemiMaterials` and overrides the default adder with `[FlowRendererKey.ADDER]: Adder`.\n    *   `nodeRegistries` and `initialData` are imported from separate files.\n\n*   `node-render.tsx`: Defines the custom node renderer `NodeRender`, sets the node's appearance, and renders the internal form via `form?.render()`. It also provides a \"×\" delete button in the top-right corner (`useClientContext().operation.deleteNode(node)`).\n\n*   `initial-data.ts`: Provides the initial data for the basic flow, including `start -> custom -> end` nodes.\n\n*   `node-registries.tsx`: Declares the set of node types (the example only registers `'custom'`).\n\n*   `adder.tsx`: Implements the custom \"Add Node\" component `Adder`, which displays a plus sign on hover. On click, it adds a new `custom` node after the current node using `FlowOperationService.addFromNode` and calls `scrollToView` to automatically position the new node.\n\n*   `App.tsx`: The application entry point, which gets the configuration from `useEditorProps` and mounts `EditorRenderer`.\n\n> **Expected Result:** By splitting the files, the code structure becomes clearer and responsibilities are more defined, making future extensions and team collaboration easier. The UI effect is the same as in the previous section (basic flow, delete button, and \"Add Node\" component are available, with a minimap and automatic view adaptation).\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step5App,\n    '/use-editor-props.tsx': step5UseEditorProps,\n    '/initial-data.ts': step5InitialData,\n    '/node-registries.tsx': step5NodeRegistries,\n    '/node-render.tsx': step5NodeRender,\n    '/adder.tsx': step5Adder,\n}} />\n\n## Step 6: Integrate Forms and History\n\n1.  Node Registration and Extension:\n\n    *   `condition`: Extended as a \"branch node\" via `extend: 'dynamicSplit'`, and returns default `blocks` (multiple branches) in `onAdd()`.\n    *   `custom`: A normal node, with default `data.title` and `data.content` set in `onAdd()`.\n    *   The optional `meta` configuration item can control node behavior (e.g., draggable, selectable, deletable, copyable, addable).\n\n2.  Enable Forms and History:\n\n    In `use-editor-props.tsx`:\n    *   `nodeEngine.enable = true`: Enables the node engine, allowing `formMeta` to be configured for node types.\n    *   `history.enable = true` and `history.enableChangeNode = true`: Enable undo/redo and listen for node data changes (e.g., form field changes).\n    *   `history.onApply(ctx)`: Triggered after applying a history record, can be used for auto-saving (the example prints `ctx.document.toJSON()`).\n    *   `getNodeDefaultRegistry(type)`: Provides a default configuration for types that are not explicitly registered:\n        *   `meta.defaultExpanded = true`: The node's internal content area is expanded by default.\n        *   `formMeta.render`: Renders the form. This example uses `<Field<string> name=\"title\">` and `<Field<string> name=\"content\">` to display a title and an editable input box, respectively.\n        ```tsx\n        getNodeDefaultRegistry(type) {\n          return {\n            type,\n            meta: { defaultExpanded: true },\n            formMeta: {\n              render: () => (\n                <>\n                  <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </>\n              ),\n            },\n          };\n        }\n        ```\n\n3.  Initialize Data and Rendering:\n\n    *   In `initial-data.ts`, include a `start` node, a `condition` node with three branches (one containing `custom`, another `break`, and one empty), and an `end` node.\n    *   Each node carries `data.title` and `data.content`. `form?.render()` in `NodeRender` will render these form fields inside the node's shell.\n    *   The `Adder` component adds a `custom` node on click, with the default `title: 'New Custom Node'` and `content: 'Custom Node Content'`.\n\n> **Expected Result:**\n>\n> *   The canvas contains `start`, `condition` (three branches), and `end` nodes. Each node displays its `title` and `content`. You can quickly add `custom` nodes to the flow using the `Adder`.\n> *   Undo/redo shortcuts are available. Node movement, addition, deletion, and form input changes are included in the history. The auto-save callback in the example will be executed when history is applied.\n> *   The expanded area of branch nodes is open by default, making it easy to view and edit internal content.\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step6App,\n    '/use-editor-props.tsx': step6UseEditorProps,\n    '/initial-data.ts': step6InitialData,\n    '/node-registries.tsx': step6NodeRegistries,\n    '/node-render.tsx': step6NodeRender,\n    '/adder.tsx': step6Adder,\n}} />\n\n## Step 7: Create a Toolbar\n\n1.  Import the Toolbar and Minimap Components:\n\n    *   In `App.tsx`, import and render `<Tools />` and a custom `<Minimap />` at the same level as `<EditorRenderer />` inside `FixedLayoutEditorProvider`. This allows them to access the editor context and canvas operation methods.\n\n2.  Control the Canvas with Tool Methods:\n\n    *   Use `usePlaygroundTools()` to get canvas operation methods: `zoomin/zoomout`, `fitView`, `changeLayout`, etc.\n    *   Display the zoom ratio in real-time: read the current canvas zoom from `tools.zoom` and display it as a percentage.\n\n3.  Integrate Undo/Redo Status:\n\n    *   Use `useClientContext()` to get `history` and listen to `history.undoRedoService.onChange` to update the availability of `Undo/Redo` in the toolbar.\n    *   Ensure history is enabled in `use-editor-props.tsx`: `history.enable = true` and `history.enableChangeNode = true`, so that undo/redo works for node data and layout changes.\n\n4.  Customize the Minimap (Optional):\n\n    *   Use `MinimapRender` and customize the container style to fix the thumbnail in the bottom-left corner, avoiding obstruction of the main interaction area.\n    *   In `use-editor-props.tsx`, pass `disableLayer: true` and `canvasStyle` (`canvasWidth/canvasHeight/canvasPadding`) to the minimap plugin to get a compact thumbnail size and margin.\n\n5.  Maintain Previous Functionality:\n\n    *   `NodeRender` continues to render form fields via `form?.render()` and provide a delete button. `Adder` supports one-click addition of `custom` nodes. The `condition/custom` type registration is maintained in `node-registries.tsx`. `initial-data.ts` contains the example flow `start → condition (three branches) → end`.\n\n> **Expected Result:**\n>\n> *   A toolbar appears in the bottom-left corner of the screen, supporting common operations like `ZoomIn/ZoomOut`, `FitView`, `ChangeLayout`, etc., and displaying the zoom ratio in real-time. The `Undo/Redo` buttons are updated based on the history state.\n> *   A custom-styled minimap is also displayed in the bottom-left corner. You can click/drag the thumbnail to quickly navigate the main canvas. Combined with the toolbar, this forms a complete editing tool area for more efficient operation.\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step7App,\n    '/use-editor-props.tsx': step7UseEditorProps,\n    '/initial-data.ts': step7InitialData,\n    '/node-registries.tsx': step7NodeRegistries,\n    '/node-render.tsx': step7NodeRender,\n    '/adder.tsx': step7Adder,\n    '/tools.tsx': step7Tools,\n    '/minimap.tsx': step7Minimap,\n}} />\n\n## Step 8: Learn More\n\n<div style={{\n  display: \"grid\",\n  gridTemplateColumns: \"1fr 1fr\",\n  gap: \"2rem\",\n  marginTop: \"1rem\",\n}}>\n  <div>\n  Learn more about Fixed Layout usage:\n  - [Load and Save](/guide/fixed-layout/load)\n  - [Nodes](/guide/fixed-layout/node)\n  - [Composite Nodes](/guide/fixed-layout/composite-nodes)\n  </div>\n  <div>\n  Learn more about FlowGram.AI features:\n  - [Forms](/guide/form/form)\n  - [Variables](/guide/variable/basic)\n  - [Materials](/materials/introduction)\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/en/guide/getting-started/free-layout.mdx",
    "content": "# Free Layout\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\nimport { CodePreview } from '@components/code-preview';\nimport step1 from '@components/free-examples/step-1.tsx?raw';\nimport step2 from '@components/free-examples/step-2.tsx?raw';\nimport step3 from '@components/free-examples/step-3.tsx?raw';\nimport step4 from '@components/free-examples/step-4.tsx?raw';\nimport step5App from '@components/free-examples/step-5/app.tsx?raw';\nimport step5InitialData from '@components/free-examples/step-5/initial-data.ts?raw';\nimport step5UseEditorProps from '@components/free-examples/step-5/use-editor-props.tsx?raw';\nimport step5NodeRender from '@components/free-examples/step-5/node-render.tsx?raw';\nimport step5NodeRegistries from '@components/free-examples/step-5/node-registries.tsx?raw';\nimport step6App from '@components/free-examples/step-6/app.tsx?raw';\nimport step6InitialData from '@components/free-examples/step-6/initial-data.ts?raw';\nimport step6UseEditorProps from '@components/free-examples/step-6/use-editor-props.tsx?raw';\nimport step6NodeRender from '@components/free-examples/step-6/node-render.tsx?raw';\nimport step6NodeRegistries from '@components/free-examples/step-6/node-registries.tsx?raw';\nimport step7App from '@components/free-examples/step-7/app.tsx?raw';\nimport step7InitialData from '@components/free-examples/step-7/initial-data.ts?raw';\nimport step7UseEditorProps from '@components/free-examples/step-7/use-editor-props.tsx?raw';\nimport step7NodeRender from '@components/free-examples/step-7/node-render.tsx?raw';\nimport step7NodeRegistries from '@components/free-examples/step-7/node-registries.tsx?raw';\nimport step7Tools from '@components/free-examples/step-7/tools.tsx?raw';\nimport step7AddNode from '@components/free-examples/step-7/add-node.tsx?raw';\nimport step7Minimap from '@components/free-examples/step-7/minimap.tsx?raw';\n\n## Step 0: Install Dependencies\n\n1. Install the editor package\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/free-layout-editor\",\n  \"pnpm\": \"pnpm add @flowgram.ai/free-layout-editor\",\n  \"yarn\": \"yarn add @flowgram.ai/free-layout-editor\",\n  \"bun\": \"bun add @flowgram.ai/free-layout-editor\",\n}} />\n\n2. Install styled-components (if not already installed)\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install styled-components\",\n  \"pnpm\": \"pnpm add styled-components\",\n  \"yarn\": \"yarn add styled-components\",\n  \"bun\": \"bun add styled-components\",\n}} />\n\n## Step 1: Import the Canvas Component\n\n1. Import the stylesheet to ensure basic styles are applied:\n   ```tsx\n   import '@flowgram.ai/free-layout-editor/index.css';\n   ```\n\n2. Use `FreeLayoutEditorProvider` to provide the editor context, and `EditorRenderer` to render the canvas:\n   ```tsx\n   const FlowGramApp = () => (\n     <FreeLayoutEditorProvider>\n       <EditorRenderer />\n     </FreeLayoutEditorProvider>\n   );\n   ```\n\n3. The remaining files can keep their default exports.\n\n> Expected result: After the page loads, only a blank canvas is displayed, with no nodes or edges.\n\n<CodePreview files={{\n    '/App.tsx': step1\n}} />\n\n## Step 2: Implement and Register the Node Component\n\n1. Import hooks and components related to node rendering:\n   - `useNodeRender`: Gets the node context (such as the form).\n   - `WorkflowNodeProps` & `WorkflowNodeRenderer`: Define and render the node shell.\n\n2. Create the `NodeRender` component to customize node size and style:\n   ```tsx\n   const NodeRender = (props: WorkflowNodeProps) => {\n     const { form } = useNodeRender();\n     return (\n       <WorkflowNodeRenderer\n         style={{ width: 280, height: 88, background: '#fff', borderRadius: 8, ... }}\n         node={props.node}\n       >\n         {form?.render()}\n       </WorkflowNodeRenderer>\n     );\n   };\n   ```\n\n3. Register in `FreeLayoutEditorProvider`:\n   - `materials.renderDefaultNode` specifies the default node renderer.\n   - `nodeRegistries` declares available node types (e.g., `custom`).\n   - `initialData` provides an initial node at position `{ x: 250, y: 100 }`.\n\n> Expected result: A draggable, custom-styled node appears on the canvas.\n\n<CodePreview files={{\n    '/App.tsx': step2\n}} />\n\n## Step 3: Add Multiple Nodes and Edges\n\n1. Add the `onAllLayersRendered` callback. After all layers are rendered, call `ctx.tools.fitView(false)` to make the canvas automatically fit the content.\n\n2. Add `canDeleteNode` & `canDeleteLine` callbacks, returning `true` to allow deleting nodes and edges.\n\n3. Extend `initialData`:\n   - Add another node of the same type at position `{ x: 400, y: 0 }`.\n   - Add an edge in the `edges` array to connect node `1` and node `2`.\n\n> Expected result:\n>\n> • The canvas displays two connected nodes and automatically centers/zooms to fit the view.\n>\n> • Select any node or edge, and press the Delete key to remove it.\n\n<CodePreview files={{\n    '/App.tsx': step3\n}} />\n\n## Step 4: Import Plugins\n\n:::info\n\n- `@flowgram.ai/free-snap-plugin`: A node snapping plugin that aligns nodes to a grid.\n- `@flowgram.ai/minimap-plugin`: A minimap plugin that provides a small map view of the canvas.\n\n:::\n\n1. Install plugin dependencies\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"pnpm\": \"pnpm add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"yarn\": \"yarn add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"bun\": \"bun add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n}} />\n\n2. Import the plugin creation functions from their respective packages:\n  - `createFreeSnapPlugin` is used for node grid snapping.\n  - `createMinimapPlugin` is used to generate the canvas minimap.\n\n3. Register the plugins in the `plugins` prop of `FreeLayoutEditorProvider`:\n  ```tsx\n  plugins={() => [\n    createMinimapPlugin({}),\n    createFreeSnapPlugin({})\n  ]}\n  ```\n\n> Expected result:\n>\n> • A draggable and zoomable minimap appears in the upper-right corner of the canvas. Clicking or dragging the minimap allows for quick navigation of the main canvas.\n>\n> • When dragging a node, it will automatically snap to nearby nodes for easy alignment.\n\n<CodePreview files={{\n    '/App.tsx': step4\n}} />\n\n\n## Step 5: Splitting Files\n\nTo avoid having excessively long files, we need to split the editor configuration, node rendering, initial data, etc., which were originally in a single component, into separate files. This facilitates maintenance, reuse, and collaboration.\n\n```sh\n- use-editor-props.ts # Canvas configuration\n- node-render.tsx # Node rendering\n- initial-data.ts # Initial data\n- node-registries.ts # Node configuration\n- App.tsx # Canvas entry point\n```\n\nFile Responsibilities\n\n- `use-editor-props.tsx`: Manages all props for FreeLayoutEditorProvider (plugins, view fitting, materials, node registration, and initial data).\n- `node-render.tsx`: Defines the custom node renderer NodeRender, responsible for its appearance and internal form rendering.\n- `initial-data.ts`: Provides the initial nodes and edges. The current example includes 5 `custom` nodes and multiple connections.\n- `node-registries.tsx`: Declares the set of node types (the example only registers 'custom').\n- `App.tsx`: The application entry point, which gets the configuration from useEditorProps and mounts EditorRenderer.\n\n> Expected result: By splitting the files, the code structure becomes clearer, responsibilities are more defined, and the code is easier to extend in the future.\n\n<CodePreview files={{\n    '/App.tsx': step5App,\n    '/use-editor-props.tsx': step5UseEditorProps,\n    '/initial-data.ts': step5InitialData,\n    '/node-registries.tsx': step5NodeRegistries,\n    '/node-render.tsx': step5NodeRender,\n}} />\n\n## Step 6: Integrating Forms and History\n\n1. Node Registration and Port Configuration\n\n- `start`: The starting node, which cannot be deleted and has only an output port by default.\n- `end`: The ending node, which cannot be deleted and has only an input port by default.\n- `custom`: A regular node, which has both input and output ports by default.\n\n2. Enable Forms and History\n\nIn `useEditorProps.tsx`:\n- `nodeEngine.enable = true`: Enables the node engine, allowing `formMeta` to be configured for node types.\n- `history.enable = true` and `history.enableChangeNode = true`: Enable undo/redo and listen for node data changes (e.g., form changes).\n- `getNodeDefaultRegistry(type)`: Provides a default configuration for types that are not explicitly registered:\n  - `meta.defaultExpanded = true`: The node's internal content area is expanded by default.\n  - `formMeta.render`: Renders the form. This example renders the title field using `<Field<string> name=\"title\">`.\n\n3. Initialize Data and Rendering\n\n- In `initial-data.ts`, set `data.title` for each node (e.g., `Start Node`, `Custom Node A/B/C`, `End Node`).\n- `form?.render()` in `NodeRender` will render the form content into the node shell, displaying the title of each node.\n\n> Expected result:\n>\n> • The canvas contains `start`, multiple `custom`, and `end` nodes, with connections matching the initial data.\n>\n> • Each node displays its `title`; selecting a node can expand it to show more form fields and interactions.\n>\n> • Undo/redo shortcuts are available and can be verified by deleting or moving nodes.\n\n<CodePreview activeFile=\"/use-editor-props.tsx\" files={{\n    '/App.tsx': step6App,\n    '/use-editor-props.tsx': step6UseEditorProps,\n    '/initial-data.ts': step6InitialData,\n    '/node-registries.tsx': step6NodeRegistries,\n    '/node-render.tsx': step6NodeRender,\n}} />\n\n## Step 7: Creating a Toolbar\n\n1. Import the Toolbar Component\n\n- In `App.tsx`, import `<Tools />` and place it at the same level as `<EditorRenderer />` inside `FreeLayoutEditorProvider`, allowing it to access the editor context and tool methods.\n\n2. Control the Canvas with Tool Methods\n\n- Use `usePlaygroundTools()` to get canvas manipulation methods: `zoomin/zoomout`, `fitView`, `autoLayout`, `switchLineType`, etc.\n- Switch edge style: Use `switchLineType` to toggle between `LineType.BEZIER` and `LineType.LINE_CHART`.\n- Display real-time zoom ratio: Read `tools.zoom` to show the current canvas zoom percentage.\n\n3. Integrate Undo/Redo State\n\n- Use `useClientContext()` to get `history` and listen to `history.undoRedoService.onChange` to update the state of the `canUndo/canRedo` buttons.\n- In `use-editor-props.tsx`, ensure history is enabled: `history.enable = true` and `history.enableChangeNode = true`, so that undo/redo works for node data changes.\n\n5. Extend Components (Optional)\n\n- Minimap: Pin the minimap to the bottom-right corner by customizing the `MinimapRender` container style to improve navigation efficiency.\n- AddNode: Provides a button to quickly add a new node. It uses `WorkflowDocument.createWorkflowNodeByType` to create and select a node in the center of the canvas.\n\n> Expected result:\n>\n> • A toolbar appears in the bottom-right corner of the page, supporting common operations like Zoom In/Zoom Out, Fit View, Auto Layout, and displaying the real-time zoom ratio.\n>\n> • The edge style can be switched (Bezier/Polyline), and the undo/redo buttons are automatically enabled/disabled based on the history state.\n>\n> • Combined with the Minimap and AddNode components, it forms a complete editing tool area for more efficient operation.\n\n<CodePreview activeFile=\"/tools.tsx\" files={{\n    '/App.tsx': step7App,\n    '/use-editor-props.tsx': step7UseEditorProps,\n    '/initial-data.ts': step7InitialData,\n    '/node-registries.tsx': step7NodeRegistries,\n    '/tools.tsx': step7Tools,\n    '/add-node.tsx': step7AddNode,\n    '/minimap.tsx': step7Minimap,\n    '/node-render.tsx': step7NodeRender,\n}} />\n\n## Step 8: Learn More\n\nClick to learn more about [Free Layout Usage](/guide/free-layout/load)\n\n<div style={{\n  display: \"grid\",\n  gridTemplateColumns: \"1fr 1fr\",\n  gap: \"2rem\",\n  marginTop: \"1rem\",\n}}>\n  <div>\n  Learn more about Free Layout:\n  - [Loading and Saving](/guide/free-layout/load)\n  - [Nodes](/guide/free-layout/node)\n  - [Lines](/guide/free-layout/line)\n  - [Ports](/guide/free-layout/port)\n  - [Sub-canvas](/guide/free-layout/sub-canvas)\n  </div>\n  <div>\n  Learn more about other FlowGram.AI features:\n  - [Form](/guide/form/form)\n  - [Variable](/guide/variable/basic)\n  - [Materials](/materials/introduction)\n  - [Runtime](/guide/runtime/introduction)\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/en/guide/getting-started/introduction.mdx",
    "content": "# Introduction\n\nFlowGram is a workflow development framework and toolkit. It helps developers build AI workflow platforms **faster** and **more easily**.\nFlowGram comes with a suite of built-in tools for workflow development: visual flow canvas, node configuration forms, variable scope chain, and ready-to-use materials.\nUse FlowGram to build your own AI workflow platform.\n\n## Why FlowGram\n\nFlowGram was originally designed to build diverse AI workflow platforms within ByteDance. These large-scale workflow platforms often have complex business logic and processes, and building them from scratch is not only time-consuming but also results in high development and maintenance costs.\n\nMany developers initially tried to use mainstream visual graphics libraries to build workflow platforms. However, these general-purpose libraries couldn't solve the core problems of workflow scenarios. Developers still had to handle a series of challenges themselves, such as node data management, dynamic forms, data validation, and variable scope chains. This led to low development efficiency and difficult long-term maintenance.\n\nTo address these pain points, we introduced FlowGram, a development framework specifically designed for workflow scenarios, aiming to help developers improve the efficiency and shorten the development cycle of creating workflow platforms. FlowGram provides the following core features:\n\n- **Workflow Canvas**: Provides visual orchestration for nodes and edges, supporting both free and fixed layouts to easily build complex flowcharts.\n- **Forms**: The form engine manages the CRUD operations of node data and provides rendering, validation, side effects, linkage, and error-capturing capabilities, simplifying the development of node configurations.\n- **Variables**: The variable engine supports scope constraints, variable structure inspection, and type inference, making it easy to manage data flow within the workflow.\n- **Materials**: Offers out-of-the-box components, side effects, and validators that developers can quickly reuse and extend, boosting development efficiency.\n\nBy combining these features, developers can focus on implementing business logic, thereby rapidly building full-featured, high-performance AI workflow platforms.\n\n## Next step\n\nPlease read [Quick start](/guide/getting-started/quick-start) to start using FlowGram.\n\nWelcome to the [GitHub Discussions](https://github.com/bytedance/flowgram.ai/discussions) and [Discord](https://discord.com/invite/SwDWdrgA9f) to communicate with us.\n\n## Appendix: Interactive Experience\n\nFlowGram offers a suite of interactive features designed for a seamless and intuitive workflow-building experience.\n\n<table className=\"rs-table\">\n  <tr>\n    <td>Smooth Motion Transitions</td>\n    <td>\n      <p>\n        FlowGram provides smooth motion transitions for all canvas elements. When nodes are resized or moved, animated transitions create a more natural and intuitive user experience. Our canvas engine is optimized for performance by rendering nodes and edges separately, making these animations fluid and efficient.\n      </p>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/common/motion.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>Intuitive Canvas Navigation</td>\n    <td>\n      <p>\n        Navigate the canvas with familiar gestures inspired by professional design tools like Sketch and Figma. Use a two-finger pinch-to-zoom on your touchpad, or simply hold the spacebar and drag to pan across the canvas with ease.\n      </p>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/common/touch-pad.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>Minimap</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/minimap.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>Undo/Redo</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/redo-undo.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>Copy/Paste (Shortcut Support)</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/copypaste.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>\n        <div>Box Selection + Drag and Drop</div>\n        <div>(Fixed)</div>\n      </div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <div className=\"rs-center\">\n          <img loading=\"lazy\" src=\"/fixed-layout/dragdrop.gif\" />\n        </div>\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>Horizontal/Vertical Layout Switch</div>\n      <div>(Fixed)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/layout-change.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>Branch Folding</div>\n      <div>(Fixed)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/fold.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>Grouping</div>\n      <div>(Fixed)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/group.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      Auto Layout\n      <div>(Free)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/autolayout.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      Snap Alignment + Guidelines\n      <div>(Free)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/snap.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      Coze Loop Sub-canvas\n      <div>(Free)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/loop.gif\" />\n      </div>\n    </td>\n  </tr>\n</table>\n"
  },
  {
    "path": "apps/docs/src/en/guide/getting-started/quick-start.mdx",
    "content": "# Quick Start\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\n\n:::info\nTo quickly experience FlowGram.AI, you can directly [open it in CodeSandbox](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) or [open it in StackBlitz](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo).\n:::\n\nChoose a way to start:\n- Option 1: Use the official template scaffolding to build a new project (⭐️ Recommended for a quick start).\n- Option 2: Integrate into an existing project by installing the editor package.\n\n## Option 1: Create a FlowGram.AI Application via the Official Template\n\n1. Use the FlowGram CLI to set up a runnable demo.\n\n<PackageManagerTabs command={{\n  npm: \"npx @flowgram.ai/create-app@latest\",\n  pnpm: \"pnpm dlx @flowgram.ai/create-app@latest\",\n  yarn: \"yarn dlx @flowgram.ai/create-app@latest\",\n  bun: \"bunx @flowgram.ai/create-app@latest\",\n}} />\n\n2. Select a template when prompted (it is recommended to choose `Free Layout Demo` for a quick start).\n\n```text\n- Free Layout Demo            # Best practice for free layout (⭐️ Recommended)\n- Free Layout Demo Simple     # Basic usage of free layout\n- Fixed Layout Demo           # Best practice for fixed layout\n- Fixed Layout Demo Simple    # Basic usage of fixed layout\n```\n\n<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(320px, 1fr))', gap: 16, marginTop: 12 }}>\n  <div>\n    <p><strong>Free Layout Demo</strong> [View Online Demo](/examples/free-layout/free-layout-simple.html)</p>\n    <img src=\"/examples/example-free-layout.png\" alt=\"Free Layout Preview\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Fixed Layout Demo</strong> [View Online Demo](/examples/fixed-layout/fixed-layout-simple.html)</p>\n    <img src=\"/examples/example-fixed-layout.png\" alt=\"Fixed Layout Preview\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Free Layout Demo Simple</strong> [View Online Demo](/examples/free-layout/free-layout-simple.html)</p>\n    <img src=\"/examples/example-free-layout-simple.png\" alt=\"Free Layout Simple Preview\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Fixed Layout Demo Simple</strong> [View Online Demo](/examples/fixed-layout/fixed-layout-simple.html)</p>\n    <img src=\"/examples/example-fixed-layout-simple.png\" alt=\"Fixed Layout Simple Preview\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n</div>\n\n3. Check the installed directory name.\n\n- For a project created with the Free Layout Demo template, the directory name is `demo-free-layout`.\n- For a project created with the Free Layout Demo Simple template, the directory name is `demo-free-layout-simple`.\n- For a project created with the Fixed Layout Demo template, the directory name is `demo-fixed-layout`.\n- For a project created with the Fixed Layout Demo Simple template, the directory name is `demo-fixed-layout-simple`.\n\n4. Enter the project directory.\n\n```sh\ncd [project-name]\n```\n\n5. Install dependencies.\n\n<PackageManagerTabs command={{\n  npm: \"npm install\",\n  pnpm: \"pnpm install\",\n  yarn: \"yarn install\",\n  bun: \"bun install\",\n}} />\n\n6. Start the development server.\n\n<PackageManagerTabs command={{\n  npm: \"npm run dev\",\n  pnpm: \"pnpm dev\",\n  yarn: \"yarn dev\",\n  bun: \"bun dev\",\n}} />\n\n## Option 2: Install the Editor Package Directly\n\n:::tip\nThis method is suitable for developers who have some familiarity with the FlowGram project.\n\nIf you are new to FlowGram, we recommend choosing Option 1 first to familiarize yourself with the project, and then gradually integrate the required code into your existing project.\n:::\n\nIf you need to add the package to an existing project, choose a layout type:\n\n<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>\n  <div>\n    <strong>Free Layout</strong>\n    <p>Nodes can be dragged freely on the canvas, and edges can be used to connect nodes to establish logical relationships between them.</p>\n    <p>Next: [Create a Free Layout Canvas](/guide/getting-started/free-layout)</p>\n    <img src=\"/free-layout/free-layout-demo.gif\" alt=\"Free Layout Demo\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <strong>Fixed Layout</strong>\n    <p>The position of nodes in the graph represents the logical relationship between them.</p>\n    <p>Next: [Create a Fixed Layout Canvas](/guide/getting-started/fixed-layout)</p>\n    <img src=\"/fixed-layout/fixed-layout-demo.gif\" alt=\"Fixed Layout Demo\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/_meta.json",
    "content": "[\n  \"background-plugin\",\n  \"minimap-plugin\",\n  \"export-plugin\",\n  \"panel-manager-plugin\",\n  \"free-auto-layout-plugin\",\n  \"free-stack-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/background-plugin.mdx",
    "content": "# @flowgram.ai/background-plugin\n\nThe background plugin is used to customize canvas background effects, supporting dot patterns, logo display, and neumorphism visual effects.\n\n## Background Configuration\n\nThe background plugin is provided through `@flowgram.ai/background-plugin`(built-in), configuration options include:\n\n### Basic Configuration\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-color.png\"/>\n\n```ts pure\n{\n  // Background color\n  backgroundColor: '#1a1a1a',\n\n  // Dot color\n  dotColor: '#ffffff',\n\n  // Dot size (pixels)\n  dotSize: 1,\n\n  // Grid spacing (pixels)\n  gridSize: 20,\n\n  // Dot opacity (0-1)\n  dotOpacity: 0.5,\n\n  // Dot fill color\n  dotFillColor: '#ffffff'\n}\n```\n\n### Logo Configuration\n\nSupports both text and image logo types:\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-logo.png\"/>\n\n```ts pure\n{\n  logo: {\n    // Logo text\n    text: 'FLOWGRAM.AI',\n\n    // Image URL (optional, higher priority than text)\n    imageUrl: 'https://example.com/logo.png',\n\n    // Position: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n    position: 'center',\n\n    // Size\n    size: 200,\n\n    // Opacity (0-1)\n    opacity: 0.25,\n\n    // Color\n    color: '#ffffff',\n\n    // Font family\n    fontFamily: 'Arial, sans-serif',\n\n    // Font weight\n    fontWeight: 'bold',\n\n    // Custom offset\n    offset: { x: 0, y: 0 }\n  }\n}\n```\n\n### Neumorphism Effect\n\nNeumorphism is a modern visual design style that creates depth through dual soft shadows:\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-neumorphism.png\"/>\n\n```ts pure\n{\n  logo: {\n    neumorphism: {\n      // Enable neumorphism effect\n      enabled: true,\n\n      // Text color\n      textColor: '#E0E0E0',\n\n      // Light shadow color\n      lightShadowColor: 'rgba(255,255,255,0.9)',\n\n      // Dark shadow color\n      darkShadowColor: 'rgba(0,0,0,0.15)',\n\n      // Shadow offset distance\n      shadowOffset: 6,\n\n      // Shadow blur radius\n      shadowBlur: 12,\n\n      // Shadow intensity\n      intensity: 0.6,\n\n      // Raised effect (true=raised, false=inset)\n      raised: true\n    }\n  }\n}\n```\n\n## Usage Example\n\n```tsx pure\n// Use background property directly in editor configuration\nconst editorProps = {\n  // Background configuration\n  background: {\n    // Dark theme background\n    backgroundColor: '#1a1a1a',\n    dotColor: '#ffffff',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.3,\n\n    // Brand logo\n    logo: {\n      text: 'FLOWGRAM.AI',\n      position: 'center',\n      size: 200,\n      opacity: 0.25,\n      color: '#ffffff',\n      fontFamily: 'Arial, sans-serif',\n      fontWeight: 'bold',\n\n      // Neumorphism effect\n      neumorphism: {\n        enabled: true,\n        textColor: '#E0E0E0',\n        lightShadowColor: 'rgba(255,255,255,0.9)',\n        darkShadowColor: 'rgba(0,0,0,0.15)',\n        shadowOffset: 6,\n        shadowBlur: 12,\n        intensity: 0.6,\n        raised: true\n      }\n    }\n  }\n}\n```\n\n## Preset Styles\n\n### Classic Dark Theme\n\n```tsx pure\nconst editorProps = {\n  background: {\n    backgroundColor: '#1a1a1a',\n    dotColor: '#ffffff',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.3,\n    logo: {\n      text: 'Your Brand',\n      position: 'center',\n      size: 200,\n      opacity: 0.25,\n      color: '#ffffff',\n      neumorphism: {\n        enabled: true,\n        textColor: '#E0E0E0',\n        lightShadowColor: 'rgba(255,255,255,0.9)',\n        darkShadowColor: 'rgba(0,0,0,0.15)',\n        shadowOffset: 6,\n        shadowBlur: 12,\n        intensity: 0.6,\n        raised: true\n      }\n    }\n  }\n}\n```\n\n### Minimal White Theme\n\n```tsx pure\nconst editorProps = {\n  background: {\n    backgroundColor: '#ffffff',\n    dotColor: '#000000',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.1,\n    logo: {\n      text: 'Your Brand',\n      position: 'center',\n      size: 200,\n      opacity: 0.1,\n      color: '#000000'\n    }\n  }\n}\n```\n\n## Notes\n\n1. **Color Matching**: Ensure sufficient contrast between logo color and background color\n2. **Opacity Settings**: Logo opacity should not be too high to avoid affecting content readability\n3. **Neumorphism Effect**: Shadow parameters should be adjusted reasonably, overly strong effects may distract attention\n4. **Performance Considerations**: Complex shadow effects may impact rendering performance, consider simplifying on low-end devices\n\n## Type Definitions\n\n```ts\ninterface BackgroundLayerOptions {\n  /** Grid spacing, default 20px */\n  gridSize?: number;\n  /** Dot size, default 1px */\n  dotSize?: number;\n  /** Dot color, default \"#eceeef\" */\n  dotColor?: string;\n  /** Dot opacity, default 0.5 */\n  dotOpacity?: number;\n  /** Background color, default transparent */\n  backgroundColor?: string;\n  /** Dot fill color, default same as stroke color */\n  dotFillColor?: string;\n  /** Logo configuration */\n  logo?: {\n    /** Logo text content */\n    text?: string;\n    /** Logo image URL */\n    imageUrl?: string;\n    /** Logo position, default 'center' */\n    position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n    /** Logo size, default 'medium' */\n    size?: 'small' | 'medium' | 'large' | number;\n    /** Logo opacity, default 0.1 */\n    opacity?: number;\n    /** Logo color (text only), default \"#cccccc\" */\n    color?: string;\n    /** Logo font family (text only), default 'Arial, sans-serif' */\n    fontFamily?: string;\n    /** Logo font weight (text only), default 'normal' */\n    fontWeight?: 'normal' | 'bold' | 'lighter' | number;\n    /** Custom offset */\n    offset?: { x: number; y: number };\n    /** Neumorphism effect configuration */\n    neumorphism?: {\n      /** Enable neumorphism effect */\n      enabled: boolean;\n      /** Text color */\n      textColor?: string;\n      /** Light shadow color */\n      lightShadowColor?: string;\n      /** Dark shadow color */\n      darkShadowColor?: string;\n      /** Shadow offset distance */\n      shadowOffset?: number;\n      /** Shadow blur radius */\n      shadowBlur?: number;\n      /** Shadow intensity */\n      intensity?: number;\n      /** Raised effect (true=raised, false=inset) */\n      raised?: boolean;\n    };\n  };\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/export-plugin.mdx",
    "content": "# @flowgram.ai/export-plugin\n\nimport { PackageManagerTabs } from '@theme';\n\n<PackageManagerTabs command={{\n  npm: \"npm install @flowgram.ai/export-plugin\"\n}} />\n\nThe export plugin provides functionality to export workflows as images (PNG, JPEG, SVG) or data files (JSON, YAML).\n\n## Quick Start\n\n1. Install\n\n<PackageManagerTabs command=\"install @flowgram.ai/export-plugin\" />\n\n2. Register the plugin\n\n```tsx pure\nimport { createDownloadPlugin } from '@flowgram.ai/export-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [createDownloadPlugin({})]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n## Options\n\n```ts pure\ncreateDownloadPlugin({\n  // Custom export filename\n  // Default: `flowgram-${nanoid(5)}.${format}`\n  getFilename: (format) => `my-workflow.${format}`,\n\n  // Watermark SVG for exported images\n  // The watermark will be displayed at the bottom-right corner\n  watermarkSVG: '<svg>...</svg>',\n})\n```\n\n## Supported Export Formats\n\n```ts pure\nimport { FlowDownloadFormat } from '@flowgram.ai/export-plugin'\n\n// Image formats\nFlowDownloadFormat.PNG   // PNG image\nFlowDownloadFormat.JPEG  // JPEG image\nFlowDownloadFormat.SVG   // SVG vector image\n\n// Data formats\nFlowDownloadFormat.JSON  // JSON data\nFlowDownloadFormat.YAML  // YAML data\n```\n\n## Using FlowDownloadService\n\nUse `FlowDownloadService` in your components to trigger exports:\n\n```tsx pure\nimport { useService } from '@flowgram.ai/free-layout-editor';\nimport { FlowDownloadService, FlowDownloadFormat } from '@flowgram.ai/export-plugin';\n\nexport const ExportButton = () => {\n  const downloadService = useService(FlowDownloadService);\n\n  const handleExportPNG = async () => {\n    await downloadService.download({ format: FlowDownloadFormat.PNG });\n  };\n\n  const handleExportJSON = async () => {\n    await downloadService.download({ format: FlowDownloadFormat.JSON });\n  };\n\n  return (\n    <div>\n      <button onClick={handleExportPNG} disabled={downloadService.downloading}>\n        Export PNG\n      </button>\n      <button onClick={handleExportJSON} disabled={downloadService.downloading}>\n        Export JSON\n      </button>\n    </div>\n  );\n};\n```\n\n## Listening to Download Status\n\n```tsx pure\nimport { useEffect, useState } from 'react';\nimport { useService } from '@flowgram.ai/free-layout-editor';\nimport { FlowDownloadService } from '@flowgram.ai/export-plugin';\n\nexport const DownloadStatus = () => {\n  const downloadService = useService(FlowDownloadService);\n  const [downloading, setDownloading] = useState(false);\n\n  useEffect(() => {\n    const disposer = downloadService.onDownloadingChange((value) => {\n      setDownloading(value);\n    });\n    return () => disposer.dispose();\n  }, [downloadService]);\n\n  return downloading ? <span>Exporting...</span> : null;\n};\n```\n\n## Type Definitions\n\n```ts pure\nimport { FlowDownloadFormat } from '@flowgram.ai/export-plugin';\n\ninterface DownloadServiceOptions {\n  // Function to customize export filename\n  getFilename?: (format: FlowDownloadFormat) => string;\n\n  // Watermark SVG string for exported images\n  watermarkSVG?: string;\n}\n\ninterface WorkflowDownloadParams {\n  // Export format\n  format: FlowDownloadFormat;\n}\n\nenum FlowDownloadFormat {\n  JSON = 'json',\n  YAML = 'yaml',\n  PNG = 'png',\n  JPEG = 'jpeg',\n  SVG = 'svg',\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/free-auto-layout-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/free-auto-layout-plugin\n\nAn automatic layout plugin based on the Dagre algorithm that provides intelligent node arrangement functionality for free layout canvases.\n\n## Features\n\n- Based on Dagre directed graph layout algorithm, automatically calculates optimal node positions\n- Supports multiple layout directions (left-to-right, top-to-bottom, etc.)\n- Configurable node spacing, margins, and other layout parameters\n- Supports recursive layout for nested containers\n- Provides animation effects and view adaptation functionality\n- Integrates with history system, supports undo/redo operations\n\n![Preview](@/public/plugin/auto-layout.gif)\n\n## Quick Start\n\n1. Installation\n\n<PackageManagerTabs command=\"install @flowgram.ai/free-auto-layout-plugin\" />\n\n2. Register Plugin\n\nThe plugin registration method is basically the same as other flowgram plugins. Just ensure not to create duplicates and pass it to the corresponding FreeLayoutEditorProvider.\n\n```tsx\nimport { createFreeAutoLayoutPlugin } from '@flowgram.ai/free-auto-layout-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeAutoLayoutPlugin({\n      layoutConfig: {\n        rankdir: 'LR', // Layout direction: left to right\n        nodesep: 100,  // Node spacing\n        ranksep: 100,  // Rank spacing\n      }\n    })\n  ]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n3. Use in React Components\n\nYou can also trigger auto layout in components using utility classes:\n\n```tsx\nimport { WorkflowAutoLayoutTool } from '@flowgram.ai/free-layout-editor';\n\nconst AutoLayoutButton = () => {\n  const tools = usePlaygroundTools();\n  const playground = usePlayground();\n\n  const handleAutoLayout = async () => {\n    await tools.autoLayout({\n      enableAnimation: true,      // Enable animation effects\n      animationDuration: 1000,     // Animation duration\n      disableFitView: false,      // Auto fit view after layout\n    });\n  }\n\n  return (\n    <button onClick={handleAutoLayout}>\n      Auto Layout\n    </button>\n  );\n};\n```\n\n4. Use Auto Layout Service\n\nExecute layout by obtaining AutoLayoutService instance through dependency injection:\n\n```tsx\nimport { AutoLayoutService } from '@flowgram.ai/free-auto-layout-plugin';\n\nclass MyLayoutService {\n  @inject(AutoLayoutService)\n  private autoLayoutService: AutoLayoutService;\n\n  async performAutoLayout() {\n    await this.autoLayoutService.layout({\n      enableAnimation: true,      // Enable animation effects\n      animationDuration: 1000,     // Animation duration\n      disableFitView: false,      // Auto fit view after layout\n    });\n  }\n}\n```\n\n## Configuration Options\n\n### LayoutConfig\n\nConfiguration parameters for the layout algorithm:\n\n```typescript\ninterface LayoutConfig {\n  /** Layout direction */\n  rankdir?: 'TB' | 'BT' | 'LR' | 'RL';\n  /** Alignment */\n  align?: 'UL' | 'UR' | 'DL' | 'DR';\n  /** Node separation within same rank */\n  nodesep?: number;\n  /** Edge separation */\n  edgesep?: number;\n  /** Rank separation */\n  ranksep?: number;\n  /** Horizontal margin */\n  marginx?: number;\n  /** Vertical margin */\n  marginy?: number;\n  /** Cycle removal algorithm */\n  acyclicer?: string;\n  /** Ranking algorithm */\n  ranker?: 'network-simplex' | 'tight-tree' | 'longest-path';\n}\n```\n\n### LayoutOptions\n\nOptions for layout execution:\n\n```typescript\ninterface LayoutOptions {\n  /** Container node, defaults to root node */\n  containerNode?: WorkflowNodeEntity;\n  /** Function to get follow node */\n  getFollowNode?: GetFollowNode;\n  /** Disable auto fit view */\n  disableFitView?: boolean;\n  /** Enable animation effects */\n  enableAnimation?: boolean;\n  /** Animation duration (milliseconds) */\n  animationDuration?: number;\n  /** Node filter function to control which nodes participate in layout calculation */\n  filterNode?: (params: { node: WorkflowNodeEntity; parent?: WorkflowNodeEntity }) => boolean;\n}\n```\n\n## Layout Algorithm\n\n### Dagre Algorithm\n\nThe plugin is implemented based on the Dagre library, which is a JavaScript library specifically designed for directed graph layout. Algorithm features:\n\n- **Hierarchical Layout**: Organizes nodes into different levels based on dependency relationships\n- **Minimize Crossings**: Attempts to minimize line crossings\n- **Even Distribution**: Evenly distributes nodes while satisfying constraints\n\n### Layout Process\n\n1. **Graph Construction**: Converts workflow nodes and connections into Dagre graph structure\n2. **Rank Calculation**: Calculates levels (ranks) based on node dependencies\n3. **Order Optimization**: Optimizes node order within each level to reduce crossings\n4. **Position Calculation**: Calculates final coordinate positions for each node\n5. **Animation Execution**: If animation is enabled, smoothly transitions to new positions\n\n## Advanced Usage\n\n### Custom Follow Node\n\nYou can customize node following relationships through the `getFollowNode` function:\n\n```typescript\nconst layoutOptions: LayoutOptions = {\n  getFollowNode: (node: LayoutNode) => {\n    // Return the node ID that should follow the current node\n    if (node.flowNodeType === 'comment') {\n      return getNearestNode(node);\n    }\n    return undefined;\n  }\n};\nawait tools.autoLayout(layoutOptions);\n```\n\n### Node Filtering\n\nYou can control which nodes participate in layout calculation through the `filterNode` function:\n\n```typescript\nconst layoutOptions: LayoutOptions = {\n  filterNode: ({ node, parent }) => {\n    // Filter out specific types of nodes\n    if (node.flowNodeType === 'comment') {\n      return false;\n    }\n\n    // Filter based on parent node conditions\n    if (parent && parent.flowNodeType.type === 'group') {\n      return false;\n    }\n\n    return true; // Include all nodes by default\n  }\n};\n\n// Execute layout with filter options\nawait tools.autoLayout(layoutOptions);\n```\n\n### Layout Only for the Specified Container\n\nThe plugin supports recursive layout for a specified container and automatically handles node arrangement within the container:\n\n```typescript\n// Execute layout for specific container\nawait autoLayoutService.layout({\n  containerNode: specificContainerNode,\n  enableAnimation: true,\n});\n```\n\n## FAQ\n\n### Q: How to trigger auto layout during initialization?\n\nA: Call the `ctx.tool.autoLayout()` method after the canvas rendering is complete to trigger auto layout.\n\n```typescript\nconst editorProps = useMemo(() => ({\n  onAllLayersRendered: (ctx) => {\n    ctx.tool.autoLayout({\n      enableAnimation: false, // Disable animation during initialization for better user experience\n    }\n    );\n  }\n}), []);\n```\n\n### Q: How to implement custom layout directions?\n\nA: Method 1: Register AutoLayout plugin in EditorProps and control layout direction through the `rankdir` parameter:\n\n```typescript\n\nimport { createFreeAutoLayoutPlugin } from '@flowgram.ai/free-auto-layout-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeAutoLayoutPlugin({\n      layoutConfig: {\n        rankdir: 'TB', // Top to bottom\n        // rankdir: 'LR', // Left to right (default)\n        // rankdir: 'RL', // Right to left\n        // rankdir: 'BT', // Bottom to top\n      }\n    })\n  ]\n}), []);\n```\n\nMethod 2: Pass layout configuration through the `layoutConfig` parameter when calling the `autoLayout` method:\n\n```typescript\nconst tools = usePlaygroundTools();\nconst playground = usePlayground();\n\nconst handleAutoLayout = async () => {\n  await tools.autoLayout({\n    layoutConfig: {\n      rankdir: 'TB', // Top to bottom\n      // rankdir: 'LR', // Left to right (default)\n      // rankdir: 'RL', // Right to left\n      // rankdir: 'BT', // Bottom to top\n    }\n  });\n}\n```\n\n### Q: How to optimize layout animation stuttering?\n\nA: For complex workflows, it's recommended to disable animation or reduce animation duration:\n\n```typescript\nlayoutOptions: {\n  enableAnimation: false, // Disable animation\n  // or\n  animationDuration: 150, // Reduce animation duration\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/free-stack-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/free-stack-plugin\n\nA layer management plugin that provides z-index layer control functionality for nodes and connections in free layout canvas.\n\n## Features\n\n- Intelligently calculates layer relationships between nodes and connections to avoid occlusion issues\n- Supports automatic top-level display for selected nodes\n- Supports highlighting of hovered nodes and connections\n- Customizable node sorting rules to control rendering order of nodes at the same level\n- Automatically handles parent-child node layer relationships\n- Supports intelligent layer management for connections, ensuring connection visibility\n- Real-time response to node selection, hover, and entity change events\n\n## Quick Start\n\n1. Installation\n\n<PackageManagerTabs command=\"install @flowgram.ai/free-stack-plugin\" />\n\n2. Register Plugin\n\nThe plugin registration method is basically the same as other flowgram plugins. Just make sure not to create duplicates and finally pass it to the corresponding FreeLayoutEditorProvider.\n\n```tsx\nimport { createFreeStackPlugin } from '@flowgram.ai/free-stack-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeStackPlugin()\n  ]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n3. Custom Node Sorting\n\nYou can customize the sorting rules for nodes at the same level through the `sortNodes` function:\n\n```tsx\nimport { createFreeStackPlugin } from '@flowgram.ai/free-stack-plugin';\nimport { WorkflowNodeType } from './nodes/constants';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeStackPlugin({\n      sortNodes: (nodes) => {\n        const commentNodes = [];\n        const otherNodes = [];\n\n        // Separate comment nodes from other nodes\n        nodes.forEach((node) => {\n          if (node.flowNodeType === WorkflowNodeType.Comment) {\n            commentNodes.push(node);\n          } else {\n            otherNodes.push(node);\n          }\n        });\n\n        // Comment nodes render at the bottom layer, other nodes at the top layer\n        return [...commentNodes, ...otherNodes];\n      },\n    })\n  ]\n}), []);\n```\n\n## Configuration Options\n\n### FreeStackPluginOptions\n\nPlugin configuration options:\n\n```typescript\ninterface FreeStackPluginOptions {\n  /** Custom node sorting function */\n  sortNodes?: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n}\n```\n\n### sortNodes Function\n\nUsed to customize sorting rules for nodes at the same level:\n\n```typescript\ntype SortNodesFunction = (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n```\n\n**Parameter Description:**\n- `nodes`: Array of nodes to be sorted\n- **Return Value**: Array of sorted nodes\n\n**Use Cases:**\n- Place specific types of nodes (like comments) at the bottom layer\n- Sort nodes by business priority\n- Sort by creation time or other attributes\n\n## Layer Management Algorithm\n\n### Basic Layer Calculation\n\nThe plugin uses an intelligent algorithm to calculate the layer for each node and connection:\n\n1. **Base Layer**: Starts calculation from `BASE_Z_INDEX` (default is 8)\n2. **Node Layer**: Calculated based on node nesting relationships and sorting rules\n3. **Connection Layer**: Ensures connections are not occluded by nodes while handling special cases\n\n### Layer Elevation Rules\n\nThe following situations will trigger layer elevation:\n\n- **Selected Nodes**: Selected nodes will be elevated to the top layer\n- **Hovered Elements**: Hovered nodes or connections will be highlighted\n- **Drawing Connections**: Connections being drawn will be placed at the top layer\n- **Parent-Child Relationship Connections**: Connections between parent-child nodes will be prioritized for display\n\n### Layer Calculation Process\n\n1. **Initialization**: Clear cache, calculate basic parameters\n2. **Node Indexing**: Establish node index mapping\n3. **Selected Node Processing**: Mark parent relationships of selected nodes\n4. **Layer Assignment**: Recursively process node layers\n5. **Connection Processing**: Calculate connection layers, ensure visibility\n6. **Style Application**: Apply calculation results to DOM elements\n\n## Advanced Usage\n\n### Complex Sorting Rules\n\nYou can implement complex node sorting logic:\n\n```typescript\nconst sortNodes = (nodes: WorkflowNodeEntity[]) => {\n  return nodes.sort((a, b) => {\n    // 1. Sort by node type priority\n    const typeOrder = {\n      [WorkflowNodeType.Comment]: 0,\n      [WorkflowNodeType.Start]: 1,\n      [WorkflowNodeType.End]: 2,\n      // ... other types\n    };\n\n    const aOrder = typeOrder[a.flowNodeType] ?? 999;\n    const bOrder = typeOrder[b.flowNodeType] ?? 999;\n\n    if (aOrder !== bOrder) {\n      return aOrder - bOrder;\n    }\n\n    // 2. Sort by creation time\n    return a.createTime - b.createTime;\n  });\n};\n```\n\n## FAQ\n\n### Q: How to keep specific types of nodes always at the bottom layer?\n\nA: Place these nodes at the front of the array through the `sortNodes` function:\n\n```typescript\nconst sortNodes = (nodes) => {\n  const backgroundNodes = nodes.filter(node =>\n    node.flowNodeType === WorkflowNodeType.Comment\n  );\n  const foregroundNodes = nodes.filter(node =>\n    node.flowNodeType !== WorkflowNodeType.Comment\n  );\n\n  return [...backgroundNodes, ...foregroundNodes];\n};\n```\n\n### Q: How to disable automatic layer management?\n\nA: Currently, the plugin does not provide a disable option. If you need complete custom layer management, it is recommended not to use this plugin and directly set z-index in node components.\n\n### Q: Performance optimization suggestions?\n\nA: The plugin already has built-in performance optimizations:\n- Uses debounce mechanism to reduce calculation frequency\n- Only recalculates layers when necessary\n- Uses Map data structure to improve lookup efficiency\n\nFor large canvases (over 1000 nodes), it is recommended to:\n- Simplify the logic of the `sortNodes` function\n- Avoid complex calculations in sorting functions\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/minimap-plugin.mdx",
    "content": "# @flowgram.ai/minimap-plugin\n\nimport { PackageManagerTabs } from '@theme';\n\n<PackageManagerTabs command={{\n  npm: \"npm install @flowgram.ai/minimap-plugin\"\n}} />\n\n\n## EditorProps\n\n```ts pure\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin'\n\n\n{\n  plugins: () => [\n    /**\n     * Minimap plugin\n     */\n    createMinimapPlugin({\n      disableLayer: true,\n      enableDisplayAllNodes: true,\n      canvasStyle: {\n        canvasWidth: 182,\n        canvasHeight: 102,\n        canvasPadding: 50,\n        canvasBackground: 'rgba(245, 245, 245, 1)',\n        canvasBorderRadius: 10,\n        viewportBackground: 'rgba(235, 235, 235, 1)',\n        viewportBorderRadius: 4,\n        viewportBorderColor: 'rgba(201, 201, 201, 1)',\n        viewportBorderWidth: 1,\n        viewportBorderDashLength: 2,\n        nodeColor: 'rgba(255, 255, 255, 1)',\n        nodeBorderRadius: 2,\n        nodeBorderWidth: 0.145,\n        nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n        overlayColor: 'rgba(255, 255, 255, 0)',\n      },\n    }),\n  ]\n}\n```\n\n## Minimap Component\n\n```tsx pure\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 16,\n        bottom: 51,\n        zIndex: 100,\n        width: 182,\n      }}\n    >\n      <MinimapRender\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </div>\n  );\n};\n\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/plugin/panel-manager-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/panel-manager-plugin\n\nA plugin for managing different types of panels.\n\n## Features\n\n- Easily integrate custom panels on the right or bottom of the canvas as React components with minimal setup.\n\n- No need for complex style adaptations—the plugin automatically calculates panel boundaries and layout.\n\n- Automatically manages the entry and exit of the panel queue.\n\n![Preview](@/public/plugin/panel-manager-1.png)\n\n## Quick Start\n\n1. Installation\n\n<PackageManagerTabs command=\"install @flowgram.ai/panel-manager-plugin\" />\n\n2. Register the plugin\n\nThe registration process is basically the same as other Flowgram plugins. Just make sure you don’t create duplicates and eventually pass it into the corresponding `LayoutEditorProvider`.\n\n```tsx\nimport { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [createPanelManagerPlugin({})]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n3. Register panel components\n\nA panel registration requires a unique key and a render function render that returns a ReactNode.\n\nFor example, here’s a node form panel:\n\n```tsx\nimport { type PanelFactory } from '@flowgram.ai/panel-manager-plugin';\n\nexport const NODE_FORM_PANEL = 'node-form-panel';\nexport const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {\n  key: NODE_FORM_PANEL,\n  defaultSize: 400,\n  render: (props: NodeFormPanelProps) => <NodeFormPanel {...props} />\n}\n```\n\nPass the defined object into the plugin:\n\n```ts\ncreatePanelManagerPlugin({\n  factories: [nodeFormPanelFactory]\n  getPopupContainer: (ctx) => ctx.playground.node.parentNode,\n  autoResize: true\n})\n```\n\n4. Open/close panels\n\nOpening and closing panels is handled through an instance of PanelManager:\n\n```ts\nimport { PanelManager } from '@flowgram.ai/panel-manager-plugin';\n\nclass NodeFormService {\n  @inject(PanelManager): panelManager: PanelManager;\n\n  openPanel(nodeId: string) {\n    this.panelManager.open(NODE_FORM_PANEL, 'right', {\n      props: {\n        nodeId\n      }\n    })\n  }\n  closePanel() {\n    this.panelManager.close(NODE_FORM_PANEL)\n  }\n}\n```\n\nAlternatively, you can also access the instance in a React component via a hook:\n\n```tsx\nimport { usePanelManager } from '@flowgram.ai/panel-manager-plugin';\n\nconst panelManager = usePanelManager();\n\n<button\n  onClick={() => panelManager.open(TEST_RUN_FORM_PANEL, 'right')}\n>\n  Test Run\n</button>\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/_meta.json",
    "content": "[\n  \"introduction\",\n  \"quick-start\",\n  \"schema\",\n  \"node\",\n  \"api\",\n  \"source-code-guide\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/api.mdx",
    "content": "---\ntitle: API\ndescription: FlowGram Runtime API\nsidebar_position: 2\n---\n\n# FlowGram Runtime API Reference\n\nFlowGram Runtime provides five core APIs for validating, running, monitoring, retrieving results, and canceling workflows. This document details the usage, parameters, and return values of these APIs.\n\n## TaskRun API\n\n### Function Description\n\nThe TaskRun API is used to start a workflow task. It takes a workflow schema and initial inputs, and returns a task ID.\n\n### Parameters\n\nTaskRun API accepts a `TaskRunInput` object as its parameter:\n\n| Parameter | Type | Required | Description |\n| --------- | ---- | -------- | ----------- |\n| schema | string | Yes | JSON string of the workflow schema, defining nodes and edges |\n| inputs | object | No | Initial input parameters for the workflow, can be empty |\n\nThe `schema` parameter is a JSON string that defines the structure of the workflow, including information about nodes and edges. The basic structure of the schema is as follows:\n\n```typescript\ninterface WorkflowSchema {\n  nodes: WorkflowNodeSchema[];\n  edges: WorkflowEdgeSchema[];\n}\n\ninterface WorkflowNodeSchema {\n  id: string;\n  type: FlowGramNode;\n  name?: string;\n  meta: {\n    position: {\n      x: number;\n      y: number;\n    };\n  };\n  data: any;\n  blocks?: WorkflowNodeSchema[];\n  edges?: WorkflowEdgeSchema[];\n}\n\ninterface WorkflowEdgeSchema {\n  sourceNodeID: string;\n  sourcePort: string;\n  targetNodeID: string;\n  targetPort: string;\n}\n```\n\n### Return Value\n\nTaskRun API returns a `TaskRunOutput` object:\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| taskID | string | Unique identifier for the task, used for subsequent queries |\n\n### Error Handling\n\nTaskRun API may throw the following errors:\n\n- **Schema parsing error**: When the provided schema is not a valid JSON string\n- **Schema structure error**: When the schema structure does not match the expected format\n- **Node type error**: When the schema includes unsupported node types\n- **Initialization error**: When the workflow fails to initialize\n\n### Usage Example\n\n```javascript\nimport { TaskRunAPI } from '@flowgram.ai/runtime-js';\n\nconst schema = JSON.stringify({\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      meta: { position: { x: 0, y: 0 } },\n      data: {}\n    },\n    {\n      id: 'llm',\n      type: 'llm',\n      meta: { position: { x: 200, y: 0 } },\n      data: {\n        modelName: 'gpt-3.5-turbo',\n        temperature: 0.7,\n        systemPrompt: 'You are an assistant',\n        prompt: 'Introduce yourself'\n      }\n    },\n    {\n      id: 'end',\n      type: 'end',\n      meta: { position: { x: 400, y: 0 } },\n      data: {}\n    }\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start',\n      sourcePort: 'out',\n      targetNodeID: 'llm',\n      targetPort: 'in'\n    },\n    {\n      sourceNodeID: 'llm',\n      sourcePort: 'out',\n      targetNodeID: 'end',\n      targetPort: 'in'\n    }\n  ]\n});\n\nconst inputs = {\n  userInput: 'Please introduce yourself'\n};\n\nasync function runWorkflow() {\n  try {\n    const result = await TaskRunAPI({\n      schema,\n      inputs\n    });\n    console.log('Task ID:', result.taskID);\n    return result.taskID;\n  } catch (error) {\n    console.error('Failed to start workflow:', error);\n  }\n}\n```\n\n### Notes\n\n- The schema must be a valid JSON string and conform to the WorkflowSchema structure\n- The workflow must include a start node (type: 'start') and an end node (type: 'end')\n- Connections between nodes must be correctly defined through edges\n- After a task is started, it will execute asynchronously. You can use the TaskReport API and TaskResult API to get the execution status and results\n\n## TaskReport API\n\n### Function Description\n\nThe TaskReport API is used to get the execution report of a workflow task, including the task status and the execution status of each node.\n\n### Parameters\n\nTaskReport API accepts a `TaskReportInput` object as its parameter:\n\n| Parameter | Type | Required | Description |\n| --------- | ---- | -------- | ----------- |\n| taskID | string | Yes | Unique identifier for the task, returned by TaskRun API |\n\n### Return Value\n\nTaskReport API returns a `TaskReportOutput` object containing the execution report of the task:\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| workflow | WorkflowStatus | Overall status of the workflow |\n| nodes | `Record<string, NodeStatus>` | Execution status of each node |\n\nThe `WorkflowStatus` structure is as follows:\n\n```typescript\ninterface WorkflowStatus {\n  status: 'idle' | 'processing' | 'success' | 'fail' | 'canceled';\n  terminated: boolean;\n}\n```\n\nThe `NodeStatus` structure is as follows:\n\n```typescript\ninterface NodeStatus {\n  status: 'idle' | 'processing' | 'success' | 'fail' | 'canceled';\n  startTime?: number;\n  endTime?: number;\n}\n```\n\n### Error Handling\n\nTaskReport API may encounter the following error situations:\n\n- **Task does not exist**: When the provided taskID does not exist, returns undefined\n- **Report generation error**: When an error occurs during report generation\n\n### Usage Example\n\n```javascript\nimport { TaskReportAPI } from '@flowgram.ai/runtime-js';\n\nasync function getTaskReport(taskID) {\n  try {\n    const report = await TaskReportAPI({ taskID });\n\n    if (!report) {\n      console.log('Task does not exist or report not generated');\n      return;\n    }\n\n    console.log('Workflow status:', report.workflow.status);\n    console.log('Workflow terminated:', report.workflow.terminated);\n\n    // Print status of each node\n    for (const [nodeId, nodeStatus] of Object.entries(report.nodes)) {\n      console.log(`Node ${nodeId} status:`, nodeStatus.status);\n      if (nodeStatus.startTime) {\n        console.log(`Node ${nodeId} start time:`, new Date(nodeStatus.startTime).toLocaleString());\n      }\n      if (nodeStatus.endTime) {\n        console.log(`Node ${nodeId} end time:`, new Date(nodeStatus.endTime).toLocaleString());\n      }\n    }\n\n    return report;\n  } catch (error) {\n    console.error('Failed to get task report:', error);\n  }\n}\n```\n\n### Notes\n\n- The task report is real-time, you can call TaskReport API multiple times to get the latest execution status\n- If the workflow has not terminated (`workflow.terminated` is false), the workflow is still executing\n- Node status can be 'idle' (not started), 'processing' (executing), 'success' (successful), 'fail' (failed), or 'canceled' (canceled)\n- It is recommended to poll the task report periodically to monitor the progress of the workflow\n\n## TaskCancel API\n\n### Function Description\n\nThe TaskCancel API is used to cancel a running workflow task.\n\n### Parameters\n\nTaskCancel API accepts a `TaskCancelInput` object as its parameter:\n\n| Parameter | Type | Required | Description |\n| --------- | ---- | -------- | ----------- |\n| taskID | string | Yes | Unique identifier for the task, returned by TaskRun API |\n\n### Return Value\n\nTaskCancel API returns a `TaskCancelOutput` object:\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| success | boolean | Indicates whether the task was successfully canceled |\n\n### Error Handling\n\nTaskCancel API may encounter the following error situations:\n\n- **Task does not exist**: When the provided taskID does not exist, returns `{ success: false }`\n- **Task already completed**: When the task has already completed or been canceled, it cannot be canceled again\n\n### Usage Example\n\n```javascript\nimport { TaskCancelAPI } from '@flowgram.ai/runtime-js';\n\nasync function cancelTask(taskID) {\n  try {\n    const result = await TaskCancelAPI({ taskID });\n\n    if (result.success) {\n      console.log('Task successfully canceled');\n    } else {\n      console.log('Failed to cancel task, task may not exist or is already completed');\n    }\n\n    return result.success;\n  } catch (error) {\n    console.error('Failed to cancel task:', error);\n    return false;\n  }\n}\n```\n\n### Notes\n\n- Task cancellation is asynchronous, after a successful cancellation request, the task may take some time to completely stop\n- Tasks that have already completed cannot be canceled\n- After canceling a task, you can check the final status of the task through TaskReport API, the status of a canceled task will change to 'canceled'\n- Canceling a task does not clear the intermediate results of the task, you can still get the results of the executed part through TaskResult API\n\n## TaskResult API\n\n### Function Description\n\nThe TaskResult API is used to get the final result of a workflow task.\n\n### Parameters\n\nTaskResult API accepts a `TaskResultInput` object as its parameter:\n\n| Parameter | Type | Required | Description |\n| --------- | ---- | -------- | ----------- |\n| taskID | string | Yes | Unique identifier for the task, returned by TaskRun API |\n\n### Return Value\n\nTaskResult API returns a `WorkflowOutputs` object containing the output results of the workflow:\n\n```typescript\ntype WorkflowOutputs = Record<string, any>;\n```\n\nThe structure of the returned object depends on the specific implementation and output definition of the workflow.\n\n### Error Handling\n\nTaskResult API may encounter the following error situations:\n\n- **Task does not exist**: When the provided taskID does not exist, returns undefined\n- **Task not completed**: When the task has not terminated, returns undefined\n- **Result retrieval error**: When an error occurs during result retrieval\n\n### Usage Example\n\n```javascript\nimport { TaskResultAPI } from '@flowgram.ai/runtime-js';\n\nasync function getTaskResult(taskID) {\n  try {\n    const result = await TaskResultAPI({ taskID });\n\n    if (!result) {\n      console.log('Task does not exist or is not yet completed');\n      return;\n    }\n\n    console.log('Task result:', result);\n    return result;\n  } catch (error) {\n    console.error('Failed to get task result:', error);\n  }\n}\n\n// Usage example: wait for task to complete and get result\nasync function waitForResult(taskID, pollingInterval = 1000, timeout = 60000) {\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeout) {\n    // Get task report\n    const report = await TaskReportAPI({ taskID });\n\n    // If task has terminated, get result\n    if (report && report.workflow.terminated) {\n      return await TaskResultAPI({ taskID });\n    }\n\n    // Wait for a while before checking again\n    await new Promise(resolve => setTimeout(resolve, pollingInterval));\n  }\n\n  throw new Error('Timeout waiting for task result');\n}\n```\n\n### Notes\n\n- Results can only be obtained when the task has terminated (completed, failed, or canceled)\n- If the task has not yet completed, TaskResult API will return undefined\n- It is recommended to check if the task has terminated through TaskReport API before calling TaskResult API to get the result\n- For canceled tasks, only partial results or no results may be available\n- The specific structure of the result depends on the definition of the workflow, and needs to be parsed according to the actual output of the workflow\n\n## TaskValidate API\n\n### Function Description\n\nThe TaskValidate API is used to validate the validity of workflow schema and input parameters before actually running the workflow. This API helps you discover potential configuration errors before starting the workflow.\n\n### Parameters\n\nTaskValidate API accepts a `TaskValidateInput` object as its parameter:\n\n| Parameter | Type | Required | Description |\n| --------- | ---- | -------- | ----------- |\n| schema | string | Yes | JSON string of the workflow schema, defining nodes and edges |\n| inputs | object | No | Initial input parameters for the workflow, used to validate if inputs meet schema requirements |\n\n### Return Value\n\nTaskValidate API returns a `TaskValidateOutput` object:\n\n| Field | Type | Description |\n| ----- | ---- | ----------- |\n| valid | boolean | Indicates whether validation passed, true means validation succeeded, false means validation failed |\n| errors | string[] | Optional field, contains specific error message list when validation fails |\n\n### Validation Content\n\nTaskValidate API validates the following content:\n\n- **Schema Structure Validation**: Checks if the schema conforms to WorkflowSchema format requirements\n- **Node Type Validation**: Validates if node types in the schema are supported\n- **Edge Connection Validation**: Checks if connections between nodes are correct\n- **Input Parameter Validation**: Validates if provided inputs meet the input requirements defined in the schema\n- **Workflow Integrity Validation**: Checks if the workflow contains necessary start and end nodes\n\n### Error Handling\n\nTaskValidate API may return the following types of validation errors:\n\n- **Schema parsing error**: When the provided schema is not a valid JSON string\n- **Schema structure error**: When the schema structure does not match the expected format\n- **Node configuration error**: When node configuration is incomplete or incorrect\n- **Connection error**: When there are issues with connections between nodes\n- **Input parameter error**: When input parameters do not meet requirements\n\n### Usage Example\n\n```javascript\nimport { TaskValidateAPI } from '@flowgram.ai/runtime-js';\n\nconst schema = JSON.stringify({\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      meta: { position: { x: 0, y: 0 } },\n      data: {\n        outputs: {\n          type: 'object',\n          properties: {\n            userInput: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            }\n          },\n          required: ['userInput'],\n        },\n      }\n    },\n    {\n      id: 'llm',\n      type: 'llm',\n      meta: { position: { x: 200, y: 0 } },\n      data: {\n        title: 'LLM_0',\n        inputsValues: {\n          prompt: {\n            type: 'ref',\n            content: ['start_0', 'userInput'],\n          }\n        },\n        inputs: {\n          type: 'object',\n          required: ['editor'],\n          properties: {\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'end',\n      type: 'end',\n      meta: { position: { x: 400, y: 0 } },\n      data: {}\n    }\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start',\n      sourcePort: 'out',\n      targetNodeID: 'llm',\n      targetPort: 'in'\n    },\n    {\n      sourceNodeID: 'llm',\n      sourcePort: 'out',\n      targetNodeID: 'end',\n      targetPort: 'in'\n    }\n  ]\n});\n\nconst inputs = {\n  userInput: 'Please introduce yourself'\n};\n\nasync function validateWorkflow() {\n  try {\n    const result = await TaskValidateAPI({\n      schema,\n      inputs\n    });\n\n    if (result.valid) {\n      console.log('Workflow validation passed, safe to run');\n      return true;\n    } else {\n      console.error('Workflow validation failed:');\n      result.errors?.forEach(error => {\n        console.error('- ' + error);\n      });\n      return false;\n    }\n  } catch (error) {\n    console.error('Error occurred during validation:', error);\n    return false;\n  }\n}\n\n// Validate before running the workflow\nasync function safeRunWorkflow() {\n  const isValid = await validateWorkflow();\n  if (isValid) {\n    // Run the workflow after validation passes\n    const runResult = await TaskRunAPI({ schema, inputs });\n    console.log('Workflow started, Task ID:', runResult.taskID);\n  } else {\n    console.log('Workflow validation failed, please fix errors and retry');\n  }\n}\n```\n\n### Notes\n\n- It is recommended to call TaskValidate API before calling TaskRun API for validation\n- Passing validation does not guarantee that the workflow will not encounter errors during runtime, but it can identify most configuration issues in advance\n- TaskValidate API typically executes faster than TaskRun API, making it suitable for real-time validation\n- Error messages in validation results can help quickly locate and fix issues\n- For complex workflows, it is recommended to use this API frequently during development for validation\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/introduction.mdx",
    "content": "---\ntitle: Introduction\ndescription: Basic concepts and design principles of FlowGram Runtime\nsidebar_position: 2\n---\n\n# Introduction to FlowGram Runtime\n\n**⚠️ FlowGram Runtime is currently in early development stage, and only supports nodejs runtime, and only work in free layout.**\n\n<div className=\"rs-highlight\">\n**⚠️ Currently in early development stage**\n\n- Unstable API, API interfaces may change, with no guarantee of backward compatibility\n- Only supports nodejs runtime, and only work in free layout\n</div>\n\nThis document introduces the basic concepts, design principles, and core features of FlowGram Runtime to help business integration developers understand and use this reference implementation of a workflow runtime engine.\n\n## What is FlowGram Runtime\n\nFlowGram Runtime is a reference implementation of a workflow runtime engine, designed to provide runtime reference for business developers. It can parse and execute graph-based workflows, supporting various node types including Start, End, LLM, Condition, Loop, and more.\n\n### Project Positioning and Goals\n\nFlowGram Runtime is **positioned as a demo rather than an SDK**, with the following main goals:\n\n- Provide design and implementation reference for workflow runtimes\n- Demonstrate how to build and extend workflow engines\n- Offer code that developers can directly learn from and modify\n- Support rapid prototyping and proof of concept\n\nAs a reference implementation, FlowGram Runtime will not be published as a package. Developers need to fork the repository and modify it according to their specific business scenarios and requirements.\n\n## Core Concepts\n\n### Workflow\n\nA workflow is a directed graph composed of nodes and edges, describing the execution order and logical relationships of a series of tasks. In FlowGram Runtime, workflows are defined in JSON format, containing nodes and edges.\n\nExample workflow definition:\n\n```json\n{\n  \"nodes\": [\n    { \"id\": \"start\", \"type\": \"Start\", \"meta\": {}, \"data\": {} },\n    { \"id\": \"llm\", \"type\": \"LLM\", \"meta\": {}, \"data\": { \"systemPrompt\": \"You are an assistant\", \"userPrompt\": \"{{start.input}}\" } },\n    { \"id\": \"end\", \"type\": \"End\", \"meta\": {}, \"data\": {} }\n  ],\n  \"edges\": [\n    { \"sourceNodeID\": \"start\", \"targetNodeID\": \"llm\" },\n    { \"sourceNodeID\": \"llm\", \"targetNodeID\": \"end\" }\n  ]\n}\n```\n\n### Node\n\nNodes are the basic execution units in a workflow, each representing a specific operation or task. FlowGram Runtime supports various node types, including:\n\n- **Start Node**: The starting point of a workflow, providing workflow input\n- **End Node**: The endpoint of a workflow, collecting workflow output\n- **LLM Node**: Calls large language models, supporting system prompts and user prompts\n- **Condition Node**: Selects different execution branches based on conditions, supporting various comparison operators\n- **Loop Node**: Performs the same operation on each element in an array, supporting sub-workflows\n\nEach node contains ID, type, metadata, and data information, with different node types having different configuration options and behaviors.\n\n### Edge\n\nEdges define the connection relationships between nodes, representing the direction of data and control flow. Each edge contains source node, target node, and optional source port information.\n\nThe definition of edges determines the execution path and data flow of the workflow, forming the foundation for building complex workflow logic.\n\n### Execution Engine\n\nThe execution engine is responsible for parsing workflow definitions, executing nodes in the defined logical order, and handling data flow between nodes. It is the core component of FlowGram Runtime, managing the entire lifecycle of workflows.\n\n## Technical Architecture\n\nFlowGram Runtime adopts a Domain-Driven Design (DDD) architecture, dividing the system into multiple domains:\n\n- **Document**: Data structures for workflow definitions, including node and edge models\n- **Engine**: Core logic for workflow execution, responsible for workflow parsing and scheduling\n- **Executor**: Responsible for executing the specific logic of various node types, such as LLM calls and condition evaluation\n- **State**: Maintains state information during workflow execution, including execution history and current status\n- **Variable**: Manages variable data during workflow execution, supporting variable storage and access\n\n### Technology Stack\n\nThe JavaScript version of FlowGram Runtime is built on the following technology stack:\n\n- **TypeScript**: Provides type safety and modern JavaScript features\n- **LangChain**: Integrates large language models and related tools\n- **OpenAI API**: Provides AI model calling capabilities\n- **Fastify**: High-performance web framework for HTTP API services\n- **tRPC**: Type-safe API framework\n\n### Module Composition\n\nThe project consists of three core modules:\n\n1. **js-core**: Core runtime library, including workflow engine, node executors, and state management\n2. **interface**: Interface definitions, defining APIs and data models\n3. **nodejs**: NodeJS service implementation, providing HTTP API and service management\n\n## Current Development Status and Limitations ⚠️\n\nFlowGram Runtime is currently in early development stage, with the following status and limitations:\n\n### Development Status\n\n- Core functionality has been implemented, including workflow engine, basic node types, and main APIs\n- Basic LLM integration is complete, supporting integration with OpenAI and LangChain\n- Basic error handling and state management mechanisms are provided\n- Includes test cases and example workflows, though documentation is relatively limited\n\n### Known Limitations\n\n- **Unstable API**: API interfaces may change, with no guarantee of backward compatibility\n- **Incomplete Features**: Some features are not fully implemented, such as ServerInfo API and Validation API\n- **Error Handling**: Error handling mechanisms are not fully refined, certain edge cases may cause exceptions\n- **Storage Mechanism**: Current storage mechanisms are relatively simple, not suitable for production environment persistence requirements\n- **Security Mechanisms**: Lacks comprehensive security mechanisms such as authentication, authorization, and input validation\n\n## Future Development Plans\n\nFuture development plans for FlowGram Runtime include:\n\n### Multi-language Support\n\nCurrently only JavaScript/TypeScript version is available, with plans to develop:\n- **Python Version**: Suitable for data science and machine learning scenarios\n- **Go Version**: Suitable for high-performance server-side scenarios\n\n### Feature Enhancements\n\n- Add more node types: Code, Intent, Batch, Break, Continue, HTTP.\n- Improve error handling and exception recovery mechanisms\n- Add complete server-side validation, including schema validation and input validation.\n- Support `Docker` deployment.\n\n### TestRun Optimization\n\n- TestRun supports input form\n- TestRun input parameter validation\n- Single node test run\n\n## Access to FlowGram Editor (Web Frontend)\n\nModify the runtime configuration in `editorProps` to server mode and configure the service address:\n\n```ts\ncreateRuntimePlugin({\n  // mode: 'browser', // remove this line\n  mode: 'server',\n  serverConfig: {\n    domain: 'localhost',\n    port: 4000,\n    protocol: 'http',\n  },\n})\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/node.mdx",
    "content": "# Nodes\n\nThis document provides a detailed introduction to the node system in FlowGram Runtime, including basic node concepts, existing node types and their usage, and how to create custom nodes.\n\nExisting Nodes:\n\n- Start Node\n- End Node\n- LLM Node\n- Condition Node\n- Loop Node\n\n> Future support will include Code, Intent, Batch, Break, Continue, and HTTP nodes\n\n## Node Overview\n\n### The Role of Nodes in FlowGram Runtime\n\nNodes are the basic execution units of FlowGram workflows, with each node representing a specific operation or function. A FlowGram workflow is essentially a directed graph formed by multiple nodes connected by edges, describing the execution process of a task. The core responsibilities of the node system include:\n\n1. **Executing Specific Operations**: Each type of node has its specific functionality, such as starting a workflow, calling an LLM model, performing conditional judgments, etc.\n2. **Processing Inputs and Outputs**: Nodes receive input data, perform operations, and produce output data\n3. **Controlling Execution Flow**: Control the execution path of the workflow through condition nodes and loop nodes\n\n### Introduction to INodeExecutor Interface\n\nAll node executors must implement the `INodeExecutor` interface, which defines the basic structure of a node executor:\n\n```typescript\ninterface INodeExecutor {\n  // Node type, used to identify different kinds of nodes\n  type: string;\n\n  // Execute method, handles the specific logic of the node\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n```\n\nWhere:\n- `type`: Node type identifier, such as 'start', 'end', 'llm', etc.\n- `execute`: Node execution method, receives execution context, returns execution result\n\n### Node Execution Process\n\nThe node execution process is as follows:\n\n1. **Preparation Phase**:\n   - Get the node's input data from the execution context\n   - Validate whether the input data meets the requirements\n\n2. **Execution Phase**:\n   - Execute the node-specific business logic\n   - Handle possible exception situations\n\n3. **Completion Phase**:\n   - Generate the node's output data\n   - Update the node status\n   - Return the execution result\n\nThe workflow engine schedules the execution of nodes in sequence according to the connection relationships between nodes. For special nodes (such as condition nodes and loop nodes), the engine will decide the next execution path based on the execution results of the node.\n\n## Detailed Introduction to Existing Nodes\n\nFlowGram Runtime currently implements five types of nodes: Start, End, LLM, Condition, and Loop. Below is a detailed introduction to each type of node's functionality, configuration, and usage examples.\n\n### Start Node\n\n#### Functionality\n\nThe Start node is the starting node of the workflow, used to receive the input data of the workflow and begin the execution of the workflow. Each workflow must have one and only one Start node.\n\n#### Configuration Options\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| outputs | JSONSchema | Yes | Defines the input data structure of the workflow |\n\n#### Usage Example\n\n```json\n{\n  \"id\": \"start_0\",\n  \"type\": \"start\",\n  \"data\": {\n    \"title\": \"Start Node\",\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"prompt\": {\n          \"type\": \"string\",\n          \"description\": \"User input prompt\"\n        }\n      },\n      \"required\": [\"prompt\"]\n    }\n  }\n}\n```\n\nIn this example, the Start node defines that the workflow needs a string type input named `prompt`.\n\n### End Node\n\n#### Functionality\n\nThe End node is the ending node of the workflow, used to collect the output data of the workflow and end the execution of the workflow. Each workflow must have at least one End node.\n\n#### Configuration Options\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| inputs | JSONSchema | Yes | Defines the output data structure of the workflow |\n| inputsValues | `Record<string, ValueSchema>` | Yes | Defines the output data values of the workflow, can be references or constants |\n\n#### Usage Example\n\n```json\n{\n  \"id\": \"end_0\",\n  \"type\": \"end\",\n  \"data\": {\n    \"title\": \"End Node\",\n    \"inputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"result\": {\n          \"type\": \"string\",\n          \"description\": \"Output result of the workflow\"\n        }\n      }\n    },\n    \"inputsValues\": {\n      \"result\": {\n        \"type\": \"ref\",\n        \"content\": [\"llm_0\", \"result\"]\n      }\n    }\n  }\n}\n```\n\nIn this example, the End node defines that the output of the workflow contains a string named `result`, whose value is referenced from the `result` output of the node with ID `llm_0`.\n\n### LLM Node\n\n#### Functionality\n\nThe LLM node is used to call large language models to perform natural language processing tasks, and is one of the most commonly used node types in FlowGram workflows.\n\n#### Configuration Options\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| modelName | string | Yes | Model name, such as \"gpt-3.5-turbo\" |\n| apiKey | string | Yes | API key |\n| apiHost | string | Yes | API host address |\n| temperature | number | Yes | Temperature parameter, controls the randomness of the output |\n| systemPrompt | string | No | System prompt, sets the role and behavior of the AI assistant |\n| prompt | string | Yes | User prompt, i.e., the question or request posed to the AI |\n\n#### Usage Example\n\n```json\n{\n  \"id\": \"llm_0\",\n  \"type\": \"llm\",\n  \"data\": {\n    \"title\": \"LLM Node\",\n    \"inputsValues\": {\n      \"modelName\": {\n        \"type\": \"constant\",\n        \"content\": \"gpt-3.5-turbo\"\n      },\n      \"apiKey\": {\n        \"type\": \"constant\",\n        \"content\": \"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n      },\n      \"apiHost\": {\n        \"type\": \"constant\",\n        \"content\": \"https://api.openai.com/v1\"\n      },\n      \"temperature\": {\n        \"type\": \"constant\",\n        \"content\": 0.7\n      },\n      \"systemPrompt\": {\n        \"type\": \"constant\",\n        \"content\": \"You are a helpful assistant.\"\n      },\n      \"prompt\": {\n        \"type\": \"ref\",\n        \"content\": [\"start_0\", \"prompt\"]\n      }\n    },\n    \"inputs\": {\n      \"type\": \"object\",\n      \"required\": [\"modelName\", \"apiKey\", \"apiHost\", \"temperature\", \"prompt\"],\n      \"properties\": {\n        \"modelName\": { \"type\": \"string\" },\n        \"apiKey\": { \"type\": \"string\" },\n        \"apiHost\": { \"type\": \"string\" },\n        \"temperature\": { \"type\": \"number\" },\n        \"systemPrompt\": { \"type\": \"string\" },\n        \"prompt\": { \"type\": \"string\" }\n      }\n    },\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"result\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n```\n\nIn this example, the LLM node uses the gpt-3.5-turbo model, with a temperature parameter of 0.7, a system prompt set to \"You are a helpful assistant\", and a user prompt referenced from the input of the Start node.\n\n### Condition Node\n\n#### Functionality\n\nThe Condition node is used to select different execution branches based on conditions, implementing conditional logic in the workflow.\n\n#### Configuration Options\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| conditions | Array | Yes | Array of conditions, each condition contains key and value |\n\nStructure of condition value:\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| left | ValueSchema | Yes | Left value, can be a reference or constant |\n| operator | string | Yes | Operator, such as \"eq\", \"gt\", etc. |\n| right | ValueSchema | Yes | Right value, can be a reference or constant |\n\nSupported operators:\n\n| Operator | Description | Applicable Types |\n|--------|------|----------|\n| eq | Equal to | All types |\n| neq | Not equal to | All types |\n| gt | Greater than | Numbers, strings |\n| gte | Greater than or equal to | Numbers, strings |\n| lt | Less than | Numbers, strings |\n| lte | Less than or equal to | Numbers, strings |\n| includes | Contains | Strings, arrays |\n| startsWith | Starts with | Strings |\n| endsWith | Ends with | Strings |\n\n#### Usage Example\n\n```json\n{\n  \"id\": \"condition_0\",\n  \"type\": \"condition\",\n  \"data\": {\n    \"title\": \"Condition Node\",\n    \"conditions\": [\n      {\n        \"key\": \"if_true\",\n        \"value\": {\n          \"left\": {\n            \"type\": \"ref\",\n            \"content\": [\"start_0\", \"value\"]\n          },\n          \"operator\": \"gt\",\n          \"right\": {\n            \"type\": \"constant\",\n            \"content\": 10\n          }\n        }\n      },\n      {\n        \"key\": \"if_false\",\n        \"value\": {\n          \"left\": {\n            \"type\": \"ref\",\n            \"content\": [\"start_0\", \"value\"]\n          },\n          \"operator\": \"lte\",\n          \"right\": {\n            \"type\": \"constant\",\n            \"content\": 10\n          }\n        }\n      }\n    ]\n  }\n}\n```\n\nIn this example, the condition node defines two branches: when the value output of the Start node is greater than 10, it takes the \"if_true\" branch, otherwise it takes the \"if_false\" branch.\n\n### Loop Node\n\n#### Functionality\n\nThe Loop node is used to perform the same operation on each element in an array, implementing loop logic in the workflow.\n\n#### Configuration Options\n\n| Option | Type | Required | Description |\n|------|------|------|------|\n| loopFor | ValueSchema | Yes | The array to iterate over, usually a reference |\n| loopOutputs | `Record<string, ValueSchema>` | Yes | Loop outputs, references to sub-node outputs |\n| blocks | `Array<NodeSchema>` | Yes | Array of nodes within the loop body |\n\n#### Usage Example\n\n```json\n{\n  \"id\": \"loop_0\",\n  \"type\": \"loop\",\n  \"data\": {\n    \"title\": \"Loop Node\",\n    \"loopFor\": {\n      \"type\": \"ref\",\n      \"content\": [\"start_0\", \"items\"]\n    },\n    \"loopOutputs\": {\n      \"results\": {\n        \"type\": \"ref\",\n        \"content\": [\"llm_1\", \"result\"]\n      }\n    }\n  },\n  \"blocks\": [\n    {\n      \"id\": \"llm_1\",\n      \"type\": \"llm\",\n      \"data\": {\n        \"inputsValues\": {\n          \"prompt\": {\n            \"type\": \"ref\",\n            \"content\": [\"loop_0_locals\", \"item\"]\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\nIn this example, the loop node iterates over the items output of the Start node (assuming it's an array), calling an LLM node for each element. Within the loop body, the current iteration element can be referenced via `loop_0_locals.item`, and the LLM node's result is referenced as Loop node's output.\n\n## How to Add Custom Nodes\n\nFlowGram Runtime is designed to be extensible, allowing developers to add custom node types. Below are the steps to implement and register custom nodes.\n\n### Steps to Implement the INodeExecutor Interface\n\n1. **Create a Node Executor Class**:\n\n```typescript\nimport { ExecutionContext, ExecutionResult, INodeExecutor } from '@flowgram.ai/runtime-interface';\n\nexport class CustomNodeExecutor implements INodeExecutor {\n  // Define node type\n  public type = 'custom';\n\n  // Implement execute method\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // 1. Get inputs from context\n    const inputs = context.inputs as CustomNodeInputs;\n\n    // 2. Validate inputs\n    if (!inputs.requiredParam) {\n      throw new Error('Required parameter missing');\n    }\n\n    // 3. Execute node logic\n    const result = await this.processCustomLogic(inputs);\n\n    // 4. Return outputs\n    return {\n      outputs: {\n        result: result\n      }\n    };\n  }\n\n  // Custom processing logic\n  private async processCustomLogic(inputs: CustomNodeInputs): Promise<string> {\n    // Implement custom logic\n    return `Processing result: ${inputs.requiredParam}`;\n  }\n}\n\n// Define input interface\ninterface CustomNodeInputs {\n  requiredParam: string;\n  optionalParam?: number;\n}\n```\n\n2. **Handle Exception Situations**:\n\n```typescript\npublic async execute(context: ExecutionContext): Promise<ExecutionResult> {\n  try {\n    const inputs = context.inputs as CustomNodeInputs;\n\n    // Validate inputs\n    if (!inputs.requiredParam) {\n      throw new Error('Required parameter missing');\n    }\n\n    // Execute node logic\n    const result = await this.processCustomLogic(inputs);\n\n    return {\n      outputs: {\n        result: result\n      }\n    };\n  } catch (error) {\n    // Handle exceptions\n    console.error('Node execution failed:', error);\n    throw error; // Or return specific error output\n  }\n}\n```\n\n### Method to Register Custom Nodes\n\nAdd the custom node executor to the node executor registry of FlowGram Runtime:\n\n```typescript\nimport { WorkflowRuntimeNodeExecutors } from './nodes';\nimport { CustomNodeExecutor } from './nodes/custom';\n\n// Register custom node executor\nWorkflowRuntimeNodeExecutors.push(new CustomNodeExecutor());\n```\n\n### Best Practices for Custom Node Development\n\n1. **Clear Node Responsibility**:\n   - Each node should have a clear single responsibility\n   - Avoid implementing multiple unrelated functionalities in one node\n\n2. **Input Validation**:\n   - Validate all required inputs before executing node logic\n   - Provide clear error messages for debugging\n\n3. **Exception Handling**:\n   - Catch and handle possible exception situations\n   - Avoid letting unhandled exceptions cause the entire workflow to crash\n\n4. **Performance Considerations**:\n   - Consider implementing timeout mechanisms for time-consuming operations\n   - Avoid long-time synchronous operations that block the main thread\n\n5. **Testability**:\n   - Consider the convenience of unit testing when designing nodes\n   - Separate core logic from external dependencies for easier mock testing\n\n6. **Documentation and Comments**:\n   - Provide detailed documentation for custom nodes\n   - Add necessary comments in the code, especially for complex logic parts\n\n### Complete Custom Node Example\n\nBelow is a complete example of a custom HTTP request node, used to send HTTP requests and handle responses:\n\n```typescript\nimport { ExecutionContext, ExecutionResult, INodeExecutor } from '@flowgram.ai/runtime-interface';\nimport axios from 'axios';\n\n// Define HTTP node input interface\ninterface HTTPNodeInputs {\n  url: string;\n  method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n  headers?: Record<string, string>;\n  body?: any;\n  timeout?: number;\n}\n\n// Define HTTP node output interface\ninterface HTTPNodeOutputs {\n  status: number;\n  data: any;\n  headers: Record<string, string>;\n}\n\nexport class HTTPNodeExecutor implements INodeExecutor {\n  // Define node type\n  public type = 'http';\n\n  // Implement execute method\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // 1. Get inputs from context\n    const inputs = context.inputs as HTTPNodeInputs;\n\n    // 2. Validate inputs\n    if (!inputs.url) {\n      throw new Error('URL parameter missing');\n    }\n\n    if (!inputs.method) {\n      throw new Error('Request method parameter missing');\n    }\n\n    // 3. Execute HTTP request\n    try {\n      const response = await axios({\n        url: inputs.url,\n        method: inputs.method,\n        headers: inputs.headers || {},\n        data: inputs.body,\n        timeout: inputs.timeout || 30000\n      });\n\n      // 4. Process response\n      const outputs: HTTPNodeOutputs = {\n        status: response.status,\n        data: response.data,\n        headers: response.headers as Record<string, string>\n      };\n\n      // 5. Return outputs\n      return {\n        outputs\n      };\n    } catch (error) {\n      if (axios.isAxiosError(error)) {\n        // Handle Axios errors\n        if (error.response) {\n          // Server returned an error status code\n          return {\n            outputs: {\n              status: error.response.status,\n              data: error.response.data,\n              headers: error.response.headers as Record<string, string>\n            }\n          };\n        } else if (error.request) {\n          // Request was sent but no response was received\n          throw new Error(`Request timeout or no response: ${error.message}`);\n        } else {\n          // Request configuration error\n          throw new Error(`Request configuration error: ${error.message}`);\n        }\n      } else {\n        // Handle non-Axios errors\n        throw error;\n      }\n    }\n  }\n}\n\n// Register HTTP node executor\nimport { WorkflowRuntimeNodeExecutors } from './nodes';\nWorkflowRuntimeNodeExecutors.push(new HTTPNodeExecutor());\n```\n\nUsage example:\n\n```json\n{\n  \"id\": \"http_0\",\n  \"type\": \"http\",\n  \"data\": {\n    \"title\": \"HTTP Request Node\",\n    \"inputsValues\": {\n      \"url\": {\n        \"type\": \"constant\",\n        \"content\": \"https://api.example.com/data\"\n      },\n      \"method\": {\n        \"type\": \"constant\",\n        \"content\": \"GET\"\n      },\n      \"headers\": {\n        \"type\": \"constant\",\n        \"content\": {\n          \"Authorization\": \"Bearer token123\"\n        }\n      }\n    },\n    \"inputs\": {\n      \"type\": \"object\",\n      \"required\": [\"url\", \"method\"],\n      \"properties\": {\n        \"url\": { \"type\": \"string\" },\n        \"method\": { \"type\": \"string\", \"enum\": [\"GET\", \"POST\", \"PUT\", \"DELETE\"] },\n        \"headers\": { \"type\": \"object\" },\n        \"body\": { \"type\": \"object\" },\n        \"timeout\": { \"type\": \"number\" }\n      }\n    },\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"status\": { \"type\": \"number\" },\n        \"data\": { \"type\": \"object\" },\n        \"headers\": { \"type\": \"object\" }\n      }\n    }\n  }\n}\n```\n\nThrough the above steps and examples, you can develop and register custom nodes according to your own needs, extending the functionality of FlowGram Runtime.\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/quick-start.mdx",
    "content": "---\ntitle: Quick Start\ndescription: Get started quickly with FlowGram Runtime\nsidebar_position: 3\n---\n\n# Quick Start\n\nThis document will help you quickly get started with FlowGram Runtime, including environment preparation, installation, configuration, creating and running workflows. Through this guide, you'll be able to set up your workflow runtime environment and run your first workflow example in a short time.\n\n## Getting the Code\n\nSince FlowGram Runtime is positioned as a demo rather than an SDK and will not be published as an npm package, you need to follow these steps to obtain and use it:\n\n### Method 1: Fork the Repository (Recommended)\n\n1. Visit the FlowGram Runtime repository\n2. Click the \"Fork\" button to create your own copy of the repository\n3. Clone your forked repository to your local machine\n\n### Method 2: Direct Clone\n\nIf you just want to try it out without submitting changes, you can clone the original repository directly:\n\n```bash\ngit clone git@github.com:bytedance/flowgram.ai.git\ncd flowgram.ai\n```\n\n## Environment Preparation\n\nBefore you start using FlowGram Runtime, please ensure your development environment meets the following requirements:\n\n- **Node.js**: Version 18.x or higher (LTS version recommended)\n\n```bash\nnvm install lts/hydrogen\nnvm alias default lts/hydrogen # set default node version\nnvm use lts/hydrogen\n```\n\n- **Package Manager**: pnpm 9+ and rush 5+\n\n```bash\nnpm i -g pnpm@10.6.5 @microsoft/rush@5.150.0\n```\n\n## Installing Dependencies and Project Setup\n\nAfter obtaining the code, you need to install dependencies and perform basic setup:\n\n1. **Install Project Dependencies**\n\n```bash\nrush install\n```\n\n2. **Build the Project**\n\n```bash\nrush build\n```\n\n## Starting the Service\n\n1. **Navigate to Runtime Directory**\n\n```bash\ncd packages/runtime/nodejs\n```\n\n2. **Start the nodejs Server**\n\n```bash\nnpm run dev\n```\n\nIf everything is working correctly, you should see the following output in the console:\n\n```\n> Listen Port: 4000\n> Server Address: http://localhost:4000\n> API Docs: http://localhost:4000/docs\n```\n\n3. **Verify the Service**\n\nYou can test the service using cURL:\n\n```bash\ncurl --location 'http://localhost:4000/api/task/run' \\\n--header 'Content-Type: application/json' \\\n--data '{\n  \"inputs\": {\n      \"test_input\": \"Hello FlowGram!\"\n  },\n  \"schema\": \"{\\\"nodes\\\":[{\\\"id\\\":\\\"start_0\\\",\\\"type\\\":\\\"start\\\",\\\"meta\\\":{\\\"position\\\":{\\\"x\\\":180,\\\"y\\\":0}},\\\"data\\\":{\\\"title\\\":\\\"Start\\\",\\\"outputs\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"test_input\\\":{\\\"key\\\":4,\\\"name\\\":\\\"test_input\\\",\\\"isPropertyRequired\\\":true,\\\"type\\\":\\\"string\\\",\\\"extra\\\":{\\\"index\\\":0}}},\\\"required\\\":[\\\"test_input\\\"]}}},{\\\"id\\\":\\\"end_0\\\",\\\"type\\\":\\\"end\\\",\\\"meta\\\":{\\\"position\\\":{\\\"x\\\":640,\\\"y\\\":0}},\\\"data\\\":{\\\"title\\\":\\\"End\\\",\\\"inputsValues\\\":{\\\"test_output\\\":{\\\"type\\\":\\\"ref\\\",\\\"content\\\":[\\\"start_0\\\",\\\"test_input\\\"]}},\\\"inputs\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"test_output\\\":{\\\"type\\\":\\\"string\\\"}}}}}],\\\"edges\\\":[{\\\"sourceNodeID\\\":\\\"start_0\\\",\\\"targetNodeID\\\":\\\"end_0\\\"}]}\"\n}'\n```\n\nYou should see the following output in the command line where the service is running:\n\n```\n> POST TaskRun - taskID:  xxxx-xxxx-xxxx-xxxx\n{ test_input: 'Hello FlowGram!' }\n> LOG Task finished:  xxxx-xxxx-xxxx-xxxx\n{ test_output: 'Hello FlowGram!' }\n```\n\n## Compiling the Service\n\n1. **Enter the Runtime Directory**\n\n```bash\ncd packages/runtime/nodejs\n```\n\n2. **Compile the Service**\n\n```bash\nnpm run build\n```\n\n2. **Start the nodejs Server**\n\n```bash\nnode dist/index.js\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/schema.mdx",
    "content": "---\ntitle: Schema\ndescription: Detailed introduction to FlowGram Schema structure and configuration\n---\n\n# Workflow Schema\n\nThis document provides a detailed introduction to the Workflow Schema structure. The Workflow Schema is the core configuration that defines workflows, describing nodes, edges, and their relationships.\n\n## Basic Structure\n\nA complete Workflow Schema contains the following main components:\n\n```typescript\ninterface WorkflowSchema {\n  nodes: WorkflowNodeSchema[];\n  edges: WorkflowEdgeSchema[];\n}\n```\n\n## Node Schema\n\nNodes are the basic units in a workflow, each with its specific type and configuration:\n\n```typescript\ninterface WorkflowNodeSchema<T = string, D = any> {\n  id: string;           // Unique identifier for the node\n  type: T;              // Node type\n  meta: WorkflowNodeMetaSchema;  // Node metadata\n  data: D & {          // Node data\n    title?: string;    // Node title\n    inputsValues?: Record<string, IFlowValue>;  // Input values\n    inputs?: IJsonSchema;   // Input schema definition\n    outputs?: IJsonSchema;  // Output schema definition\n    [key: string]: any;     // Other custom data\n  };\n  blocks?: WorkflowNodeSchema[];  // Child nodes (for composite nodes)\n  edges?: WorkflowEdgeSchema[];   // Connections between child nodes\n}\n```\n\n### Node Metadata\n\n```typescript\ninterface WorkflowNodeMetaSchema {\n  position: PositionSchema;        // Node position in canvas\n}\n```\n\n## Edge Schema\n\nEdges define the connections between nodes:\n\n```typescript\ninterface WorkflowEdgeSchema {\n  sourceNodeID: string;    // Source node ID\n  targetNodeID: string;    // Target node ID\n  sourcePortID?: string;   // Source port ID (optional)\n  targetPortID?: string;   // Target port ID (optional)\n}\n```\n\n## Variable Types\n\nWorkflow supports various variable types:\n\n```typescript\nenum WorkflowVariableType {\n  String = 'string',    // String\n  Integer = 'integer',  // Integer\n  Number = 'number',    // Number\n  Boolean = 'boolean',  // Boolean\n  Object = 'object',    // Object\n  Array = 'array',      // Array\n  Null = 'null'         // Null\n}\n```\n\n### Flow Values\n\nIn node input values, the following types are supported:\n\n```typescript\ntype IFlowValue =\n  | IFlowConstantValue     // Constant value\n  | IFlowRefValue          // Reference value\n  | IFlowExpressionValue   // Expression value\n  | IFlowTemplateValue;    // Template value\n```\n\nDetailed definition for each type:\n\n```typescript\ninterface IFlowConstantValue {\n  type: 'constant';\n  content?: string | number | boolean;\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n\ninterface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n}\n\ninterface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n}\n```\n\n## JSON Schema\n\nNode input and output definitions use JSON Schema format:\n\n```typescript\ninterface IJsonSchema<T = string> {\n  type: T;                 // Data type\n  default?: any;           // Default value\n  title?: string;          // Title\n  description?: string;    // Description\n  enum?: (string | number)[];  // Enumeration values\n  properties?: Record<string, IJsonSchema<T>>;  // Object properties\n  additionalProperties?: IJsonSchema<T>;        // Additional properties\n  items?: IJsonSchema<T>;                       // Array item definition\n  required?: string[];                          // Required fields\n  $ref?: string;                                // Reference\n  extra?: {                                     // Extra configuration\n    index?: number;                             // Index\n    weak?: boolean;                             // Weak type comparison\n    formComponent?: string;                     // Form component\n    [key: string]: any;\n  };\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/runtime/source-code-guide.mdx",
    "content": "---\ntitle: Source Code Guide\ndescription: Analysis of FlowGram Runtime source code structure and implementation details\n---\n\n# FlowGram Runtime Source Code Guide\n\nThis document aims to help developers gain a deep understanding of FlowGram Runtime's source code structure and implementation details, providing guidance for customization and extension. Since FlowGram Runtime is positioned as a reference implementation rather than an SDK for direct use, understanding its internal implementation is particularly important for developers.\n\n## Project Structure Overview\n\n### Directory Structure\n\nThe FlowGram Runtime JS project has the following directory structure:\n\n```\npackages/runtime\n├── js-core/          # Core runtime library\n│   ├── src/\n│   │   ├── application/    # Application layer, API implementation\n│   │   ├── domain/         # Domain layer, core business logic\n│   │   ├── infrastructure/ # Infrastructure layer, technical support\n│   │   ├── nodes/          # Node executor implementations\n│   │   └── index.ts        # Entry point\n│   ├── package.json\n│   └── tsconfig.json\n├── interface/        # Interface definitions\n│   ├── src/\n│   │   ├── api/       # API interface definitions\n│   │   ├── domain/    # Domain model interface definitions\n│   │   ├── engine/    # Engine interface definitions\n│   │   ├── node/      # Node interface definitions\n│   │   └── index.ts   # Entry point\n│   ├── package.json\n│   └── tsconfig.json\n└── nodejs/           # NodeJS service implementation\n    ├── src/\n    │   ├── api/       # HTTP API implementation\n    │   ├── server/    # Server implementation\n    │   └── index.ts   # Entry point\n    ├── package.json\n    └── tsconfig.json\n```\n\n### Module Organization\n\nFlowGram Runtime JS employs a modular design, primarily divided into three core modules:\n\n1. **interface**: Defines the system's interfaces and data structures, serving as the foundation for other modules\n2. **js-core**: Implements the core functionality of the workflow engine, including workflow parsing, node execution, state management, etc.\n3. **nodejs**: Provides an HTTP API service based on NodeJS, allowing the workflow engine to be called via HTTP interfaces\n\n### Dependencies\n\nThe dependencies between modules are as follows:\n\n```mermaid\ngraph TD\n    nodejs --> js-core\n    js-core --> interface\n    nodejs -.-> interface\n```\n\n- **interface** is the foundational module with no dependencies on other modules\n- **js-core** depends on interfaces defined in the interface module\n- **nodejs** depends on functionality provided by the js-core module, while also using interface definitions from the interface module\n\nKey external dependencies include:\n\n- **TypeScript**: Provides type safety and object-oriented programming support\n- **LangChain**: Used for integrating large language models\n- **OpenAI API**: Provides the default implementation for LLM nodes\n- **fastify**: Used to implement HTTP API services\n- **tRPC**: Used for type-safe API definitions and calls\n\n## Core Module Analysis\n\n### js-core Module\n\nThe js-core module is the core of FlowGram Runtime, implementing the main functionality of the workflow engine. This module adopts a Domain-Driven Design (DDD) architecture, divided into application, domain, and infrastructure layers.\n\n#### Application Layer\n\nThe application layer is responsible for coordinating domain objects and implementing system use cases. Key files:\n\n- `application/workflow.ts`: Workflow application service, implementing workflow validation, execution, cancellation, querying, etc.\n- `application/api.ts`: API implementation, including TaskValidate, TaskRun, TaskResult, TaskReport, TaskCancel, etc.\n\n#### Domain Layer\n\nThe domain layer contains core business logic and domain models. Key directories and files:\n\n- `domain/engine/`: Workflow execution engine, responsible for workflow parsing and execution\n  - `engine.ts`: Workflow engine implementation, containing core logic for node execution, state management, etc.\n  - `validator.ts`: Workflow validator, checking the validity of workflow definitions\n- `domain/document/`: Workflow document model, representing the structure of workflows\n  - `workflow.ts`: Workflow definition model\n  - `node.ts`: Node definition model\n  - `edge.ts`: Edge definition model\n- `domain/executor/`: Node executors, responsible for executing specific node logic\n  - `executor.ts`: Node executor base class and factory\n- `domain/variable/`: Variable management, handling variable storage and references in workflows\n  - `manager.ts`: Variable manager, responsible for variable storage, retrieval, and parsing\n  - `store.ts`: Variable storage, providing variable persistence\n- `domain/status/`: Status management, tracking the execution status of workflows and nodes\n  - `center.ts`: Status center, managing workflow and node statuses\n- `domain/snapshot/`: Snapshot management, recording intermediate states of workflow execution\n  - `center.ts`: Snapshot center, managing node execution snapshots\n- `domain/report/`: Report generation, collecting detailed information on workflow execution\n  - `center.ts`: Report center, generating workflow execution reports\n\n#### Infrastructure Layer\n\nThe infrastructure layer provides technical support, including logging, events, containers, etc. Key files:\n\n- `infrastructure/logger.ts`: Logging service, providing logging functionality\n- `infrastructure/event.ts`: Event service, providing event publishing and subscription functionality\n- `infrastructure/container.ts`: Dependency injection container, managing object creation and lifecycle\n- `infrastructure/error.ts`: Error handling, defining error types and handling methods in the system\n\n#### Node Executors\n\nThe nodes directory contains executor implementations for various node types. Key files:\n\n- `nodes/start.ts`: Start node executor\n- `nodes/end.ts`: End node executor\n- `nodes/llm.ts`: LLM node executor, integrating large language models\n- `nodes/condition.ts`: Condition node executor, implementing conditional branching\n- `nodes/loop.ts`: Loop node executor, implementing loop logic\n\n### interface Module\n\nThe interface module defines the system's interfaces and data structures, serving as the foundation for other modules. Key directories and files:\n\n- `api/`: API interface definitions\n  - `api.ts`: Defines the API interfaces provided by the system\n  - `types.ts`: API-related data type definitions\n- `domain/`: Domain model interface definitions\n  - `document.ts`: Workflow document-related interfaces\n  - `engine.ts`: Workflow engine-related interfaces\n  - `executor.ts`: Node executor-related interfaces\n  - `variable.ts`: Variable management-related interfaces\n  - `status.ts`: Status management-related interfaces\n  - `snapshot.ts`: Snapshot management-related interfaces\n  - `report.ts`: Report generation-related interfaces\n- `engine/`: Engine interface definitions\n  - `types.ts`: Engine-related data type definitions\n- `node/`: Node interface definitions\n  - `types.ts`: Node-related data type definitions\n\n### nodejs Module\n\nThe nodejs module provides an HTTP API service based on NodeJS, allowing the workflow engine to be called via HTTP interfaces. Key directories and files:\n\n- `api/`: HTTP API implementation\n  - `router.ts`: API route definitions\n  - `handlers.ts`: API handler functions\n- `server/`: Server implementation\n  - `server.ts`: HTTP server implementation\n  - `config.ts`: Server configuration\n\n## Key Implementation Details\n\n### Workflow Engine\n\nThe workflow engine is the core of FlowGram Runtime, responsible for workflow parsing and execution. Its main implementation is located in `js-core/src/domain/engine/engine.ts`.\n\nThe main functions of the workflow engine include:\n\n1. **Workflow Parsing**: Converting workflow definitions into internal models\n2. **Node Scheduling**: Determining the execution order of nodes based on edges defined in the workflow\n3. **Node Execution**: Calling node executors to execute node logic\n4. **State Management**: Tracking the execution status of workflows and nodes\n5. **Variable Management**: Handling data transfer between nodes\n6. **Error Handling**: Managing exceptions during execution\n\nKey code snippet:\n\n```typescript\n// Core method for workflow execution\npublic async run(params: RunParams): Promise<RunResult> {\n  const { schema, inputs, options } = params;\n\n  // Create workflow context\n  const context = this.createContext(schema, inputs, options);\n\n  try {\n    // Initialize workflow\n    await this.initialize(context);\n\n    // Execute workflow\n    await this.execute(context);\n\n    // Get workflow result\n    const result = await this.getResult(context);\n\n    return {\n      status: 'success',\n      outputs: result\n    };\n  } catch (error) {\n    // Error handling\n    return {\n      status: 'fail',\n      error: error.message\n    };\n  }\n}\n\n// Execute workflow\nprivate async execute(context: IContext): Promise<void> {\n  // Get start node\n  const startNode = context.workflow.getStartNode();\n\n  // Start execution from the start node\n  await this.executeNode({ context, node: startNode });\n\n  // Wait for all nodes to complete execution\n  await this.waitForCompletion(context);\n}\n\n// Execute node\npublic async executeNode(params: { context: IContext; node: INode }): Promise<void> {\n  const { context, node } = params;\n\n  // Get node executor\n  const executor = this.getExecutor(node.type);\n\n  // Prepare node inputs\n  const inputs = await this.prepareInputs(context, node);\n\n  // Execute node\n  const result = await executor.execute({\n    node,\n    inputs,\n    context\n  });\n\n  // Process node outputs\n  await this.processOutputs(context, node, result.outputs);\n\n  // Schedule next nodes\n  await this.scheduleNextNodes(context, node);\n}\n```\n\n### Node Executors\n\nNode executors are responsible for executing the specific logic of nodes. Each node type has a corresponding executor implementation located in the `js-core/src/nodes/` directory.\n\nThe basic interface for node executors is defined in `interface/src/domain/executor.ts`:\n\n```typescript\nexport interface INodeExecutor {\n  type: string;\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n```\n\nTaking the LLM node executor as an example, its implementation is in `js-core/src/nodes/llm.ts`:\n\n```typescript\nexport class LLMExecutor implements INodeExecutor {\n  public type = 'llm';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = context.inputs as LLMExecutorInputs;\n\n    // Create LLM provider\n    const provider = this.createProvider(inputs);\n\n    // Prepare prompts\n    const systemPrompt = inputs.systemPrompt || '';\n    const userPrompt = inputs.prompt || '';\n\n    // Call LLM\n    const result = await provider.call({\n      systemPrompt,\n      userPrompt,\n      options: {\n        temperature: inputs.temperature\n      }\n    });\n\n    // Return result\n    return {\n      outputs: {\n        result: result.content\n      }\n    };\n  }\n\n  private createProvider(inputs: LLMExecutorInputs): ILLMProvider {\n    // Create different providers based on model name\n    if (inputs.modelName.startsWith('gpt-')) {\n      return new OpenAIProvider({\n        apiKey: inputs.apiKey,\n        apiHost: inputs.apiHost,\n        modelName: inputs.modelName\n      });\n    }\n\n    throw new Error(`Unsupported model: ${inputs.modelName}`);\n  }\n}\n```\n\n### Variable Management\n\nVariable management is an important part of workflow execution, responsible for handling data transfer between nodes. Its main implementation is in the `js-core/src/domain/variable/` directory.\n\nThe core of variable management is the variable manager and variable storage:\n\n- **Variable Manager**: Responsible for parsing, getting, and setting variables\n- **Variable Storage**: Provides persistent storage for variables\n\nKey code snippet:\n\n```typescript\n// Variable manager\nexport class VariableManager implements IVariableManager {\n  constructor(private store: IVariableStore) {}\n\n  // Resolve variable references\n  public async resolve(ref: ValueSchema, scope?: string): Promise<any> {\n    if (ref.type === 'constant') {\n      return ref.content;\n    } else if (ref.type === 'ref') {\n      const path = ref.content as string[];\n      return this.get(path, scope);\n    }\n    throw new Error(`Unsupported value type: ${ref.type}`);\n  }\n\n  // Get variable value\n  public async get(path: string[], scope?: string): Promise<any> {\n    const [nodeID, key, ...rest] = path;\n    const value = await this.store.get(nodeID, key, scope);\n\n    if (rest.length === 0) {\n      return value;\n    }\n\n    // Handle nested properties\n    return this.getNestedProperty(value, rest);\n  }\n\n  // Set variable value\n  public async set(nodeID: string, key: string, value: any, scope?: string): Promise<void> {\n    await this.store.set(nodeID, key, value, scope);\n  }\n}\n```\n\n### State Storage\n\nState storage is responsible for managing the execution state of workflows and nodes. Its main implementation is in the `js-core/src/domain/status/` and `js-core/src/domain/snapshot/` directories.\n\nThe core components of state management include:\n\n- **Status Center**: Manages the status of workflows and nodes\n- **Snapshot Center**: Records snapshots of node execution\n- **Report Center**: Generates workflow execution reports\n\nKey code snippet:\n\n```typescript\n// Status center\nexport class StatusCenter implements IStatusCenter {\n  private workflowStatus: Record<string, WorkflowStatus> = {};\n  private nodeStatus: Record<string, Record<string, NodeStatus>> = {};\n\n  // Set workflow status\n  public setWorkflowStatus(workflowID: string, status: WorkflowStatus): void {\n    this.workflowStatus[workflowID] = status;\n  }\n\n  // Get workflow status\n  public getWorkflowStatus(workflowID: string): WorkflowStatus {\n    return this.workflowStatus[workflowID] || 'idle';\n  }\n\n  // Set node status\n  public setNodeStatus(workflowID: string, nodeID: string, status: NodeStatus): void {\n    if (!this.nodeStatus[workflowID]) {\n      this.nodeStatus[workflowID] = {};\n    }\n    this.nodeStatus[workflowID][nodeID] = status;\n  }\n\n  // Get node status\n  public getNodeStatus(workflowID: string, nodeID: string): NodeStatus {\n    return this.nodeStatus[workflowID]?.[nodeID] || 'idle';\n  }\n}\n```\n\n## Design Patterns and Architectural Decisions\n\n### Domain-Driven Design\n\nFlowGram Runtime adopts a Domain-Driven Design (DDD) architecture, dividing the system into application, domain, and infrastructure layers. This architecture helps separate concerns, making the code more modular and maintainable.\n\nKey domain concepts include:\n\n- **Workflow**: Represents a complete workflow definition\n- **Node**: Basic execution unit in a workflow\n- **Edge**: Line connecting nodes, representing execution flow\n- **Execution Context**: Environment for workflow execution\n- **Variable**: Data in the workflow execution process\n\n### Factory Pattern\n\nFlowGram Runtime uses the Factory pattern to create node executors, enabling the system to dynamically create corresponding executors based on node types.\n\n```typescript\n// Node executor factory\nexport class NodeExecutorFactory implements INodeExecutorFactory {\n  private executors: Record<string, INodeExecutor> = {};\n\n  // Register node executor\n  public register(executor: INodeExecutor): void {\n    this.executors[executor.type] = executor;\n  }\n\n  // Create node executor\n  public create(type: string): INodeExecutor {\n    const executor = this.executors[type];\n    if (!executor) {\n      throw new Error(`No executor registered for node type: ${type}`);\n    }\n    return executor;\n  }\n}\n```\n\n### Strategy Pattern\n\nFlowGram Runtime uses the Strategy pattern to handle execution logic for different types of nodes, with each node type having a corresponding execution strategy.\n\n```typescript\n// Node executor interface (strategy interface)\nexport interface INodeExecutor {\n  type: string;\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n\n// Concrete strategy implementation\nexport class StartExecutor implements INodeExecutor {\n  public type = 'start';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // Start node execution logic\n  }\n}\n\nexport class EndExecutor implements INodeExecutor {\n  public type = 'end';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // End node execution logic\n  }\n}\n```\n\n### Observer Pattern\n\nFlowGram Runtime uses the Observer pattern to implement the event system, allowing components to publish and subscribe to events.\n\n```typescript\n// Event emitter\nexport class EventEmitter implements IEventEmitter {\n  private listeners: Record<string, Function[]> = {};\n\n  // Subscribe to event\n  public on(event: string, listener: Function): void {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(listener);\n  }\n\n  // Publish event\n  public emit(event: string, ...args: any[]): void {\n    const eventListeners = this.listeners[event];\n    if (eventListeners) {\n      for (const listener of eventListeners) {\n        listener(...args);\n      }\n    }\n  }\n}\n```\n\n### Dependency Injection\n\nFlowGram Runtime uses Dependency Injection to manage dependencies between components, making components more loosely coupled and testable.\n\n```typescript\n// Dependency injection container\nexport class Container {\n  private static _instance: Container;\n  private registry: Map<any, any> = new Map();\n\n  public static get instance(): Container {\n    if (!Container._instance) {\n      Container._instance = new Container();\n    }\n    return Container._instance;\n  }\n\n  // Register service\n  public register<T>(token: any, instance: T): void {\n    this.registry.set(token, instance);\n  }\n\n  // Get service\n  public resolve<T>(token: any): T {\n    const instance = this.registry.get(token);\n    if (!instance) {\n      throw new Error(`No instance registered for token: ${token}`);\n    }\n    return instance;\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/_meta.json",
    "content": "[\n  \"basic\",\n  \"variable-output\",\n  \"variable-consume\",\n  \"concept\",\n  \"custom-scope-chain\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/basic.mdx",
    "content": "---\ndescription: What is Variable? And why Variable Engine needed?\n---\n\n# Introduction\n\n:::warning\n\nThe VariableEngine in this document belongs to the **Design** of FlowGram, which is different from the VariableEngine in the Runtime.\n\n:::\n\n## Reading Path\n\n- Build a mental model for “what a variable is” and “why the variable engine matters” here first.\n- Then get hands-on: [Output Variables](./variable-output.mdx) → [Consume Variables](./variable-consume.mdx) so you learn to produce variables before reading them.\n- When questions about scope or types pop up during practice, return to [Core Concepts](./concept.mdx) for detailed terminology.\n\n## What is a Variable?\n\nImagine you're building a complex Lego model where each module needs to connect precisely. In the world of Workflows, **variables** play a similar role as \"connectors.\" They are the \"messengers\" used to pass information between different nodes.\n\nSimply put, a variable is a named container where you can store various things, such as user input, calculation results, or data retrieved from somewhere.\n\nA variable typically consists of three parts:\n\n- **Name (Unique Identifier)**: Similar to a personal name, it lets you pinpoint a specific variable. For example, `userName`, `orderId`.\n- **Value**: The content inside the container. It can be a number `123`, text `\"Hello FlowGram!\"`, or a switch state `true` / `false`.\n- **Type**: Specifies what kind of things this container can hold. For instance, some can only hold numbers, while others can only hold text.\n\n---\n\nFor example, in an \"Intelligent Q&A\" flow:\n\n<div style={{display: 'flex', gap: '20px'}}>\n  <img style={{width: \"50%\"}} loading=\"lazy\" src=\"/variable/variable-biz-context-websearch-llm.png\" alt=\"Smart Q&A Flow\" />\n  <div>\n    <p style={{marginTop: 10}}>1. **`WebSearch` Node**: Responsible for searching the web and putting the found knowledge (e.g., the answer to \"What's the weather like today?\") into a variable named `natural_language_desc`.</p>\n    <p style={{marginTop: 5}}>2. **`LLM` Node**: It takes the `natural_language_desc` \"messenger,\" reads its content, and then answers the user in a more natural and friendly way.</p>\n    <p style={{marginTop: 5}}>3. In this process, the type of `natural_language_desc` is \"string\" because it contains text content.</p>\n  </div>\n</div>\n\n### Design-Time Definition vs. Runtime Evaluation\n\nIn **design time** (while drawing the flow), you only need to determine the **definition** of a variable: its name, type, and optional metadata. The variable engine manages these definitions as structured data.\n\nWhen entering **runtime**, FlowGram assigns values to each execution based on those definitions. Focus on structure and constraints during design; at runtime every node can rely on the same definitions to read and write data reliably.\n\n## Why Do You Need a Variable Engine?\n\nAs the complexity of workflows increases, so do the number and management difficulty of variables.\n\nTo address this challenge, FlowGram provides a powerful **Variable Engine**.\n\nIt acts like a professional \"data steward,\" systematically managing all variables to ensure the clarity and stability of the data flow.\n\nEnabling the Variable Engine will bring you the following core advantages:\n\n<div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: \"25px\" }}>\n  <div style={{ gridColumn: \"span 2\" }}>\n    <b>Scope Constraints: Precise Data Access Control</b>\n    <p className=\"rs-tip\">The variable engine precisely controls the effective range (scope) of each variable. Like giving every room its own key, it keeps variables accessible only to the intended nodes, preventing data pollution and surprise logic errors.</p>\n    <div style={{display: \"flex\", gap: \"25px\"}}>\n      <div>\n        <img loading=\"lazy\" src=\"/variable/variable-scope-feature-1.png\" alt=\"Scope Constraint Example 1\" />\n        <p style={{marginTop: '10px'}}>The `query` variable defined in the `Start` node can be easily accessed by the subsequent `LLM` and `End` nodes.</p>\n      </div>\n      <div>\n        <img loading=\"lazy\" src=\"/variable/variable-scope-feature-2.png\" alt=\"Scope Constraint Example 2\" />\n        <p style={{marginTop: '10px'}}>The `LLM` node lives inside a `Condition` branch—effectively a separate room—so the `End` node outside cannot access its `result` variable.</p>\n      </div>\n    </div>\n  </div>\n  <div>\n    <b>Variable Structure Insight: Easily Understand Complex Data</b>\n    <p className=\"rs-tip\">When a variable has a complex structure (for example, a deeply nested object), the variable engine lets you expand it layer by layer so every piece of data stays easy to locate.</p>\n    <img loading=\"lazy\" src=\"/variable/variable-tree-management.gif\" alt=\"Variable Structure Perspective\" />\n    <p style={{marginTop: '10px'}}>You can review every node’s output variables and their hierarchy at a glance, almost like inspecting a well-organized tree.</p>\n  </div>\n  <div>\n    <b>Automatic Type Inference: A Spark of Genius</b>\n    <p className=\"rs-tip\">No need to declare every variable type by hand. The variable engine infers types from context automatically.</p>\n    <img loading=\"lazy\" src=\"/variable/variable-batch-auto-infer.gif\" />\n    <p style={{marginTop: '10px'}}>For example, if the `Start` node’s `arr` variable changes type, the `Batch` node’s `item` output updates automatically to stay aligned.</p>\n  </div>\n</div>\n\n:::tip\n\nGet the read/write flow working first; when you need a deeper understanding of scope chains, ASTs, declarations, or expressions, go back to [Core Concepts](./concept.mdx).\n\n:::\n\n## How to Enable the Variable Engine?\n\nYou can enable the Variable Engine with a simple configuration to experience its powerful features.\n\n[> View API Details](https://flowgram.ai/auto-docs/editor/interfaces/VariablePluginOptions.html)\n\n```tsx pure title=\"use-editor-props.ts\" {3}\n// Enable the Variable Engine in EditorProps\n{\n  variableEngine: {\n    // Set to true to enable the Variable Engine\n    enable: true\n  }\n}\n```\n\n## What to Read Next (Suggested Order)\n\n- [Output Variables](./variable-output.mdx): Learn how to produce variables in nodes, plugins, and global scope.\n- [Consume Variables](./variable-consume.mdx): Then master how to read variables safely in nodes and UI.\n- [Variable Concepts](./concept.mdx): Finally, revisit scope, AST, declarations, types, and other core terms.\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/concept.mdx",
    "content": "---\ndescription: Introduces the core concepts of the variable engine.\n---\n\n# Concepts\n\n\n:::tip\n\nComplete [Output Variables](./variable-output.mdx) → [Consume Variables](./variable-consume.mdx) first, then come back to this article as a reference manual. We use 🌟 to highlight concepts worth mastering early.\n\n:::\n\n## Reading Path\n\n- Skim the terminology navigator below to confirm the term you need is covered.\n- Study the “Core Concepts” diagram to link variables, scopes, and AST into one picture.\n- Jump to the sections you need based on the problem at hand—no need to read in order.\n\n:::info{title=\"📖 Quick Terminology Lookup\"}\n\n- **Core Ideas**\n  - [Variable](#variable) 🌟: A data container defined during design and evaluated at runtime.\n  - [Scope](#scope-) 🌟: A container for variables that also maintains relationships with other scopes.\n  - [AST](#ast-) 🌟: The structured storage used to keep variable information inside a scope.\n- **AST Related**\n  - [ASTNode](#astnode): Nodes in the AST tree, each describing a piece of variable information.\n  - [ASTNodeJSON](#astnodejson): The JSON serialization of an ASTNode.\n  - [Declaration](#declaration) 🌟: Identifier + definition, the smallest information unit in the variable engine.\n  - [Type](#type) 🌟: A definition that constrains the range of possible values for a variable.\n  - [Expression](#expression): Consumes variables and produces a new one.\n- **Scope Relationships**\n  - [Scope Chain](#scope-chain): Determines which other scopes a scope can read variables from.\n  - [Dependency Scope](#dependency-scope): The upstream scopes whose outputs are readable here.\n  - [Covering Scope](#covering-scope): The downstream scopes that can read this scope’s outputs.\n  - [Node Scope](#node-scope) 🌟: The public variable set of a node.\n  - [Node Private Scope](#node-private-scope): Variables only accessible to the node and its children.\n  - [Global Scope](#global-scope): Shared variables readable from every scope.\n\n:::\n\n\n\n## Core Concepts\n\nYou can connect the variable engine’s core concepts through the diagram below:\n\n<img src=\"/variable/concept/concepts-en.png\" alt=\"Variable Core Concepts Relationship Diagram\" width=\"600\" />\n\n:::info{title=\"How to read the diagram\"}\n\n- Green nodes represent **what the information is**, such as variables, types, or expressions.\n- Red nodes represent **how the information is stored**, namely AST nodes.\n- Purple nodes represent **where the information lives**, i.e., scopes.\n- Dashed nodes and lines depict **how the information flows**, which is the scope chain.\n\n:::\n\nTo keep things tangible, hold on to one real-world case:\n\n> A “Batch” node reads the array output from an upstream HTTP node → iterates to produce `item` → and continues using `item` inside its child nodes.\n\nEvery concept mentioned in that workflow appears below, so feel free to cross-reference as you read.\n\n\n### Variable\n\nVariables are containers defined during design time and evaluated during runtime. See [Variable Introduction](./basic.mdx) for a deeper primer.\n\n:::warning{title=\"⚠️ Different Focus on Variables in Design and Runtime\"}\n\n**In process design, variables only focus on definitions, not values**. The value of a variable is dynamically calculated at the process's [runtime](/guide/runtime/introduction).\n\n:::\n\n### Scope 🌟\n\nA scope is a **container** that bundles **variable information** and keeps track of **relationships with other scopes**. In short, a scope decides “who can access which variables.”\n\nIts boundaries vary by business scenario; the three most common cases are:\n\n| Scene | Example |\n| :--- | :--- |\n| Nodes in a process can be defined as scopes | <img src=\"/variable/concept/scope-1.png\" alt=\"Node Scope\" width=\"600\" /> |\n| The global variable sidebar can also be defined as a scope | <img src=\"/variable/concept/scope-2.png\" alt=\"Global Scope\" width=\"600\" /> |\n| Components (including variables) in UI editing can be defined as scopes | <img src=\"/variable/concept/scope-3.png\" alt=\"Component Scope\" width=\"600\" /> |\n\n\n\n:::warning{title=\"Why does FlowGram abstract the concept of a scope outside of nodes?\"}\n\n1. Node ≠ scope: a single node may need both a public scope and a private one.\n2. Some scopes, like the global drawer, live outside of any node.\n3. Certain nodes require multiple layers of scopes (e.g., a loop’s private scope), which can’t be expressed with the node concept alone.\n\n:::\n\n### AST 🌟\n\nScopes store variable information through an `AST`. Treat it as a tree where each node describes a declaration, type, or expression.\n\n:::tip\n\nThrough `scope.ast` you can access the tree inside a scope and perform CRUD operations on variable information.\n\n:::\n\n\n#### ASTNode\n\n`ASTNode` is the **basic information unit** used in the variable engine to **store variable information**. It can model various **pieces of information**:\n\n- **Declarations**: such as `VariableDeclaration`, used to declare new variables.\n- **Types**: such as `StringType`, used to represent the String type.\n- **Expressions**: such as `KeyPathExpression`, used to reference variables.\n\n:::info{title=\"ASTNode has the following features\"}\n\n- **Tree Structure**: `ASTNode` can be nested to form a tree (`AST`) to represent complex variable structures.\n- **Serialization**: `ASTNode` can be converted to and from JSON format (`ASTNodeJSON`) for storage or transmission.\n- **Extensibility**: New features can be added by extending the `ASTNode` base class.\n- **Reactivity**: Changes in `ASTNode` values trigger events, enabling a reactive programming model.\n\n:::\n\n#### ASTNodeJSON\n\n`ASTNodeJSON` is the **pure JSON serialization** of an `ASTNode`. We usually construct it on the design side and let the variable engine instantiate it later.\n\nIts most important field is `kind`, which indicates the type of the `ASTNode`:\n\n```tsx\n/**\n * Equivalent to the JavaScript code:\n * `var var_index: string`\n */\n{\n  kind: 'VariableDeclaration',\n  key: 'var_index',\n  type: { kind: 'StringType' },\n}\n```\n\nWhen using the variable engine, we describe variable information with `ASTNodeJSON`. The engine then **instantiates** it into an `ASTNode` and stores it in the scope.\n\n```tsx\n/**\n * Instantiate ASTNodeJSON into an ASTNode and add it to the scope using the scope.setVar method\n */\nconst variableDeclaration: VariableDeclaration = scope.setVar({\n  kind: 'VariableDeclaration',\n  key: 'var_index',\n  type: { kind: 'StringType' },\n});\n\n/**\n * After ASTNodeJSON is instantiated into an ASTNode, you can listen for changes reactively\n */\nvariableDeclaration.onTypeChange((newType) => {\n  console.log('Variable type changed', newType);\n})\n\n```\n\n:::info{title=\"Concept Comparison\"}\n\nThe relationship between `ASTNodeJSON` and `ASTNode` is similar to the relationship between `JSX` and `VDOM` in React.\n- `ASTNodeJSON` is instantiated into `ASTNode` by the variable engine.\n- `JSX` is instantiated into `VDOM` by the React engine.\n\n:::\n\n:::warning{title=\"❓ Why not use Json Schema\"}\n\n[`Json Schema`](https://json-schema.org/) is a format for describing the structure of JSON data:\n\n- `Json Schema` only describes the type information of a variable, while `ASTNodeJSON` can also contain other information about the variable (e.g., its initial value).\n- `ASTNodeJSON` can be instantiated into an `ASTNode` by the variable engine, enabling capabilities like reactive listening.\n- `Json Schema` is good at describing Json types, while `ASTNodeJSON` can define more complex behaviors through custom extensions.\n\nIn terms of technical selection, the `VariableEngine` requires more powerful extension and expression capabilities. Therefore, `ASTNodeJSON` is needed to describe richer and more complex variable information, such as implementing dynamic type inference and automatic linking by defining the initial value of variables.\n\nHowever, as an industry-standard format for describing JSON types, `Json Schema` has advantages in ease of use, cross-team communication, and ecosystem (e.g., ajv, zod). Therefore, we use Json Schema extensively in our [**Materials**](/materials/introduction) to lower the barrier to entry.\n\n:::\n\n:::tip\n\nThe variable engine provides `ASTFactory` for **type-safe** creation of `ASTNodeJSON`:\n\n```tsx\nimport { ASTFactory } from '@flowgram/editor';\n\n/**\n * Type-safely create a VariableDeclaration ASTNodeJSON\n *\n * Equivalent to:\n * {\n *   kind: 'VariableDeclaration',\n *   key: 'var_index',\n *   type: { kind: 'StringType' },\n * }\n */\nASTFactory.createVariableDeclaration({\n  key: 'var_index',\n  type: { kind: 'StringType' },\n});\n```\n:::\n\n\n\n\n### Declaration 🌟\n\nDeclaration = Identifier (Key) + Definition. In design mode, a declaration is an `ASTNode` that stores an identifier plus variable information—the smallest unit that can be referenced.\n\n- Identifier (Key): The index used to access a declaration.\n- Definition: The information carried by the declaration. For a variable, the definition = type + right-hand value.\n\n\n:::info{title=\"Example: Declarations in JavaScript\"}\n\n**Variable Declaration** = Identifier + Variable Definition (Type + Initial Value)\n\n```javascript\n/**\n * Identifier: some_var\n * Variable Definition: type is number, initial value is 10\n */\nconst some_var: number = 10;\n```\n\n**Function Declaration** = Identifier + Function Definition (Function Parameters and Return Value + Function Body Implementation)\n\n```javascript\n/**\n * Identifier: add\n * Function Definition: parameters are two number variables a, b, and the return value is a number variable\n */\nfunction add(a: number, b: number): number {\n  return a + b;\n}\n```\n\n**Struct Declaration** = Identifier + Struct Definition (Fields + Types)\n\n```javascript\n/**\n * Identifier: Point\n * Struct Definition: fields are x, y, both of type number\n */\ninterface Point {\n  x: number;\n  y: number;\n}\n```\n\n:::\n\n\n:::tip{title=\"The Role of Identifiers\"}\n\n- The `Identifier` is the **index** of a declaration, used to access the `Definition` within the declaration.\n- Example: During compilation, a programming language uses the `Identifier` to find the type `Definition` of a variable for type checking.\n\n:::\n\n\nThe variable engine currently only provides **variable field declaration** (`BaseVariableField`), and extends it to two types of declarations: **variable declaration** (`VariableDeclaration`) and **property declaration** (`Property`).\n\n- Variable Field Declaration (`BaseVariableField`) = Identifier + Variable Field Definition (Type + Metadata + Initial Value)\n- Variable Declaration (`VariableDeclaration`) = **Globally Unique** Identifier + Variable Definition (Type + Metadata + Initial Value + Order within Scope)\n- Property Declaration (`Property`) = **Unique within Object** Identifier + Property Definition (Type + Metadata + Initial Value)\n\n\n\n\n\n### Type 🌟\n\nTypes **constrain the range of variable values**. In design mode, a type is also an `ASTNode`. Understanding types helps you reason about what a variable can store and what an expression returns.\n\nThe variable engine has built-in **basic types** from JSON:\n- `StringType`: string\n- `IntegerType`: integer\n- `NumberType`: floating-point number\n- `BooleanType`: boolean\n- `ObjectType`: object, which can be drilled down into `Property` declarations.\n- `ArrayType`: array, which can be drilled down into other types.\n\nIt also adds:\n- `MapType`: key-value pairs, where both keys and values can have type definitions.\n- `CustomType`: can be custom extended by the user, such as date, time, file types, etc.\n\n### Expression\n\nAn expression takes **0 or more variables as input**, processes them in a **specific way**, and returns a new **variable**. Design time only records “who it depends on” and the inferred return type—runtime handles the actual value calculation.\n\n```mermaid\ngraph LR\n\nInput_Variable_1 --input--> Expression\nInput_Variable_2 --input--> Expression\n\nExpression --returns--> Output_Variable\n\nstyle Expression stroke:#333,stroke-width:3px;\n```\n\nIn **design mode**, an expression is an `ASTNode`. Modeling focuses on:\n\n- Which variable declarations does the expression **use**?\n- How is the expression's **return type** inferred?\n\n```mermaid\ngraph LR\n\nVariable_Declaration_1 --input--> Expression_in_Design_Mode\nVariable_Declaration_2 --input--> Expression_in_Design_Mode\nExpression_in_Design_Mode --infers--> Return_Type\n\nstyle Expression_in_Design_Mode stroke:#333,stroke-width:3px;\n\n```\n\n\n:::info{title=\"Example: Expression Inference in Design Mode\"}\n\nSuppose we have an expression described in JavaScript code as `ref_var + 1`.\n\nWhich variable declarations does the expression **use**?\n- The variable declaration corresponding to the `ref_var` identifier.\n\nHow is the expression's **return type** inferred?\n- If the type of `ref_var` is `IntegerType`, the return type of `ref_var + 1` is `IntegerType`.\n- If the type of `ref_var` is `NumberType`, the return type of `ref_var + 1` is `NumberType`.\n- If the type of `ref_var` is `StringType`, the return type of `ref_var + 1` is `StringType`.\n\n:::\n\n:::info{title=\"Example: How the Variable Engine Implements Type Inference + Linking\"}\n\n<div style={{  }}>\n  <div style={{ width: 500 }}>\n     <img loading=\"lazy\" src=\"/variable/variable-batch-auto-infer.gif\" alt=\"Automatic Type Inference\" style={{ width: \"100%\"}} />\n  </div>\n\n  <div style={{ minWidth: 500 }}>\n\n\nThe figure shows a common example: a batch processing node references the output variable of a preceding node, iterates over it, and obtains an `item` variable. The type of the `item` variable automatically changes with the type of the output variable of the preceding node.\n\nThe ASTNodeJSON for this example can be represented as:\n\n```tsx\nASTFactory.createVariableDeclaration({\n  key: 'item',\n  initializer: ASTFactory.createEnumerateExpression({\n    enumerateFor: ASTFactory.createKeyPathExpression({\n      keyPath: ['start_0', 'arr']\n    })\n  })\n})\n```\n\nThe type inference chain is as follows:\n\n```mermaid\ngraph LR\n\n  Array_String[\"Array&lt;String&gt;\"]\n  Ref_Var[\"Variable with type\n Array&lt;String&gt;\"]\n\n  VariableDeclaration --initializer--> EnumerateExpression\n  KeyPathExpression --references--> Ref_Var\n  KeyPathExpression --return type--> Array_String\n  EnumerateExpression --iterates over--> KeyPathExpression\n  EnumerateExpression --return type--> String\n  VariableDeclaration -.inferred type.-> String\n  Array_String -.extracts subtype via iteration.-> String\n  Ref_Var -.type.-> Array_String\n\n```\n  </div>\n</div>\n\n\n\n\n:::\n\n\n### Scope Chain\n\nThe scope chain defines **which other scopes a scope can read variables from**. Think of it as the whitelist for variable access. The variable engine exposes an abstract class, and product teams can implement custom scope chains as needed.\n\nOut of the box, the engine ships with **free-layout** and **fixed-layout** scope chain implementations.\n\n\n#### Dependency Scope\n\n`Dependency scope` = the upstream scopes whose output variables the current scope can access.\n\nYou can access a scope's `Dependency Scope` via `scope.depScopes`.\n\n\n#### Covering Scope\n\n`Covering scope` = the downstream scopes that can access the current scope’s output variables.\n\nYou can access a scope's `Covering Scope` via `scope.coverScopes`.\n\n\n## Variables in the Canvas\n\nFlowGram defines the following special types of scopes in the canvas:\n\n### Node Scope 🌟\n\nAlso known as `Node Public Scope`, this scope can access the variables of the `Node Scope` of **upstream nodes**, and its output variable declarations can also be accessed by the `Node Scope` of **downstream nodes**.\n\nThe `Node Scope` can be set and retrieved via `node.scope`. Its scope chain relationship is shown in the figure below:\n\n```mermaid\ngraph BT\n\n  subgraph Current_Node\n    Child_Node_1.scope\n    Child_Node_2.scope\n    Current_Node.scope\n  end\n\n  Child_Node_1.scope -.depends on.-> Upstream_Node.scope\n  Child_Node_2.scope -.depends on.-> Upstream_Node.scope\n  Current_Node.scope -.depends on.-> Upstream_Node.scope\n  Downstream_Node.scope -.depends on.-> Upstream_Node.scope\n  Downstream_Node.scope -.depends on.-> Current_Node.scope\n\n  Downstream_Node.scope -.-|\"<font color=red>❌ Not accessible</font>\"| Child_Node_1.scope\n  Downstream_Node.scope -.-|\"<font color=red>❌ Not accessible</font>\"| Child_Node_2.scope\n\n  linkStyle 5 stroke:red,stroke-width:2px\n  linkStyle 6 stroke:red,stroke-width:2px\n\n\n  style Current_Node.scope fill:#f9f,stroke:#333,stroke-width:3px\n```\n\n:::warning\n\nIn the default scope logic, the output variables of a child node's `Node Scope` cannot be accessed by the **downstream nodes of the parent node**.\n\n:::\n\n\n### Node Private Scope\n\nThe output variables of a `Node Private Scope` can only be accessed within the **current node's** `Node Scope` and its **child nodes'** `Node Scope`. This is similar to the concept of `private variables` in programming languages.\n\nThe `Node Private Scope` can be set and retrieved via `node.privateScope`. Its scope chain relationship is shown in the figure below:\n\n```mermaid\ngraph BT\n  subgraph Current_Node\n    Child_Node_1.scope -.depends on.-> Current_Node.privateScope\n    Current_Node.scope -.depends on.-> Current_Node.privateScope\n    Child_Node_2.scope -.depends on.-> Current_Node.privateScope\n  end\n\n  Current_Node -.all depend on.-> Upstream_Node.scope\n  Downstream_Node.scope -.depends on.-> Current_Node.scope\n  Downstream_Node.scope -.depends on.-> Upstream_Node.scope\n\n\n  style Current_Node.privateScope fill:#f9f,stroke:#333,stroke-width:3px\n  style Current_Node.scope stroke:#333,stroke-width:3px\n```\n\n\n### Global Scope\n\nVariables in the `Global Scope` are readable from **all node scopes and node private scopes**, yet the global scope itself **does not depend on any other scope**. It’s ideal for configuration, constants, environment context, and other shared data.\n\nFor how to set the global scope, see [Output Global Variables](./variable-output#output-global-variables). Its scope chain relationship is shown in the figure below:\n\n```mermaid\ngraph RL\n\n subgraph Current_Node\n    Child_Node_1.scope\n    Child_Node_2.scope\n    Current_Node.scope\n    Current_Node.privateScope\n  end\n\n  Current_Node.scope -.depends on.-> Global_Scope\n  Upstream_Node.scope -.depends on.-> Global_Scope\n  Downstream_Node.scope -.depends on.-> Global_Scope\n  Current_Node.privateScope -.depends on.-> Global_Scope\n  Child_Node_1.scope -.depends on.-> Global_Scope\n  Child_Node_2.scope -.depends on.-> Global_Scope\n\n  style Current_Node.scope stroke:#333,stroke-width:3px\n  style Global_Scope fill:#f9f,stroke:#333,stroke-width:3px\n\n```\n\n\n\n## Overall Architecture\n\n![Architecture Diagram](/variable/concept/arch-en.png)\n\nThe variable engine follows the Dependency Inversion Principle (DIP) and is split into three layers according to code stability, abstraction level, and proximity to business logic:\n\n### Variable Abstraction Layer\n\nThis abstraction layer is the most stable part of the architecture. It defines the core interfaces—`ASTNode`, `Scope`, `ScopeChain`, and more—that upper layers extend.\n\n### Variable Implementation Layer\n\nThis layer sits closer to product requirements and evolves more often. The engine provides a set of built-in `ASTNode` and `ScopeChain` implementations, and you can register new ones or override defaults via dependency injection when your domain demands it.\n\n### Variable Material Layer\n\nThe outermost layer adopts the Facade pattern to boost usability, packaging complex capabilities into “materials” that can be reused directly.\n\n- For the use of variable materials, see: [Materials](/materials/introduction)\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/custom-scope-chain.mdx",
    "content": "---\ndescription: How to customize the scope chain\n---\n\n# Scope Chain\n\n:::info{title=\"Before You Read\"}\n\n- We recommend completing [Output Variables](./variable-output.mdx) and [Consume Variables](./variable-consume.mdx) first.\n- If the idea of scopes still feels fuzzy, review [Core Concepts – Scope Chain](./concept#scope-chain) before diving in.\n\n:::\n\n## Default Scope Chain Logic\n\nFor details, see: [Scope in Canvas](./concept#variables-in-the-canvas)\n\n\n## Customize in `editor-props`\n\nThe customization logic for the scope is usually done in `editor-props` through `variableEngine.chainConfig`.\n\n```tsx pure title=\"use-editor-props.tsx\" {6-8}\n// ...\n{\n  // ...\n  variableEngine: {\n    enable: true,\n    chainConfig: {\n      // Customize scope chain logic\n    }\n  }\n  // ...\n}\n// ...\n```\n\n\n### Whether child nodes can be depended on by subsequent nodes\n\n**By default, child nodes cannot be depended on by subsequent nodes of the parent node**.\n\nIf you need to customize this logic, you need to configure it in `variableEngine.chainConfig.isNodeChildrenPrivate`.\n\n\n```tsx pure title=\"use-editor-props.tsx\" {8-15}\n{\n  variableEngine: {\n    enable: true,\n    chainConfig: {\n      /**\n       * Customize: Whether child nodes can be depended on by subsequent nodes of the parent node\n       */\n      isNodeChildrenPrivate(node) {\n        // When a certain type of custom node is hit, allow its child nodes to be depended on by subsequent nodes\n        if (node.flowNodeType === 'Your_Custom_Type') {\n          return false;\n        }\n        // Otherwise: by default, child nodes are not allowed to be depended on by subsequent nodes\n        return true;\n      },\n    }\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/variable-consume.mdx",
    "content": "---\ndescription: Introduction to consuming variables output by FlowGram's variable engine\n---\n\n# Consuming Variables\n\nIn FlowGram, when a node wants to use variables from preceding nodes, it needs to consume those variables.\n\n:::info{title=\"Reading Tips\"}\n\n- We recommend finishing [Output Variables](./variable-output.mdx) first so you know how variables are produced; this guide is the second stop focused on “how to access them.”\n- To quickly preview the variable selector experience, try [VariableSelector](#variableselector) first; continue with the API sections when you need code-level access to variable lists.\n- All examples assume the **node scope**. When private or global scopes show up, refer to [Core Concepts – Variables in the Canvas](./concept#variables-in-the-canvas) for additional context.\n\n:::\n\n## `VariableSelector`\n\nTo make it easier for you to integrate variable selection functionality into your applications, the official materials provide the `VariableSelector` component.\n\nSee documentation: [VariableSelector](/materials/components/variable-selector)\n\n## Getting Accessible Variable Tree\n\nIn canvas nodes, we often need to get **variables available in the current scope** and display them in a tree structure for users to select and operate.\n\n:::info{title=\"Common Needs at a Glance\"}\n\n- **Just list variables** → `useAvailableVariables`\n- **Need drill-down for objects/arrays** → `ASTMatch` + recursive rendering\n- **Need precise subscriptions** → go straight to `scope.available`\n\n:::\n\n### `useAvailableVariables`\n\n`useAvailableVariables` is a lightweight Hook that directly returns an array of variables available in the current scope (`VariableDeclaration[]`).\n\n```tsx pure title=\"use-variable-tree.tsx\" {7}\nimport {\n  type BaseVariableField,\n  useAvailableVariables,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// .... In React component or Hook\nconst availableVariables = useAvailableVariables();\n\nconst renderVariable = (variable: BaseVariableField) => {\n  // You can render each variable according to your needs here\n  // ....\n}\n\nreturn availableVariables.map(renderVariable);\n\n// ....\n```\n\n### Getting Object Type Variable Drill-down\n\nWhen a variable's type is `Object`, we often need to be able to \"drill down\" into its interior to access its properties. The `ASTMatch.isObject` method can help us determine if a variable type is an object. If it is, we can recursively render its `properties`.\n\n:::tip\n\nEach layer of the variable tree is essentially a declaration (`BaseVariableField`). For objects, `properties` gives you the next-level declaration array.\n\n:::\n\n```tsx pure title=\"use-variable-tree.tsx\" {12}\nimport {\n  type BaseVariableField,\n  ASTMatch,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ....\n\nconst renderVariable = (variable: BaseVariableField) => ({\n  title: variable.meta?.title,\n  key: variable.key,\n  // Only Object type variables can be drilled down\n  children: ASTMatch.isObject(variable.type) ? variable.type.properties.map(renderVariable) : [],\n});\n\n// ....\n```\n\n### Getting Array Type Variable Drill-down\n\nSimilar to `Object` type, when encountering an `Array` type variable, we also want to display its internal structure. For arrays, we usually care about the type of their elements. `ASTMatch.isArray` can determine if a variable type is an array. It's worth noting that the element type of an array can be any type, and it might even be another array. Therefore, we need a recursive helper function `getTypeChildren` to handle this situation.\n\n```tsx pure title=\"use-variable-tree.tsx\" {13,16}\nimport {\n  type BaseVariableField,\n  type BaseType,\n  ASTMatch,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ....\n\nconst getTypeChildren = (type?: BaseType): BaseVariableField[] => {\n  if (!type) return [];\n\n  // Get Object properties\n  if (ASTMatch.isObject(type)) return type.properties;\n\n  // Recursively get Array element type\n  if (ASTMatch.isArray(type)) return getTypeChildren(type.items);\n\n  return [];\n};\n\nconst renderVariable = (variable: BaseVariableField) => ({\n  title: variable.meta?.title,\n  key: variable.key,\n  children: getTypeChildren(variable.type).map(renderVariable),\n});\n\n// ....\n```\n\n## `scope.available`\n\n`scope.available` is one of the cores of the variable system, which can perform more advanced variable retrieval and monitoring actions on **variables available within the scope**.\n\n:::info{title=\"When to reach for scope.available\"}\n\n- You need to read or validate a variable by `keyPath`.\n- You want to manipulate visibility outside of React hooks (e.g., in plugins or services).\n- You need fine-grained subscriptions without refreshing the entire list.\n\n:::\n\n### `useScopeAvailable`\n\n`useScopeAvailable` can directly return `scope.available` in React.\n\n```tsx\nimport { useScopeAvailable } from '@flowgram.ai/free-layout-editor';\n\nconst available = useScopeAvailable();\n\n// The available object contains variable list and other APIs\nconsole.log(available.variables);\n\n// Get a single variable\nconsole.log(available.getByKeyPath(['start_0', 'xxx']));\n\n// Monitor changes in a single variable\navailable.trackByKeyPath(['start_0', 'xxx'], () => {\n  // ...\n})\n```\n\n:::info{title=\"Main Difference from useAvailableVariables\"}\n\n*   **Return Value Different**: `useAvailableVariables` directly returns an array of variables, while `useScopeAvailable` returns a `ScopeAvailableData` object that includes a `variables` property and other methods.\n*   **Applicable Scenario**: When you need to perform more complex operations on variables, such as tracking changes in a single variable through `trackByKeyPath`, `useScopeAvailable` is your best choice.\n\n:::\n\n:::warning{title=\"useScopeAvailable automatically refreshes when available variables change\"}\n\nIf you don't want automatic refresh, you can turn it off through the autoRefresh parameter:\n\n```tsx\nuseScopeAvailable({ autoRefresh: false })\n```\n:::\n\n### `getByKeyPath`\n\nThrough `getByKeyPath`, you can get a specific variable field (including variables nested in Object or Array) from the accessible variables in the current scope.\n\n```tsx {6,13-17}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect, useState } from 'react';\n\nfunction VariableDisplay({ keyPath }: { keyPath:string[] }) {\n  const available = useScopeAvailable();\n  const variableField = available.getByKeyPath(keyPath)\n\n  return <div>{variableField.meta?.title}</div>;\n}\n```\n\n`getByKeyPath` is often used in variable validation, such as:\n\n```tsx\nconst validateVariableInNode = (keyPath: string, node: FlowNodeEntity) => {\n  // Validate whether the variable can be accessed by the current node\n  return Boolean(node.scope.available.getByKeyPath(keyPath))\n}\n```\n\n### `trackByKeyPath`\n\nWhen you only care about changes to a specific variable field (including variables nested in Object or Array), `trackByKeyPath` allows you to precisely \"subscribe\" to updates of that variable without causing component re-renders due to changes in other unrelated variables, thus achieving more refined performance optimization.\n\n:::tip\n\nCombine it with `autoRefresh: false` to avoid wide re-renders—only update local state when the tracked variable changes.\n\n:::\n\n```tsx {6,13-17}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect, useState } from 'react';\n\nfunction UserNameDisplay() {\n  // Turn off autoRefresh to prevent re-renders triggered by any variable changes\n  const available = useScopeAvailable({ autoRefresh: false });\n  const [userName, setUserName] = useState('');\n\n  useEffect(() => {\n    // Define the variable path we want to track\n    const keyPath = ['user', 'name'];\n\n    // Start tracking!\n    const disposable = available.trackByKeyPath(keyPath, (nameField) => {\n      // When the user.name variable field changes, this callback function will be triggered\n      // nameField is the changed variable field, from which we can get the latest default value\n      setUserName(nameField?.meta.default || '');\n    });\n\n    // Cancel tracking when the component unmounts to avoid memory leaks\n    return () => disposable.dispose();\n  }, [available]); // The dependency is the available object\n\n  return <div>User Name: {userName}</div>;\n}\n```\n\n### Overall Listening API\n\nIn addition to `trackByKeyPath`, `ScopeAvailableData` also provides a set of event listening APIs for overall variable changes, allowing you to more precisely control the response logic for variable changes.\n\nThis is very useful when dealing with complex scenarios that require manual management of subscriptions.\n\nBelow we use a table to compare these three core listening APIs in detail:\n\n| API & Callback Parameters | Trigger Timing | Core Difference and Applicable Scenario |\n| :--- | :--- | :--- |\n| `onVariableListChange: (variables: VariableDeclaration[]) => void` | When the **list structure** of available variables changes. | **Only cares about the list itself**. For example, an upstream node added/removed an output variable, causing the total number or members of available variables to change. It doesn't care about changes within variables and drill-downs. Applicable to scenarios where you need to update UI based on the presence or quantity of variable lists. |\n| `onAnyVariableChange: (changedVariable: VariableDeclaration) => void` | When the **type, metadata, and drill-down fields** of **any** variable in the list change. | **Only cares about updates to variable definitions**. For example, a user modified the type of an output variable. It doesn't care about changes to the list structure. Applicable to scenarios where you need to respond to changes in the content of any variable. |\n| `onListOrAnyVarChange: (variables: VariableDeclaration[]) => void` | When **either** of the above two situations occurs. | **The most comprehensive listening**, combining the previous two. Both changes to the list structure and changes to any variable will trigger it. Applicable to \"fallback\" scenarios where you need to respond to any possible changes. |\n\nLet's see how to use these APIs in components through a specific example.\n\n```tsx {5,9-11,14-16,19-22}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect } from 'react';\n\nfunction AdvancedListenerComponent() {\n  const available = useScopeAvailable({ autoRefresh: false });\n\n  useEffect(() => {\n    // 1. Listen to list structure changes\n    const listChangeDisposable = available.onVariableListChange((variables) => {\n      console.log('The structure of the available variable list has changed! The new list length is:', variables.length);\n    });\n\n    // 2. Listen to any variable changes\n    const valueChangeDisposable = available.onAnyVariableChange((changedVariable) => {\n      console.log(`The definition of variable '${changedVariable.keyPath.join('.')}' has changed`);\n    });\n\n    // 3. Listen to all changes (structure or individual variable interior)\n    const allChangesDisposable = available.onListOrAnyVarChange((variables) => {\n      console.log('The variable list or one of its variables has changed!');\n      // Note: The callback parameter here is the complete variable list, not a single changed variable\n    });\n\n    // When the component unmounts, be sure to clean up all listeners to prevent memory leaks\n    return () => {\n      listChangeDisposable.dispose();\n      valueChangeDisposable.dispose();\n      allChangesDisposable.dispose();\n    };\n  }, [available]);\n\n  return <div>Please check the console for variable change logs...</div>;\n}\n```\n\n:::warning\n\nThese APIs all return a `Disposable` object. To avoid memory leaks and unnecessary calculations, you must call its `dispose()` method in the cleanup function of `useEffect` to cancel the listening.\n\n:::\n\n## Getting Output Variables of Current Scope\n\n### `useOutputVariables`\n\n`useOutputVariables` can get **output variables of the current scope** and **automatically trigger a refresh** when the output variable list or drill-down changes.\n\n```tsx\nconst variables = useOutputVariables();\n```\n\n:::tip\n\n`useOutputVariables` is available in flowgram@0.5.6 and later versions. If you are on an earlier version, you can implement it with the following code:\n\n```tsx\nconst scope = useCurrentScope();\nconst refresh = useRefresh();\n\nuseEffect(() => {\n  const disposable = scope.output.onListOrAnyVarChange(() => {\n    refresh();\n  });\n\n  return () => disposable.dispose();\n}, [])\n\nconst variables = scope.variables;\n```\n:::\n\n## Other APIs\n\n### Getting Current Scope\n\nYou can get the current scope through [`useCurrentScope`](https://flowgram.ai/auto-docs/editor/functions/useCurrentScope).\n\n```tsx\nconst scope = useCurrentScope()\n\nscope.output.variables\n\nscope.available\n\n```\n\n### Setting Current Scope\n\nYou can set the current scope through [`ScopeProvider`](https://flowgram.ai/auto-docs/editor/functions/ScopeProvider).\n\n```tsx\n// set the scope of current node\n<ScopeProvider scope={node.scope}>\n  <YourUI />\n</ScopeProvider>\n\n// set to private scope of current node\n<ScopeProvider scope={node.privateScope}>\n  <YourUI />\n</ScopeProvider>\n```\n"
  },
  {
    "path": "apps/docs/src/en/guide/variable/variable-output.mdx",
    "content": "---\ndescription: Introduction to using FlowGram's variable engine to output variables\n---\n\n# Output Variables\n\nWe primarily categorize output variables into three types:\n\n1. **Output Node Variables**: Typically produced by the node and available for subsequent nodes to use.\n2. **Output Node Private Variables**: Output variables limited to the node's interior (including child nodes) and not accessible by external nodes.\n3. **Output Global Variables**: Available throughout the entire flow, readable by any node, suitable for storing public states or configurations.\n\n:::info{title=\"Reading Guide\"}\n\n- After [Variable Introduction](./basic.mdx), start here to practice how variables are produced.\n- If you work from form configuration, begin with “Method 1: Synchronization via Form Side Effects.” If you need runtime logic or batch updates, skip to the plugin or UI sections.\n- Every example uses `ASTFactory`; revisit [Core Concepts – AST](./concept#ast-) if you need a refresher.\n\n:::\n\n## Output Node Variables\n\nOutput node variables are bound to the lifecycle of the current node: they are created with the node and removed when the node is deleted. (See [Node Scope](./concept#node-scope) for details.)\n\nWe typically have three ways to output node variables:\n\n:::info{title=\"How to pick a method\"}\n\n- Variable definitions tied to form inputs → Method 1.\n- Variables generated at runtime or synchronized in batches → Method 2.\n- Writing variables directly in UI is only for temporary debugging; avoid Method 3 in production.\n\n:::\n\n### Method 1: Synchronization via Form Side Effects\n\n[Form side effects](/guide/form/form#side-effects-effect) are usually configured in the node's `form-meta.ts` file and are the most common way to define node output variables.\n\n:::info{title=\"When to use it\"}\n\n- The node’s variable model can be derived from form fields.\n\n:::\n\n#### `provideJsonSchemaOutputs`\n\nIf the structure of the output variables required by a node matches the [JSON Schema](https://json-schema.org/) structure, you can use the `provideJsonSchemaOutputs` side effect (Effect) material.\n\nprovideJsonSchemaOutputs uses the [`createEffectFromVariableProvider`](/guide/variable/variable-output) factory function to create variable providers.\n\nSee documentation: [provideJsonSchemaOutputs](/materials/effects/provide-json-schema-outputs)\n\n#### `createEffectFromVariableProvider` Custom Output\n\n`provideJsonSchemaOutputs` only adapts to `JsonSchema`. If you want to define your own set of Schema, you'll need to customize form side effects.\n\n:::note\n\nFlowGram provides `createEffectFromVariableProvider`, which only requires defining a `parse` function to customize your variable synchronization side effect:\n- `parse` is called when the form value is initialized and updated\n- The input of `parse` is the current field's form value\n- The output of `parse` is variable AST\n\n:::\n\nIn the following example, we create output variables for two form fields `path.to.value` and `path.to.value2`:\n\n```tsx pure title=\"form-meta.ts\" {26-37,40-56}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n  type ASTNodeJSON\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport function createTypeFromValue(typeValue: string): ASTNodeJSON | undefined {\n  switch (typeValue) {\n    case 'string':\n      return ASTFactory.createString();\n    case 'number':\n      return ASTFactory.createNumber();\n    case 'boolean':\n      return ASTFactory.createBoolean();\n    case 'integer':\n      return ASTFactory.createInteger();\n    default:\n      return;\n  }\n}\n\nexport const formMeta =  {\n  effect: {\n    // Create first variable\n    // = node.scope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value': createEffectFromVariableProvider({\n      // parse form value to variable\n      parse(v: string, { node }) {\n        return [{\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `uid_${node.id}`,\n          type: createTypeFromValue(v)\n        }]\n      }\n    }),\n    // Create second variable\n    // = node.scope.setVar('path.to.value2', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value2': createEffectFromVariableProvider({\n      // parse form value to variable\n      parse(v: { name: string; typeValue: string }[], { node }) {\n        return {\n          meta: {\n            title: `Second Output Variable For ${node.form.getValueIn(\"title\")}`,\n          },\n          key: `uid_${node.id}_2`,\n          type: ASTFactory.createObject({\n            properties: v.map(_item => ASTFactory.createProperty({\n              key: _item.name,\n              type: createTypeFromValue(_item.typeValue)\n            }))\n          })\n        }\n      }\n    }),\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n:::tip\n\nIf your Schema is complex and you're not sure how to parse it into AST, you can refer to the official material's implementation of JSON Schema conversion to AST: [JsonSchemaUtils.schemaToAST](https://github.com/bytedance/flowgram.ai/blob/main/packages/variable-engine/json-schema/src/json-schema/utils.ts)\n\n:::\n\n:::warning\n\nWhen using the VariableSelector official material for variable selection, **each output variable defined in the current node will be displayed as an independent tree node**, rather than being grouped by node by default.\n\nFor more details, please refer to the [VariableSelector Material Documentation](/materials/components/variable-selector)\n\n:::\n\n\n\n#### Synchronizing Multiple Form Fields to One Variable\n\nIf synchronizing multiple fields to one variable, you need to use the `namespace` field of `createEffectFromVariableProvider` to synchronize variable data from multiple fields to the same namespace.\n\n```tsx pure title=\"form-meta.ts\" {11}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Get information from multiple form fields\n */\nconst variableSyncEffect = createEffectFromVariableProvider({\n  // Must be added to ensure side effects from different fields synchronize to the same namespace\n  namespace: 'your_namespace',\n\n  // Parse form value to variable\n  parse(_, { form, node }) {\n    // Note: The form field requires flowgram version > 0.5.5, prior versions can get it through node.form\n    return [{\n      meta: {\n        title: `Title_${form.getValueIn('path.to.value')}_${form.getValueIn('path.to.value2')}`,\n      },\n      key: `uid_${node.id}`,\n      type: ASTFactory.createCustomType({ typeName: \"CustomVariableType\" })\n    }]\n  }\n})\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': variableSyncEffect,\n    'path.to.value2': variableSyncEffect,\n  },\n  render: () => (\n   // ...\n  )\n}\n```\n\n#### Using `node.scope` API in Side Effects\n\nIf `createEffectFromVariableProvider` doesn't meet your needs, you can also directly use the `node.scope` API in form side effects for more flexible variable operations.\n\n:::note\n\n`node.scope` returns a variable scope object for a node, which has several core methods mounted on it:\n\n- `setVar(variable)`: Set a variable.\n- `setVar(namespace, variable)`: Set a variable under a specified namespace.\n- `getVar()`: Get all variables.\n- `getVar(namespace)`: Get variables under a specified namespace.\n- `clearVar()`: Clear all variables.\n- `clearVar(namespace)`: Clear variables under a specified namespace.\n\n:::\n\n```tsx pure title=\"form-meta.tsx\" {10-18,29-38}\nimport { Effect } from '@flowgram.ai/editor';\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.scope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Title_${value}`,\n            },\n            key: `uid_${context.node.id}`,\n            type: ASTFactory.createString(),\n          })\n        )\n\n        console.log(\"View generated variables\", context.node.scope.getVar())\n\n      }) as Effect,\n    }],\n    'path.to.value2': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.scope.setVar(\n          'namespace_2',\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Title_${value}`,\n            },\n            key: `uid_${context.node.id}_2`,\n            type: ASTFactory.createNumber(),\n          })\n        )\n\n        console.log(\"View generated variables\", context.node.scope.getVar('namespace_2'))\n\n      }) as Effect,\n    }],\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n### Method 2: Synchronizing Variables via Plugins\n\nIn addition to static configuration in forms, we can also freely and dynamically manipulate node variables in plugins through `node.scope`.\n\n:::info{title=\"When to use it\"}\n\n- You need to create or adjust variables across multiple nodes in bulk.\n- You want to auto-populate default variables when the canvas initializes.\n\n:::\n\n#### Updating via Specified Node's Scope\n\nThe following example demonstrates how to obtain the `Scope` of the start node in the `onInit` lifecycle of a plugin and perform a series of operations on its variables.\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {10-22}\nimport {\n  FlowDocument,\n  definePluginCreator,\n  PluginCreator,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      const startNode = ctx.get(FlowDocument).getNode('start_0');\n      const startScope =  startNode.scope!\n\n      // Set Variable For Start Scope\n      startScope.setVar(\n        ASTFactory.createVariableDeclaration({\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `uid`,\n          type: ASTFactory.createString(),\n        })\n      )\n    }\n  })\n```\n\n#### Synchronizing Variables in onNodeCreate\n\nThe following example demonstrates how to obtain the Scope of a newly created node through `onNodeCreate` and implement variable synchronization by listening to `node.form.onFormValuesChange`.\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {10,29}\nimport {\n  FlowDocument,\n  definePluginCreator,\n  PluginCreator,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      ctx.get(FlowDocument).onNodeCreate(({ node }) => {\n        const syncVariable = (title: string) => {\n          node.scope?.setVar(\n            ASTFactory.createVariableDeclaration({\n              key: `uid_${node.id}`,\n              meta: {\n                title,\n                icon: iconVariable,\n              },\n              type: ASTFactory.createString(),\n            })\n          );\n        };\n\n        if (node.form) {\n          // sync variable on init\n          syncVariable(node.form.getValueIn('title'));\n\n          // listen to form values change\n          node.form?.onFormValuesChange(({ values, name }) => {\n            // title field changed\n            if (name.match(/^title/)) {\n              syncVariable(values[name]);\n            }\n          });\n        }\n      });\n    }\n  })\n```\n\n### Method 3: Synchronizing Variables in UI (Not Recommended)\n\n:::warning\nDirectly synchronizing variables in UI (Method 3) is a **strongly discouraged** practice. It breaks the principle of **separation of data and rendering**, leading to tight coupling between data and rendering, which may cause:\n\n- Closing the node sidebar prevents variable synchronization, resulting in inconsistency between data and rendering.\n- If the canvas enables performance optimization to only render nodes visible in the view, and the node is not in the view, the联动 logic will fail.\n\n:::\n\nThe following example demonstrates how to synchronously update variables in `formMeta.render` through the `useCurrentScope` event.\n\n```tsx pure title=\"form-meta.ts\" {13}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * Get information from form\n */\nconst FormRender = () => {\n  /**\n   * Get current scope for setting variables later\n   */\n  const scope = useCurrentScope()\n\n  return <>\n    <UserCustomForm\n      onValuesChange={(values) => {\n        scope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: values.title,\n            },\n            key: `uid`,\n            type: ASTFactory.createString(),\n          })\n        )\n      }}\n    />\n  </>\n}\n\nexport const formMeta = {\n  render: () => <FormRender />\n}\n```\n\n## Output Node Private Variables\n\nPrivate variables are variables that can only be accessed within the current node and its child nodes. (See [Node Private Scope](./concept#node-private-scope).)\n\n:::tip\n\nQuick rule of thumb: if a variable only serves the node’s internal implementation and shouldn’t be exposed downstream, keep it in `node.privateScope`.\n\n:::\n\nHere we only list two methods, and other methods can be inferred from [Output Node Variables](#output-node-variables).\n\n### Method 1: `createEffectFromVariableProvider`\n\n`createEffectFromVariableProvider` provides the parameter `scope` for specifying the variable's scope.\n- When `scope` is set to `private`, the variable's scope is the current node's private scope `node.privateScope`\n- When `scope` is set to `public`, the variable's scope is the current node's scope `node.scope`\n\n```tsx pure title=\"form-meta.ts\" {11}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const formMeta =  {\n  effect: {\n    // Create variable in privateScope\n    // = node.privateScope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value': createEffectFromVariableProvider({\n      scope: 'private',\n      // parse form value to variable\n      parse(v: string, { node }) {\n        return [{\n          meta: {\n            title: `Private_${v}`,\n          },\n          key: `uid_${node.id}_locals`,\n          type: ASTFactory.createBoolean(),\n        }]\n      }\n    }),\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n### Method 2: `node.privateScope`\n\nThe API design of `node.privateScope` is almost identical to the node scope (`node.scope`), both providing methods like `setVar`, `getVar`, `clearVar`, etc., and both supporting namespaces. For details, please refer to [`node.scope`](#using-nodescope-api-in-side-effects).\n\n```tsx pure title=\"form-meta.tsx\" {10-18}\nimport { Effect } from '@flowgram.ai/editor';\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.privateScope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Your Private Variable Title`,\n            },\n            key: `uid_${context.node.id}`,\n            type: ASTFactory.createInteger(),\n          })\n        )\n\n        console.log(\"View generated variables\", context.node.privateScope.getVar())\n\n      }) as Effect,\n    }],\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n## Output Global Variables\n\nGlobal variables are like the “shared memory” of the entire flow—any node or plugin can read and modify them. They work well for state that persists across the flow, such as user information or environment configuration. (See [Global Scope](./concept#global-scope).)\n\n:::info{title=\"When to choose the global scope\"}\n\n- The variable is reused across multiple nodes or even plugins.\n- The variable should be decoupled from a specific node (e.g., environment config, user context).\n- You need to write it during initialization so downstream nodes can simply read it.\n\n:::\n\nSimilar to node variables, we also have two main ways to obtain the global variable scope (`GlobalScope`).\n\n### Method 1: Obtaining in Plugins\n\nIn the plugin's context (`ctx`), we can directly \"inject\" an instance of `GlobalScope`:\n\n```tsx pure title=\"global-variable-plugin.tsx\" {10-20}\nimport {\n  GlobalScope,\n  definePluginCreator,\n  PluginCreator\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createGlobalVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      const globalScope = ctx.get(GlobalScope)\n\n      globalScope.setVar(\n         ASTFactory.createVariableDeclaration({\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `your_variable_global_unique_key`,\n          type: ASTFactory.createString(),\n        })\n      )\n    }\n  })\n```\n\n### Method 2: Obtaining in UI\n\nIf you want to interact with global variables in a React component on the canvas, you can use the `useService` Hook to obtain an instance of `GlobalScope`:\n\n```tsx pure title=\"global-variable-component.tsx\" {7}\nimport {\n  GlobalScope,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nfunction GlobalVariableComponent() {\n  const globalScope = useService(GlobalScope)\n\n  // ...\n\n  const handleChange = (v: string) => {\n    globalScope.setVar(\n      ASTFactory.createVariableDeclaration({\n        meta: {\n          title: `Your Output Variable Title`,\n        },\n        key: `uid_${v}`,\n        type: ASTFactory.createString(),\n      })\n    )\n  }\n\n  return <Input onChange={handleChange}/>\n}\n```\n\n### Global Scope API\n\nThe API design of `GlobalScope` is almost identical to the node scope (`node.scope`), both providing methods like `setVar`, `getVar`, `clearVar`, etc., and both supporting namespaces. For details, please refer to [`node.scope`](#using-nodescope-api-in-side-effects).\n\nHere's a comprehensive example of operating global variables in a plugin:\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {11-39}\nimport {\n  GlobalScope,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ...\n\nonInit(ctx, options) {\n  const globalScope = ctx.get(GlobalScope);\n\n  // 1. Create, Update, Read, Delete Variable in GlobalScope\n  globalScope.setVar(\n    ASTFactory.createVariableDeclaration({\n      meta: {\n        title: `Your Output Variable Title`,\n      },\n      key: `your_variable_global_unique_key`,\n      type: ASTFactory.createString(),\n    })\n  )\n\n  console.log(globalScope.getVar())\n\n  globalScope.clearVar()\n\n  // 2. Create, Update, Read, Delete Variable in GlobalScope's namespace: 'namespace_1'\n    globalScope.setVar(\n      'namespace_1',\n      ASTFactory.createVariableDeclaration({\n        meta: {\n          title: `Your Output Variable Title 2`,\n        },\n        key: `uid_2`,\n        type: ASTFactory.createString(),\n      })\n  )\n\n  console.log(globalScope.getVar('namespace_1'))\n\n  globalScope.clearVar('namespace_1')\n\n  // ...\n}\n```\n\nSee: [Class: GlobalScope](https://flowgram.ai/auto-docs/editor/classes/GlobalScope.html)\n"
  },
  {
    "path": "apps/docs/src/en/index.md",
    "content": "---\npageType: home\n\nhero:\n  name: FlowGram.AI\n  text: Workflow development framework\n  tagline: Building workflow platforms easily - Canvas, Form, Variable, Materials\n  actions:\n    - theme: brand\n      text: Quick Start\n      link: /guide/getting-started/introduction\n    - theme: alt\n      text: GitHub\n      link: https://github.com/bytedance/flowgram.ai\n  image:\n    src: /transparent-logo.svg\n    alt: Logo\nfeatures:\n  - title: Coze\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-coze-en.png\" alt=\"Coze\"/></div>\n  - title: Feishu Low-Code Platform Workflow\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-apaas-en.png\" alt=\"Feishu Low-Code Platform Workflow\"/></div>\n  - title: Feishu Base\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-bitable-en.png\" alt=\"Feishu Base\"/></div>\n  - title: nndeploy\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-nndeploy.png\" alt=\"nndeploy\"/></div>\n  - title: Certimate\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-certimate.png\" alt=\"Certimate\"/></div>\n---\n"
  },
  {
    "path": "apps/docs/src/en/materials/_meta.json",
    "content": "[\n  \"introduction\",\n  \"cli\",\n  {\n    \"type\": \"dir\",\n    \"name\": \"components\",\n    \"label\": \"Form Components\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"effects\",\n    \"label\": \"Form Effects\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"form-plugins\",\n    \"label\": \"Form Plugins\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"validate\",\n    \"label\": \"Validate\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"common\",\n    \"label\": \"Common\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/en/materials/cli.mdx",
    "content": "# Material Management CLI\n\nFlowgram provides a dedicated CLI command-line tool to help you manage official materials in your project.\n\n## Installation\n\nYou can run it directly with npx:\n\n```bash\nnpx @flowgram.ai/cli@latest [command]\n```\n\n## Sync Materials to Project\n\nUse the `materials` command to add the source code of official materials to your project for customization:\n\n```bash\n# Interactive material selection\nnpx @flowgram.ai/cli@latest materials\n\n# Directly specify material\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n\n# Specify multiple materials (comma-separated)\nnpx @flowgram.ai/cli@latest materials components/variable-selector,effect/provideJsonSchemaOutputs\n\n# Select multiple materials (interactive multi-select)\nnpx @flowgram.ai/cli@latest materials --select-multiple\n```\n\nAfter running, the CLI will prompt you to select materials to add to your project:\n\n```console\n🚀 Welcome to @flowgram.ai form-materials CLI!\n📁 Project: /path/to/your/project\n🎯 Flowgram Version: 1.0.0\n  - Target material root: /path/to/your/project/src/form-materials\n\n🚀 The following materials will be added to your project\n📦 components/json-schema-editor\n📦 components/variable-selector\n📦 effect/provideJsonSchemaOutputs\n\n✅ These npm dependencies is added to your package.json\n- @semi-design/icons\n- lodash-es\n- classnames\n\n➡️ Please run npm install to install dependencies\n```\n\n### Advanced Options\n\nThe `materials` command supports the following options:\n\n| Option | Description | Example |\n|------|------|------|\n| `--refresh-project-imports` | Refresh project imports for copied materials | `npx @flowgram.ai/cli@latest materials components/json-schema-editor --refresh-project-imports` |\n| `--target-material-root-dir <dir>` | Specify target directory for material copying | `npx @flowgram.ai/cli@latest materials --target-material-root-dir src/custom-materials` |\n| `--select-multiple` | Enable interactive multi-select mode | `npx @flowgram.ai/cli@latest materials --select-multiple` |\n\n## Find Used Materials\n\nUse the `find-used-materials` command to analyze project code and find all used official materials:\n\n```bash\nnpx @flowgram.ai/cli@latest find-used-materials\n```\n\nOutput example:\n\n```console\n🚀 Welcome to @flowgram.ai form-materials CLI!\n📁 Project: /path/to/your/project\n🎯 Flowgram Version: 1.0.0\n\n👀 The exports of components/json-schema-editor is JsonSchemaEditor,JsonSchemaEditorProps\n👀 The exports of components/variable-selector is VariableSelector,VariableSelectorProps\n\n👀 Searching src/components/MyForm.tsx\n🔍 import { JsonSchemaEditor } from '@flowgram.ai/form-materials'\nimport components/json-schema-editor by JsonSchemaEditor\n\n👀 Searching src/pages/Settings.tsx\n🔍 import { VariableSelector } from '@flowgram.ai/form-materials'\nimport components/variable-selector by VariableSelector\n\n📦 All used materials:\ncomponents/json-schema-editor,components/variable-selector\n```\n\nThis command will:\n- Scan all TypeScript files in the project\n- Analyze import statements from `@flowgram.ai/form-materials`\n- Identify specific materials being used\n- Output detailed usage location information\n\n## Case Run Down\n\n### Sync all used official materials in the project to the src/custom-materials directory\n\n1. Use the `find-used-materials` command to see the official materials used in the project.\n\n```bash\nnpx @flowgram.ai/cli@latest find-used-materials\n```\n\nAfter the command runs, it will output a list of official materials used in the project.\n\n```console\n📦 All used materials:\ncomponents/json-schema-editor,components/variable-selector\n```\n\n2. Use the `materials` command to add the source code of these materials to the project's src/custom-materials directory.\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor,components/variable-selector \\\n  --target-material-root-dir src/custom-materials \\\n  --refresh-project-imports\n```\n\n- The `--refresh-project-imports` option will refresh the import paths for copied materials in the project, ensuring the latest customized versions are used.\n- The `--target-material-root-dir src/custom-materials` option specifies the target directory for material copying as src/custom-materials.\n\n\n## FAQ\n\n### Q: Can't find the newly added dependencies after CLI adds them?\nA: Please check if you have run `npm install` to install the newly added dependencies."
  },
  {
    "path": "apps/docs/src/en/materials/common/_meta.json",
    "content": "[\n  \"flow-value\",\n  \"json-schema-preset\",\n  \"inject-material\",\n  \"disable-declaration-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/materials/common/disable-declaration-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/common/disable-declaration-plugin';\n\n# DisableDeclarationPlugin\n\n:::note{title=\"\"}\n\nIn the design of the materials library, **\"nodes themselves\" are defined as VariableDeclaration**.\n\nComponents in the materials library such as [`VariableSelector`](../components/variable-selector), [`PromptEditorWithVariables`](../components/prompt-editor-with-variables), and [`SQLEditorWithVariables`](../components/sql-editor-with-variables) all support selecting **\"node\"** by default.\n\n:::\n\nDisableDeclarationPlugin can **disable variable declarations (only allowing drilling down)** , making \"node\" unselectable.\n\n## Demo\n\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"use-editor-props.tsx\"\nimport { createDisableDeclarationPlugin } from '@flowgram.ai/form-materials';\n\n// ...\n{\n  plugins: () => [createDisableDeclarationPlugin()],\n}\n// ...\n```\n\n## API\n\n### createDisableDeclarationPlugin\n\nUsed to create a plugin that disables variable declarations. This plugin intercepts events from the variable engine and marks all variable declarations as disabled.\n\n```ts\nexport const createDisableDeclarationPlugin = definePluginCreator<void>({...});\n```\n\n**Parameters**: None\n\n**Return Value**: A plugin instance that can be directly added to the Editor's plugin list.\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/plugins/disable-declaration-plugin/create-disable-declaration-plugin.ts\"\n/>\n\nYou can copy the source code locally using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials plugins/disable-declaration-plugin\n```\n\n### Directory Structure\n\n```plaintext\npackages/materials/form-materials/src/plugins/disable-declaration-plugin/\n└── create-disable-declaration-plugin.ts  # Main implementation file of the plugin\n```\n\n### Core Implementation\n\nThe core implementation of DisableDeclarationPlugin is very concise, mainly including the following steps:\n\n1. **Plugin Initialization**: Create the plugin through `definePluginCreator` and obtain the variable engine instance in the `onInit` hook\n2. **Event Listening**: Listen to the `NewAST` and `UpdateAST` events of the variable engine\n3. **Processing Logic**: When a variable declaration type (`VariableDeclaration`) AST node is detected, set its `meta.disabled` property to `true`\n\nThis implementation ensures that all newly created or updated variable declarations are automatically disabled, making them unselectable in variable selectors and other components.\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`ASTMatch`](https://flowgram.ai/auto-docs/editor/modules/ASTMatch): A utility class for matching and determining AST node types\n- [`definePluginCreator`](https://flowgram.ai/auto-docs/core/functions/definePluginCreator): A function to define plugin creators\n- [`GlobalEventActionType`](https://flowgram.ai/auto-docs/editor/interfaces/GlobalEventActionType): Type definitions for global event actions\n- [`VariableEngine`](https://flowgram.ai/auto-docs/editor/classes/VariableEngine): Variable engine responsible for managing and processing variable-related operations\n"
  },
  {
    "path": "apps/docs/src/en/materials/common/flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\n\n# FlowValue\n\nFlowValue is a special type used in Flowgram Official Materials to represent data. It can be a constant, reference, expression, or template, providing a flexible way for data transfer and processing in workflows.\n\n## FlowValue Types\n\nFlowValue supports the following four types:\n\n### 1. Constant\nA fixed value that does not change at runtime. It can contain any type of data, such as strings, numbers, booleans, or objects.\n\n```typescript\n{\n  type: 'constant',\n  content: 'Hello World', // Can be any type\n  schema?: { type: \"string\" },    // Optional JSON Schema definition\n  extra?: { index: 1 }  // Additional information, such as ordering, etc.\n}\n```\n\n### 2. Reference\nA reference to other variables or data in the workflow, specifying the reference location through a path array.\n\n```typescript\n{\n  type: 'ref',\n  content: ['variable', 'name'], // Reference path array\n  extra?: { index: 1 }  // Additional information, such as ordering, etc.\n}\n```\n\n### 3. Template\nA string template containing variable placeholders, using the `{{variable}}` syntax to embed variables.\n\n```typescript\n{\n  type: 'template',\n  content: 'Hello {{user.name}}!', // Template string\n  extra?: { index: 1 }  // Additional information, such as ordering, etc.\n}\n```\n\n### 4. WIP: Expression\nA JavaScript expression that will be evaluated at runtime.\n\n```typescript\n{\n  type: 'expression',\n  content: 'a + b', // JavaScript, Python, etc. expression string\n  extra?: { index: 1 }  // Additional information, such as ordering, etc.\n}\n```\n\n::: warning\nThe expression type is currently in WIP status and does not support type derivation capabilities at the moment. There may be breaking changes in the future.\n:::\n\n## FlowValueUtils Utility Class\n\nFlowValueUtils provides rich utility functions for handling FlowValues:\n\n### Type Checking Functions\n\n- `isConstant(value)` - Check if it's a constant type\n- `isRef(value)` - Check if it's a reference type\n- `isExpression(value)` - Check if it's an expression type\n- `isTemplate(value)` - Check if it's a template type\n- `isConstantOrRef(value)` - Check if it's a constant or reference type\n- `isFlowValue(value)` - Check if it's a valid FlowValue type\n\n### Traversal and Extraction Functions\n\n- `traverse(value, options)` - Traverse all FlowValues in an object, with type filtering support\n- `getTemplateKeyPaths(templateValue)` - Extract all variable paths from templates\n\n### Schema Inference Functions\n\n- `inferConstantJsonSchema(constantValue)` - Infer JSON Schema based on constant value\n- `inferJsonSchema(values, scope)` - Infer corresponding JSON Schema based on FlowValue\n\n## Usage Examples\n\n### Using Utility Functions\n\n```typescript\n// Type checking\nif (FlowValueUtils.isConstant(value)) {\n  console.log('This is a constant value:', value.content);\n}\n\n// Traverse FlowValues\nfor (const { value, path } of FlowValueUtils.traverse(data, {\n  includeTypes: ['ref', 'template']\n})) {\n  console.log(`Found ${value.type} at path: ${path}`);\n}\n\n// Extract template variables\nconst templatePaths = FlowValueUtils.getTemplateKeyPaths(templateValue);\nconsole.log('Template variables:', templatePaths); // [['user', 'name'], ['count']]\n```\n\n## Type Definitions\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/shared/flow-value/types.ts\"\n/>\n\nThe core type definitions of FlowValue include:\n\n- `IFlowValue` - Union type of FlowValue\n- `IFlowConstantValue` - Constant type interface\n- `IFlowRefValue` - Reference type interface\n- `IFlowExpressionValue` - Expression type interface\n- `IFlowTemplateValue` - Template type interface\n- `IFlowConstantRefValue` - Union type of constant or reference types\n- `IInputsValues` - Mapping type of input values\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/shared/flow-value\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials shared/flow-value\n```\n\n### Directory Structure\n\n```\nflow-value/\n├── index.ts           # Main entry file, exports all types and utility functions\n├── types.ts           # Type definitions file, contains all FlowValue interface definitions\n├── schema.ts          # Zod schema definitions for runtime type validation\n├── utils.ts           # Complete implementation of FlowValueUtils utility class\n└── README.md          # Module documentation\n```\n\n### Third-party APIs Used\n\n- [Zod](https://v3.zod.dev/): Used for type validation and data parsing to determine if FlowValue schemas meet expectations.\n"
  },
  {
    "path": "apps/docs/src/en/materials/common/inject-material.mdx",
    "content": "import { SourceCode } from '@theme';\n\n# Material Dependency Injection\n\n:::tip{title=\"Component Materials Supporting Dependency Injection in the Material Library\"}\n\n- [InjectDynamicValueInput](../components/dynamic-value-input)\n- [InjectTypeSelector](../components/type-selector)\n- [InjectVariableSelector](../components/variable-selector)\n\n:::\n\n## Background: Why Does the Material Library Need Dependency Injection?\n\n### ❌ Tight Coupling: Traditional Dependency Problems\n\n```mermaid\ngraph TD\n    A[Material A] --> B[Material B]\n    B --> D[Material D]\n    C[Material C] --> D\n\n    style D fill:#ff4757\n    style A fill:#ffa502\n    style B fill:#ffa502\n    style C fill:#ffa502\n\n    note[\"💥 Problem: Changes to D require modifications to A, B, and C\"]\n```\n\n**Issues:** Chain reactions, high maintenance costs\n\n### ✅ Decoupling: Dependency Injection Solution\n\n```mermaid\ngraph TD\n    A[Material A] --> RenderKey[Material D RenderKey]\n    B[Material B] --> RenderKey\n    C[Material C] --> RenderKey\n\n    RenderKey -.-> BaseD[Default Material D]\n    CustomD[Custom Material D] -.-> RenderKey\n\n    style RenderKey fill:#3498db\n    style BaseD fill:#2ed573\n    style CustomD fill:#26d0ce\n    style A fill:#a55eea\n    style B fill:#a55eea\n    style C fill:#a55eea\n\n    note2[\"✅ A, B, C depend on abstract interfaces, decoupled from D's implementation\"]\n```\n\n**Advantages:** Hot-swappable, parallel development, version compatibility\n\n## Usage\n\n### Creating Injectable Component Materials\n\n```tsx\nimport { createInjectMaterial } from '@flowgram.ai/form-materials';\nimport { VariableSelector } from './VariableSelector';\n\n// Wrap the component using the createInjectMaterial higher-order component\nconst InjectVariableSelector = createInjectMaterial(VariableSelector);\n\n// Now you can use it like a normal component\nfunction MyComponent() {\n  return <InjectVariableSelector value={value} onChange={handleChange} />;\n}\n```\n\n### Registering Custom Components\n\nWhen a component material is created as an injectable material component and used by other materials, you can inject a custom renderer for that material in `use-editor-props.tsx`:\n\n```tsx\nimport { useEditorProps } from '@flowgram.ai/editor';\nimport { YourCustomVariableSelector } from './YourCustomVariableSelector';\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nfunction useCustomEditorProps() {\n  const editorProps = useEditorProps({\n    materials: {\n      components: {\n        // By default, the component's Function Name is used as renderKey\n        'VariableSelector': YourCustomVariableSelector,\n        'TypeSelector': YourCustomTypeSelector,\n      }\n    }\n  });\n\n  return editorProps;\n}\n```\n\n### Using Custom renderKey\n\nIf your component needs a specific renderKey:\n\n**Method 1:** Specify renderKey through the second parameter of createInjectMaterial\n\n```tsx\nconst InjectCustomComponent = createInjectMaterial(MyComponent, {\n  renderKey: 'my-custom-key'\n});\n// When registering\n{\n  materials: {\n    components: {\n      'my-custom-key': MyCustomRenderer\n    }\n  }\n}\n```\n\n**Method 2:** Or directly set the renderKey property of the component\n\n```tsx\nMyComponent.renderKey = 'my-custom-key';\nconst InjectCustomComponent = createInjectMaterial(MyComponent);\n// When registering\n{\n  materials: {\n    components: {\n      [MyComponent.renderKey]: MyCustomRenderer\n    }\n  }\n}\n\n```\n\n:::note{title=\"Render Key Priority\"}\n\nThe determination of component render keys follows this priority order:\n\n1. `params.renderKey` (second parameter of createInjectMaterial)\n2. `Component.renderKey` (renderKey property of the component itself)\n3. `Component.name` (display name of the component)\n4. Empty string (final fallback)\n\n:::\n\n## API Reference\n\n```typescript\ninterface CreateInjectMaterialOptions {\n  renderKey?: string;\n}\n\nfunction createInjectMaterial<Props>(\n  Component: React.FC<Props> & { renderKey?: string },\n  params?: CreateInjectMaterialOptions\n): React.FC<Props>\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/shared/inject-material/index.tsx\"\n/>\n\nYou can copy the source code to your local environment using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials shared/inject-material\n```\n\n### Core Sequence Diagram\n\nComplete component registration and rendering sequence diagram:\n\n```mermaid\nsequenceDiagram\n    participant App as Application\n    participant Editor as use-editor-props\n    participant Registry as FlowRendererRegistry\n    participant Inject as InjectMaterial\n    participant Default as Default Component\n    participant Custom as Custom Component\n\n    Note over App,Custom: Component Registration Phase\n    App->>Editor: Call use-editor-props()\n    Editor->>Editor: Configure materials.components\n    Editor->>Registry: Register component with FlowRendererRegistry\n    Registry->>Registry: Store mapping relationship\n    Registry-->>App: Registration complete\n\n    Note over App,Custom: Component Rendering Phase\n    App->>Inject: Render InjectMaterial component\n    Inject->>Registry: Query renderer (getRendererComponent)\n\n    alt Custom renderer exists\n        Registry-->>Inject: Return custom React component\n        Inject->>Custom: Render using custom component\n        Custom-->>App: Render custom UI\n    else No custom renderer\n        Registry-->>Inject: Return null or type mismatch\n        Inject->>Default: Render using default component\n        Default-->>App: Render default UI\n    end\n```\n"
  },
  {
    "path": "apps/docs/src/en/materials/common/json-schema-preset.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/common/json-schema-preset';\n\n# Type Management\n\nType management is implemented in two parts:\n\n1. **Material Layer** (preset type definitions in the material library):\n   - Extends the type engine to define default renderers, condition rule configurations, etc. for types\n   - Provides preset definitions for default types in the material library (constant input renderer, condition rule configuration, etc.)\n   - Provides Editor Plugin for easily extending custom types\n2. **Engine Layer** (core type engine, provided by `@flowgram.ai/json-schema`)\n   - Provides basic definitions of Json types, including icons, name display, etc.\n   - Provides BaseTypeManager, which can extend type definitions beyond Json Schema\n   - Provides `JsonSchemaUtils` to implement mutual conversion between JSONSchema and AST\n\n## Case Demonstration\n\n### Adding Color Type\n\n<BasicStory />\n\n```tsx pure title=\"use-editor-props.tsx\"\nimport { createTypePresetPlugin } from \"@flowgram.ai/form-materials\";\n\n// ...\n{\n  plugins: () => [\n    createTypePresetPlugin({\n      types: [\n        types: [\n          {\n            type: 'color',\n            icon: <IconColorPalette />,\n            label: 'Color',\n            ConstantRenderer: ({ value, onChange }) => (\n              <div className=\"json-schema-color-picker-container \">\n                <ColorPicker\n                  alpha={true}\n                  usePopover={true}\n                  value={value ? ColorPicker.colorStringToValue(value) : undefined}\n                  onChange={(_value) => onChange?.(_value.hex)}\n                />\n              </div>\n            ),\n            conditionRule: {\n              eq: { type: 'color' },\n            },\n          },\n        ],\n      },\n    }),\n  ],\n}\n// ...\n\n```\n\n### Getting Type Definitions\n\n```tsx\nconst typeManager = useTypeManager();\n\n// Get type definition based on schema\nconst type = typeManager.getTypeBySchema({ type: \"color\" });\nconst type2 = typeManager.getTypeBySchema({ type: \"array\", items: { type: \"color\" } });\n\n// Get type definition based on type name\nconst type3 = typeManager.getTypeByName(\"color\");\n```\n\n## API\n\n### createTypePresetPlugin\n\nCreate an Editor Plugin for extending material library preset type definitions or disabling certain preset types in the material library.\n\n```typescript\nfunction createTypePresetPlugin(options: TypePresetPluginOptions): Plugin;\n\ninterface TypePresetPluginOptions {\n  // Array of custom type definitions to add\n  types?: TypePresetRegistry[];\n  // Array of type names to remove\n  unregisterTypes?: string[];\n}\n\ninterface TypePresetRegistry {\n  // Type name\n  type: string;\n  // Type icon\n  icon?: React.ReactNode;\n  // Type label\n  label?: string;\n  // Constant renderer component\n  ConstantRenderer: React.FC<ConstantRendererProps>;\n  // Condition rule configuration\n  conditionRule?: IConditionRule | IConditionRuleFactory;\n  // Other properties inherited from base type\n}\n\ninterface ConstantRendererProps<Value = any> {\n  value?: Value;\n  onChange?: (value: Value) => void;\n  readonly?: boolean;\n  [key: string]: any;\n}\n```\n\n## Source Code Guide\n\nMaterial layer source code: <SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/plugins/json-schema-preset\"\n/>\n\nUse the CLI command to copy the material layer source code to local:\n\n```bash\nnpx @flowgram.ai/cli@latest materials plugins/json-schema-preset\n```\n\nEngine layer source code: <SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema/src\"\n/>\n\nDue to its complexity, the engine layer currently needs to be used through the separate `@flowgram.ai/json-schema` package, and does not support downloading source code via CLI commands.\n\n### Material Layer Core Logic\n\nThe definitions added in the material layer are used by the following materials:\n- [ConstantInput](../components/constant-input): Get constant input corresponding to the type\n  - Source code: <SourceCode\n    href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/constant-input/index.tsx\"\n  />\n- [ConditionContext](../components/condition-context): Get Condition rules corresponding to the type\n  - Source code: <SourceCode\n    href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/components/condition-context/hooks/use-condition.tsx\"\n  />\n\n### Engine Layer Core Logic\n\n#### JsonSchemaTypeManager Class Structure\n\n```mermaid\nclassDiagram\n  direction LR\n  class BaseTypeManager {\n      -getTypeNameFromSchema(typeSchema): string\n      +getTypeByName(typeName): Registry\n      +getTypeBySchema(type): Registry\n      +getDefaultTypeRegistry(): Registry\n      +getAllTypeRegistries(): Registry[]\n      +register(registry): void\n      +unregister(typeName): void\n  }\n\n  class JsonSchemaTypeManager {\n      +constructor()\n      +getTypeRegistriesWithParentType(parentType): Registry[]\n      +getTypeSchemaDeepChildField(type): Schema\n      +getComplexText(type): string\n      +getDisplayIcon(type): ReactNode\n      +getTypeSchemaProperties(type): Record\n      +getPropertiesParent(type): Schema\n      +getJsonPaths(type): string[]\n      +canAddField(type): boolean\n      +getDefaultValue(type): unknown\n  }\n\n  BaseTypeManager <|-- JsonSchemaTypeManager\n\n  class JsonSchemaTypeRegistry {\n      +type: string\n      +label: string\n      +icon: ReactNode\n      +container: boolean\n      +getJsonPaths(typeSchema): string[]\n      +getDisplayIcon(typeSchema): ReactNode\n      +getPropertiesParent(typeSchema): Schema\n      +canAddField(typeSchema): boolean\n      +getDefaultValue(): unknown\n      +getValueText(value): string\n      +getTypeSchemaProperties(typeSchema): Record\n      +getStringValueByTypeSchema(typeSchema): string\n      +getTypeSchemaByStringValue(optionValue): Schema\n      +getDefaultSchema(): Schema\n      +customComplexText(typeSchema): string\n  }\n  <<Interface>> JsonSchemaTypeRegistry\n\n  JsonSchemaTypeManager --> JsonSchemaTypeRegistry: register\n```\n\n#### JsonSchemaTypeManager Function Overview\n\n**Core Functions**:\n\n1. **Type Registration and Management**\n   - `register(registry)`: Register new type definitions\n   - `unregister(typeName)`: Remove registered types\n   - `getAllTypeRegistries()`: Get all registered types\n   - `getTypeByName(typeName)`: Get type definition by type name\n   - `getTypeBySchema(schema)`: Get corresponding type definition by schema\n\n2. **Type Information Retrieval**\n   - `getTypeNameFromSchema(schema)`: Extract type name from schema\n   - `getTypeRegistriesWithParentType(parentType)`: Get all types under specified parent type\n   - `getTypeSchemaDeepChildField(type)`: Get the deepest child field of a type\n   - `getComplexText(type)`: Get complex text representation of type (e.g., Array\\<String\\>)\n   - `getDisplayIcon(type)`: Get display icon of type\n\n3. **Type Property Operations**\n   - `getTypeSchemaProperties(type)`: Get property definitions of type\n   - `getPropertiesParent(type)`: Get parent node of properties\n   - `getJsonPaths(type)`: Get json paths of type in flow schema\n   - `canAddField(type)`: Determine if fields can be added to the type\n   - `getDefaultValue(type)`: Get default value of type\n\n**Initialization Process**:\n\nIn the constructor, JsonSchemaTypeManager automatically registers a series of default type definitions:\n- defaultTypeDefinitionRegistry: Default type definition\n- stringRegistryCreator: String type\n- integerRegistryCreator: Integer type\n- numberRegistryCreator: Number type\n- booleanRegistryCreator: Boolean type\n- objectRegistryCreator: Object type\n- arrayRegistryCreator: Array type\n- unknownRegistryCreator: Unknown type\n- mapRegistryCreator: Map type\n- dateTimeRegistryCreator: DateTime type\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/_meta.json",
    "content": "[\n  \"type-selector\",\n  \"json-schema-editor\",\n  \"json-schema-creator\",\n  \"variable-selector\",\n  \"dynamic-value-input\",\n  \"condition-row\",\n  \"db-condition-row\",\n  \"condition-context\",\n  \"inputs-values\",\n  \"inputs-values-tree\",\n  \"prompt-editor\",\n  \"prompt-editor-with-variables\",\n  \"prompt-editor-with-inputs\",\n  \"code-editor\",\n  \"json-editor-with-variables\",\n  \"sql-editor-with-variables\",\n  \"coze-editor-extensions\",\n  \"constant-input\",\n  \"display-schema-tag\",\n  \"display-schema-tree\",\n  \"display-flow-value\",\n  \"display-inputs-values\",\n  \"display-outputs\",\n  \"assign-row\",\n  \"assign-rows\",\n  \"batch-outputs\",\n  \"batch-variable-selector\",\n  \"blur-input\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/assign-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { AssignModeStory, DeclareModeStory } from 'components/form-materials/components/assign-row';\n\n# AssignRow\n\nAssignRow is an assignment row component that supports two operation modes: **assignment mode (assign)** and **declaration mode (declare)**.\n\n- In assignment mode: the left side is a variable selector, and the right side is dynamic value input;\n- In declaration mode: the left side is a text input box, and the right side is dynamic value input.\n\n## Examples\n\n### Assignment Mode\n\nAssignRow **defaults to assignment mode**. In assignment mode, the left side is a variable selector and the right side is dynamic value input:\n\n<AssignModeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRow } from '@flowgram.ai/form-materials';\nimport { AssignValueType } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<AssignValueType | undefined> name=\"assign_row\">\n        {({ field }) => (\n          <AssignRow value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Declaration Mode\n\nIn declaration mode, the left side is variable name input and the right side is dynamic value input:\n\n<DeclareModeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRow } from '@flowgram.ai/form-materials';\nimport { AssignValueType } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<AssignValueType | undefined> name=\"assign_row\">\n        {({ field }) => (\n          <AssignRow\n            value={{\n              operator: 'declare',\n              left: 'newVariable',\n              right: {\n                type: 'constant',\n                content: 'Hello World',\n                schema: { type: 'string' },\n              },\n            }}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### AssignRow Props\n\n| Property | Type | Default | Description |\n|--------|------|--------|------|\n| `value` | `AssignValueType` | - | The value of the assignment row, containing operator, left value, and right value |\n| `onChange` | `(value?: AssignValueType) => void` | - | Callback function when value changes |\n| `onDelete` | `() => void` | - | Callback function when delete button is clicked |\n| `readonly` | `boolean` | `false` | Whether it is read-only mode |\n\n### AssignValueType\n\n```typescript\ntype AssignValueType =\n  | {\n      operator: 'assign';\n      left?: IFlowRefValue;      // Variable reference\n      right?: IFlowValue;        // Dynamic value\n    }\n  | {\n      operator: 'declare';\n      left?: string;             // Variable name\n      right?: IFlowValue;        // Dynamic value\n    };\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/assign-row\"\n/>\n\nYou can copy the source code locally using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/assign-row\n```\n\n### Directory Structure\n\n```\nassign-row/\n├── index.tsx     # Main implementation of AssignRow component\n└── types.ts      # Type definition file\n```\n\n### Core Implementation\n\nThe core logic of the AssignRow component is to render different left input controls based on the `operator` field:\n\n1. **Assignment mode (`operator: 'assign'`)**: Renders `InjectVariableSelector` on the left for selecting existing variables\n2. **Declaration mode (`operator: 'declare'`)**: Renders `BlurInput` on the left for inputting new variable names\n3. **Unified right side**: Regardless of the mode, the right side renders `InjectDynamicValueInput`, supporting both constant and variable input\n\n#### Component Structure\n\n```mermaid\ngraph TD\n    A[AssignRow Component] --> B{Determine operator type}\n    B -->|assign| C[Render variable selector]\n    B -->|declare| D[Render text input box]\n\n    C --> E[Right side dynamic value input]\n    D --> E\n\n    E --> F[Optional delete button]\n\n    C --> G[Support onDelete callback]\n    D --> G\n    F --> G\n```\n\n### Dependencies\n\n#### Other Components\n\n[**VariableSelector**](./variable-selector)\n- `InjectVariableSelector`: Dependency-injected variable selector\n\n[**DynamicValueInput**](./dynamic-value-input)\n- `InjectDynamicValueInput`: Dependency-injected dynamic value input component\n\n[**BlurInput**](./blur-input)\n- `BlurInput`: Blur input component\n\n#### Third-party Libraries\n\n[**Semi Design**](https://semi.design/zh-CN/)\n- `IconButton`: Icon button component\n- `IconMinus`: Minus icon"
  },
  {
    "path": "apps/docs/src/en/materials/components/assign-rows.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/assign-rows';\n\n# AssignRows\n\nAssignRows is an assignment row list component implemented based on `FieldArray`, supporting dynamic addition and deletion of assignment rows.\n\nThe component provides two action buttons: **Assign** and **Declare**, which can add assignment mode and declaration mode assignment rows respectively. Each assignment row can be configured and deleted independently.\n\n:::tip\n\n`AssignRows` is typically used together with the [`infer-assign-plugin`](../form-plugins/infer-assign-plugin) form plugin to convert defined declarations into node output variables and achieve automatic type linkage.\n\n:::\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRows } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <AssignRows name=\"assign_rows\" />\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### AssignRows Props\n\n| Property | Type | Default | Description |\n|--------|------|--------|------|\n| `name` | `string` | - | Form field name for FieldArray |\n| `readonly` | `boolean` | `false` | Whether it is read-only mode |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/assign-rows\"\n/>\n\nYou can copy the source code locally using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/assign-rows\n```\n\n### Directory Structure\n\n```\nassign-rows/\n└── index.tsx     # Main implementation of AssignRows component\n```\n\n### Core Implementation\n\nThe core functionality of the AssignRows component is dynamic list management implemented based on `FieldArray`:\n\n1. **Dynamic Addition**: Provides two buttons to add assignment mode and declaration mode rows respectively\n2. **Dynamic Deletion**: Each row supports independent deletion operations\n3. **State Management**: Uses `FieldArray` to manage the state of the entire list\n4. **Component Reuse**: Each row reuses the `AssignRow` component\n\n#### Component Workflow\n\n```mermaid\ngraph TD\n    A[AssignRows Component] --> B[FieldArray Wrapper]\n    B --> C[Render existing row list]\n    C --> D[Each row uses AssignRow]\n    D --> E[Supports onChange and onDelete]\n\n    B --> F[Add button area]\n    F --> G[Assign button]\n    F --> H[Declare button]\n\n    G --> I[Add assignment row]\n    H --> J[Add declaration row]\n\n    I --> K[Call field.append]\n    J --> K\n\n    E --> L[Call field.remove]\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `FieldArray`: Form array field component for managing dynamic lists\n- `FieldArrayRenderProps`: FieldArray render property types\n\n#### Other Components\n\n[**AssignRow**](./assign-row)\n- `AssignRow`: Assignment row component that handles single row logic\n- `AssignValueType`: Assignment row value type definition\n\n#### Third-party Libraries\n\n[**Semi Design**](https://semi.design/zh-CN/)\n- `Button`: Button component\n- `IconPlus`: Plus icon"
  },
  {
    "path": "apps/docs/src/en/materials/components/batch-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/batch-outputs';\n\n# BatchOutputs\n\n`BatchOutputs` is a key-value pair editor component for configuring loop outputs. In loop node scenarios, it allows users to define output values to be collected in each iteration, which will eventually be aggregated into arrays.\n\n**Core Features:**\n\n- ➕ **Dynamic Add/Remove**: Users can freely add or remove output key-value pairs\n- ✏️ **Key Name Editing**: Define a unique key name for each output\n- 🔗 **Variable Reference**: Reference variables available within the loop body through a variable selector\n- 👁️ **Readonly Mode**: Supports readonly display for viewing scenarios\n\n:::warning\n\n`BatchOutputs` must be used with [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) to work properly. This is because:\n1. The component handles UI interaction, collecting output key-value pairs configured by the user\n2. The plugin is responsible for converting configurations into variable declarations and adjusting the scope chain\n\n:::\n\n:::info{title=\"Complete Solution Overview\"}\n\nImplementing a complete loop node requires the following three materials working together:\n\n| Material | Type | Responsibility |\n|------|------|------|\n| [BatchVariableSelector](./batch-variable-selector) | Component | Select the array data source for the loop |\n| [provideBatchInputEffect](../effects/provide-batch-input) | Effect | Generate `item` and `index` local variables |\n| **BatchOutputs** + [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) | Component + Plugin | Configure loop outputs and generate array-type variables |\n\n:::\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"About FormHeader, FormContent, FormItem\"}\n\nThe `FormHeader`, `FormContent`, and `FormItem` in the code above are user-defined layout components for unified form styling. You can implement them according to your project needs or replace them with other UI components.\n\n:::\n\n### Readonly Mode\n\nBy setting the `readonly` property, you can disable editing functionality, suitable for viewing or preview scenarios:\n\n```tsx pure\n<BatchOutputs\n  readonly\n  value={{\n    names: { type: 'ref', content: ['item', 'name'] },\n    ages: { type: 'ref', content: ['item', 'age'] },\n  }}\n/>\n```\n\n## API Reference\n\n### BatchOutputs Props\n\n| Property | Type | Default | Description |\n|--------|------|--------|------|\n| `value` | `Record<string, IFlowRefValue \\| undefined>` | - | Output key-value object, key is the output name, value is the variable reference |\n| `onChange` | `(value?: Record<string, IFlowRefValue \\| undefined>) => void` | - | Callback function when value changes |\n| `readonly` | `boolean` | `false` | Whether in readonly mode |\n| `hasError` | `boolean` | `false` | Whether to show error state |\n| `style` | `React.CSSProperties` | - | Custom styles |\n\n### Value Type Description\n\n```typescript\ntype ValueType = Record<string, IFlowRefValue | undefined>;\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n```\n\n#### Value Structure Example\n\n```typescript\n{\n  names: { type: 'ref', content: ['loop_1_locals', 'item', 'name'] },\n  ages: { type: 'ref', content: ['loop_1_locals', 'item', 'age'] },\n  scores: { type: 'ref', content: ['loop_1_locals', 'item', 'score'] },\n}\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/batch-outputs\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/batch-outputs\n```\n\n### Directory Structure\n\n```\nbatch-outputs/\n├── index.tsx          # Main component implementation\n├── types.ts           # Type definitions\n└── styles.css         # Style file\n```\n\n### Core Implementation\n\n#### Component Structure\n\nThe BatchOutputs component is based on the `useObjectList` hook for dynamic list management, each row contains:\n- **Input**: For editing output key names\n- **InjectVariableSelector**: For selecting variable references\n- **Delete Button**: Delete the current row\n\n#### Data Flow\n\n```mermaid\ngraph TD\n    A[BatchOutputs Component] --> B[useObjectList Hook]\n    B --> C[list state]\n    B --> D[add method]\n    B --> E[updateKey method]\n    B --> F[updateValue method]\n    B --> G[remove method]\n\n    C --> H[Render List]\n    H --> I[Input Key Name Editing]\n    H --> J[VariableSelector Variable Selection]\n    H --> K[Delete Button]\n\n    D --> L[Add Button]\n\n    I --> E\n    J --> F\n    K --> G\n    \n    E --> M[onChange Callback]\n    F --> M\n    G --> M\n    M --> N[Update External value]\n```\n\n#### useObjectList Hook\n\n`useObjectList` is a general-purpose dynamic object list management hook with core features:\n\n1. **List State Management**: Maintains list items with unique IDs\n2. **Bidirectional Sync**: Synchronizes and updates the list when the `value` property changes\n3. **CRUD Operations**: Provides `add`, `remove`, `updateKey`, `updateValue` methods\n\n```typescript\ninterface UseObjectListOptions<T> {\n  value?: Record<string, T | undefined>;\n  onChange?: (value?: Record<string, T | undefined>) => void;\n}\n\ninterface UseObjectListReturn<T> {\n  list: Array<{ id: string; key: string; value: T | undefined }>;\n  add: () => void;\n  remove: (id: string) => void;\n  updateKey: (id: string, newKey: string) => void;\n  updateValue: (id: string, newValue: T) => void;\n}\n\nconst { list, add, updateKey, updateValue, remove } = useObjectList({\n  value,\n  onChange,\n});\n```\n\n#### Complete Data Flow Sequence Diagram\n\n```mermaid\nsequenceDiagram\n    participant User as User\n    participant UI as BatchOutputs Component\n    participant Hook as useObjectList\n    participant Form as Form System\n    participant Plugin as batchOutputsPlugin\n\n    User->>UI: Click Add Button\n    UI->>Hook: Call add()\n    Hook->>Hook: Generate new list item (with unique ID)\n    Hook->>Form: Trigger onChange\n    Form->>Plugin: Form value changed\n    Plugin->>Plugin: Generate variable declaration\n\n    User->>UI: Edit key name\n    UI->>Hook: Call updateKey(id, newKey)\n    Hook->>Form: Trigger onChange\n    Form->>Plugin: Form value changed\n    Plugin->>Plugin: Update variable declaration\n\n    User->>UI: Select variable\n    UI->>Hook: Call updateValue(id, refValue)\n    Hook->>Form: Trigger onChange\n    Form->>Plugin: Form value changed\n    Plugin->>Plugin: Update variable declaration\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/modules/I18n): Internationalization tool for button text\n\n#### Dependent Materials\n\n[**useObjectList**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/hooks/use-object-list)\n- Dynamic object list management hook, handles list CRUD operations\n\n[**InjectVariableSelector**](./variable-selector)\n- Injection-based variable selector for selecting variable references\n\n#### Third-party Dependencies\n\n- `@douyinfe/semi-ui`: UI component library, uses Button, Input components\n- `@douyinfe/semi-icons`: Icon library, uses IconDelete, IconPlus icons\n\n## FAQ\n\n### Why do I need to use both BatchOutputs component and batchOutputsPlugin?\n\nThis is a separation of concerns design:\n\n| Role | Responsibility |\n|------|------|\n| `BatchOutputs` Component | Provides UI interaction, lets users configure output key names and variable references |\n| `batchOutputsPlugin` | Handles data logic, converts configurations to variable declarations and adjusts scope chain |\n\nUsing the component alone only collects data and cannot generate valid output variables; using the plugin alone has no UI to configure data.\n\n### What's the difference between BatchOutputs and InputsValues?\n\n| Feature | BatchOutputs | InputsValues |\n|------|--------------|--------------|\n| Purpose | Loop output configuration | Node input configuration |\n| Value Type | `Record<string, IFlowRefValue>` | `IInputsValues` |\n| Variable Reference | Only supports variable references | Supports constants and variable references |\n| Use Case | Output aggregation for Loop nodes | Input parameters for general nodes |\n\n### How to customize the variable selector filter conditions?\n\nCurrently, `BatchOutputs` internally uses `InjectVariableSelector` and does not support custom filter conditions. If customization is needed, you can refer to the source code to implement your own component:\n\n```tsx\nimport { useObjectList } from '@flowgram.ai/form-materials';\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nfunction CustomBatchOutputs(props) {\n  const { list, add, updateKey, updateValue, remove } = useObjectList(props);\n  \n  return (\n    <div>\n      {list.map((item) => (\n        <div key={item.id}>\n          <Input value={item.key} onChange={(v) => updateKey(item.id, v)} />\n          <VariableSelector\n            value={item.value?.content}\n            onChange={(v) => updateValue(item.id, { type: 'ref', content: v })}\n            includeSchema={{ type: 'string' }}\n          />\n          <Button onClick={() => remove(item.id)}>Delete</Button>\n        </div>\n      ))}\n      <Button onClick={() => add()}>Add</Button>\n    </div>\n  );\n}\n```\n\n### How to get the generated output variable types?\n\nWhen used with `batchOutputsPlugin` and if `inferTargetKey` is configured, the JSON Schema of the output variables will be automatically written to the specified field when the form is submitted:\n\n```typescript\nplugins: [\n  createBatchOutputsFormPlugin({ \n    outputKey: 'loopOutputs', \n    inferTargetKey: 'outputs'\n  })\n]\n```\n\nExample of form data structure after submission:\n\n```typescript\n{\n  loopOutputs: {\n    names: { type: 'ref', content: ['item', 'name'] },\n    ages: { type: 'ref', content: ['item', 'age'] },\n  },\n  outputs: {\n    type: 'object',\n    properties: {\n      names: { type: 'array', items: { type: 'string' } },\n      ages: { type: 'array', items: { type: 'number' } },\n    }\n  }\n}\n```\n\n### How to handle duplicate key names?\n\nCurrently, the component does not automatically detect duplicate key names. It is recommended to add validation logic at the form level:\n\n```typescript\nconst formMeta: FormMeta = {\n  validate: {\n    loopOutputs: (value) => {\n      if (!value) return;\n      const keys = Object.keys(value);\n      const uniqueKeys = new Set(keys);\n      if (keys.length !== uniqueKeys.size) {\n        return 'Output key names cannot be duplicated';\n      }\n    },\n  },\n};\n```\n\n## Related Materials\n\n- [BatchVariableSelector](./batch-variable-selector): Array variable selector for selecting loop input\n- [provideBatchInputEffect](../effects/provide-batch-input): Loop input variable parsing effect\n- [batchOutputsPlugin](../form-plugins/batch-outputs-plugin): Loop output plugin, handles scope chain and type inference\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/batch-variable-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/batch-variable-selector';\n\n# BatchVariableSelector\n\nBatchVariableSelector is a component for selecting array-type variables. It's a wrapper around [VariableSelector](./variable-selector) that automatically filters the variable tree to show only array-type variables and provides private scope support. It's commonly used in batch processing scenarios (such as selecting loop data sources in Loop nodes).\n\n**Core Features:**\n\n- 🔍 **Auto Filtering**: Only displays array-type (`type: 'array'`) variables\n- 🔐 **Private Scope**: Provides isolated variable scope through [`PrivateScopeProvider`](../../guide/variable/concept#node-private-scope)\n- 🎯 **Specialized Scenarios**: Designed for batch processing, loops, and other scenarios requiring array data sources\n- 📦 **Ready to Use**: No need to manually configure schema filtering conditions, automatically applies array type constraints\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { BatchVariableSelector, VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      {/* BatchVariableSelector only shows array-type variables */}\n      <Field<string[] | undefined> name=\"batch_variable\">\n        {({ field }) => (\n          <BatchVariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n\n      {/* VariableSelector shows all variable types */}\n      <Field<string[] | undefined> name=\"normal_variable\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### BatchVariableSelector Props\n\nBatchVariableSelector inherits all properties from [VariableSelector](./variable-selector), but the `includeSchema` property is fixed to array-type filtering and cannot be customized.\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `string[]` | - | Selected variable path array |\n| `onChange` | `(value?: string[]) => void` | - | Callback when variable selection changes |\n| `config` | `VariableSelectorConfig` | `{}` | Configuration object (same as VariableSelector) |\n| `readonly` | `boolean` | `false` | Whether in read-only mode |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `style` | `React.CSSProperties` | - | Custom styles |\n| `triggerRender` | `(props: TriggerRenderProps) => React.ReactNode` | - | Custom trigger renderer |\n\n:::warning\n`includeSchema` and `excludeSchema` properties are not available in BatchVariableSelector, as the component internally uses `{ type: 'array', extra: { weak: true } }` as the fixed filtering condition.\n:::\n\n### VariableSelectorConfig\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `placeholder` | `string` | `'Select Variable'` | Placeholder text |\n| `notFoundContent` | `string` | `'Undefined'` | Content to display when variable is not found |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/batch-variable-selector\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/batch-variable-selector\n```\n\n### Directory Structure\n\n```\nbatch-variable-selector/\n└── index.tsx           # Main component implementation with BatchVariableSelector core logic\n```\n\n### Core Implementation\n\n#### Private Scope Mechanism\n\nBatchVariableSelector provides an independent variable scope for child components through [`PrivateScopeProvider`](../../guide/variable/concept#node-private-scope):\n\n```tsx\n<PrivateScopeProvider>\n  <VariableSelector {...props} includeSchema={batchVariableSchema} />\n</PrivateScopeProvider>\n```\n\n`PrivateScopeProvider` creates a [node private scope](../../guide/variable/concept#node-private-scope), which is crucial in batch processing scenarios:\n\n- **Loop variable isolation**: In Loop nodes, loop variables for each iteration (such as `item`, `index`) are stored in the private scope, preventing pollution of the external scope\n- **Avoid naming conflicts**: Temporary variables defined inside batch processing nodes will not conflict with external variable names\n- **Support nested structures**: Complex batch processing logic can define multi-level variable structures in the private scope\n- **Data security**: Variables in the private scope can only be accessed by the current node and its child nodes, ensuring data security\n\n:::info\n\nFor more detailed information about scopes, please refer to the [Variable Concept Documentation](../../guide/variable/concept#variables-in-canvas).\n\n:::\n\n#### Array Type Filtering\n\nThe component internally uses the following fixed schema for filtering:\n\n```typescript\nconst batchVariableSchema: IJsonSchema = {\n  type: 'array',\n  extra: { weak: true },\n};\n```\n\n- `type: 'array'`: Only display array-type variables\n- `extra: { weak: true }`: Enable weak type matching, allowing potentially compatible types\n\n### Overall Flow\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant BatchVariableSelector\n    participant PrivateScopeProvider\n    participant VariableSelector\n    participant useVariableTree\n    participant VariableTree\n\n    User->>BatchVariableSelector: Select array variable\n    BatchVariableSelector->>PrivateScopeProvider: Set private scope context\n    PrivateScopeProvider->>VariableSelector: Pass array filtering schema\n    VariableSelector->>useVariableTree: Get available variable list\n    useVariableTree->>VariableTree: Query variable tree data\n    VariableTree-->>useVariableTree: Return all variables\n    useVariableTree-->>VariableSelector: Filter array-type variables\n    VariableSelector-->>BatchVariableSelector: Render array variable selector\n    BatchVariableSelector-->>User: Display selectable array variables\n```\n\n### FlowGram APIs Used\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/plugins/node-variable-plugin)\n- [`PrivateScopeProvider`](https://flowgram.ai/auto-docs/node-variable-plugin/functions/PrivateScopeProvider): Context Provider for private variable scope\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition for variable type filtering\n\n### Dependent Materials\n\n[**VariableSelector**](./variable-selector) Base variable selector component\n- `VariableSelector`: Core variable selection component, BatchVariableSelector is its wrapper version\n- `VariableSelectorProps`: Property type definitions\n\n## FAQ\n\n### Why is PrivateScopeProvider needed?\n\n`PrivateScopeProvider` provides variable scope isolation, which is important in the following scenarios:\n\n1. **Loop Nodes**: In Loop nodes, each iteration needs an independent scope to store loop variables (such as `item`, `index`). Refer to [Variable Concept - Node Private Scope](../../guide/variable/concept#node-private-scope)\n2. **Nested Structures**: When there are nested variable declarations inside nodes, avoiding naming conflicts with external variables\n3. **Component Reuse**: Ensuring variables don't interfere with each other when the same component is used in different contexts\n4. **Data Security**: Variables in the private scope can only be accessed by the current node and its child nodes, ensuring data security\n\nFor more information about scope chains and variable access permissions, please refer to [Variable Concept - Scope Chain](../../guide/variable/concept#scope-chain).\n\n### What's the difference between BatchVariableSelector and VariableSelector?\n\n| Feature | BatchVariableSelector | VariableSelector |\n|---------|----------------------|------------------|\n| Variable Type Filtering | Fixed to array type | Customizable |\n| Scope | Built-in private scope | Uses current scope |\n| Use Cases | Batch processing, loops, etc. | General variable selection |\n| Schema Configuration | Not configurable | Fully configurable |\n\n### How to get the actual value of the selected variable?\n\nBatchVariableSelector returns a variable path (`string[]`). To get the actual value, you need to use [`provideBatchInputEffect`](../effects/provide-batch-input) in the form's effect:\n\n```typescript\nexport const formMeta = {\n  render: YourFormRender,\n  effect: {\n    yourFieldName: provideBatchInputEffect,\n  },\n};\n```\n\n`provideBatchInputEffect` automatically resolves variable references and injects them into the form data.\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/blur-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, PlaceholderStory, DisabledStory } from 'components/form-materials/components/blur-input';\n\n# BlurInput\n\nBlurInput is a special input component that only triggers value updates when the input loses focus, suitable for scenarios where frequent update operations need to be avoided.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input\" defaultValue=\"Initial text\">\n        {({ field }) => (\n          <>\n            <BlurInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"Please enter text\"\n            />\n            <p className=\"mt-2\">Current value: {field.value}</p>\n            <p className=\"text-sm text-gray-500\">\n              Note: Value updates after clicking outside the input\n            </p>\n          </>\n        )}\n      </Field>\n    </>\n  )\n}\n```\n\n### With Placeholder\n\n<PlaceholderStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input_placeholder\" defaultValue=\"\">\n        {({ field }) => (\n          <>\n            <BlurInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"This is an input field with placeholder\"\n            />\n            <p className=\"mt-2\">Current value: {field.value || 'Empty'}</p>\n          </>\n        )}\n      </Field>\n    </>\n  )\n}\n```\n\n### Disabled State\n\n<DisabledStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input_disabled\" defaultValue=\"Disabled state text\">\n        {({ field }) => (\n          <BlurInput value={field.value} onChange={(value) => field.onChange(value)} disabled />\n        )}\n      </Field>\n    </>\n  )\n}\n```\n## API Reference\n\n### Properties\n\n| Property | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| value | `string` | - | The value of the input |\n| onChange | `(value: string, event?: React.FocusEvent) => void` | - | Callback function triggered when focus is lost |\n| placeholder | `string` | - | Placeholder text for the input |\n| disabled | `boolean` | `false` | Whether to disable the input |\n| ref | `React.RefObject<HTMLInputElement>` | - | Reference to the input element |\n| Other properties | Inherited from [Semi UI Input component](https://semi.design/input/) | - | Supports all other properties of the Semi UI Input component |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/blur-input\"\n/>\n\nYou can copy the source code to your local machine using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/blur-input\n```\n\n### Directory Structure\n\n```\ncomponents/blur-input/\n└── index.tsx  # Component implementation file\n```\n\n### Core Implementation\n\nThe core implementation of the BlurInput component is very concise and mainly includes the following parts:\n\n1. **State Management**: Uses `useState` to maintain internal value state, decoupled from the incoming `value` property\n2. **Value Synchronization**: Uses `useEffect` to update the internal state when the external `value` changes\n3. **Delayed Update**: Only updates the internal state during user input, and calls the externally passed `onChange` callback only when the `onBlur` event is triggered\n\nThis implementation ensures that value updates only occur when the user finishes inputting and clicks outside the area, effectively reducing unnecessary update operations, especially suitable for scenarios requiring form validation or remote data requests.\n\n### Dependency Analysis\n\n#### Third-party Libraries\n\n[**Semi UI Input component**](https://semi.design/input/) provides basic input box functionality support, and the BlurInput component inherits all its properties and adds the feature of updating on blur.\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/code-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/code-editor';\n\n# CodeEditor\n\nCodeEditor is a powerful code editor component built on CodeMirror 6, supporting syntax highlighting and intelligent suggestions for multiple programming languages. It provides dedicated editor versions for TypeScript, Python, SQL, Shell, JSON, and more.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { CodeEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string | undefined> name=\"code_editor\">\n        {({ field }) => (\n          <CodeEditor\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            languageId=\"typescript\"\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Dedicated Language Editors\n\n:::warning Note\nImporting dedicated language editors separately can reduce the dependencies required for bundling\n:::\n\n```tsx\n// TypeScript Editor\nimport { TypeScriptCodeEditor } from '@flowgram.ai/form-materials';\n\n// Python Editor\nimport { PythonCodeEditor } from '@flowgram.ai/form-materials';\n\n// SQL Editor\nimport { SQLCodeEditor } from '@flowgram.ai/form-materials';\n\n// Shell Editor\nimport { ShellCodeEditor } from '@flowgram.ai/form-materials';\n\n// JSON Editor\nimport { JsonCodeEditor } from '@flowgram.ai/form-materials';\n```\n\n## API Reference\n\n### CodeEditor Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `string` | - | Editor content |\n| `onChange` | `(value: string) => void` | - | Callback function when content changes |\n| `languageId` | `'python' \\| 'typescript' \\| 'shell' \\| 'json' \\| 'sql'` | - | Code language type |\n| `theme` | `'dark' \\| 'light'` | `'light'` | Editor theme |\n| `placeholder` | `string` | - | Placeholder text |\n| `activeLinePlaceholder` | `string` | - | Current line placeholder hint |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `mini` | `boolean` | `false` | Whether it's mini mode |\n| `options` | `Options` | - | CodeMirror configuration options |\n\n### Language Support\n\nCodeEditor supports the following languages:\n\n- **typescript**: TypeScript/JavaScript\n- **python**: Python\n- **sql**: SQL\n- **shell**: Shell scripts\n- **json**: JSON\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/code-editor\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/code-editor\n```\n\n### Directory Structure Explanation\n\n```\ncode-editor/\n├── index.tsx           # Unified export file\n├── editor.tsx          # Base editor component BaseCodeEditor\n├── editor-all.tsx      # Full-featured editor (deprecated)\n├── editor-ts.tsx       # TypeScript editor\n├── editor-python.tsx   # Python editor\n├── editor-sql.tsx      # SQL editor\n├── editor-shell.tsx    # Shell editor\n├── editor-json.tsx     # JSON editor\n├── factory.tsx         # Editor factory function\n├── theme/              # Theme configuration\n│   ├── dark.ts         # Dark theme\n│   ├── light.ts        # Light theme\n│   └── index.ts        # Theme export\n├── utils.ts            # Utility functions\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### BaseCodeEditor\n\nThe base editor component, a simple wrapper around coze-editor, provides basic syntax highlighting, theme switching, mini mode, read-only mode, and other features.\n\n:::warning Note\nBaseCodeEditor does not include any language logic loading; language logic is implemented in dedicated editors.\n:::\n\n#### Dedicated Editors\nEach language has a corresponding dedicated editor, implemented through dynamic import:\n\n```typescript\nexport const loadTypescriptLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-typescript').then((module) => {\n    // TypeScript language loading logic\n  });\n\nexport const TypeScriptCodeEditor = CodeEditorFactory<true>(\n  loadTypescriptLanguage,\n  {\n    displayName: 'TypeScriptCodeEditor',\n    fixLanguageId: 'typescript',\n  }\n);\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/coze-editor\n\n@flowgram.ai/coze-editor is a code editor component built on CodeMirror 6. See source code: [coze-dev/rush-dev](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor)\n\n- `createRenderer`: Create editor\n- `preset-code`: Code editor preset configuration\n- `EditorProvider`: Editor context provider\n- `ActiveLinePlaceholder`: Current line placeholder component\n\n#### @codemirror/view\n- `EditorView`: CodeMirror editor view\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[CodeEditor Component] --> B[Select languageId\n or dedicated editor]\n    B --React.lazy--> C[Load dependencies\n needed for languageId]\n    C --> D[BaseCodeEditor]\n    D --> E[CozeEditor]\n    E --> F[Syntax highlighting]\n    E --> G[Theme application]\n    E --> H[Event handling]\n```\n\n### Performance Optimization\n\n- **Lazy Loading**: Each language is loaded on demand\n- **Dedicated Editors**: It is recommended to use XXXCodeEditor instead of the generic CodeEditor to optimize bundling speed"
  },
  {
    "path": "apps/docs/src/en/materials/components/condition-context.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/condition-context';\n\n# ConditionContext\n\nConditionContext is a context management system for condition configuration, used to uniformly manage condition rules and operator configurations, providing a consistent configuration environment for condition components.\n\n:::tip\n\nThe condition configuration context of ConditionContext can affect the following materials:\n\n- [**ConditionRow**](./condition-row)\n- [**DBConditionRow**](./db-condition-row)\n\n:::\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport React from 'react';\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport {\n  ConditionProvider,\n  ConditionRow,\n  DBConditionRow,\n  type ConditionOpConfigs,\n  type IConditionRule\n} from '@flowgram.ai/form-materials';\n\nconst OPS: ConditionOpConfigs = {\n  cop: {\n    abbreviation: 'C',\n    label: 'Custom Operator',\n  },\n};\n\nconst RULES: Record<string, IConditionRule> = {\n  string: {\n    cop: { type: 'string' },\n  },\n};\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <ConditionProvider ops={OPS} rules={RULES}>\n        <Field<any | undefined> name=\"condition_row\">\n          {({ field }) => (\n            <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n          )}\n        </Field>\n        <Field<any | undefined> name=\"db_condition_row\">\n          {({ field }) => (\n            <DBConditionRow\n              options={[\n                {\n                  label: 'UserName',\n                  value: 'username',\n                  schema: { type: 'string' },\n                },\n              ]}\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n          )}\n        </Field>\n      </ConditionProvider>\n    </>\n  ),\n};\n```\n\n## API Reference\n\n### ConditionProvider\n\nA context provider component for condition configuration, used to uniformly manage condition rules and operator configurations.\n\n| Parameter | Type | Required | Description |\n|-----------|------|----------|-------------|\n| `rules` | `Record<string, IConditionRule>` | No | Condition rule configuration |\n| `ops` | `ConditionOpConfigs` | No | Operator configuration |\n| `children` | `React.ReactNode` | Yes | Child components |\n\n### useCondition\n\nA hook to get condition configuration, which obtains corresponding configuration information based on data type and operator.\n\nFor specific usage, you can refer to the use of `useCondition` in [ConditionRow's source code](https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/components/condition-row/index.tsx).\n\n### ConditionPresetOp\n\nA preset operator enumeration, providing commonly used comparison operators.\n\n| Enum Value | Description | Abbreviation |\n|------------|-------------|--------------|\n| `EQ` | Equal | `=` |\n| `NEQ` | Not Equal | `≠` |\n| `GT` | Greater Than | `>` |\n| `GTE` | Greater Than or Equal | `>=` |\n| `LT` | Less Than | `<` |\n| `LTE` | Less Than or Equal | `<=` |\n| `IN` | In | `∈` |\n| `NIN` | Not In | `∉` |\n| `CONTAINS` | Contains | `⊇` |\n| `NOT_CONTAINS` | Not Contains | `⊉` |\n| `IS_EMPTY` | Is Empty | `=` |\n| `IS_NOT_EMPTY` | Is Not Empty | `≠` |\n| `IS_TRUE` | Is True | `=` |\n| `IS_FALSE` | Is False | `=` |\n\n### Type Definitions\n\n```typescript\n// Operator configuration\ninterface ConditionOpConfig {\n  label: string; // Operator label\n  abbreviation: string; // Operator abbreviation\n  rightDisplay?: string; // Right side display text (when right side is not a value)\n}\n\n// Operator configuration collection\ntype ConditionOpConfigs = Record<string, ConditionOpConfig>;\n\n// Condition rule\ntype IConditionRule = Record<string, string | IJsonSchema | null>;\n\n// Condition rule factory function\ntype IConditionRuleFactory = (\n  schema?: IJsonSchema\n) => Record<string, string | IJsonSchema | null>;\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/condition-context\"\n/>\n\nYou can copy the source code to your local machine using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/condition-context\n```\n\n### Directory Structure\n\n```\ncondition-context/\n├── context.tsx        # Context implementation\n├── hooks/             # Hook functions directory\n│   └── use-condition.tsx  # useCondition hook implementation\n├── index.tsx          # Unified export file\n├── op.ts              # Preset operator definitions\n└── types.ts           # Type definitions\n```\n\n### Core Implementation\n\n#### ConditionContext Workflow\n\nHere is the sequence diagram of the workflow for ConditionProvider and useCondition Hook:\n\n```mermaid\nsequenceDiagram\n    participant App as Application Component\n    participant Provider as ConditionProvider\n    participant Context as ConditionContext\n    participant Child as ConditionRow or DBConditionRow\n    participant Hook as useCondition Hook\n    participant TypeManager as useTypeManager\n\n    %% Initialization Phase\n    App->>Provider: Pass rules and ops configurations\n    Provider->>Context: Create Context and set default values\n    Provider->>Provider: Receive rules and ops parameters\n    Provider->>Context: Update configurations in Context\n    Provider-->>App: Render child components\n\n    %% Usage Phase\n    Child->>Hook: Call useCondition\n    Hook->>Context: Get configurations via useConditionContext()\n    Hook->>TypeManager: Get type manager\n    Hook->>Hook: Merge user rules and context rules\n    Hook->>Hook: Merge user operators and context operators\n    Hook->>TypeManager: Get type configuration based on leftSchema\n    Hook->>Hook: Calculate condition rules for current type\n    Hook->>Hook: Generate available operator options list\n    Hook->>Hook: Calculate target value's data type Schema\n    Hook-->>Child: Return {rule, opConfig, opOptionList, targetSchema}\n    Child-->>App: Render condition component based on returned configurations\n\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): Internationalization utility class\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/types/IJsonSchema): JSON Schema type definition\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/condition-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/condition-row';\n\n# ConditionRow\n\nConditionRow is a conditional expression component used to build variable comparison logic. It supports selecting variables, choosing comparison operators, and inputting comparison values. It can automatically adapt available operators and value types based on the variable type.\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/condition-row.png\" alt=\"Condition Row Component\" style={{ width: '50%' }} />\n  *The first condition is the query variable containing Hello Flow, the second condition is the enable variable being true*\n</div>\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { ConditionRow } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any | undefined> name=\"condition_row\">\n        {({ field }) => (\n          <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Condition Row with Initial Value\n\n```tsx\n<ConditionRow\n  value={{\n    left: { type: 'ref', content: 'user.age' },\n    operator: 'gt',\n    right: { type: 'constant', content: 18, schema: { type: 'number' } }\n  }}\n  onChange={(value) => console.log('Condition changed:', value)}\n/>\n```\n\n## API Reference\n\n### ConditionRow Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `ConditionRowValueType` | - | Conditional expression value |\n| `onChange` | `(value?: ConditionRowValueType) => void` | - | Callback function when condition changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `ruleConfig` | `{ ops?: ConditionOpConfigs; rules?: Record<string, IConditionRule> }` | - | Operator and rule configuration |\n| `style` | `React.CSSProperties` | - | Custom styles |\n\n### ConditionRowValueType\n\n```typescript\ninterface ConditionRowValueType {\n  left?: IFlowRefValue;           // Left variable reference\n  operator?: string;            // Operator\n  right?: IFlowConstantRefValue; // Right constant value\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content: string; // Variable path, e.g., \"user.name\"\n}\n\ninterface IFlowConstantRefValue {\n  type: 'constant';\n  content: any;           // Constant value\n  schema: IJsonSchema;  // Value type definition\n}\n```\n\n### Supported Comparison Operators\n\nBased on the type of the left variable, ConditionRow will automatically provide corresponding comparison operators:\n\n- **String type**: equals, not_equals, contains, not_contains, starts_with, ends_with\n- **Number type**: equals, not_equals, gt, gte, lt, lte\n- **Boolean type**: equals, not_equals\n- **Array type**: contains, not_contains, empty, not_empty\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/condition-row\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/condition-row\n```\n\n### Directory Structure Explanation\n\n```\ncondition-row/\n├── index.tsx           # Main component implementation, containing ConditionRow core logic\n├── types.ts            # Type definitions\n├── styles.tsx          # Style definitions using styled-components\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### Variable Type Inference\nThe component automatically infers the JSON Schema type based on the selected left variable:\n\n```typescript\nconst leftSchema = useMemo(() => {\n  if (!variable) return undefined;\n  return JsonSchemaUtils.astToSchema(variable.type, { drilldown: false });\n}, [variable?.type?.hash]);\n```\n\n#### Dynamic Operator Adaptation\nAvailable operators are obtained based on the left variable type through the `useCondition` Hook:\n\n```typescript\nconst { rule, opConfig, opOptionList, targetSchema } = useCondition({\n  leftSchema,\n  operator,\n});\n```\n\n#### Right Value Type Auto-Matching\nThe type of the right input field is automatically matched based on the operator and left variable type:\n\n```typescript\ntargetSchema ? (\n  <InjectDynamicValueInput\n    schema={targetSchema}\n    // ... other properties\n  />\n) : (\n  // Placeholder input\n)\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/json-schema\n- `JsonSchemaUtils.astToSchema()`: Convert AST type to JSON Schema\n- `IJsonSchema`: JSON Schema type definition\n\n#### @flowgram.ai/variable-core\n- `useScopeAvailable()`: Get available variables in current scope\n\n#### @flowgram.ai/i18n\n- `I18n`: Internationalization support\n\n#### Internal Components\n- [`InjectVariableSelector`](./variable-selector): Variable selector\n- [`InjectDynamicValueInput`](./dynamic-value-input): Dynamic value input component\n- `useCondition`: Conditional logic Hook\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[ConditionRow Component] --> B[Select Left Variable]\n    B --> C[Infer Variable Type]\n    C --> D[Get Available Operators]\n    D --> E[Select Comparison Operator]\n    E --> F[Determine Right Value Type]\n    F --> G[Input Comparison Value]\n    G --> H[onChange Callback]\n\n    J[Variable Selector] --> B\n    K[Dynamic Value Input] --> G\n    L[Condition Context] --> D\n```"
  },
  {
    "path": "apps/docs/src/en/materials/components/constant-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, FallbackRendererStory, CustomStrategyStory } from 'components/form-materials/components/constant-inputs';\n\n# ConstantInput\n\nConstantInput is a constant input component that automatically selects the appropriate input type renderer based on the provided JSON Schema.\n\nThe component supports custom rendering strategies and fallback renderers, capable of handling constant inputs for various data types.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"constant_string\" defaultValue=\"Hello World\">\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n:::tip{title=\"Register Constant Input for New Types\"}\n\nRefer to [Type Management](../common/json-schema-preset) to configure constant inputs when registering types\n\n:::\n\n### Fallback Renderer\n\nWhen the component cannot find a suitable renderer, it will use the fallback renderer:\n\n<FallbackRendererStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"constant_fallback\" defaultValue={{ custom: 'data' }}>\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'custom-unsupported-type' }}\n            fallbackRenderer={({ value, onChange, readonly }) => (\n              <div style={{ padding: '8px', background: '#f0f0f0', border: '1px dashed #ccc' }}>\n                <p>Fallback renderer for unsupported type</p>\n              </div>\n            )}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Custom Strategy\n\nUse custom rendering strategies to override default behavior:\n\n<CustomStrategyStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"constant_custom\" defaultValue=\"Custom Value\">\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'object' }}\n            strategies={[\n              {\n                hit: (schema) => schema.type === 'object',\n                Renderer: ({ value, onChange, readonly }) => (\n                  <p>Object is not supported now</p>\n                ),\n              },\n            ]}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### ConstantInput Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `any` | - | Input value |\n| `onChange` | `(value: any) => void` | - | Callback function when value changes |\n| `schema` | `IJsonSchema` | - | JSON Schema used to determine the renderer |\n| `strategies` | `Strategy[]` | - | Array of custom rendering strategies |\n| `fallbackRenderer` | `React.FC<ConstantRendererProps>` | - | Fallback renderer used when no suitable renderer is found |\n| `readonly` | `boolean` | `false` | Whether it is read-only mode |\n\n### Strategy Interface\n\n```typescript\ninterface Strategy<Value = any> {\n  hit: (schema: IJsonSchema) => boolean;\n  Renderer: React.FC<ConstantRendererProps<Value>>;\n}\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/constant-input\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/constant-input\n```\n\n### Directory Structure\n\n```\nconstant-input/\n├── index.tsx    # Main component implementation, contains ConstantInput core logic\n└── types.ts     # Type definitions, including Strategy interface and PropsType\n```\n\n### Core Implementation\n\n#### Renderer Selection Logic\n\nThe component selects the appropriate renderer through the following steps:\n\n1. **Strategy Matching**: First checks if there are any strategies in the `strategies` array that match the current schema\n2. **Type Manager**: If no matching strategy is found, uses the type manager (`useTypeManager`) to get the renderer corresponding to the schema\n3. **Fallback Rendering**: If all above fail, uses the provided `fallbackRenderer` or the default disabled input box\n\n#### Type System Integration\n\nConstantInput is deeply integrated with [**Material Type Management**](../common/json-schema-preset):\n\n- Gets the type manager through the `useTypeManager` Hook\n- Uses `typeManager.getTypeBySchema(schema)` to get the renderer for the corresponding type\n- Supports all JSON Schema standard types (string, number, boolean, object, array, etc.)\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): Type manager Hook\n\n#### Third-party Libraries\n\n[**Semi UI**](https://semi.design/en-US)\n- Basic input component library, provides default input controls\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/coze-editor-extensions.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory as PromptWithVariablesStory } from 'components/form-materials/components/prompt-editor-with-variables';\nimport { BasicStory as PromptWithInputsStory } from 'components/form-materials/components/prompt-editor-with-inputs';\n\n# CozeEditorExtensions\n\nCozeEditorExtensions is a set of functional extensions based on [coze-editor](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor), providing variable selection, Inputs selector, and variable tag echo capabilities.\n\n- `EditorVariableTree`: Monitors trigger characters like `@`/`{`, pops up an available variable tree, and writes the selected item into the editor.\n- `EditorVariableTagInject`: Renders `{{variable.path}}` text with markup, displaying variable icons, titles, and echo hints.\n- `EditorInputsTree`: Constructs a hierarchical tree based on node `inputsValues`, supporting insertion of `{{inputs.xxx}}` references in prompts.\n\n## Demo\n\n### Variable Tree + Tag Echo\n\n<PromptWithVariablesStory />\n\n```tsx pure title=\"prompt-editor-with-extensions.tsx\"\nimport {\n  PromptEditor,\n  EditorVariableTree,\n  EditorVariableTagInject,\n  EditorInputsTree,\n} from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <PromptEditor value={value} onChange={onChange}>\n      <EditorVariableTree triggerCharacters={TRIGGER_CHARACTERS} />\n      <EditorVariableTagInject />\n    </PromptEditor>\n  );\n}\n```\n\n:::tip{title=\"Trigger Method\"}\n`EditorVariableTree` defaults to monitoring `{`, `{{`, and `@`. You can customize trigger characters through `triggerCharacters`.\n:::\n\n### Node Inputs Reference\n\n<PromptWithInputsStory />\n\n\n```tsx pure title=\"prompt-editor-with-extensions.tsx\"\nimport {\n  PromptEditor,\n  EditorVariableTree,\n  EditorVariableTagInject,\n  EditorInputsTree,\n} from '@flowgram.ai/form-materials';\n\n\nexport function PromptEditorWithExtensions({ value, onChange, inputsValues }) {\n  return (\n    <PromptEditor value={value} onChange={onChange}>\n      <EditorInputsTree inputsValues={inputsValues} />\n    </PromptEditor>\n  );\n}\n```\n\n\n### Render Keys and Secondary Extension\n\nCozeEditorExtensions declares fixed `renderKey` through [`createInjectMaterial`](../common/inject-material):\n\n- `EditorVariableTree.renderKey = 'EditorVariableTree'`\n- `EditorVariableTagInject.renderKey = 'EditorVariableTagInject'`\n- `EditorInputsTree.renderKey = 'EditorInputsTree'`\n\nYou can register renderers with the same name in `use-editor-props` to replace the default behavior. For example, customize the variable selection panel:\n\n```tsx pure title=\"override-variable-tree.tsx\"\nimport { useEditorProps } from '@flowgram.ai/editor';\nimport { EditorVariableTree } from '@flowgram.ai/form-materials';\nimport { CustomVariableTree } from './custom-variable-tree';\n\nexport function useCustomEditorProps() {\n  return useEditorProps({\n    materials: {\n      components: {\n        [EditorVariableTree.renderKey!]: CustomVariableTree,\n      },\n    },\n  });\n}\n```\n\n\n## API Reference\n\n### EditorVariableTree Props\n\n| Property Name | Type | Default | Description |\n|--------|------|--------|------|\n| `triggerCharacters` | `string[]` | `['{', '{}', '@']` | Character set that triggers variable tree popup |\n\n### EditorVariableTagInject Props\n\n| Property Name | Type | Default | Description |\n|--------|------|--------|------|\n| _None_ | - | - | Component has no additional properties, rendering takes effect immediately |\n\n### EditorInputsTree Props\n\n| Property Name | Type | Default | Description |\n|--------|------|--------|------|\n| `inputsValues` | `IInputsValues` | - | Node Inputs key-value data, supports `FlowValue` reference |\n| `triggerCharacters` | `string[]` | `['{', '{}', '@']` | Character set that triggers Inputs selector |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/coze-editor-extensions\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/coze-editor-extensions\n```\n\n### Directory Structure Explanation\n\n```\ncoze-editor-extensions/\n├── index.tsx                 # Export injectable materials\n├── extensions/\n│   ├── inputs-tree.tsx       # Inputs Tree implementation\n│   ├── variable-tag.tsx      # Variable tag rendering\n│   └── variable-tree.tsx     # Variable tree selector\n└── styles.css                # Tag styles and popup styles\n```\n\n### Core Implementation Explanation\n\n**EditorVariableTree**\n- Popup positioning and scroll synchronization: `Mention` + `PositionMirror` combination ensures Popover always stays close to cursor position, scrolling triggers repositioning through `rePosKey`\n- Variable tree data source: `useVariableTree` reads variables registered in `Scope.available`, automatically applies schema filtering, disabled states, and icons\n\n**EditorVariableTagInject**\n- Tag rendering and subscription: Uses CodeMirror `MatchDecorator` to replace `{{xxx}}` text with `Tag` widget, and subscribes to variable title/Icon updates for real-time echo\n\n**EditorInputsTree**\n- Inputs tree construction: Supports both `FlowValue` and regular objects, recursively generates hierarchical nodes, and uniformly formats them as `{{path}}` before insertion\n\n### Dependencies\n\n#### Other Materials\n\n[**InjectMaterial**](../common/inject-material)\n- `createInjectMaterial`: Creates injectable material components\n\n#### Third-party Libraries\n\n[**coze-editor**](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor)\n- Base editor component\n\n[**CodeMirror MatchDecorator**](https://codemirror.net/docs/ref/#search.MatchDecorator)\n- Used for text matching and replacement of variable tags"
  },
  {
    "path": "apps/docs/src/en/materials/components/db-condition-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/db-condition-row';\n\n# DBConditionRow\n\nDBConditionRow is a database condition row component used for building database query conditions. It provides field selection, operator selection, and value input functionality, which can automatically display appropriate operators and input controls based on field types.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DBConditionRow } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any | undefined> name=\"db_condition_row\">\n        {({ field }) => (\n          <DBConditionRow\n            options={[\n              {\n                label: 'TransactionID',\n                value: 'transaction_id',\n                schema: { type: 'integer' },\n              },\n              {\n                label: 'Amount',\n                value: 'amount',\n                schema: { type: 'number' },\n              },\n              {\n                label: 'Description',\n                value: 'description',\n                schema: { type: 'string' },\n              },\n              {\n                label: 'Archived',\n                value: 'archived',\n                schema: { type: 'boolean' },\n              },\n              {\n                label: 'CreateTime',\n                value: 'create_time',\n                schema: { type: 'date-time' },\n              },\n            ]}\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### DBConditionRow Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `DBConditionRowValueType` | - | The value of the condition row, including left (field name), operator (operator), and right (value) |\n| `onChange` | `(value?: DBConditionRowValueType) => void` | - | Callback function when value changes |\n| `options` | `DBConditionOptionType[]` | - | List of optional fields, each field contains label, value, and schema |\n| `readonly` | `boolean` | `false` | Whether it is read-only mode |\n| `style` | `React.CSSProperties` | - | Custom styles |\n| `ruleConfig` | `{ ops?: ConditionOpConfigs; rules?: Record<string, IConditionRule> }` | - | **Deprecated**, use ConditionContext instead |\n\n### DBConditionRowValueType Type\n\n```typescript\ninterface DBConditionRowValueType {\n  left?: string; // Field name\n  operator?: string; // Operator\n  right?: IFlowConstantRefValue; // Value, supports constant or variable reference\n}\n```\n\n### DBConditionOptionType Type\n\n```typescript\ninterface DBConditionOptionType {\n  label: string | JSX.Element; // Field display name\n  value: string; // Field value\n  schema: IJsonSchema; // Field type definition\n}\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/db-condition-row\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/db-condition-row\n```\n\n### Directory Structure Explanation\n\n```\ndb-condition-row/\n├── index.tsx    # Main component implementation\n├── types.ts     # Type definitions\n└── styles.css   # Style files\n```\n\n### Core Implementation Explanation\n\n#### Component Implementation Process\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant LeftValue\n    participant useCondition\n    participant Operator\n    participant RightValue\n\n    User->>LeftValue: Select field\n    LeftValue->>LeftValue: Update left value\n    LeftValue->>useCondition: Trigger condition calculation\n    useCondition->>useCondition: Get field type information\n    useCondition-->>Operator: Return available operator list\n    User->>Operator: Select operator\n    Operator->>Operator: Update operator value\n    Operator->>useCondition: Recalculate target type\n    useCondition-->>RightValue: Return targetSchema\n\n    alt targetSchema exists\n        RightValue->>RightValue: Render InjectDynamicValueInput\n        User->>RightValue: Input or select value\n        RightValue->>RightValue: Update right value\n    else targetSchema does not exist\n        RightValue->>RightValue: Render disabled input box\n        RightValue->>RightValue: Display default prompt text\n    end\n\n    RightValue-->>User: Return complete condition object\n```\n\n1. **Left Value: Field Selector**: Uses Semi UI's Select component to display available fields based on options, and shows field type icons.\n\n2. **Operator: Operator Selector**: Gets the list of operators matching the field type through the useCondition Hook, and updates the operator after user selection.\n\n3. **Right Value: Value Input Component**: Dynamically displays appropriate input controls based on the selected field type and operator:\n   - When targetSchema exists, use the InjectDynamicValueInput component\n   - Otherwise, display a disabled input box with prompt information\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): Internationalization utility class\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n\n#### Other Materials\n\n[**ConditionContext**](./condition-context)\n- `useCondition`: Hook to get condition configuration\n- `ConditionOpConfigs`: Operator configuration type\n- `IConditionRule`: Condition rule type\n\n[**DynamicValueInput**](./dynamic-value-input)\n- `InjectDynamicValueInput`: Injectable dynamic value input component\n\n#### Third-party Libraries\n\n[**Semi UI**](https://semi.design/)\n- [`Select`](https://semi.design/input/select): Selector component\n- [`Button`](https://semi.design/basic/button): Button component\n- [`Input`](https://semi.design/input/input): Input box component\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/display-flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-flow-value';\n\n# DisplayFlowValue\n\nDisplayFlowValue is a component used to visually display [Flow Value](../common/flow-value) data types. It can infer the corresponding JSON Schema based on the type of Flow Value and present it to users through a user-friendly interface.\n\n## Case Demonstration\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => <DisplayFlowValue value={field.value} title=\"Display Flow Value\" />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### Props\n\n| Property Name | Type | Required | Default Value | Description |\n| --- | --- | --- | --- | --- |\n| value | `IFlowValue` | No | - | Flow Value data to be displayed |\n| title | `string \\| JSX.Element` | No | - | Title text displayed on the tag |\n| showIconInTree | `boolean` | No | - | Whether to show icons in the Schema tree |\n| typeManager | `JsonSchemaTypeManager` | No | - | Manager used to obtain type display information |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-flow-value\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-flow-value\n```\n\n### Core Implementation Explanation\n\nDisplayFlowValue component's main function is to convert Flow Value into JSON Schema and display it visually. The core implementation includes:\n\n1. **Type Inference**: Infer the corresponding JSON Schema based on different types of Flow Value\n   - For reference type, obtain variable type information from the scope\n   - For template type, default inference is string type\n   - For constant type, infer its type based on the constant value\n\n2. **Visual Display**: Display the inferred Schema through the DisplaySchemaTag component\n   - Use Semi UI's Popover and Tag components\n   - Click on the tag to view the detailed Schema tree structure\n\n3. **Error Handling**: When the reference type points to a non-existent variable, a warning color will be displayed\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- `JsonSchemaTypeManager`: Used to obtain type display information\n- `JsonSchemaUtils`: Provides JSON Schema related utility functions\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `useScopeAvailable`: Get available variables in the current scope\n\n#### Other Materials\n\n[**DisplaySchemaTag**](./display-schema-tag) Tag component for displaying JSON Schema\n\n[**FlowValueUtils**](../common/flow-value)\n- [FlowValueUtils.inferJsonSchema](../common/flow-value#schema-inference-functions)\n\n#### Third-party Libraries\n\n[**Semi UI**](https://semi.design/)\n- `Popover`: Pop-up component\n- `Tag`: Tag component\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/display-inputs-values.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-inputs-values';\n\n# DisplayInputsValues\n\nDisplayInputsValues is a component used to visually display tree-structured input values.\n\nIt can **recursively** traverse input value objects, parse the types of [Flow Values](../common/flow-value) within them, and derive the JSON Schema structure for each field.\n\n:::tip\n\nDisplayInputsValues supports displaying values configured by both [InputsValues](./inputs-values) and [InputsValuesTree](./inputs-values-tree) components.\n\n:::\n\n## Case Demonstration\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValuesTree, DisplayInputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => <DisplayInputsValues value={field.value} />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### Props\n\n| Property Name | Type | Required | Default Value | Description |\n| --- | --- | --- | --- | --- |\n| value | `IInputsValues` | No | - | Input value data to be displayed |\n| showIconInTree | `boolean` | No | - | Whether to show icons in the Schema tree |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-inputs-values\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-inputs-values\n```\n\n### Core Implementation Explanation\n\nThe main function of the DisplayInputsValues component is to recursively display tree-structured input values. The core implementation includes:\n\n1. **Data Traversal**: Traverse the input value object and process each key-value pair\n2. **Type Differentiation**:\n   - For Flow Value type values, use the DisplayFlowValue component for display\n   - For plain object type values, use the DisplayInputsValueAllInTag component to display their JSON Schema structure\n3. **Recursive Display**: Support for displaying multi-level nested data structures\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `useScopeAvailable`: Get available variables in the current scope\n\n[**@flowgram.ai/form-materials**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials)\n- `FlowValueUtils`: Flow Value related utility functions\n- `DisplayFlowValue`: Component for displaying Flow Value\n- `DisplaySchemaTag`: Tag component for displaying JSON Schema\n\n#### Third-party Libraries\n\n[**lodash-es**](https://lodash.com/)\n- `isPlainObject`: Check if a value is a plain object"
  },
  {
    "path": "apps/docs/src/en/materials/components/display-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { DisplayFromScopeStory, DisplayFromFieldStory } from 'components/form-materials/components/display-outputs';\n\n# DisplayOutputs\n\nDisplayOutputs is a component for visually displaying node output variables, supporting automatic retrieval of output variables from scope or manual specification of output schema through field values.\n\n## Demo\n\n### Getting Output Variables from Scope\n\nWhen `displayFromScope` is set to `true`, the component automatically retrieves output variables from the current node scope and displays them:\n\n<DisplayFromScopeStory />\n\n```tsx pure title=\"form-meta.tsx\" {27}\nimport { DisplayOutputs, provideJsonSchemaOutputs } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined> name=\"outputs\">\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <p>Display Output Schema:</p>\n      <DisplayOutputs displayFromScope />\n    </>\n  ),\n  effect: {\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n### Specifying Output Schema through Field Value\n\nWhen `displayFromScope` is not set, the component receives the output JSON Schema through the `value` prop:\n\n<DisplayFromFieldStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplayOutputs } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined>\n        name=\"outputs\"\n        defaultValue={{\n          type: 'object',\n          properties: {\n            result: { type: 'string' },\n            status: { type: 'number' },\n          },\n        }}\n      >\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <p>Display Output Schema:</p>\n      <Field<IJsonSchema | undefined> name=\"outputs\">\n        {({ field }) => <DisplayOutputs value={field.value} />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### DisplayOutputs Props\n\n| Property | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| value | `IJsonSchema` | - | JSON Schema object to display, used when `displayFromScope` is `false` |\n| displayFromScope | `boolean` | `false` | Whether to automatically get output variables from current scope |\n| showIconInTree | `boolean` | `false` | Whether to show icons in tree structure |\n| typeManager | `JsonSchemaTypeManager` | - | Custom type manager |\n| style | `React.CSSProperties` | - | Custom styles |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-outputs\"\n/>\n\nCopy source code locally using CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials display-outputs\n```\n\n### Directory Structure\n\n```plaintext\ndisplay-outputs/\n├── index.tsx    # Component main entry, implements DisplayOutputs component\n└── styles.css   # Component styles\n```\n\n### Core Implementation\n\nCore logic of the DisplayOutputs component:\n\n1. **Scope Mode** (`displayFromScope=true`): Uses `useCurrentScope` Hook to get output variables from current scope, converts variable list to JSON Schema properties through `scope?.output.variables`\n\n2. **Field Mode** (`displayFromScope=false`): Directly uses JSON Schema object passed through `value` prop\n\n3. **Variable Listening**: In scope mode, component listens for output variable changes and automatically refreshes display\n\n4. **Rendering Implementation**: Renders each property of JSON Schema as `DisplaySchemaTag` component, supporting type icons and warning state display\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`useCurrentScope`](https://flowgram.ai/auto-docs/editor/functions/useCurrentScope): Hook to get current scope\n- [`useRefresh`](https://flowgram.ai/api/hooks/use-refresh): Hook to force component refresh\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n- [`JsonSchemaUtils`](https://flowgram.ai/auto-docs/json-schema/modules/JsonSchemaUtils): JSON Schema utility functions\n- [`JsonSchemaTypeManager`](https://flowgram.ai/auto-docs/json-schema/classes/JsonSchemaTypeManager): Type manager\n\n#### Other Materials\n\n[**DisplaySchemaTag**](./display-schema-tag)\n- `DisplaySchemaTag`: Tag component for displaying individual JSON Schema properties\n\n#### Third-party Libraries\n\n[**React**](https://react.dev/)\n- `useLayoutEffect`: Used for listening to scope variable changes\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/display-schema-tag.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-schema-tag';\n\n# DisplaySchemaTag\n\nDisplaySchemaTag is a tag component used to display JSON Schema. When users click on the tag, a detailed schema tree structure will pop up, making it easy to view complex data structures.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplaySchemaTag } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <DisplaySchemaTag\n        title=\"Transaction\"\n        value={{\n          type: 'object',\n          properties: {\n            transaction_id: { type: 'integer' },\n            amount: { type: 'number' },\n            description: { type: 'string' },\n            archived: { type: 'boolean' },\n            owner: {\n              type: 'object',\n              properties: {\n                id: { type: 'integer' },\n                username: { type: 'string' },\n                friends: {\n                  type: 'array',\n                  items: {\n                    type: 'object',\n                    properties: {\n                      id: { type: 'integer' },\n                      username: { type: 'string' },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        }}\n      />\n    </>\n  ),\n}\n```\n\n## API Reference\n\n| Property | Type | Default Value | Description |\n| :--- | :--- | :--- | :--- |\n| title | `JSX.Element \\| string` | - | The title text displayed on the tag |\n| value | `IJsonSchema` | `{}` | The JSON Schema object to display |\n| showIconInTree | `boolean` | - | Whether to show icons in the popped-up tree structure |\n| warning | `boolean` | `false` | Whether to show warning state (yellow tag) |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-schema-tag\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-schema-tag\n```\n\n### Directory Structure\n\n```plaintext\ncomponents/display-schema-tag/\n├── index.tsx  # Main component implementation\n└── styles.css # Component styles\n```\n\n### Core Implementation\n\nDisplaySchemaTag component is implemented based on Semi UI's Popover and Tag components, with DisplaySchemaTree integrated internally to display detailed schema structure in a pop-up box.\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): Type manager Hook\n\n\n#### Other Materials\n\n[**DisplaySchemaTree**](./display-schema-tree) Used to display detailed schema tree structure in the pop-up box\n\n#### Third-party Libraries\n\n[**Semi UI**](https://semi.design/en-US)\n- `Popover`: Pop-up box component\n- `Tag`: Tag component\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/display-schema-tree.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-schema-tree';\n\n# DisplaySchemaTree\n\nDisplaySchemaTree is a tree component used to visually display JSON Schema structure, which can recursively show hierarchical relationships of complex data structures such as objects and arrays.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplaySchemaTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <DisplaySchemaTree\n        value={{\n          type: 'object',\n          properties: {\n            transaction_id: { type: 'integer' },\n            amount: { type: 'number' },\n            description: { type: 'string' },\n            archived: { type: 'boolean' },\n            owner: {\n              type: 'object',\n              properties: {\n                id: { type: 'integer' },\n                username: { type: 'string' },\n                friends: {\n                  type: 'array',\n                  items: {\n                    type: 'object',\n                    properties: {\n                      id: { type: 'integer' },\n                      username: { type: 'string' },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        }}\n      />\n    </>\n  ),\n}\n```\n\n## API Reference\n\n| Property | Type | Default Value | Description |\n| :--- | :--- | :--- | :--- |\n| value | `IJsonSchema` | `{}` | The JSON Schema object to display |\n| drilldown | `boolean` | `true` | Whether to expand nested property structure |\n| showIcon | `boolean` | `true` | Whether to show type icons |\n| typeManager | `JsonSchemaTypeManager` | - | Type manager for getting type-related information |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-schema-tree\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-schema-tree\n```\n\n### Directory Structure\n\n```plaintext\ncomponents/display-schema-tree/\n├── index.tsx  # Main component implementation\n└── styles.css # Component styles\n```\n\n### Core Implementation\n\nDisplaySchemaTree component displays JSON Schema hierarchical structure through recursive rendering.\n\nIt uses `useTypeManager` to obtain type-related information, including icons, display text, etc. The component recursively renders the child properties of the Schema internally, supporting multi-level nested data structure display.\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): Type manager Hook"
  },
  {
    "path": "apps/docs/src/en/materials/components/dynamic-value-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithSchemaStory } from 'components/form-materials/components/dynamic-value-input';\n\n# DynamicValueInput\n\nDynamicValueInput is a dynamic value input component that supports both constant and variable input modes. It can automatically select the appropriate input type based on the provided schema and offers variable selection functionality. The component can intelligently switch between constant input and variable selection.\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/dynamic-value-input.png\" alt=\"DynamicValueInput Component\" style={{ width: '50%' }} />\n</div>\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DynamicValueInput } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### With Schema Constraints\n\n<WithSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DynamicValueInput } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Constant Mode\n\n```tsx\n<DynamicValueInput\n  value={{\n    type: 'constant',\n    content: 'Hello World',\n    schema: { type: 'string' }\n  }}\n  onChange={handleChange}\n/>\n```\n\n### Variable Mode\n\n```tsx\n<DynamicValueInput\n  value={{\n    type: 'ref',\n    content: ['start_0', 'query']\n  }}\n  onChange={handleChange}\n/>\n```\n\n## API Reference\n\n### DynamicValueInput Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `IFlowConstantRefValue` | - | Input value, supports constant or variable reference |\n| `onChange` | `(value?: IFlowConstantRefValue) => void` | - | Callback function when value changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `style` | `React.CSSProperties` | - | Custom styles |\n| `schema` | `IJsonSchema` | - | JSON Schema to constrain input type |\n| `constantProps` | `ConstantInputProps` | - | Additional properties passed to constant input component |\n\n### IFlowConstantRefValue\n\n```typescript\ntype IFlowConstantRefValue =\n  | IFlowConstantValue  // Constant value\n  | IFlowRefValue;     // Variable reference\n\ninterface IFlowConstantValue {\n  type: 'constant';\n  content: any;           // Constant value\n  schema: IJsonSchema;  // Value type definition\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content: string; // Variable path, e.g., \"user.name\"\n}\n```\n\n### Mode Switching\n\nThe component supports intelligent switching between two input modes:\n\n1. **Constant Mode**: Direct value input\n2. **Variable Mode**: Select variables within scope\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/dynamic-value-input\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/dynamic-value-input\n```\n\n### Directory Structure Explanation\n\n```\ndynamic-value-input/\n├── index.tsx           # Main component implementation, containing DynamicValueInput core logic\n├── hooks.ts            # Custom Hooks for handling variable references and schema selection\n├── styles.tsx          # Style definitions using styled-components\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### Variable Reference Handling\nGet variable reference information through the `useRefVariable` Hook:\n\n```typescript\nconst refVariable = useRefVariable(value);\n```\n\n#### Schema Selection Management\nManage type selection through the `useSelectSchema` Hook:\n\n```typescript\nconst [selectSchema, setSelectSchema] = useSelectSchema(\n  schemaFromProps,\n  constantProps,\n  value\n);\n```\n\n#### Mode Switching Logic\nThe component determines whether to render constant input or variable selector by judging `value.type`:\n\n```typescript\nif (value?.type === 'ref') {\n  // Render variable selector\n  return <InjectVariableSelector />;\n} else {\n  // Render constant input\n  return <ConstantInput />;\n}\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/json-schema\n- `JsonSchemaUtils`: JSON Schema utility class\n- `IJsonSchema`: JSON Schema type definition\n- `useTypeManager`: Type manager Hook\n\n#### @flowgram.ai/variable-core\n- `useScopeAvailable`: Get available variables in current scope\n\n#### Internal Components\n- [`InjectVariableSelector`](./variable-selector): Variable selector\n- [`TypeSelector`](./type-selector): Type selector\n- `ConstantInput`: Constant input component\n- `createInjectMaterial`: Create injectable material components\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[DynamicValueInput Component] --> B{Determine Input Mode}\n    B -->|Constant Mode| C[Render Constant Input]\n    B -->|Variable Mode| D[Render Variable Selector]\n\n    C --> E[Select Input Type Based on Schema]\n    D --> F[Display Available Variable List]\n\n    E --> G[User Input Value]\n    F --> H[User Select Variable]\n\n    G --> I[Generate Constant Value]\n    H --> J[Generate Variable Reference]\n\n    I --> K[onChange Callback]\n    J --> K\n\n```"
  },
  {
    "path": "apps/docs/src/en/materials/components/inputs-values-tree.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/inputs-values-tree';\n\n# InputsValuesTree\n\nInputsValuesTree is a component for displaying and editing **tree-structured input values**. Each leaf node is a key-value pair, with values supporting both constant and variable input modes through the DynamicValueInput component. The component uses a tree hierarchy display with expand/collapse functionality, making it suitable for building complex nested data structures.\n\n:::tip{title=\"Difference from InputsValues\"}\n\n- **Structure Difference**: InputsValues only supports flat key-value pairs, while InputsValuesTree supports tree-structured nested data\n- **Display Method**: InputsValues uses a simple list display, while InputsValuesTree uses a tree structure with indentation and expand/collapse functionality\n- **Usage Scenarios**: InputsValues is suitable for simple key-value pair configurations, while InputsValuesTree is suitable for complex multi-level data structure configurations\n\n:::\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValuesTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputs_values\"\n        defaultValue={{\n          a: {\n            b: {\n              type: 'ref',\n              content: ['start_0', 'str'],\n            },\n            c: {\n              type: 'constant',\n              content: 'hello',\n            },\n          },\n          d: {\n            type: 'constant',\n            content: '{ \"a\": \"b\"}',\n            schema: { type: 'object' },\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n| Property | Type | Default Value | Description |\n| :--- | :--- | :--- | :--- |\n| value | `IInputsValues` | - | Tree-structured input value object |\n| onChange | `(value?: IInputsValues) => void` | - | Callback function when value changes |\n| readonly | `boolean` | `false` | Whether in read-only mode |\n| hasError | `boolean` | `false` | Whether to show error state |\n| schema | `IJsonSchema` | - | JSON Schema definition for validation and type hints |\n| style | `React.CSSProperties` | - | Custom styles |\n| constantProps | `{ strategies?: ConstantInputStrategy[]; [key: string]: any }` | - | Configuration properties for constant input component |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/inputs-values-tree\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/inputs-values-tree\n```\n\n### Directory Structure\n\n```plaintext\ncomponents/inputs-values-tree/\n├── index.tsx          # Component entry file\n├── row.tsx            # Tree row component, handles display and editing of individual nodes\n├── types.ts           # Type definitions\n├── icon.tsx           # Icon components\n├── styles.css         # Component styles\n└── hooks/\n    └── use-child-list.tsx # Custom hook for handling child node lists\n```\n\n### Core Implementation\n\nInputsValuesTree component is primarily used for displaying and editing tree-structured input values, supporting two types of values: constant values and reference values.\n\n#### Workflow Sequence Diagram\n\n```mermaid\nsequenceDiagram\n    participant User as User\n    participant Tree as InputsValuesTree\n    participant ObjectList as useObjectList\n    participant Row as InputValueRow\n    participant ChildList as useChildList\n    participant DynamicInput as InjectDynamicValueInput\n\n    User->>Tree: Provide initial tree data\n    Tree->>ObjectList: Initialize top-level data\n    ObjectList-->>Tree: Return list operation methods\n    Tree->>Row: Render each top-level node\n    Row->>ChildList: Check for child nodes\n    ChildList-->>Row: Return child node list and operation methods\n\n    alt User expands node\n        User->>Row: Click expand button\n        Row->>Row: Toggle collapse state\n        Row->>Row: Render child node list\n    end\n\n    alt User adds node\n        User->>Row: Click add button\n        Row->>ObjectList: Call add method\n        ObjectList-->>Tree: Update data\n        Tree-->>User: Trigger onChange callback\n    end\n\n    alt User modifies node value\n        User->>DynamicInput: Edit value\n        DynamicInput->>Row: Call onUpdateValue\n        Row->>ObjectList: Update node value\n        ObjectList-->>Tree: Update data\n        Tree-->>User: Trigger onChange callback\n    end\n```\n\nKey Features:\n\n1. **Tree Structure Display**: Displays nested tree data structures through recursion\n2. **Value Type Support**: Supports both constant values (strings, numbers, booleans, etc.) and reference values that point to other nodes in the workflow\n3. **CRUD Operations**: Supports adding, deleting, and modifying node keys and values\n4. **Configurable**: Customize constant input component behavior through constantProps\n\nThe component internally uses the useObjectList hook to manage object lists and the InputValueRow component to render each row of data. When the user clicks the add button, an empty string-type constant value is added by default.\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): Internationalization utility\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n\n#### Third-party Libraries\n\n[**Semi UI**](https://semi.design/en-US)\n- `Button`: Button component\n- `IconPlus`: Plus icon component\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/inputs-values.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithSchemaStory } from 'components/form-materials/components/inputs-values';\n\n# InputsValues\n\nInputsValues is a key-value pair input list component used to collect and manage a set of input parameters. Each key-value pair supports both constant and variable input modes, implemented through the DynamicValueInput component for flexible input methods.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### With Schema Constraints\n\n<WithSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValues\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{\n              type: 'string',\n            }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Custom Constant Input Strategy\nYou can customize the input behavior of each value through `constantProps`:\n\n```typescript\nconst customStrategies = [\n  {\n    type: 'string',\n    render: (props) => <CustomStringInput {...props} />\n  },\n  {\n    type: 'number',\n    render: (props) => <CustomNumberInput {...props} />\n  }\n];\n\n<InputsValues\n  constantProps={{\n    strategies: customStrategies\n  }}\n/>\n```\n\n## API Reference\n\n### InputsValues Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `Record<string, IFlowValue \\| undefined>` | - | Key-value pair data |\n| `onChange` | `(value?: Record<string, IFlowValue \\| undefined>) => void` | - | Callback function when data changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `style` | `React.CSSProperties` | - | Custom styles |\n| `schema` | `IJsonSchema` | - | JSON Schema to constrain all value types |\n| `constantProps` | `ConstantInputProps` | - | Additional properties passed to DynamicValueInput |\n\n### Data Structure\n\n```typescript\ninterface PropsType {\n  value?: Record<string, IFlowValue | undefined>;\n  onChange: (value?: Record<string, IFlowValue | undefined>) => void;\n  // ... other properties\n}\n\ntype IFlowValue =\n  | IFlowConstantValue  // Constant value\n  | IFlowRefValue;     // Variable reference\n\ninterface IFlowConstantValue {\n  type: 'constant';\n  content: any;           // Constant value\n  schema: IJsonSchema;  // Value type definition\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content: string; // Variable path, e.g., \"user.name\"\n}\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/inputs-values\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/inputs-values\n```\n\n### Directory Structure Explanation\n\n```\ninputs-values/\n├── index.tsx           # Main component implementation, containing InputsValues core logic\n├── types.ts            # Type definitions\n├── styles.tsx          # Style definitions using styled-components\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### Key-Value Pair Management\nUse the `useObjectList` Hook to manage the key-value pair list:\n\n```typescript\nconst { list, updateKey, updateValue, remove, add } = useObjectList<IFlowValue | undefined>({\n  value,\n  onChange,\n  sortIndexKey: 'extra.index',\n});\n```\n\n#### Dynamic Value Input Integration\nEach value uses the `InjectDynamicValueInput` component for input:\n\n```typescript\n<InjectDynamicValueInput\n  value={item.value as IFlowConstantRefValue}\n  onChange={(v) => updateValue(item.id, v)}\n  schema={schema}\n  constantProps={constantProps}\n/>\n```\n\n#### Key Name Input\nUse the `BlurInput` component to implement key name input and validation:\n\n```typescript\n<BlurInput\n  value={item.key}\n  onChange={(v) => updateKey(item.id, v)}\n  placeholder={I18n.t('Input Key')}\n/>\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/i18n\n- `I18n`: Internationalization support\n\n#### Internal Components\n- `InjectDynamicValueInput`: Dynamic value input component\n- `BlurInput`: Blur input component\n- `useObjectList`: Object list management Hook\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[InputsValues Component] --> B[Render Key-Value Pair List]\n    B --> C[Each Key-Value Pair]\n    C --> D[Key Name Input Box]\n    C --> E[Value Input Component]\n    C --> F[Delete Button]\n\n    D --> G[User Input Key Name]\n    E --> H[User Input Value]\n    F --> I[User Delete Key-Value Pair]\n```"
  },
  {
    "path": "apps/docs/src/en/materials/components/json-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-editor-with-variables';\n\n# JsonEditorWithVariables\n\nJsonEditorWithVariables is an enhanced JSON editor that supports inserting variable references into JSON. Built on JsonCodeEditor, it integrates variable selectors and variable tag injection functionality, allowing users to reference variables in JSON strings using the `{{variable}}` syntax.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"json_editor_with_variables\" defaultValue={`{ \"a\": {{start_0.str}} }`}>\n        {({ field }) => (\n          <JsonEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### JSON with Variables Example\n\n```json\n{\n  \"user_info\": {\n    \"name\": \"{{start_0.name}}\",\n    \"email\": \"{{start_0.email}}\",\n  }\n}\n```\n\n### Variable Insertion\n\nEnter the `@` character in the editor to trigger the variable selector.\n\nAfter entering `@`, a list of available variables will be displayed. Selecting a variable will automatically insert it in the `{{variable.name}}` format.\n\n## API Reference\n\n### JsonEditorWithVariables Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `string` | - | JSON string content |\n| `onChange` | `(value: string) => void` | - | Callback function when content changes |\n| `theme` | `'dark' \\| 'light'` | `'light'` | Editor theme |\n| `placeholder` | `string` | - | Placeholder text |\n| `activeLinePlaceholder` | `string` | `'Press @ to select variable'` | Current line placeholder hint |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `options` | `Options` | - | CodeMirror configuration options |\n\n### Variable Syntax\n\nUse double curly brace syntax to reference variables in JSON strings:\n\n```json\n{\n  \"key\": \"{{variable.path}}\"\n}\n```\n\nSupported variable formats:\n- `{{start_0.name}}` - Simple variable\n- `{{start_0.address.city}}` - Nested property\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-editor-with-variables\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-editor-with-variables\n```\n\n### Directory Structure Explanation\n\n```\njson-editor-with-variables/\n├── index.tsx           # Lazy loading export file\n├── editor.tsx          # Main component implementation\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### Variable Syntax Parsing\nParse variable references in JSON through regular expressions:\n\n```typescript\nconst matches = findAllMatches(originalSource, /\\{\\{([^\\}]*)\\}\\}/g);\n```\n\n#### Variable Selector Integration\nIntegrate `EditorVariableTree` and `EditorVariableTagInject` extensions:\n\n```typescript\n<EditorVariableTree triggerCharacters={TRIGGER_CHARACTERS} />\n<EditorVariableTagInject />\n```\n\n#### Trigger Character\nUse `@` as the trigger character for variable selection:\n\n```typescript\nconst TRIGGER_CHARACTERS = ['@'];\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/coze-editor\n- `JsonCodeEditor`: JSON code editor\n- `transformerCreator`: Syntax transformer creator\n- `EditorVariableTree`: Variable tree selector\n- `EditorVariableTagInject`: Variable tag injector\n- `Text`: Text processing tool\n\n#### @flowgram.ai/i18n\n- `I18n`: Internationalization support\n\n#### coze-editor-extensions Material\n- `EditorVariableTree`: Variable tree selection trigger\n- `EditorVariableTagInject`: Variable tag display\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[JsonEditorWithVariables Component] --> B[Render JsonCodeEditor]\n    B --> C[Load JSON Syntax Highlighting]\n    B --> D[Integrate Variable Extensions]\n\n    D --> E[EditorVariableTree]\n    D --> F[EditorVariableTagInject]\n\n    E --> G[Listen for @ Trigger]\n    F --> H[Process Variable Tags]\n\n    G --> I[Display Variable List]\n    I --> J[Select and Insert Variable]\n\n    H --> L[Render Variable Tag Styles]\n\n    J --> M[Update Editor Content]\n```\n\n#### Custom Trigger Characters\nCan extend support for more trigger characters:\n\n```typescript\nconst CUSTOM_TRIGGERS = ['@', '#', '$'];\n\n<JsonCodeEditor\n  options={{\n    transformer: customTransformer,\n  }}\n>\n  <EditorVariableTree triggerCharacters={CUSTOM_TRIGGERS} />\n</JsonCodeEditor>\n```"
  },
  {
    "path": "apps/docs/src/en/materials/components/json-schema-creator.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-schema-creator';\n\n# JsonSchemaCreator\n\nJsonSchemaCreator is a component that automatically generates JSON Schema from JSON strings. It provides a button to trigger a modal where users can paste JSON data, and the component automatically analyzes the data structure and generates the corresponding JSON Schema.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaCreator } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined> name=\"json_schema\">\n        {({ field }) => (\n          <div>\n            <JsonSchemaCreator\n              onSchemaCreate={(schema) => field.onChange(schema)}\n            />\n            <div style={{ marginTop: 16 }}>\n              <JsonSchemaEditor\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            </div>\n          </div>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### JsonSchemaCreator Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `onSchemaCreate` | `(schema: IJsonSchema) => void` | - | Callback function after schema generation |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-schema-creator\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-creator\n```\n\n### Directory Structure\n\n```\njson-schema-creator/\n├── index.tsx              # Main component export, contains JsonSchemaCreator and JsonSchemaCreatorProps\n├── json-schema-creator.tsx # Main component implementation, contains button and state management\n├── json-input-modal.tsx   # JSON input modal component\n└── utils/\n    └── json-to-schema.ts  # Core utility function for JSON to Schema conversion\n```\n\n### Core Implementation\n\n#### JSON Parsing and Schema Generation Process\n\nThe following is the complete interaction sequence diagram for JSON Schema creation:\n\n```mermaid\nsequenceDiagram\n    participant User as User\n    participant Creator as JsonSchemaCreator\n    participant Modal as JsonInputModal\n    participant Parser as jsonToSchema\n    participant Generator as generateSchema\n\n    %% Initial interaction\n    User->>Creator: Click \"Create from JSON\" button\n    Creator->>Creator: setVisible(true)\n    Creator->>Modal: Show modal\n\n    %% User input and confirmation\n    User->>Modal: Input JSON string\n    User->>Modal: Click confirm\n\n    %% JSON parsing and Schema generation\n    Modal->>Parser: Call jsonToSchema(jsonString)\n    Parser->>Parser: JSON.parse(jsonString)\n\n    %% Recursive Schema generation\n    Parser->>Generator: Call generateSchema(data)\n\n    %% Process based on data type\n    alt Data is null\n        Generator->>Generator: Return {type: 'string'}\n    else Data is array\n        Generator->>Generator: schema = {type: 'array'}\n        loop For each array element\n            Generator->>Generator: Recursively call generateSchema(item)\n        end\n    else Data is object\n        Generator->>Generator: schema = {type: 'object', properties: {}, required: []}\n        loop For each property\n            Generator->>Generator: Recursively call generateSchema(value)\n            Generator->>Generator: Add to properties and required\n        end\n    else Primitive type\n        Generator->>Generator: Return {type: typeof value}\n    end\n\n    %% Return result\n    Generator-->>Parser: Return generated schema\n    Parser-->>Modal: Return IJsonSchema\n\n    %% Callback and close\n    Modal->>Creator: Call onSchemaCreate(schema)\n    Creator->>Creator: setVisible(false)\n    Creator->>Modal: Close modal\n    Creator-->>User: Schema creation completed\n```\n\n### FlowGram APIs Used\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema type definition\n\n### Other Components Dependencies\n\n[**JsonCodeEditor**](./code-editor) Code editor component\n- Used for editing JSON data in the modal\n\n### Third-party Libraries Used\n\n[**@douyinfe/semi-ui**](https://semi.design/)\n- `Button`: Button component to trigger modal\n- `Modal`: Modal container\n- `Typography`: Text component"
  },
  {
    "path": "apps/docs/src/en/materials/components/json-schema-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-schema-editor';\n\n# JsonSchemaEditor\n\nJsonSchemaEditor is a visual JSON Schema editor that supports creating and editing complex JSON Schema structures. It provides a tree-structured interface to intuitively define properties of various types such as objects, arrays, strings, and numbers, supporting nested structures and required field marking.\n\nYou can learn about the JsonSchema protocol through the following documentation:\n\n- [Json Schema Official Site](https://json-schema.org/learn)\n- [Json Schema Specification](https://json-schema.org/specification)\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema> name=\"json_schema\" defaultValue={{ type: 'object' }}>\n        {({ field }) => (\n          <JsonSchemaEditor\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### JsonSchemaEditor Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `IJsonSchema` | `{ type: 'object' }` | JSON Schema object |\n| `onChange` | `(value: IJsonSchema) => void` | - | Callback function when schema changes |\n| `config` | `ConfigType` | `{}` | Editor configuration options |\n| `className` | `string` | - | Custom style class name |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n\n### ConfigType\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `placeholder` | `string` | `'Enter variable name'` | Property name placeholder |\n| `descTitle` | `string` | `'Description'` | Description field title |\n| `descPlaceholder` | `string` | `'Help LLM understand this property'` | Description field placeholder |\n| `defaultValueTitle` | `string` | `'Default Value'` | Default value field title |\n| `defaultValuePlaceholder` | `string` | `'Default value'` | Default value placeholder |\n| `addButtonText` | `string` | `'Add'` | Add button text |\n\n### Supported Types\n\n| Type | Description | Example |\n|------|-------------|---------|\n| `string` | String type | `\"hello\"` |\n| `number` | Number type | `42` |\n| `boolean` | Boolean type | `true` |\n| `object` | Object type | `{}` |\n| `array` | Array type | `[]` |\n| `null` | Null type | `null` |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-schema-editor\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n```\n\n### Directory Structure Explanation\n\n```\njson-schema-editor/\n├── index.tsx           # Main component implementation\n├── types.ts            # Type definitions\n├── hooks.ts            # State management hooks\n├── styles.ts           # Style components\n├── default-value.tsx   # Default value editor\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### Tree Structure Management\nThe component uses recursive `PropertyEdit` components to render nested Schema structures:\n\n```typescript\nfunction PropertyEdit(props: {\n  value?: PropertyValueType;\n  config?: ConfigType;\n  onChange?: (value: PropertyValueType) => void;\n  onRemove?: () => void;\n  readonly?: boolean;\n  $level?: number;\n  $isLast?: boolean;\n})\n```\n\n#### Property Edit State Management\nUse the `usePropertiesEdit` hook to manage Schema CRUD operations:\n\n```typescript\nconst {\n  propertyList,\n  onAddProperty,\n  onRemoveProperty,\n  onEditProperty\n} = usePropertiesEdit(value, onChangeProps);\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/json-schema\n- `IJsonSchema`: JSON Schema type definition\n\n#### @flowgram.ai/i18n\n- `I18n`: Internationalization support\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[JsonSchemaEditor] --> B[Render Root Property]\n    B --> C[PropertyEdit Component]\n    C --> D[Property Name Input]\n    C --> E[Type Selection]\n    C --> F[Required Mark]\n    C --> G[Action Buttons]\n\n    G --> H[Expand/Collapse Details]\n    G --> I[Add Child Property]\n    G --> J[Delete Property]\n\n    H --> K[Description Input]\n    H --> L[Default Value Setting]\n\n    I --> M[Recursively Render Child Properties]\n    M --> C\n```\n\n### Advanced Features\n\n#### Nested Structure Support\nSupports unlimited levels of nested objects and arrays:\n\n```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"level1\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"level2\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"level3\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n#### Array Element Definition\nSupports defining Schema for array elements:\n\n```json\n{\n  \"type\": \"array\",\n  \"items\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"id\": { \"type\": \"number\" },\n      \"name\": { \"type\": \"string\" }\n    }\n  }\n}\n```\n\n#### Required Field Management\nSupports marking fields as required and automatically updating the `required` array:\n\n```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": { \"type\": \"string\" },\n    \"email\": { \"type\": \"string\" }\n  },\n  \"required\": [\"email\"]\n}\n```\n\n### Use Cases\n\n- **API Documentation Generation**: Create request/response Schema for REST APIs\n- **Form Validation**: Define validation rules for form fields\n- **Data Modeling**: Create data structure models\n- **Configuration Management**: Define configuration file structures\n- **Code Generation**: Provide Schema input for code generators"
  },
  {
    "path": "apps/docs/src/en/materials/components/prompt-editor-with-inputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithoutCanvas } from 'components/form-materials/components/prompt-editor-with-inputs';\n\n# PromptEditorWithInputs\n\nPromptEditorWithInputs is an enhanced prompt editor that integrates input variable management functionality. Built on PromptEditor, it additionally provides a tree-structured variable selector for inputs, enabling users to conveniently reference and manage input variables in prompt templates.\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithInputs } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<IInputsValues | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: { type: 'constant', content: '123' },\n          b: { type: 'ref', content: ['start_0', 'obj'] },\n        }}\n      >\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<IInputsValues | undefined> name=\"inputsValues\">\n        {({ field: inputsField }) => (\n          <Field<IFlowTemplateValue | undefined>\n            name=\"prompt_editor_with_inputs\"\n            defaultValue={{\n              type: 'template',\n              content: '# Query \\n {{b.obj2.num}}',\n            }}\n          >\n            {({ field }) => (\n              <PromptEditorWithInputs\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                inputsValues={inputsField.value || {}}\n              />\n            )}\n          </Field>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Inputs Insertion\n\nEnter the `@`, `{` characters in the editor to trigger the Inputs selector.\n\nAfter entering `@`, `{`, a list of available variables will be displayed. Selecting a variable will automatically insert it in the `{{inputs.path}}` format.\n\n### Usage Without Canvas\n\n:::warning\n\nWhen used without canvas, due to inability to access variables, inputsValues does not support value definitions of type: 'ref'.\n\n:::\n\n<WithoutCanvas />\n\n```tsx pure title=\"with-canvas.tsx\"\nexport const WithoutCanvas = () => {\n  const [value, setValue] = useState<IFlowTemplateValue | undefined>({\n    type: 'template',\n    content: '# Role \\n You are a helpful assistant. \\n\\n # Query \\n {{b.obj2.num}} \\n\\n',\n  });\n\n  return (\n    <div>\n      <PromptEditorWithInputs\n        value={value}\n        onChange={(value) => setValue(value)}\n        inputsValues={{\n          a: { type: 'constant', content: '123' },\n          b: {\n            c: {\n              d: { type: 'constant', content: 456 },\n            },\n            e: { type: 'constant', content: 789 },\n          },\n        }}\n      />\n    </div>\n  );\n};\n```\n\n\n\n## API Reference\n\n### PromptEditorWithInputs Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `{ type: 'template', content: string }` | - | Prompt template content |\n| `inputsValues` | `IInputsValues` | `{}` | Input variable key-value pairs |\n| `onChange` | `(value: { type: 'template', content: string }) => void` | - | Callback function when content changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `placeholder` | `string` | - | Placeholder text |\n| `activeLinePlaceholder` | `string` | - | Current line placeholder hint |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `disableMarkdownHighlight` | `boolean` | `false` | Whether to disable Markdown highlighting |\n| `options` | `Options` | - | CodeMirror configuration options |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor-with-inputs\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor-with-inputs\n```\n\n### Directory Structure Explanation\n\n```\nprompt-editor-with-inputs/\n└──  index.tsx          # Main component implementation\n```\n\n### Core Implementation Explanation\n\n#### Input Variable Integration\n\nPromptEditorWithInputs extends the basic [PromptEditor](./prompt-editor) and adds node inputs reference functionality based on [coze-editor-extensions](./coze-editor-extensions).\n\n### Dependencies\n\n[**PromptEditor**](./prompt-editor)\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorInputsTree`: Input tree selection trigger\n\n[**FlowValue**](../common/flow-value)\n- `IInputsValues`: Input variable type definition\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/prompt-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, StringOnlyStory } from 'components/form-materials/components/prompt-editor-with-variables';\n\n# PromptEditorWithVariables\n\nPromptEditorWithVariables is an enhanced prompt editor that integrates variable management functionality. Built on PromptEditor, it provides a variable tree selector and variable tag injection, enabling users to conveniently reference and manage variables in prompt templates.\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/prompt-editor-with-variables.png\" alt=\"Component\" style={{ width: '50%' }} />\n  *Prompts in LLM_3 and LLM_4 reference loop batch processing variables*\n</div>\n\n## Demo\n\n### Basic Usage\n\n:::tip{title=\"Variable Insertion\"}\n\nEnter the `@`, `{` characters in the editor to trigger the variable selector.\n\nAfter entering `@`, `{`, a list of available variables will be displayed. Selecting a variable will automatically insert it in the `{{variable.path}}` format.\n\n:::\n\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any> name=\"prompt_template\" defaultValue={{\n              type: 'template',\n              content: `# Role\nYou are a helpful assistant\n\n# Query\n{{start_0.str}}`,\n            }}>\n        {({ field }) => (\n          <PromptEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### String Only Variables\n\n<StringOnlyStory />\n\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithVariables, VariableSelectorProvider } from '@flowgram.ai/form-materials';\n\nconst STRING_ONLY_SCHEMA = { type: 'string' };\n\nconst formMeta = {\n  render: () => (\n    <VariableSelectorProvider includeSchema={STRING_ONLY_SCHEMA}>\n      <Field<any> name=\"prompt_template\">\n        {({ field }) => (\n          <PromptEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </VariableSelectorProvider>\n  ),\n}\n```\n\n## API Reference\n\n### PromptEditorWithVariables Props\n\n| Property | Type | Default | Description |\n|--------|------|--------|------|\n| `value` | `{ type: 'template', content: string }` | - | Prompt template content |\n| `onChange` | `(value: { type: 'template', content: string }) => void` | - | Callback function when content changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `placeholder` | `string` | - | Placeholder text |\n| `activeLinePlaceholder` | `string` | - | Current line placeholder hint |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `disableMarkdownHighlight` | `boolean` | `false` | Whether to disable Markdown highlighting |\n| `options` | `Options` | - | CodeMirror configuration options |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor-with-variables\n```\n\n### Directory Structure Explanation\n\n```\nprompt-editor-with-variables/\n└── index.tsx           # Main component implementation\n```\n\n### Core Implementation Explanation\n\n#### Variable Capability Integration\n\nPromptEditorWithVariables extends the basic [PromptEditor](./prompt-editor) and adds variable reference and tag display functionality based on [CozeEditorExtensions](./coze-editor-extensions).\n\n### Dependent Materials\n\n[**PromptEditor**](./prompt-editor)\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorVariableTree`: Variable tree selection trigger\n- `EditorVariableTagInject`: Variable Tag display\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/prompt-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/prompt-editor';\n\n# PromptEditor\n\nPromptEditor is a professional prompt editor component built on Coze Editor, supporting Markdown syntax highlighting, Jinja template syntax highlighting, and other features, suitable for creating and editing AI prompt templates.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any | undefined>\n        name=\"prompt_editor\"\n        defaultValue={{\n          type: 'template',\n          content: '# Role\\n You are a helpful assistant',\n        }}\n      >\n        {({ field }) => (\n          <PromptEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n| Property | Type | Default Value | Description |\n| :--- | :--- | :--- | :--- |\n| value | `{ type: 'template'; content: string }` | - | Editor value object |\n| onChange | `(value: { type: 'template'; content: string }) => void` | - | Callback function when value changes |\n| readonly | `boolean` | `false` | Whether in read-only mode |\n| placeholder | `string` | - | Placeholder text |\n| activeLinePlaceholder | `React.ReactNode` | - | Active line placeholder |\n| style | `React.CSSProperties` | - | Custom styles |\n| hasError | `boolean` | `false` | Whether to show error state |\n| disableMarkdownHighlight | `boolean` | `false` | Whether to disable Markdown highlighting |\n| options | `Partial<InferValues<Preset[number]>>` | - | Additional editor options |\n| children | `React.ReactNode` | - | Child components |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor\n```\n\n### Directory Structure\n\n```plaintext\ncomponents/prompt-editor/\n├── index.tsx          # Component entry file\n├── editor.tsx         # Editor core implementation\n├── types.tsx          # Type definitions\n├── styles.css         # Component styles\n└── extensions/        # Editor extensions\n    ├── jinja.tsx      # Jinja template syntax highlighting\n    ├── markdown.tsx   # Markdown syntax highlighting\n    └── language-support.tsx # Language support configuration\n```\n\n### Core Implementation\n\nThe PromptEditor component is built on @flowgram.ai/coze-editor and provides professional prompt editing functionality. Key features include:\n\n1. **Markdown Syntax Highlighting**: Supports syntax highlighting for Markdown elements such as headings, italics, bold, lists, etc.\n2. **Jinja Template Syntax Highlighting**: Supports syntax highlighting for Jinja template engine, facilitating the creation of dynamic prompts\n3. **Responsive Updates**: Listens for external value changes and synchronizes to the editor\n4. **Customizability**: Supports multiple configuration options such as custom styles and placeholders\n\nThe component internally uses lazy loading to optimize performance and provides core editor functionality through EditorProvider and Renderer components.\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/coze-editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/coze-editor)\n- `Renderer`: Editor rendering component\n- `EditorProvider`: Editor context provider\n- `ActiveLinePlaceholder`: Active line placeholder component\n- `preset-prompt`: Prompt editor preset plugin\n\n#### Third-party Libraries\n\n[**CodeMirror**](https://codemirror.net/)\n- Provides underlying editor functionality support\n\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/sql-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/sql-editor-with-variables';\n\n# SQLEditorWithVariables\n\nSQLEditorWithVariables is an enhanced SQL editor that supports inserting variable references in SQL. It is built on top of SQLCodeEditor and integrates a variable selector and variable tag injection, allowing users to reference variables using the `{{variable}}` syntax in SQL strings.\n\n## Examples\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { SQLEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string | undefined>\n        name=\"sql_editor_with_variables\"\n        defaultValue=\"SELECT * FROM users WHERE user_id = {{start_0.str}}\"\n      >\n        {({ field }) => (\n          <SQLEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### SQL Example with Variables\n\n```sql\nSELECT\n  id, name, email\nFROM users\nWHERE id = {{start_0.user_id}}\n  AND email LIKE '%{{start_0.domain}}'\nLIMIT 10;\n```\n\n### Variable Insertion\n\nTyping `@` or `{` in the editor can trigger the variable selector.\n\nAfter typing `@` or `{`, a list of available variables will be displayed. Selecting a variable will automatically insert it in the `{{variable.name}}` format.\n\n## API Reference\n\n### SQLEditorWithVariables Props\n\n| Property | Type | Default | Description |\n|---|---|---|---|\n| `value` | `string` | - | The content of the SQL string |\n| `onChange` | `(value: string) => void` | - | Callback function when the content changes |\n| `theme` | `'dark' \\| 'light'` | `'light'` | The theme of the editor |\n| `placeholder` | `string` | - | Placeholder text |\n| `activeLinePlaceholder` | `string` | `'Type @ to select a variable'` | Placeholder hint for the current line |\n| `readonly` | `boolean` | `false` | Whether it is in read-only mode |\n| `options` | `Options` | - | CodeMirror configuration options |\n\n### Variable Syntax\n\nUse double curly braces to reference variables in the SQL string:\n\n```sql\nSELECT * FROM orders WHERE user_id = {{variable.path}}\n```\n\nSupported variable formats:\n- `{{start_0.name}}` - Simple variable\n- `{{start_0.address.city}}` - Nested property\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/sql-editor-with-variables\"\n/>\n\nYou can copy the source code locally using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/sql-editor-with-variables\n```\n\n### Directory Structure\n\n```\nsql-editor-with-variables/\n├── index.tsx           # Lazy-loaded export file\n├── editor.tsx          # Main component implementation\n└── README.md          # Component documentation\n```\n\n### Core Implementation\n\n#### Variable Selector Integration\nIntegrate the `EditorVariableTree` and `EditorVariableTagInject` extensions:\n\n```typescript\n<EditorVariableTree />\n<EditorVariableTagInject />\n```\n\n#### Trigger Characters\nBy default, `@` is used as the trigger character for variable selection. If you need to customize the trigger characters, you can extend it using the underlying `SQLCodeEditor`:\n\n```typescript\nimport { SQLCodeEditor } from '@flowgram.ai/form-materials';\n\n<SQLCodeEditor>\n  <EditorVariableTree triggerCharacters={[\"@\", \"#\", \"$\"]} />\n  <EditorVariableTagInject />\n</SQLCodeEditor>\n```\n\n### Used Flowgram APIs\n\n#### @flowgram.ai/coze-editor\n- `SQLCodeEditor`: SQL code editor\n- `EditorVariableTree`: Variable tree selector\n- `EditorVariableTagInject`: Variable tag injector\n\n#### @flowgram.ai/i18n\n- `I18n`: Internationalization support\n\n#### coze-editor-extensions Materials\n- `EditorVariableTree`: Trigger for variable tree selection\n- `EditorVariableTagInject`: Display for variable tags\n\n### Overall Flow\n\n```mermaid\ngraph TD\n    A[SQLEditorWithVariables Component] --> B[Render SQLCodeEditor]\n    B --> C[Load SQL syntax highlighting]\n    B --> D[Integrate variable extensions]\n\n    D --> E[EditorVariableTree]\n    D --> F[EditorVariableTagInject]\n\n    E --> G[Listen for @ trigger]\n    F --> H[Process variable tags]\n\n    G --> I[Display variable list]\n    I --> J[Select and insert variable]\n\n    H --> L[Render variable tag style]\n\n    J --> M[Update editor content]\n```\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/type-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/type-selector';\n\n# TypeSelector\n\nTypeSelector is a type selector component used for selecting JSON Schema types in forms. It supports both basic types and composite types (such as array types).\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/type-selector.png\" alt=\"TypeSelector Component\" style={{ width: '50%' }} />\n</div>\n\n## Demo\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { TypeSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Partial<IJsonSchema> | undefined> name=\"type_selector\" defaultValue={{ type: 'string' }}>\n        {({ field }) => (\n          <TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### TypeSelector Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `Partial<IJsonSchema>` | - | Selected type value, conforms to JSON Schema format |\n| `onChange` | `(value?: Partial<IJsonSchema>) => void` | - | Callback function when type selection changes |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `disabled` | `boolean` | `false` | Whether it's disabled (deprecated, use readonly instead) |\n| `style` | `React.CSSProperties` | - | Custom styles |\n\n### Type Format Description\n\nTypeSelector supports the following JSON Schema type formats:\n\n- **Basic types**: `{ type: 'string' }`, `{ type: 'number' }`, `{ type: 'boolean' }`, etc.\n- **Array types**: `{ type: 'array', items: { type: 'string' } }`\n- **Nested arrays**: `{ type: 'array', items: { type: 'array', items: { type: 'string' } } }`\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/type-selector\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/type-selector\n```\n\n### Directory Structure Explanation\n\n```\ntype-selector/\n├── index.tsx           # Main component implementation, contains TypeSelector core logic\n└── README.md          # Component documentation\n```\n\n### Core Implementation Explanation\n\n#### getTypeSelectValue\nConverts JSON Schema object to array format required by Cascader component:\n\n```typescript\n// Input: { type: 'array', items: { type: 'string' } }\n// Output: ['array', 'string']\n```\n\n#### parseTypeSelectValue\nConverts Cascader component's array value back to JSON Schema object:\n\n```typescript\n// Input: ['array', 'string']\n// Output: { type: 'array', items: { type: 'string' } }\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/json-schema\n- `useTypeManager()`: Get type manager for handling JSON Schema type display and validation\n- `IJsonSchema`: JSON Schema type definition\n- `JsonSchemaTypeManager`: Type manager class, provides type registration, icon display, and other features\n\n### Overall Process\n\n```mermaid\ngraph LR\n    A[TypeSelector Component] --> B[useTypeManager Hook]\n    B --> C[Get type registry]\n    C --> D[Generate Cascader options]\n    D --> E[Render cascader selector]\n    E --> F[User selects type]\n    F --> G[onChange callback]\n    G --> H[Update JSON Schema value]\n```\n"
  },
  {
    "path": "apps/docs/src/en/materials/components/variable-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, FilterSchemaStory, CustomFilterStory, CustomVariableTreeStory } from 'components/form-materials/components/variable-selector';\n\n# VariableSelector\n\nVariableSelector is a component for selecting variables in the current scope, with filtering capabilities based on variable types.\n\n:::warning\n\nIn the variable tree of VariableSelector, **every leaf node and non-leaf node is a variable**.\n\nIn the official material library design, each node outputs **an ObjectType variable declaration** where:\n\n- The **metadata of the variable includes the node's title and node's Icon**, which is parsed and displayed by VariableSelector\n- The drill-down fields of the variable are **recursively displayed within the node variable** by VariableSelector\n\n:::\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/variable-selector.png\" alt=\"VariableSelector Component\" style={{ width: '50%' }} />\n</div>\n\n## Demo\n\n### Direct Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Filter Variable Types\n\n<FilterSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            includeSchema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Custom Filter Logic\n\n<CustomFilterStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </VariableSelectorProvider>\n  ),\n}\n```\n\n### Get Variable Tree via useVariableTree\n\n<CustomVariableTreeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { useVariableTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => {\n    const treeData = useVariableTree({});\n\n    return (\n      <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n        <FormHeader />\n        <Tree treeData={treeData} defaultExpandAll />\n      </VariableSelectorProvider>\n    );\n  },\n}\n```\n\n:::tip\n\n`useVariableTree` is also used in components like [`PromptEditorWithVariables`](./prompt-editor-with-variables), [`SQLEditorWithVariables`](./sql-editor-with-variables), [`JsonEditorWithVariables`](./json-editor-with-variables) to get the variable tree of the current scope.\n\n:::\n\n## API Reference\n\n### VariableSelector Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `value` | `string[]` | - | Selected variable path array |\n| `onChange` | `(value?: string[]) => void` | - | Callback function when variable selection changes |\n| `config` | `VariableSelectorConfig` | `{}` | Configuration object |\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type inclusion filter conditions |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type exclusion filter conditions |\n| `readonly` | `boolean` | `false` | Whether it's read-only mode |\n| `hasError` | `boolean` | `false` | Whether to display error state |\n| `style` | `React.CSSProperties` | - | Custom styles |\n| `triggerRender` | `(props: TriggerRenderProps) => React.ReactNode` | - | Custom trigger renderer |\n\n### VariableSelectorConfig\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `placeholder` | `string` | `'Select variable'` | Placeholder text |\n| `notFoundContent` | `string` | `'Not defined'` | Content displayed when variable is not found |\n\n### VariableSelectorProvider Props\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `skipVariable` | `(variable?: BaseVariableField) => boolean` | - | Custom variable filter function |\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type inclusion filter conditions |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type exclusion filter conditions |\n| `children` | `React.ReactNode` | - | Child components |\n\n### useVariableTree\n\n| Property | Type | Default | Description |\n|----------|------|---------|-------------|\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type inclusion filter conditions |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | Variable type exclusion filter conditions |\n| `skipVariable` | `(variable?: BaseVariableField) => boolean` | - | Custom variable filter function |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/variable-selector\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/variable-selector\n```\n\n### Directory Structure Explanation\n\n```\nvariable-selector/\n├── index.tsx           # Main component implementation, contains VariableSelector core logic\n├── context.tsx         # Provides `VariableSelectorContext` for global variable filtering configuration\n├── use-variable-tree.tsx # Custom Hook for processing variable tree data transformation and filtering\n└── styles.css          # Style file\n```\n\n### Overall Process\n\n```mermaid\ngraph TD\n    A[VariableSelector Component] ---> useVariableTree\n\n    K[VariableSelectorProvider] --> L[Global filter configuration]\n    subgraph useVariableTree\n      C[useAvailableVariables] --> D[Get all available variables]\n      D --> F[Apply filter conditions]\n      F --> G[Generate tree structure]\n      M[includeSchema/excludeSchema] --> F\n    end\n\n    G --> H[Render TreeSelect]\n    L --> F\n```\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/variable-core\n- `useAvailableVariables()`: Get all available variables in the current scope\n- `BaseVariableField`: Basic variable field type, includes variable key, type, metadata, etc.\n\n#### @flowgram.ai/json-schema\n- `useTypeManager()`: Get type manager for handling variable type display and validation\n- `IJsonSchema`: JSON Schema type definition for variable type validation\n- `JsonSchemaUtils`: JSON Schema utility class, provides type matching and conversion functions\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/_meta.json",
    "content": "[\n  \"auto-rename-ref\",\n  \"listen-ref-schema-change\",\n    \"listen-ref-value-change\",\n    \"provide-batch-input\",\n    \"provide-json-schema-outputs\",\n    \"sync-variable-title\",\n    \"validate-when-variable-sync\"\n  ]\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/auto-rename-ref.mdx",
    "content": "# autoRenameRef\n\nautoRenameRef is an automatic reference renaming effect that automatically updates all reference values and template values referencing that field when the key name of a form field changes.\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/auto-rename-ref.gif\" alt=\"autoRenameRef Effect\" style={{ width: '80%' }} />\n  *When the query variable name changes, automatically rename references in downstream inputs*\n</div>\n\n## Demo\n\nimport { BasicStory } from 'components/form-materials/effects/auto-rename-ref';\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effects: {\n    \"inputsValues\": autoRenameRefEffect,\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"inputsValues\">\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### Usage in Complex Forms\n\n```tsx pure title=\"form-meta.tsx\"\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effects: {\n    // Apply automatic renaming effect to multiple fields\n    \"inputsValues\": autoRenameRefEffect,\n    \"outputsValues\": autoRenameRefEffect,\n    \"config.data\": autoRenameRefEffect,\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"inputsValues\"> {/* inputsValues implementation */} </Field>\n      <Field<any> name=\"outputsValues\"> {/* outputsValues implementation */} </Field>\n      <Field<any> name=\"config.data\"> {/* config.data implementation */} </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### Supported Value Types\n\nThe automatic renaming of autoRenameEffect is only triggered when the data under the specified key meets the following conditions:\n\n- **Reference Value (ref)**: `{ type: 'ref', content: ['field', 'path'] }`\n- **Template Value (template)**: `{ type: 'template', content: 'Hello {{user.name}}' }`\n- **Complex Structure Containing Reference and Template Values**:\n  ```json\n  {\n    \"a\": {\n      \"type\": \"ref\",\n      \"content\": [\"start_0\", \"str\"]\n    },\n    \"b\": {\n      \"c\": {\n        \"type\": \"template\",\n        \"content\": \"Hello {{a}}\"\n      }\n    }\n  }\n  ```\n\n## Source Code Guide\n\nimport { SourceCode } from '@theme';\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram/tree/main/packages/materials/form-materials/src/effects/auto-rename-ref\"\n/>\n\nCopy source code locally using CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/auto-rename-ref\n```\n\n### Core Process\n\n1. **Listen to Rename Events**: Listen for field key name changes through `VariableFieldKeyRenameService`\n2. **Traverse Reference Values**: Find all reference values and template values\n3. **Match Key Paths**: Check if the reference path matches the path before the change\n4. **Update References**: Update matching reference paths to the new key path\n\n```mermaid\ngraph TD\n    A[Listen to Rename Events] --> B[renameService.onRename triggered]\n    B --> C[Traverse all values traverseRef]\n\n    C --> D{Value Type Judgment}\n\n    D -->|ref type| E[Check path match\n    isKeyPathMatch]\n\n    D -->|template type| F[Check template path match\n    traverse + isKeyPathMatch]\n\n    E -->|Match| G[Update reference path\n    value.content = newPath]\n\n    E -->|No match| I\n\n    F -->|Match| H[Replace template content\n    value.content.replace]\n\n    F -->|No match| I\n\n    G --> I[Update form value\n    form.setValueIn]\n\n    H --> I\n\n    I --> J{More values to process?}\n    J -->|Yes| C\n    J -->|No| K[Complete]\n\n    style A fill:#e1f5fe\n    style K fill:#e8f5e8\n    style B fill:#fff3e0\n    style G fill:#ffebee\n    style H fill:#ffebee\n```\n\n### Core APIs Used\n\n#### @flowgram.ai/variable-core\n- `VariableFieldKeyRenameService`: Field key name rename listener\n\n#### @flowgram.ai/node\n- `DataEvent.onValueInit`: Value initialization event\n- `Effect`: Effect type definition\n\n#### FlowValueUtils\n- `FlowValueUtils.getTemplateKeyPaths()`: Extract all key paths from templates\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/listen-ref-schema-change.mdx",
    "content": "# listenRefSchemaChange\n\nListen to JSON schema changes of reference variable types and trigger callback functions when changes occur.\n\n## Demo\n\nimport { BasicStory } from 'components/form-materials/effects/listen-ref-schema-change';\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { listenRefSchemaChange } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effect: {\n    'inputsValues.*': listenRefSchemaChange(({ name, schema, form, formValues }) => {\n      form.setValueIn(\n        `log`,\n        `${form.getValueIn(`log`) || ''}* ${name}: ${JSON.stringify(schema)} \\n`\n      );\n    }),\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: {\n            type: 'ref',\n            content: ['start_0', 'str'],\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<any> name=\"log\" defaultValue={'When schema updated, log changes:\\n'}>\n        {({ field }) => (\n          <pre style={{ padding: 4, background: '#f5f5f5', fontSize: 12 }}>{field.value}</pre>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Reference\n\n### listenRefSchemaChange\n\n```typescript\nlistenRefSchemaChange(\n  cb: (props: EffectFuncProps<IFlowRefValue> & { schema?: IJsonSchema }) => void\n): EffectOptions[]\n```\n\n#### Parameters\n\n| Parameter | Type | Description |\n|----------|------|-------------|\n| `cb` | `(props: EffectFuncProps<IFlowRefValue> & { schema?: IJsonSchema }) => void` | Callback function triggered when the referenced schema changes |\n\n#### Callback Function Parameters\n\n| Parameter | Type | Description |\n|----------|------|-------------|\n| `name` | `string` | Field name |\n| `value` | `IFlowRefValue` | Reference value object |\n| `form` | `IForm` | Form instance |\n| `schema` | `IJsonSchema` | Converted JSON Schema |\n\n### Supported Value Types\n\n- `IFlowRefValue`: Reference type value, contains `type: 'ref'` and `content: string[]`\n\n## Source Code Guide\n\nimport { SourceCode } from '@theme';\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/listen-ref-schema-change\"\n/>\n\nCopy source code locally using CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/listen-ref-schema-change\n```\n\n### Directory Structure\n\n```\nlisten-ref-schema-change/\n└── index.ts          # Main entry file\n```\n\n### How It Works\n\n1. **Value Change Listening**: Listen to `DataEvent.onValueInitOrChange` events\n2. **Type Checking**: Check if the value is of `ref` type\n3. **Path Tracking**: Use `trackByKeyPath` to track reference paths\n4. **Schema Conversion**: Convert AST type to JSON Schema\n5. **Callback Trigger**: Trigger callback when the referenced type changes\n\n### Flowgram APIs Used\n\n#### @flowgram.ai/variable-core\n\n- `getNodeScope(context.node).available.trackByKeyPath`: Track value changes for specified paths, trigger callbacks when values corresponding to paths change.\n\n#### @flowgram.ai/json-schema\n\n- `JsonSchemaUtils.astToSchema`: Convert AST type nodes to JSON Schema objects.\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/listen-ref-value-change.mdx",
    "content": "# listenRefValueChange\n\nListen for changes for the referenced **variable definition** and trigger a callback function when it changes.\n\n## Case Demo\n\nimport { BasicStory } from 'components/form-materials/effects/listen-ref-value-change';\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { listenRefValueChange } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effect: {\n    'inputsValues.*': listenRefValueChange(({ name, variable, form }) => {\n      form.setValueIn(\n        `log`,\n        `${form.getValueIn(`log`) || ''}* ${name}: ${JSON.stringify(\n          variable?.toJSON() || {}\n        )} \\n`\n      );\n    }),\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: {\n            type: 'ref',\n            content: ['start_0', 'str'],\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<any> name=\"log\" defaultValue={'When variable value updated, log changes:\\n'}>\n        {({ field }) => (\n          <pre style={{ width: 500, padding: 4, background: '#f5f5f5', fontSize: 12 }}>\n            {field.value}\n          </pre>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API Introduction\n\n### listenRefValueChange\n\n```typescript\nlistenRefValueChange(\n  cb: (props: EffectFuncProps<IFlowRefValue> & { variable?: BaseVariableField }) => void\n): EffectOptions[]\n```\n\n#### Parameters\n\n| Parameter | Type | Description |\n|---|---|---|\n| `cb` | `(props: EffectFuncProps<IFlowRefValue> & { variable?: BaseVariableField }) => void` | Callback function, triggered when the referenced value changes |\n\n#### Callback Function Parameters\n\n| Parameter | Type | Description |\n|---|---|---|\n| `name` | `string` | Field name |\n| `value` | `IFlowRefValue` | Reference value object |\n| `form` | `IForm` | Form instance |\n| `variable` | `BaseVariableField` | The variable instance pointed to by the reference |\n\n### Supported Value Types\n\n- `IFlowRefValue`: Reference type value, including `type: 'ref'` and `content: string[]`\n\n## Source Code Guide\n\nimport { SourceCode } from '@theme';\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/listen-ref-value-change\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/listen-ref-value-change\n```\n\n### Directory Structure\n\n```\nlisten-ref-value-change/\n└── index.ts          # Main entry file\n```\n\n### How It Works\n\n1. **Listen for value changes**: Listen for the `DataEvent.onValueInitOrChange` event\n2. **Type check**: Check if the value is of `ref` type\n3. **Path tracking**: Use `trackByKeyPath` to track the reference path and get the referenced variable\n4. **Callback trigger**: Trigger the callback when the value of the referenced variable changes\n\n### flowgram APIs Used\n\n#### @flowgram.ai/variable-core\n\n- `getNodeScope(context.node).available.trackByKeyPath`: Tracks changes to the value of the specified path and triggers a callback when the value corresponding to the path changes.\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/provide-batch-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/provide-batch-input';\n\n# provideBatchInputEffect\n\n`provideBatchInputEffect` is a form effect specifically designed for loop node scenarios. It parses the loop input array variable into two local variables:\n\n- **item**: The current iteration's array element, with type automatically inferred from the input array's element type\n- **index**: The current iteration's index, with type `number`\n\n**Core Features:**\n\n- 🔄 **Automatic Type Inference**: Automatically infers the `item` element type from the array type\n- 🔒 **Private Scope**: Generated variables are stored in the node's private scope, accessible only to the current node and its child nodes\n- 🎯 **Loop-specific**: Designed specifically for batch processing/loop scenarios\n\nThis allows child nodes within the loop body to reference the `item` and `index` variables for processing array elements individually.\n\n:::info{title=\"Complete Solution Overview\"}\n\nImplementing a complete loop node requires the following three materials working together:\n\n| Material | Type | Responsibility |\n|------|------|------|\n| [BatchVariableSelector](../components/batch-variable-selector) | Component | Select the array data source for the loop |\n| **provideBatchInputEffect** | Effect | Generate `item` and `index` local variables |\n| [BatchOutputs](../components/batch-outputs) + [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) | Component + Plugin | Configure loop outputs and generate array-type variables |\n\n:::\n\n## Demo\n\n### Basic Usage\n\n:::tip\n\nAfter selecting an array-type variable, `provideBatchInputEffect` will automatically generate `item` and `index` local variables, which can be seen in the variable selector below.\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"About FormHeader, FormContent, FormItem\"}\n\nThe `FormHeader`, `FormContent`, and `FormItem` in the code above are user-defined layout components for unified form styling. You can implement them according to your project needs or replace them with other UI components.\n\n:::\n\n## API Reference\n\n### provideBatchInputEffect\n\nProvides a form effect that parses the loop input array variable into `item` and `index` local variables.\n\n```typescript\nimport { provideBatchInputEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta: FormMeta = {\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n};\n```\n\n#### Parameters\n\nThis effect is created internally using `createEffectFromVariableProvider` with the following configuration:\n\n| Property | Value | Description |\n|------|------|------|\n| `private` | `true` | Generated variables are stored in the node's private scope |\n\n:::tip{title=\"About the private parameter\"}\n\nSetting `private: true` stores variables in `node.privateScope` instead of `node.scope`. This means:\n- Variables are only visible within the current node and its child nodes\n- They cannot be accessed by downstream nodes of the parent node\n- Suitable for temporary iteration variables in loop scenarios\n\nSee: [Node Private Scope](../../guide/variable/concept#node-private-scope)\n\n:::\n\n#### Return Value\n\n- `EffectOptions[]`: Array of form effect options for `formMeta.effect` configuration\n\n#### Generated Variable Structure\n\nThe effect creates a variable `${nodeId}_locals` in the current node's **private scope** with the following structure:\n\n| Field | Type | Description |\n|------|------|------|\n| `item` | Inferred from array element type | The current iteration's array element |\n| `index` | `number` | The current iteration's index |\n\n#### Generated AST Structure Example\n\nAssuming the loop input variable path is `['start_0', 'list']`, the generated AST structure is:\n\n```typescript\n{\n  kind: 'VariableDeclaration',\n  key: 'loop_1_locals',\n  meta: {\n    title: 'Loop Node',\n    icon: 'loop-icon'\n  },\n  type: {\n    kind: 'ObjectType',\n    properties: [\n      {\n        kind: 'Property',\n        key: 'item',\n        initializer: {\n          kind: 'EnumerateExpression',\n          enumerateFor: {\n            kind: 'KeyPathExpression',\n            keyPath: ['start_0', 'list']\n          }\n        }\n      },\n      {\n        kind: 'Property',\n        key: 'index',\n        type: { kind: 'NumberType' }\n      }\n    ]\n  }\n}\n```\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-batch-input/index.ts\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/provide-batch-input\n```\n\n### Directory Structure\n\n```\nprovide-batch-input/\n└── index.ts           # Main implementation file, exports provideBatchInputEffect form effect\n```\n\n### Core Implementation\n\n#### Variable Generation Logic\n\n`provideBatchInputEffect` uses the [`createEffectFromVariableProvider`](../../guide/variable/variable-output) factory function to create a variable provider. Key characteristics:\n\n1. **Private Variables**: Setting `private: true` makes generated variables visible only within the current node's scope\n2. **Element Type Inference**: Uses `ASTFactory.createEnumerateExpression` to infer element type from the array type\n3. **Index Variable**: Fixed as `number` type\n\n#### Type Inference Principle\n\n`EnumerateExpression` is an expression type provided by the variable engine for inferring element type from array type:\n\n```mermaid\ngraph LR\n    A[\"Array&lt;string&gt;\"] --> B[EnumerateExpression]\n    B --> C[\"string\"]\n\n    D[\"Array&lt;object&gt;\"] --> E[EnumerateExpression]\n    E --> F[\"object\"]\n```\n\nWhen the upstream variable type changes, the `item` type will **automatically update accordingly**.\n\n#### Variable Generation Flow Sequence Diagram\n\n```mermaid\nsequenceDiagram\n    participant Form as Form System\n    participant Parser as parse function\n    participant AST as ASTFactory\n    participant Node as Current Node\n\n    Form->>Parser: Provide IFlowRefValue value\n    Parser->>Node: Get node info\n    Node-->>Parser: Return node ID\n    Parser->>AST: Create variable declaration\n    Note over Parser: Key: ${nodeId}_locals\n    Parser->>AST: Create Object type\n    Parser->>AST: Create item property\n    Note over AST: Use createEnumerateExpression\n    Note over AST: Infer element type from array keyPath\n    Parser->>AST: Create index property\n    Note over AST: Fixed as number type\n    AST-->>Parser: Return created variable declaration\n    Parser-->>Form: Return variable declaration array\n```\n\n#### Key Code Analysis\n\n```typescript\nexport const provideBatchInputEffect: EffectOptions[] = createEffectFromVariableProvider({\n  private: true,\n  parse: (value: IFlowRefValue, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'item',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'index',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/types/EffectOptions): Form effect options type\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): Node registry type definition\n- [`createEffectFromVariableProvider`](../../guide/variable/variable-output): Factory function to create form effects from variable providers\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- [`ASTFactory`](https://flowgram.ai/auto-docs/editor/modules/ASTFactory): AST creation factory for generating variable declarations\n- `ASTFactory.createEnumerateExpression`: Creates enumerate expression for inferring element type from array type\n- `ASTFactory.createKeyPathExpression`: Creates key path expression for referencing variable paths\n\n#### Dependent Materials\n\n[**BatchVariableSelector**](../components/batch-variable-selector)\n- Used to select array-type variables, works with `provideBatchInputEffect`\n\n## FAQ\n\n### Why can the item type be automatically inferred?\n\n`provideBatchInputEffect` uses `ASTFactory.createEnumerateExpression` to create the `item` variable. `EnumerateExpression` is a special expression that:\n\n1. Receives an array-type variable reference (via `KeyPathExpression`)\n2. Automatically infers the array's element type as its return type\n3. Automatically triggers type linkage updates when the upstream array type changes\n\nSee: [Variable Concepts - Expressions](../../guide/variable/concept#expressions)\n\n### Why can't I see the generated variables in the variable selector?\n\nCheck the following points:\n\n1. **Scope Issue**: Variables generated by `provideBatchInputEffect` are private (stored in `node.privateScope`), only accessible to the current node and its child nodes\n2. **Configuration Location**: Ensure `provideBatchInputEffect` is configured on the correct field path\n3. **Component Configuration**: If using `BatchVariableSelector`, it automatically provides `PrivateScopeProvider`; if using regular `VariableSelector`, you need to manually wrap it with `PrivateScopeProvider`\n\n### How to customize the variable names for item and index?\n\nCurrently, `provideBatchInputEffect` does not support custom variable names. If customization is needed, you can refer to the source code and use `createEffectFromVariableProvider` to create your own effect:\n\n```typescript\nimport { createEffectFromVariableProvider, ASTFactory } from '@flowgram.ai/editor';\n\nexport const customBatchInputEffect = createEffectFromVariableProvider({\n  private: true,\n  parse: (value, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'currentItem',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'currentIndex',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n```\n\n### What's the relationship with batchOutputsPlugin?\n\n| Material | Responsibility | Generated Variables |\n|------|------|------|\n| `provideBatchInputEffect` | Handles loop **input** | `item`, `index` (private variables) |\n| `batchOutputsPlugin` | Handles loop **output** | User-configured output keys (public variables, array type) |\n\nBoth work together to form the complete loop node variable logic:\n\n```mermaid\ngraph LR\n    A[Array Input] --> B[provideBatchInputEffect]\n    B --> C[item, index]\n    C --> D[Child Node Processing]\n    D --> E[BatchOutputs Configuration]\n    E --> F[batchOutputsPlugin]\n    F --> G[Array Output]\n```\n\n## Related Materials\n\n- [BatchVariableSelector](../components/batch-variable-selector): Array variable selector for selecting loop input\n- [BatchOutputs](../components/batch-outputs): Loop output configuration component\n- [batchOutputsPlugin](../form-plugins/batch-outputs-plugin): Loop output plugin, handles scope chain and type inference\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/provide-json-schema-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/provide-json-schema-output';\n\n# provideJsonSchemaOutputs\n\nprovideJsonSchemaOutputs is a form effect that converts JSON Schema definitions into output variables in the FlowGram variable engine.\n\nIt automatically parses the JSON Schema structure defined in the form into variable declarations, allowing other nodes in the workflow to reference these outputs.\n\n## Examples\n\n### Basic Usage\n\n:::tip\n\n`provideJsonSchemaOutputs` is typically used with [`syncVariableTitle`](./sync-variable-title) to ensure variables **real-time synchronize with node titles**.\n\n:::\n\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaEditor, provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined>\n        name=\"outputs\"\n        defaultValue={{\n          type: 'object',\n          properties: {\n            name: { type: 'string' },\n            age: { type: 'number' },\n          },\n        }}\n      >\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n  effect: {\n    // Sync title to variable\n    title: syncVariableTitle,\n    // Convert JSON Schema to output variables\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n\n## API Reference\n\n### provideJsonSchemaOutputs\n\nProvides a form effect that converts JSON Schema to workflow output variables.\n\n#### Parameters\n- No direct parameters, used directly in formMeta.effect as a form effect\n\n#### Return Value\n- `EffectOptions[]`: Array of form effect options for formMeta.effect configuration\n\n#### Working Principle\n\nThis form effect will:\n1. Get the JSON Schema value defined in the form\n2. Convert the Schema to FlowGram's AST type\n3. Create variable declaration with key name as the current node ID\n4. Set variable metadata (title, icon, etc.)\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-json-schema-outputs/index.ts\"\n/>\n\nUse CLI command to copy source code to local:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/provide-json-schema-outputs\n```\n\n### Directory Structure\n\n```\nprovide-json-schema-outputs/\n└── index.ts           # Main implementation file, exports provideJsonSchemaOutputs form effect\n```\n\n### Core Implementation\n\n#### Variable Generation Logic\n\nprovideJsonSchemaOutputs uses the [`createEffectFromVariableProvider`](/guide/variable/variable-output) factory function to create variable providers. It uses the `JsonSchemaUtils.schemaToAST` function within the effect to convert the JSON Schema filled in the form into AST.\n\n:::tip\n\n`JsonSchemaUtils.schemaToAST` recursively parses JSON schema to generate AST, see  [utils.ts](https://github.com/bytedance/flowgram.ai/blob/main/packages/variable-engine/json-schema/src/json-schema/utils.ts)\n\n:::\n\nVariable generation flow sequence diagram:\n\n```mermaid\nsequenceDiagram\n    participant Form as Form System\n    participant Parser as parse function\n    participant AST as ASTFactory\n    participant Node as Current Node\n    participant SchemaUtils as JsonSchemaUtils\n\n    Form->>Parser: Provide JSON Schema value\n    Parser->>Node: Get node information\n    Node-->>Parser: Return node ID and registry\n    Parser->>SchemaUtils: Call schemaToAST to convert type\n    SchemaUtils-->>Parser: Return converted AST type\n    Parser->>AST: Create variable declaration\n    Parser->>Parser: Set variable key name as node ID\n    Parser->>Node: Get form title\n    alt Form has title\n        Node-->>Parser: Return form title\n    else Form has no title\n        Node-->>Parser: Use node ID as title\n    end\n    Parser->>Node: Get node icon\n    Node-->>Parser: Return node icon\n    AST-->>Parser: Return created variable declaration\n    Parser-->>Form: Return variable declaration array\n```\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`JsonSchemaUtils`](https://flowgram.ai/auto-docs/json-schema/modules/JsonSchemaUtils): JSON Schema utility class for converting Schema to AST\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema interface definition\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/types/EffectOptions): Form effect options type\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): Node registration type definition\n- [`createEffectFromVariableProvider`](/guide/variable/variable-output): Factory function to create form effects from variable providers\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`ASTFactory`](https://flowgram.ai/auto-docs/editor/modules/ASTFactory): AST creation factory for generating variable declarations\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/sync-variable-title.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/sync-variable-title';\n\n# syncVariableTitle\n\nsyncVariableTitle is a form effect used to synchronize the node's title to the metadata of its output variables.\n\nWhen the node title changes, it automatically updates the title and icon metadata of all output variables.\n\n| Before | After |\n| --- | --- |\n| <img loading=\"lazy\" src=\"/materials/sync-variable-title-without-effect.gif\" alt=\"syncVariableTitle not introduced\" style={{ width: 500 }} /> *When node title changes, it cannot automatically sync to variables* | <img loading=\"lazy\" src=\"/materials/sync-variable-title-with-effect.gif\" alt=\"syncVariableTitle introduced\" style={{ width: 500 }} /> *Node title in variables will automatically sync* |\n\n## Example Demonstration\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { syncVariableTitle } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <p>Please Edit Title below to sync to variables:</p>\n      <Field<string | undefined> name=\"title\">\n        {({ field }) => (\n          <Input value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n  effect: {\n    // Sync the title to variables\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n## API Reference\n\n### syncVariableTitle\n\nProvides a form effect for synchronizing the node's title to the metadata of its output variables.\n\n#### Parameters\n- No direct parameters, used directly in formMeta.effect as a form effect\n\n#### Return Value\n- `EffectOptions[]`: Form effect options array for formMeta.effect configuration\n\n#### Working Principle\n\nThis form effect:\n1. Listens to the `onValueChange` event of the node title field\n2. When the title changes, retrieves all output variables of the node\n3. Updates the metadata of each output variable, including title and icon\n4. If the title is empty, uses the node ID as the title\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/sync-variable-title\"\n/>\n\nUse the CLI command to copy the source code to your local machine:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/sync-variable-title\n```\n\n### Directory Structure\n\n```\nsync-variable-title/\n└── index.ts           # Main implementation file, exports syncVariableTitle form effect\n```\n\n### Synchronization Logic\n\n`syncVariableTitle` implements real-time synchronization from node title to variable metadata by registering a `DataEvent.onValueChange` event listener.\n\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`DataEvent`](https://flowgram.ai/auto-docs/editor/enums/DataEvent): Data event enumeration for listening to value change events\n- [`Effect`](https://flowgram.ai/auto-docs/editor/types/Effect): Side effect function type definition\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/interfaces/EffectOptions): Side effect configuration options interface\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): Node registration type definition\n- [`FlowNodeVariableData`](https://flowgram.ai/auto-docs/editor/classes/FlowNodeVariableData): Node variable data class providing node variable management functionality\n"
  },
  {
    "path": "apps/docs/src/en/materials/effects/validate-when-variable-sync.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/validate-when-variable-sync';\n\n# validateWhenVariableSync\n\nWhen accessible variables of a field change, re-trigger validation for the specified **error fields**.\n\n:::note{title=\"Why error fields?\"}\n\nError messages for error fields may be derived from the validity of variable references.\n\nIf the **accessible variables of a field change, making the variable references of the field change from invalid to valid**, there's a need to re-validate the current field to clear previous error messages.\n\n:::\n\n| Before Introduction | After Introduction |\n| --- | --- |\n| <img loading=\"lazy\" src=\"/materials/validate-when-variable-sync-without-effect.gif\" alt=\"syncVariableTitle not introduced\" style={{ width: 500 }} /> *Variable changes from invalid to valid, error message persists* | <img loading=\"lazy\" src=\"/materials/validate-when-variable-sync-with-effect.gif\" alt=\"syncVariableTitle introduced\" style={{ width: 500 }} /> *Variable changes from invalid to valid, error message automatically cleared* |\n\n## Example Demonstration\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  effect: {\n    value: validateWhenVariableSync(),\n  },\n  validate: {\n    value: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        errorMessages: {\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n  },\n};\n```\n\n## API Reference\n\n`validateWhenVariableSync(options?: { scope?: 'private' | 'public' })`\n\n| Parameter | Type | Description | Default Value | Required |\n| --- | --- | --- | --- | --- |\n| `options.scope` | `'private' \\| 'public'` | Variable scope type, specifies whether to listen for changes in private or public variables | - | No |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/validate-when-variable-sync/index.ts\"\n/>\n\nUse the CLI command to copy the source code to local:\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/validate-when-variable-sync\n```\n\n### Directory Structure\n\n```\npackages/materials/form-materials/src/effects/validate-when-variable-sync/\n└── index.ts           # Core implementation and export\n```\n\n### Core Implementation Explanation\n\nThis effect automatically triggers validation for error fields by listening to changes in accessible variables of the field, ensuring that error messages are cleared promptly when variable references change from invalid to valid.\n\nMain process:\n1. Listen for form initialization event `DataEvent.onValueInit`\n2. Get the corresponding node scope (private or public) based on configuration\n3. Listen to the variable change event in the scope `available.onListOrAnyVarChange`\n4. When variables change, filter out error fields that include the current field name\n5. Re-validate the filtered error fields\n6. Return a cleanup function to release event listeners\n\n### Dependency Overview\n\n#### flowgram API\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- `scope.available.onListOrAnyVarChange`: Listen for changes in available variables in the scope\n"
  },
  {
    "path": "apps/docs/src/en/materials/form-plugins/_meta.json",
    "content": "[\n  \"batch-outputs-plugin\",\n  \"infer-assign-plugin\",\n  \"infer-inputs-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/en/materials/form-plugins/batch-outputs-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithInferSchemaStory } from 'components/form-materials/form-plugins/batch-outputs-plugin';\n\n# batchOutputsPlugin\n\n`batchOutputsPlugin` is a form plugin for loop nodes that implements two core functions:\n\n1. **Output Variable Generation**: Converts variable references collected within the loop body into array-type output variables\n2. **Scope Chain Transformation**: Adjusts the variable scope chain so that the loop node's outputs can correctly depend on child node outputs\n\n**Core Features:**\n\n- 🔄 **Array Wrapping**: Automatically wraps variables referenced within the loop body as array-type outputs\n- 🔗 **Scope Chain Adjustment**: Allows the loop node's output variables to correctly depend on child node outputs\n- 📊 **Schema Inference**: Optional configuration to automatically infer the JSON Schema of output variables\n\n:::tip{title=\"Use Cases\"}\n\n- **Loop Nodes**: Need to collect data in each iteration and aggregate into arrays\n- **Batch Processing Nodes**: Need to summarize results from multiple subtasks\n- **Any Container Node with Child Nodes**: Need to collect output variables from child nodes\n\n:::\n\n:::warning\n\n`BatchOutputs` component must be used with `batchOutputsPlugin` to work properly. This is because:\n1. The component handles UI interaction, collecting output key-value pairs configured by the user\n2. The plugin is responsible for converting configurations into variable declarations and adjusting the scope chain\n\n:::\n\n:::info{title=\"Complete Solution Overview\"}\n\nImplementing a complete loop node requires the following three materials working together:\n\n| Material | Type | Responsibility |\n|------|------|------|\n| [BatchVariableSelector](../components/batch-variable-selector) | Component | Select the array data source for the loop |\n| [provideBatchInputEffect](../effects/provide-batch-input) | Effect | Generate `item` and `index` local variables |\n| [BatchOutputs](../components/batch-outputs) + **batchOutputsPlugin** | Component + Plugin | Configure loop outputs and generate array-type variables |\n\n:::\n\n## Demo\n\n### Basic Usage\n\n:::tip\n\nClick the Debug panel in the upper right corner of the demo to view the generated output variables and JSON data sent to the backend\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"About FormHeader, FormContent, FormItem\"}\n\nThe `FormHeader`, `FormContent`, and `FormItem` in the code above are user-defined layout components for unified form styling. You can implement them according to your project needs or replace them with other UI components.\n\n:::\n\n### With Schema Inference\n\n<WithInferSchemaStory />\n\nWhen the `inferTargetKey` parameter is configured, the plugin will automatically infer the JSON Schema of output variables when the form is submitted and store it in the specified field.\n\n## API Reference\n\n### createBatchOutputsFormPlugin\n\nFactory function to create the batch outputs plugin.\n\n```typescript\nfunction createBatchOutputsFormPlugin(options: {\n  outputKey: string;\n  inferTargetKey?: string;\n}): FormPlugin;\n```\n\n| Property | Type | Default | Description |\n|--------|------|--------|------|\n| `outputKey` | `string` | - | The field path in the form where loop output configurations are stored |\n| `inferTargetKey` | `string` | - | Optional, the field path where the inferred JSON Schema will be stored |\n\n### provideBatchOutputsEffect\n\nOutput variable generation effect that converts `Record<string, IFlowRefValue>` format configurations into array-type variable declarations.\n\n```typescript\nimport { provideBatchOutputsEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta: FormMeta = {\n  effect: {\n    loopOutputs: provideBatchOutputsEffect,\n  },\n};\n```\n\n:::tip\n\nTypically, using `createBatchOutputsFormPlugin` is sufficient as it already includes `provideBatchOutputsEffect` internally. Only use this effect directly when you need the variable generation functionality without scope chain transformation.\n\n:::\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/batch-outputs-plugin/index.ts\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/batch-outputs-plugin\n```\n\n### Directory Structure\n\n```\nbatch-outputs-plugin/\n└── index.ts           # Complete plugin implementation, includes variable generation and scope chain transformation logic\n```\n\n### Core Implementation\n\n#### Plugin Structure\n\n`createBatchOutputsFormPlugin` is created using `defineFormPluginCreator` and contains two core lifecycles:\n\n1. **onSetupFormMeta**: Configures form metadata, registers effects and submit formatting\n2. **onInit**: Registers scope chain transformer during initialization\n\n#### Output Variable Generation\n\n`provideBatchOutputsEffect` converts `Record<string, IFlowRefValue>` into variable declarations:\n\n```mermaid\ngraph LR\n    A[loopOutputs Configuration] --> B[provideBatchOutputsEffect]\n    B --> C[Variable Declaration]\n\n    subgraph Transformation Process\n        D[\"{ names: { type: 'ref', content: ['item', 'name'] } }\"]\n        E[\"names: WrapArray(item.name)\"]\n        D --> E\n    end\n\n    C --> F[Output: names type is Array]\n```\n\nKey point: Uses `ASTFactory.createWrapArrayExpression` to wrap single value types as array types.\n\n#### Scope Chain Transformation\n\nThis is the most core functionality of this plugin, solving the variable scope problem for loop nodes.\n\n:::info{title=\"Why is scope chain transformation needed?\"}\n\nIn the default scope chain logic (refer to [Scope Chain Concept](../../guide/variable/concept#scope-chain)):\n- Child node output variables **cannot** be accessed by downstream nodes of the parent node\n- Loop node output variables **cannot** depend on child node outputs\n\nHowever, in loop scenarios, we need:\n- The loop node's outputs (such as aggregated arrays) to depend on values produced by child nodes in each iteration\n- Child node variables to \"cover\" to the parent loop node's scope\n\n:::\n\nThe following diagram illustrates how scope chain transformation works:\n\n```mermaid\ngraph TB\n    subgraph Default Scope Chain\n        direction TB\n        A1[Upstream Node.scope] --> B1[Loop Node.scope]\n        B1 --> C1[Downstream Node.scope]\n        B1 --> D1[Child Node.scope]\n        D1 -.❌ Not Accessible.-> C1\n    end\n\n    subgraph Transformed Scope Chain\n        direction TB\n        A2[Upstream Node.scope] --> B2[Loop Node.scope]\n        B2 --> C2[Downstream Node.scope]\n        B2 --> D2[Child Node.scope]\n        D2 -.✅ Can Cover.-> B2\n        B2 -.Depends On.-> D2\n    end\n```\n\nTwo core methods of the transformer:\n\n| Method | Purpose | Description |\n|------|------|------|\n| `transformCovers` | Extend cover scopes | Allows child node variables to \"cover\" to the parent loop node |\n| `transformDeps` | Adjust dependency scopes | Allows the loop node's public scope to depend on its child nodes' scopes |\n\n```mermaid\nsequenceDiagram\n    participant Loop as Loop Node\n    participant Child as Child Node\n    participant Transform as ScopeChainTransformService\n\n    Note over Loop,Child: Problem: Loop node output depends on child node output\n\n    Loop->>Transform: Register transformer\n\n    Note over Transform: transformCovers (Cover Relationship)\n    Child->>Transform: Child node queries cover scopes\n    Transform-->>Child: Return [Parent Loop Node Scope]\n    Note over Child: Child node variables can cover parent loop node\n\n    Note over Transform: transformDeps (Dependency Relationship)\n    Loop->>Transform: Loop node queries dependency scopes\n    Transform-->>Loop: Return [Private Scope, ...Child Node Scopes]\n    Note over Loop: Loop node output depends on child node output\n```\n\n#### Key Code Analysis\n\n**1. Variable Generation Logic**\n\n```typescript\nexport const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: Record<string, IFlowRefValue>, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      type: ASTFactory.createObject({\n        properties: Object.entries(value).map(([_key, value]) =>\n          ASTFactory.createProperty({\n            key: _key,\n            initializer: ASTFactory.createWrapArrayExpression({\n              wrapFor: ASTFactory.createKeyPathExpression({\n                keyPath: value?.content || [],\n              }),\n            }),\n          })\n        ),\n      }),\n    }),\n  ],\n});\n```\n\n**2. Scope Chain Transformer**\n\n```typescript\nchainTransformService.registerTransformer(transformerId, {\n  transformCovers: (covers, ctx) => {\n    const node = ctx.scope.meta?.node;\n    if (node?.parent?.flowNodeType === batchNodeType) {\n      return [...covers, getNodeScope(node.parent)];\n    }\n    return covers;\n  },\n  transformDeps(scopes, ctx) {\n    const scopeMeta = ctx.scope.meta;\n    if (scopeMeta?.type === FlowNodeScopeType.private) {\n      return scopes;\n    }\n    const node = scopeMeta?.node;\n    if (node?.flowNodeType === batchNodeType) {\n      const childBlocks = node.blocks;\n      return [\n        getNodePrivateScope(node),\n        ...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),\n      ];\n    }\n    return scopes;\n  },\n});\n```\n\n**3. Schema Inference (Optional)**\n\n```typescript\nif (inferTargetKey) {\n  addFormatOnSubmit((formData, ctx) => {\n    const outputVariable = getNodeScope(ctx.node).output.variables?.[0];\n    if (outputVariable?.type) {\n      set(formData, inferTargetKey, JsonSchemaUtils.astToSchema(outputVariable?.type));\n    }\n    return formData;\n  });\n}\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: Factory function to define form plugins\n- `EffectOptions`: Form effect options type\n- `createEffectFromVariableProvider`: Create form effects from variable providers\n- `FlowNodeRegistry`: Node registry type definition\n- `FlowNodeScopeType`: Node scope type enum\n- `getNodeScope`: Get the node's public scope\n- `getNodePrivateScope`: Get the node's private scope\n- `ScopeChainTransformService`: Scope chain transform service\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- `ASTFactory`: AST creation factory\n- `ASTFactory.createWrapArrayExpression`: Create array wrapping expression\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `JsonSchemaUtils.astToSchema`: Convert AST to JSON Schema\n\n#### Dependent Materials\n\n[**BatchOutputs**](../components/batch-outputs)\n- Loop output configuration component for collecting output key-value pairs\n\n[**BatchVariableSelector**](../components/batch-variable-selector)\n- Array variable selector for selecting loop input\n\n[**provideBatchInputEffect**](../effects/provide-batch-input)\n- Loop input effect that generates item and index local variables\n\n## FAQ\n\n### Why do I need to use both BatchOutputs component and batchOutputsPlugin?\n\nThis is a separation of concerns design:\n\n| Role | Responsibility |\n|------|------|\n| `BatchOutputs` Component | Provides UI interaction, lets users configure output key names and variable references |\n| `batchOutputsPlugin` | Handles data logic, converts configurations to variable declarations and adjusts scope chain |\n\nUsing the component alone only collects data and cannot generate valid output variables; using the plugin alone has no UI to configure data.\n\n### When do I need to configure inferTargetKey?\n\nWhen you need to persist the output variable type information to form data (e.g., the backend needs to know the output JSON Schema), configuring `inferTargetKey` will automatically infer and store the Schema when the form is submitted.\n\n### How to use it with provideBatchInputEffect?\n\nA complete loop node is typically configured like this:\n\n```typescript\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [\n    createBatchOutputsFormPlugin({\n      outputKey: 'loopOutputs',\n      inferTargetKey: 'outputs'\n    })\n  ],\n};\n```\n\n- `provideBatchInputEffect` is responsible for generating `item` and `index` local variables from the loop input array\n- `batchOutputsPlugin` is responsible for aggregating variables collected within the loop body into array outputs\n\n### Will scope chain transformation affect other nodes?\n\nNo. The transformer precisely matches the current node type through `batchNodeType` and only applies to that node type. The transformer ID also includes the node type to prevent duplicate registration.\n\n## Related Materials\n\n- [provideBatchInputEffect](../effects/provide-batch-input): Loop input variable parsing\n- [BatchOutputs](../components/batch-outputs): Loop output configuration component\n- [BatchVariableSelector](../components/batch-variable-selector): Array variable selector\n- [inferInputsPlugin](./infer-inputs-plugin): Input parameter Schema inference plugin\n"
  },
  {
    "path": "apps/docs/src/en/materials/form-plugins/infer-assign-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/form-plugins/infer-assign-plugin';\n\n# inferAssignPlugin\n\n`inferAssignPlugin` is a form plugin used for variable assignment nodes to automatically derive output variables. It is typically used in conjunction with the [`AssignRows`](../components/assign-rows) component.\n\nThe plugin implements the following capabilities for `declare` (declaring new variables) in `AssignRows`:\n- Automatically generates node output variables, where the variable name is the left-hand value in the `declare` operator, and the variable type is automatically inferred based on the `right` value.\n- When submitting data to the backend, automatically generates the corresponding JSON Schema based on the output variable types.\n\n## Examples\n\n### Basic Usage\n\n:::tip\n\nClick the Debug panel in the top right corner of the demo to view the JSON data sent to the backend\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { createInferAssignPlugin, AssignRows, DisplayOutputs } from '@flowgram.ai/form-materials';\n\nexport const VariableFormRender = ({ form }) => {\n  return (\n    <>\n      <FormHeader />\n      <AssignRows\n        name=\"assign\"\n        defaultValue={[\n          // Declare variable from constant\n          {\n            operator: 'declare',\n            left: 'userName',\n            right: {\n              type: 'constant',\n              content: 'John Doe',\n              schema: { type: 'string' },\n            },\n          },\n          // Declare variable from variable\n          {\n            operator: 'declare',\n            left: 'userInfo',\n            right: {\n              type: 'ref',\n              content: ['start_0', 'obj'],\n            },\n          },\n          // Assign existing variable\n          {\n            operator: 'assign',\n            left: {\n              type: 'ref',\n              content: ['start_0', 'str'],\n            },\n            right: {\n              type: 'constant',\n              content: 'Hello Flowgram',\n              schema: { type: 'string' },\n            },\n          },\n        ]}\n      />\n      <DisplayOutputs displayFromScope />\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: VariableFormRender,\n  plugins: [\n    createInferAssignPlugin({\n      assignKey: 'assign',\n      outputKey: 'outputs'\n    })\n  ],\n};\n```\n\n## API Reference\n\n```typescript\nfunction createInferAssignPlugin(options: {\n  assignKey: string;\n  outputKey: string;\n}): FormPlugin;\n```\n\n| Property | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| assignKey | `string` | - | Field path in the form for storing the assignment operations array, value type is `AssignValueType[]` |\n| outputKey | `string` | - | Field path for storing the output JSON Schema |\n\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/infer-assign-plugin\"\n/>\n\nYou can copy the source code locally using the CLI command:\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/infer-assign-plugin\n```\n\n### Directory Structure\n\n```plaintext\ninfer-assign-plugin/\n└── index.ts                  # Main plugin entry, creates and exports the plugin\n```\n\n### Core Implementation\n\n\n```mermaid\nsequenceDiagram\n    participant Form as Form\n    participant Plugin as inferAssignPlugin\n    participant Provider as VariableProvider\n    participant Scope as Variable Scope\n    participant Runtime as Backend Runtime\n\n    Form->>Plugin: onSetupFormMeta()\n    Plugin->>Form: Register variable provision side effects\n\n    loop Iterate through each assignment operation\n        alt operator = 'declare'\n          Plugin->>Plugin: Generate variable declaration (left -> VariableDeclaration)\n        end\n    end\n\n    Plugin->>Provider: Provide variables to scope\n    Provider->>Scope: Register output variables\n    Plugin-->>Runtime: Derive output variable JSON Schema\n\n```\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: Factory function for defining form plugins\n- `FormPlugin`: Form plugin type definition\n- `FormPluginSetupMetaCtx`: Plugin setup context, provides `mergeEffect`, `addFormatOnSubmit` methods\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `IJsonSchema`: JSON Schema type definition\n\n#### Other Dependencies\n\n[**FlowValue**](../common/flow-value)\n- `FlowValueUtils.inferJsonSchema()`: Infers JSON Schema from IFlowValue\n- `FlowValueUtils.isConstant()`, `FlowValueUtils.isRef()`: Type checking utilities\n- `IFlowValue`: Union type for Flow values\n- `IFlowRefValue`: Variable reference type\n- `IFlowConstantValue`: Constant type\n"
  },
  {
    "path": "apps/docs/src/en/materials/form-plugins/infer-inputs-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/form-plugins/infer-inputs-plugin';\n\n# inferInputsPlugin\n\n`inferInputsPlugin` is a **form plugin for automatically inferring JSON Schema of input parameters**. It can **automatically generate corresponding JSON Schema structures based on IFlowValue types (constants or variable references) before data is sent to the backend runtime**, providing type information for backend runtime type validation and backend interfaces.\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/infer-inputs-plugin.png\" alt=\"Schema Inference\" style={{ width: '75%' }} />\n  *When transmitted to the backend runtime, the JSON Schema of the inputs field is inferred from the values in inputsValues*\n</div>\n\n:::tip{title=\"Applicable Scenarios\"}\n\n- **HTTP Nodes**: Infer Schema for request headers and query parameters\n- **Code Nodes**: Infer type structure for code input parameters\n- **Function Call Nodes**: Infer Schema for function parameters\n- **Any nodes accepting dynamic inputs**: Need to provide input type information for the backend\n\n:::\n\n## Demo\n\n### Basic Usage\n\n:::tip\n\nClick the Debug panel in the top right corner of the demo to view the JSON data sent to the backend\n\n:::\n\n<BasicStory />\n\nInfer Schema for HTTP request headers and body:\n\n```tsx pure title=\"form-meta.tsx\"\nimport { createInferInputsPlugin, InputsValue, InputsValuesTree } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nexport const HttpFormRender = ({ form }) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<Record<string, IFlowValue>> name=\"headersValues\">\n          {({ field }) => (\n            <InputsValues\n              value={field.value}\n              onChange={(val) => field.onChange(val)}\n            />\n          )}\n        </Field>\n        <Field<Record<string, IFlowValue>> name=\"bodyValues\">\n          {({ field }) => (\n            <InputsValuesTree\n              value={field.value}\n              onChange={(val) => field.onChange(val)}\n            />\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: HttpFormRender,\n  plugins: [\n    // Infer Schema for headers\n    createInferInputsPlugin({\n      sourceKey: 'headersValues',\n      targetKey: 'headersSchema'\n    }),\n    // Infer Schema for body\n    createInferInputsPlugin({\n      sourceKey: 'bodyValues',\n      targetKey: 'bodySchema'\n    })\n  ],\n};\n```\n\n## API Reference\n\n```typescript\nfunction createInferInputsPlugin(options: {\n  sourceKey: string;\n  targetKey: string;\n  scope?: 'private' | 'public';\n  ignoreConstantSchema?: boolean;\n}): FormPlugin;\n```\n\n| Property | Type | Default | Description |\n| :--- | :--- | :--- | :--- |\n| sourceKey | `string` | - | Field path in the form that stores input values, value type is an object or array containing IFlowValue |\n| targetKey | `string` | - | Field path where the inferred JSON Schema is stored |\n| scope | `'private' \\| 'public'` | `public` | Specifies the scope type used for variable resolution |\n| ignoreConstantSchema | `boolean` | `false` | Whether to strip the Schema of constant values during submission (only keep Schema of variable references) |\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/infer-inputs-plugin/index.ts\"\n/>\n\nUse CLI command to copy source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/infer-inputs-plugin\n```\n\n### Directory Structure\n\n```plaintext\ninfer-inputs-plugin/\n└── index.tsx  # Complete plugin implementation, including Schema inference and bidirectional conversion logic\n```\n\n### Core Implementation\n\nThe core function of `inferInputsPlugin` is to infer JSON Schema from IFlowValue objects. For constant values, it directly uses their `schema` field; for variable references, it queries variable types from the scope; for expressions and templates, it infers them as corresponding basic types. The plugin also supports `ignoreConstantSchema` optimization, stripping constant Schema during submission and automatically restoring it during initialization.\n\n#### Workflow Sequence Diagram\n\n```mermaid\nsequenceDiagram\n    participant Form as Form\n    participant Plugin as inferInputsPlugin\n    participant Scope as Variable Scope\n    participant Backend as Backend\n\n    Note over Form: Form Submission\n    Form->>Plugin: onSubmit triggered\n    Plugin->>Plugin: Read sourceKey data\n\n    loop Iterate each IFlowValue\n        alt Type is constant\n            Plugin->>Plugin: Use value.schema\n            opt ignoreConstantSchema = true\n                Plugin->>Plugin: Strip schema\n            end\n        else Type is ref\n            Plugin->>Scope: Query variable type\n            Scope->>Plugin: Return variable Schema\n        else Type is expression/template\n            Plugin->>Plugin: Infer as any/string type\n        end\n    end\n\n    Plugin->>Plugin: Merge all Schemas\n    Plugin->>Form: Set to targetKey\n    Form->>Backend: Submit data containing Schema\n\n    Note over Form: Form Initialization\n    Backend->>Form: Return data (may lack constant Schema)\n    Form->>Plugin: onInit triggered\n    opt ignoreConstantSchema = true\n        Plugin->>Plugin: Restore constant Schema from sourceKey\n        Plugin->>Form: Update targetKey\n    end\n```\n\nCore Features:\n\n1. **Automatic Schema Inference**: Scans IFlowValue objects in form data and automatically infers their JSON Schema\n2. **Variable Type Resolution**: For variable references, resolves the actual type of variables from the scope\n3. **Constant Schema Optimization**: Optionally strips constant Schema during submission to reduce backend data load\n4. **Bidirectional Conversion**: Restores Schema during form initialization and generates Schema during form submission\n\n### Dependencies\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: Factory function for defining form plugins\n- `FormPlugin`: Form plugin type definition\n- `FormPluginSetupMetaCtx`: Plugin setup context, provides `addFormatOnInit`, `addFormatOnSubmit` methods\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `IJsonSchema`: JSON Schema type definition\n\n#### Other Dependencies\n\n[**FlowValue**](../common/flow-value)\n- `FlowValueUtils.inferJsonSchema()`: Infer JSON Schema of IFlowValue\n- `FlowValueUtils.traverse()`: Traverse nested FlowValue structures\n- `FlowValueUtils.isConstant()`, `FlowValueUtils.isRef()`: Type judgment tools\n- `IFlowValue`: Union type of Flow values\n- `IFlowConstantValue`: Constant type, contains `schema` field\n- `IFlowRefValue`: Variable reference type, contains variable path"
  },
  {
    "path": "apps/docs/src/en/materials/introduction.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# Getting Started\n\n## How to Use?\n\n### Use via Package Import\n\nOfficial form materials can be used directly through package import:\n\n<PackageManagerTabs command=\"install @flowgram.ai/form-materials\" />\n\n```tsx\nimport { JsonSchemaEditor } from '@flowgram.ai/form-materials'\n```\n\n\n### Add Material Source Code via CLI\n\n\nIf your business has customization requirements for components (e.g., changing text, styles, business logic), it is recommended to **add material source code to the project via CLI for customization**:\n\n```bash\nnpx @flowgram.ai/cli@latest materials\n```\n\nAfter running, the CLI will prompt the user to select materials to add to the project:\n\n```console\n? Select one material to add: (Use arrow keys)\n❯ components/json-schema-editor\n  components/type-selector\n  components/variable-selector\n```\n\nUsers can also directly add source code for specified materials via CLI:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n```\n\nAfter the CLI runs successfully, the relevant materials will be automatically added to the `src/form-materials` directory under the current project.\n\n:::warning Notes\n\n1. Official materials are currently implemented based on [Semi Design](https://semi.design/). If your business has underlying component library requirements, you can replace them by copying the source code via CLI\n2. Some materials depend on third-party npm libraries, which will be automatically installed when the CLI runs\n3. Some materials depend on other official materials, and the source code of these dependent materials will be added to the project together when the CLI runs\n\n:::\n\n## Appendix: Finding Materials by Node Type\n\n### Implementing Node Input and Output\n\n**Parameter Configuration**\n- Node Input: [InputsValues](./components/inputs-values), [InputsValuesTree](./components/inputs-values-tree)\n- Node Output: [JsonSchemaEditor](./components/json-schema-editor)\n- Input Validation: [validateFlowValue](./validate/validate-flow-value)\n- Output Variable Generation: [provideJsonSchemaOutputs](./effects/provide-json-schema-outputs)\n\n**Parameter Display**\n- Input Display: [DisplayInputsValues](./components/display-inputs-values)\n- Output Display: [DisplayOutputs](./components/display-outputs)\n\n### Implementing Code Nodes\n\n- Code Editor: [CodeEditor](./components/code-editor)\n\n### Implementing LLM Nodes\n\n- Prompt Editor: [PromptEditor](./components/prompt-editor)\n- Prompt Editor with Variables: [PromptEditorWithVariables](./components/prompt-editor-with-variables)\n\n### Implementing Condition Branch Nodes\n\n- Single-line Condition Branch Configuration: [ConditionRow](./components/condition-row)\n- Variable Listening for Branch Linkage: [listenRefSchemaChange](./effects/listen-ref-schema-change), [listenRefValueChange](./effects/listen-ref-value-change)\n\n### Implementing Database Nodes\n\n- Single-line Database Query Condition Configuration: [DBConditionRow](./components/db-condition-row)\n- SQL Editor: [SQLCodeEditor](./components/code-editor)\n- SQL Editor with Variables: [SQLEditorWithVariables](./components/sql-editor-with-variables)\n\n\n#### Implementing Loop Nodes\n\n**Loop Input**\n- Loop Input Array Variable Selector: [BatchVariableSelector](./components/batch-variable-selector)\n- Item, Index Derivation: [provideBatchInput](./effects/provide-batch-input)\n\n**Loop Output**\n- Loop Output Array Variable Selector: [BatchOutputs](./components/batch-outputs)\n- Output Variable Scope Chain Adjustment + Type Derivation: [batchOutputsPlugin](./form-plugins/batch-outputs-plugin)\n\n\n### Implementing HTTP Nodes\n\n- JSON Editor: [JsonCodeEditor](./components/code-editor)\n- JSON Editor with Variables: [JsonEditorWithVariables](./components/json-editor-with-variables)\n\n### Implementing Variable Assignment/Declaration Nodes\n\n- Single-line Variable Assignment, Declaration: [AssignRow](./components/assign-row)\n- Variable Assignment, Declaration Configuration List: [AssignRows](./components/assign-rows)\n- Variable Declaration Type Automatic Derivation: [inferAssignPlugin](./form-plugins/infer-assign-plugin)\n"
  },
  {
    "path": "apps/docs/src/en/materials/validate/_meta.json",
    "content": "[\n  \"validate-flow-value\"\n]"
  },
  {
    "path": "apps/docs/src/en/materials/validate/validate-flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/validate/validate-flow-value';\n\n# validateFlowValue\n\nvalidateFlowValue is a validation function for verifying the **requiredness and variable reference validity** of [`FlowValue`](../common/flow-value).\n\n## Case Demonstration\n\n### Basic Usage\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { validateFlowValue } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  validate: {\n    dynamic_value_input: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        errorMessages: {\n          required: 'Value is required',\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n    required_dynamic_value_input: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        required: true,\n        errorMessages: {\n          required: 'Value is required',\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n    prompt_editor: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        required: true,\n        errorMessages: {\n          required: 'Prompt is required',\n          unknownVariable: 'Unknown Variable In Template',\n        },\n      }),\n  },\n  render: ({ form }) => (\n    <>\n      <FormHeader />\n      <b>Validate variable valid</b>\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field, fieldState }) => (\n          <>\n            <DynamicValueInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <b>Validate required value</b>\n      <Field<any> name=\"required_dynamic_value_input\">\n        {({ field, fieldState }) => (\n          <>\n            <DynamicValueInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <b>Validate required and variables valid in prompt</b>\n      <Field<any> name=\"prompt_editor\">\n        {({ field, fieldState }) => (\n          <>\n            <PromptEditorWithVariables\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <Button onClick={() => form.validate()}>Trigger Validate</Button>\n    </>\n  ),\n};\n```\n\n## API Reference\n\n### validateFlowValue Function\n\n```typescript\nexport function validateFlowValue(value: IFlowValue | undefined, ctx: Context): {\n  level: FeedbackLevel.Error;\n  message: string;\n} | undefined;\n```\n\n#### Parameters\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `value` | `IFlowValue \\| undefined` | The FlowValue to validate |\n| `ctx` | `Context` | Validation context |\n\n#### Context Interface\n\n```typescript\ninterface Context {\n  node: FlowNodeEntity;\n  required?: boolean; // Whether required\n  errorMessages?: {\n    required?: string; // Required error message\n    unknownVariable?: string; // Unknown variable error message\n  };\n}\n```\n\n#### Return Value\n\n- If validation passes, returns `undefined`\n- If validation fails, returns an object containing error level and error message\n\n### Supported Validation Types\n\n1. **Required Validation**: When `required` is set to `true`, verifies if the value exists and is not empty\n2. **Reference Variable Validation**: For values of type `ref`, verifies if the referenced variable exists\n3. **Template Variable Validation**: For values of type `template`, verifies if all variables referenced in the template exist\n\n## Source Code Guide\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/validate/validate-flow-value/index.ts\"\n/>\n\nUse the CLI command to copy the source code locally:\n\n```bash\nnpx @flowgram.ai/cli@latest materials validate/validate-flow-value\n```\n\n### Directory Structure\n\n```\nvalidate-flow-value/\n└── index.tsx           # Main function implementation, containing validateFlowValue core logic\n```\n\n### Core Implementation\n\n#### Required Validation Logic\n\n```typescript\nif (required && (isNil(value) || isNil(value?.content) || value?.content === '')) {\n  return {\n    level: FeedbackLevel.Error,\n    message: requiredMessage,\n  };\n}\n```\n\n#### Reference Variable Validation Logic\n\n```typescript\nif (value?.type === 'ref') {\n  const variable = node.scope.available.getByKeyPath(value?.content || []);\n  if (!variable) {\n    return {\n      level: FeedbackLevel.Error,\n      message: unknownVariableMessage,\n    };\n  }\n}\n```\n\n#### Template Variable Validation Logic\n\n```typescript\nif (value?.type === 'template') {\n  const allRefs = FlowValueUtils.getTemplateKeyPaths(value);\n\n  for (const ref of allRefs) {\n    const variable = node.scope.available.getByKeyPath(ref);\n    if (!variable) {\n      return {\n        level: FeedbackLevel.Error,\n        message: unknownVariableMessage,\n      };\n    }\n  }\n}\n```\n\n### Flowgram APIs Used\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`FeedbackLevel`](https://flowgram.ai/auto-docs/editor/enums/FeedbackLevel): Feedback level enum\n\n### Dependencies on Other Materials\n\n[**FlowValue**](../common/flow-value)\n- `IFlowValue`: FlowValue type definition\n- `FlowValueUtils`: FlowValue utility class\n  - `getTemplateKeyPaths`: Method to extract all variable reference paths from templates\n\n### Third-party Libraries\n\n[**lodash-es**](https://lodash.com/)\n- `isNil`: Checks if a value is null or undefined\n"
  },
  {
    "path": "apps/docs/src/global.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ndeclare module \"*?raw\" {\n  const content: string;\n  export default content;\n}\n"
  },
  {
    "path": "apps/docs/src/zh/_nav.json",
    "content": "[\n  {\n    \"text\": \"指引\",\n    \"link\": \"/guide/getting-started/introduction\",\n    \"activeMatch\": \"/guide/\"\n  },\n  {\n    \"text\": \"物料\",\n    \"link\": \"/materials/introduction\",\n    \"activeMatch\": \"/materials/\"\n  },\n  {\n    \"text\": \"例子\",\n    \"link\": \"/examples/\",\n    \"activeMatch\": \"/examples/\"\n  },\n  {\n    \"text\": \"API\",\n    \"link\": \"/api/\",\n    \"activeMatch\": \"/api/\"\n  },\n  {\n    \"text\": \"掘金专栏\",\n    \"link\": \"https://juejin.cn/column/7479814468601315362\"\n  },\n  {\n    \"text\": \"TypeDocs\",\n    \"link\": \"/auto-docs/\",\n    \"activeMatch\": \"/auto-docs/\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/zh/api/_meta.json",
    "content": "[\n  {\n    \"type\": \"file\",\n    \"name\": \"index\",\n    \"label\": \"API Overview\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"core\",\n    \"label\": \"Core\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"hooks\",\n    \"label\": \"Hooks\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"components\",\n    \"label\": \"Components\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"services\",\n    \"label\": \"Services\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"utils\",\n    \"label\": \"Utils\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/zh/api/common-apis.mdx",
    "content": "# 常用 API\n\n## FlowDocument （自动化布局文档数据）\n\n```ts\n// 通过 hook 获取，也可以通过ctx\nconst doc = useService<FlowDocument>(FlowDocument)\n\ndoc.fromJSON(data) // 加载数据\ndoc.getAllNodes() // 获取所有节点\ndoc.traverseDFS(node => {}) // 深度遍历节点\ndoc.toJSON() // TODO 这里老版本的数据，还没优化，业务最好自己使用 traverseDFS 实现 json 转换\n\ndoc.addFromNode(targetNode, json) // 插入到指定节点的后边\n\ndoc.onNodeCreate(({ node, json }) => {}) // 监听节点创建，data 为创建时候的json数据\ndoc.onNodeDispose(({ node }) => {}) // 监听节点删除\n```\n\n## WorkflowDocument (自由连线布局文档数据) 继承自 FlowDocument\n\n```ts\nconst doc = useService<WorkflowDocument>(WorkflowDocument)\n\ndoc.fromJSON(data) // 加载数据\ndoc.toJSON() // 导出数据\ndoc.getAllNodes() // 获取所有节点\ndoc.linesManager.getAllLines() // 获取所有线条\n\n// 创建节点\ndoc.createWorkflowNode({ id: nanoid(), type: 'xxx', data: {}, meta: { position: { x: 0, y: 0 } } })\n// 创建线条，from和to 为对应要连线的节点id， fromPort, toPort 如果为单个端口可以不指定\ndoc.linesManager.createLine({ from, to, fromPort, toPort })\n\n// 监听变化，这里会监听线条和节点等事件\ndoc.onContentChange((e) => {\n\n})\n```\n\n## FlowNodeEntity（节点）\n\n```ts\nnode.flowNodeType // 当前节点的type类型\nnode.transform.bounds // 获取节点的外围矩形框, 包含 x,y,width,height\nnode.updateExtInfo({ title: 'xxx' }) // 设置扩展数据, 响应式会刷新节点\nnode.getExtInfo<T>() // 获取扩展数据\nnode.getNodeRegister() // 拿到当前节点的定义\n\nnode.dispose() // 删除节点\n\n// renderData 是 节点 ui相关数据\nconst renderData = node.renderData\nrenderData.node // 当前节点的domNode\nrenderData.expanded // 当前节点是否展开，可以设置\n\n// 拿到所有上游输入和输出节点（自由连线布局）\nnode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData).allInputNodes\nnode.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData).allOutputNodes\n```\n\n## Playground （画布）\n\n```ts\n// 通过 hook 获取，也可以通过ctx\nconst playground = useService(Playground)\n\n// 滚动到指定的节点并居中\nctx.playground.config.scrollToView({\n   entities: [node]\n   scrollToCenter： true\n   easing: true // 缓动动画\n})\n\n// 滚动画布\nctx.playground.config.scroll({\n  scrollX: 0\n  scrollY: 0\n})\n\n// 适配屏幕\nctx.playground.config.fitView(\n  doc.root.getData<FlowNodeTransformData>().bounds, // 需要居中的矩形框，这里拿节点根节点的大小代表最大的框\n  true， // 是否缓动\n  20, // padding，留出空白间距\n)\n\n// 缩放\nctx.playground.config.zoomin()\nctx.playground.config.zoomout()\nctx.playground.config.finalScale // 当前缩放比例\n```\n\n## SelectionService (选择器)\n\n```ts\nconst selectionService = useService<SelectionService>()\n\nselection.selection // 返回当前选中的节点数组，也可以修改，如选中节点 seleciton.selection = [node]\n\nselection.onSelectionChanged(() => {}) // 监听变化\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/editor-renderer.mdx",
    "content": "# EditorRenderer\n\n画布渲染组件，需要 配合 `FixedLayoutEditorProvider` 或 `FreeLayoutEditorProvider` 使用\n\n```tsx pure\nfunction App() {\n  return (\n    <FixedLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" style={{ /* style */}}>\n        {/* 如果提供 children，该内容会放到 画布 div 下边 */}\n      </EditorRenderer>\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/fixed-layout-editor-provider.mdx",
    "content": "# FixedLayoutEditorProvider\n\n固定布局画布配置器，支持 ref\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FixedLayoutEditorProvider {...editorProps} ref={ref}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/fixed-layout-editor.mdx",
    "content": "# FixedLayoutEditor\n\n\n固定布局画布, 等价于 `FixedLayoutEditorProvider` 和 `EditorRenderer` 的组合\n\n```tsx pure\nimport { FixedLayoutEditor, FixedLayoutPluginContext } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n\n  return (\n    <FixedLayoutEditor className=\"demo-editor\" {...editorProps} ref={ref} />\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/free-layout-editor-provider.mdx",
    "content": "# FreeLayoutEditorProvider\n\n自由布局画布配置器，支持 ref\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FreeLayoutEditorProvider {...editorProps} ref={ref}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/free-layout-editor.mdx",
    "content": "# FreeLayoutEditor\n\n自由布局画布, 等价于 `FreeLayoutEditorProvider` 和 `EditorRenderer` 的组合\n\n```tsx pure\nimport { FreeLayoutEditor, FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    console.log(ref.current.document.toJSON())\n  }, [])\n  return (\n    <FreeLayoutEditor className=\"demo-editor\" {...editorProps} ref={ref} />\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/components/workflow-node-renderer.mdx",
    "content": "# WorkflowNodeRenderer(free)\n\n自由布局节点容器\n\n## Usage\n\n```tsx pure\nimport { useNodeRender, WorkflowNodeRenderer } from '@flowgram.ai/free-layout-editor';\n\nexport const BaseNode = () => {\n  /**\n   * 提供节点渲染相关的方法\n   */\n  const { form } = useNodeRender()\n  /**\n   * WorkflowNodeRenderer 会添加节点拖拽事件及 端口渲染，如果要深度定制，可以看该组件源代码:\n   * https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx\n   */\n  return (\n    <WorkflowNodeRenderer\n      className=\"demo-free-node\"\n      node={props.node}\n      // 可选的端口颜色自定义\n      portPrimaryColor=\"#4d53e8\"        // 激活状态颜色 (linked/hovered)\n      portSecondaryColor=\"#9197f1\"      // 默认状态颜色\n      portErrorColor=\"#ff4444\"          // 错误状态颜色\n      portBackgroundColor=\"#ffffff\"     // 背景颜色\n    >\n      {\n        // 表单渲染通过 formMeta 生成\n        form?.render()\n      }\n    </WorkflowNodeRenderer>\n  )\n};\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/_meta.json",
    "content": "[\n  \"flow-document\",\n  \"flow-node-entity\",\n  \"workflow-document\",\n  \"workflow-lines-manager\",\n  \"workflow-line-entity\",\n  \"playground\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/flow-document.mdx",
    "content": "# FlowDocument\n\n流程数据文档 (固定布局), 存储流程的所有节点数据\n\n[> API Detail](https://flowgram.ai/auto-docs/document/classes/FlowDocument.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document)\n```\n\n:::danger\n对节点的操作最好通过 [ctx.operation](/api/services/flow-operation-service.html) 进行操作, 这样才能绑定到 redo/undo\n:::\n\n\n## root\n\n获取画布的根节点，所有节点都挂在根节点下边\n\n```ts pure\nconsole.log(ctx.document.root);\n```\n\n## originTree\n\n画布真实的节点树\n\n```ts pure\n// 监听节点树的变化，如 节点添加/删除/移动\nconst refresh = useRefresh()\nuseEffect(() => {\n  const toDispose = ctx.document.originTree.onTreeChange(() => {\n    // Tree Change\n    refresh()\n  });\n  return () => toDispose.dispose()\n}, [])\n```\n\n## renderTree\n\n画布渲染时的节点树，为了提升性能，渲染的树会随着节点分支折叠而变化，并非真实的树\n\n## getAllNodes\n\n获取所有节点数据\n\n```ts pure\nconst nodes = ctx.document.getAllNodes();\n```\n\n## getNode\n\n通过指定 id 获取节点\n```ts pure\nctx.document.getNode('start')\n```\n\n## getNodeRegistry\n\n获取节点的定义, 节点定义可以根据业务自己扩展配置项\n\n```ts pure\nconst startNodeRegistry = ctx.document.getNodeRegistry<FlowNodeRegistry>('start')\n```\n\n## fromJSON/toJSON\n\n导入和导出数据\n\n```ts pure\nconst json = ctx.document.toJSON();\nctx.document.fromJSON(json);\n```\n\n## registerFlowNodes\n\n注册节点的配置项目, 支持继承\n\n```ts pure\nconst node1: FlowNodeRegistry = {\n  type: 'node1',\n  meta: {}\n}\n\nconst node2: FlowNodeRegistry = {\n  type: 'node2',\n  extend: 'node1' // 继承 node1 的配置\n}\nctx.document.registerFlowNodes(node1, node2)\n```\n\n## addNode\n\n添加节点\n\n```ts pure\nctx.document.addNode({\n  id: 'node1',\n  type: 'start',\n  meta: {},\n  data: {},\n  parent: ctx.document.root // 可以指定父节点\n});\n\n```\n\n## addFromNode\n\n添加到指定节点的后边\n\n```ts pure\nctx.document.addFromNode(\n ctx.document.getNode('start'),\n { id: 'node1', type: 'custom', data: {} }\n);\n\n```\n\n## addBlock\n\n为指定节点添加分支节点\n\n```ts pure\n\nctx.document.addBlock(ctx.document.getNode('condition'), { id: 'if_1', type: 'block', data: {} })\n```\n\n## removeNode\n\n删除节点\n\n```ts pure\nctx.document.removeNode('node1');\n```\n\n## onNodeCreate/onNodeUpdate/onNodeDispose\n\n节点创建/更新/销毁事件, 返回事件的注销函数\n\n```tsx pure\n\nuseEffect(() => {\n  const toDispose1 = ctx.document.onNodeCreate((node) => {\n    console.log('onNodeCreate', node);\n  });\n  const toDispose2 = ctx.document.onNodeUpdate((node) => {\n    console.log('onNodeUpdate', node);\n  });\n  const toDispose3 = ctx.document.onNodeDispose((node) => {\n    console.log('onNodeDispose', node);\n  });\n  return () => {\n    toDispose1.dispose()\n    toDispose2.dispose()\n    toDispose3.dispose()\n  }\n}, []);\n```\n## traverse\n\n从指定节点遍历所有子节点, 默认根节点\n\n```ts pure\n/**\n *\n * traverse all nodes, O(n)\n *   R\n *   |\n *   +---1\n *   |   |\n *   |   +---1.1\n *   |   |\n *   |   +---1.2\n *   |   |\n *   |   +---1.3\n *   |   |    |\n *   |   |    +---1.3.1\n *   |   |    |\n *   |   |    +---1.3.2\n *   |   |\n *   |   +---1.4\n *   |\n *   +---2\n *       |\n *       +---2.1\n *\n *  sort: [1, 1.1, 1.2, 1.3, 1.3.1, 1.3.2, 1.4, 2, 2.1]\n */\nctx.document.traverse((node, depth, index) => {\n  console.log(node.id);\n}, ctx.document.root);\n```\n\n## toString\n\n返回节点结构的字符串快照\n\n```ts pure\nconsole.log(ctx.document.toString())\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/flow-node-entity.mdx",
    "content": "# FlowNodeEntity/WorkflowNodeEntity\n\n节点实体，`WorkflowNodeEntity` 为节点别名用于自由布局节点, 节点实体采用 [ECS](/guide/concepts/ecs.html) 架构, 为 `Entity`\n\n[> API Detail](https://flowgram.ai/auto-docs/document/classes/FlowNodeEntity-1.html)\n\n## Properties\n\n- id: `string` 节点 id\n- flowNodeType: `string` | `number` 节点类型\n- version `number` 节点版本，可以用于判断节点状态是否更新\n\n## Accessors\n\n- document: `FlowDocument | WorkflowDocument` 文档链接\n- bounds: `Rectangle` 获取节点的 x，y，width，height, 等价于 `transform.bounds`\n- blocks: `FlowNodeEntity[]` 获取子节点, 包含折叠的子节点, 等价于 `collapsedChildren`\n- collapsedChildren: `FlowNodeEntity[]` 获取子节点, 包含折叠的子节点\n- allCollapsedChildren: `FlowNodeEntity[]` 获取所有子节点，包括所有折叠的子节点\n- children: `FlowNodeEntity[]` 获取子节点, 不包含折叠的子节点\n- pre: `FlowNodeEntity | undefined` 获取上一个节点\n- next: `FlowNodeEntity | undefined` 获取下一个节点\n- parent: `FlowNodeEntity | undefined` 获取父节点\n- originParent: `FlowNodeEntity | undefined` 获取原始父节点, 这个用于固定布局分支的第一个节点(orderIcon) 找到整个虚拟分支\n- allChildren: `FlowNodeEntity[]` 获取所有子节点, 不包含折叠的子节点\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) 获取节点的 transform 矩阵数据\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) 获取节点的渲染数据, 包含渲染状态等\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) 获取节点的表单数据, 等价于 [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量作用域\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量私有作用域\n- lines: [WorkflowNodeLinesData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodeLinesData.html) 自由布局线条数据\n- ports: [WorkflowNodePortsData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodePortsData.html) 自由布局端口数据\n\n\n## Methods\n\n### getExtInfo\n\n获取节点的扩展信息, 可以通过 `updateExtInfo` 更新扩展信息\n\n```\nnode.getExtInfo<{ test: string }>()\n```\n\n### updateExtInfo\n\n更新扩展数据, 更新不会记录到 `redo/undo`, 如果需要记录，请实现 [history](/guide/advanced/history.html) 服务\n\n```\nnode.updateExtInfo<{ test: string }>({\n  test: 'test'\n})\n```\n\n### getNodeRegistry\n\n获取节点注册器, 等价于 `ctx.document.getNodeRegistry(node.flowNodeType)`\n```ts pure\nconst nodeRegistry = node.getNodeRegistry<FlowNodeRegistry>()\n```\n\n### getData\n\n等价于 [ECS](/guide/concepts/ecs.html) 架构 里获取 Entity 的 Component\n\n```ts pure\nnode.getData(FlowNodeTransformData) // transform 矩阵数据, 包含节点的 x，y，width，height 等信息\nnode.getData(FlowNodeRenderData) // 节点的渲染数据, 包含渲染状态等数据\nnode.lines // 自由布局的线条数据\n\n```\n\n### addData\n\n等价于 [ECS](/guide/concepts/ecs.html) 架构 里添加 Entity 的 Component\n\n```ts pure\n\n// 自定义 EntityData\nclass CustomEntityData extends EntityData<{ key0: string }> {\n  static type = 'CustomEntityData';\n  getDefaultData() {\n    return {\n      key0: 'test'\n    }\n  }\n}\n\n// 添加 Enitty Component\nnode.addData(CustomEntityData)\n\n\n// 更新 Entity Component 数据\nnode.getData(CustomEntityData).update({ key0: 'new value' })\n\n```\n\n### getService\n\n节点访问 [IOC](/guide/concepts/ioc.html) 服务\n\n```ts pure\nnode.getService(SelectionService)\n```\n\n### dispose\n\n节点从画布中销毁\n\n### onDispose\n\n节点销毁事件\n\n```ts pure\nuseEffect(() => {\n  const toDispose = node.onDispose(() => {\n    console.log('Dispose node')\n  })\n  return () => toDispose.dispose()\n}, [node])\n```\n\n### toJSON\n\n导出节点数据\n\n:::note 节点数据基本结构:\n\n- id: `string` 节点唯一标识, 必须保证唯一\n- meta: `object` 节点的 ui 配置信息，如自由布局的 `position` 信息放这里\n- type: `string | number` 节点类型，会和 `nodeRegistries` 中的 `type` 对应\n- data: `object` 节点表单数据, 业务可自定义\n- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`\n\n:::\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/playground.mdx",
    "content": "# Playground\n\n画布实例\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/Playground.html)\n\n```ts pure\nconst ctx = useClientContext()\n\nconsole.log(ctx.playground)\n\n```\n## config\n\n画布配置, 提供 zoom、scroll 等状态\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/PlaygroundConfigEntity.html)\n\n### updateConfig\n- zoom `number` 当前缩放比例\n- scrollX\n- scrollY\n- minZoom\n- maxZoom\n- readonly\n- disabled\n- width\n- height\n\n```ts pure\n// get current config state\nctx.playground.config.config.zoom\nctx.playground.config.config.readonly\n\n// updateConfig\nctx.playground.config.updateConfig({\n  zoom: 0.8,\n  minZoom: 0.1,\n  maxZoom: 2,\n  readonly: true\n})\n```\n\n### fitView\n\n节点适应画布窗口, 需要传入节点的 bounds\n\n```ts pure\n/**\n * 适应大小\n * @param bounds {Rectangle} 目标大小\n * @param easing {number} 是否开启动画，默认开启\n * @param padding {number} 边界空白\n */\nctx.playground.config.fitView(node.bounds, true, 10)\n```\n\n### scrollToView\n\n指定节点位置并滚动到画布可见区域, 如果位置已经在可见区域则不会滚动，除非加上 `scrollToCenter` 强制滚动\n\n```ts pure\n\n/**\n * 详细参数说明\n * @param opts {PlaygroundConfigRevealOpts}\n**/\ninterface PlaygroundConfigRevealOpts {\n  entities?: Entity[]\n  position?: PositionSchema // 滚动到指定位置，并居中\n  bounds?: Rectangle // 滚动的 bounds\n  scrollDelta?: PositionSchema\n  zoom?: number // 需要缩放的比例\n  easing?: boolean // 是否开启缓动，默认开启\n  easingDuration?: number // 默认 500 ms\n  scrollToCenter?: boolean // 是否强制滚动到中心\n}\n\nctx.playground.config.scrollToView({\n  bounds: ctx.document.getNode('start').bounds,\n})\n```\n\n### zoomin\n\n放大画布\n\n### zoomout\n\n缩小画布\n\n### getPoseFromMouseEvent\n\n将浏览器鼠标位置转成画布坐标系\n\n```ts pure\n\nconst pos: { x: number, y: number } = ctx.playground.config.getPoseFromMouseEvent(domMouseEvent)\n\n```\n\n### scroll\n\n滚动画布, 需要传入滚动位置, 以及是否平滑滚动, 滚动时间\n\n```ts pure\nctx.playground.config.scroll({ scrollX: 100, scrollY: 100 }, true, 300)\n```\n\n### isViewportVisible\n\n判断当前节点是否在视窗以内\n\n```ts pure\nctx.playground.config.isViewportVisible(node.bounds)\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/workflow-document.mdx",
    "content": "# WorkflowDocument (free)\n\n自由布局文档数据，继承自 [FlowDocument](/api/core/flow-document.html)\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowDocument.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document)\n```\n\n:::tip\n由于历史原因， 带 `Workflow` 前缀的都代表自由布局\n:::\n\n## linesManager\n\n自由布局线条管理，见 [WorkflowLinesManager](/api/core/workflow-lines-manager.html)\n\n## createWorkflowNodeByType\n\n根据节点类型创建自由布局节点\n\n```ts pure\nconst node = ctx.document.createWorkflowNodeByType(\n 'custom',\n  { x: 100, y: 100 },\n  {\n    id: 'xxxx',\n    data: {}\n  }\n)\n```\n\n## onContentChange\n\n监听自由布局画布数据变化\n\n```ts pure\n\nexport enum WorkflowContentChangeType {\n  /**\n   * 添加节点\n   */\n  ADD_NODE = 'ADD_NODE',\n  /**\n   * 删除节点\n   */\n  DELETE_NODE = 'DELETE_NODE',\n  /**\n   * 移动节点\n   */\n  MOVE_NODE = 'MOVE_NODE',\n  /**\n   * 节点数据更新 （表单引擎数据 或者 extInfo 数据）\n   */\n  NODE_DATA_CHANGE = 'NODE_DATA_CHANGE',\n  /**\n   * 添加线条\n   */\n  ADD_LINE = 'ADD_LINE',\n  /**\n   * 删除线条\n   */\n  DELETE_LINE = 'DELETE_LINE',\n  /**\n   * 节点Meta信息变更\n   */\n  META_CHANGE = 'META_CHANGE',\n}\n\nexport interface WorkflowContentChangeEvent {\n  type: WorkflowContentChangeType;\n  /**\n   * 当前触发的元素的json数据，toJSON 需要主动触发\n   */\n  toJSON: () => any;\n  /*\n   * 当前的事件的 entity\n   */\n  entity: WorkflowNodeEntity | WorkflowLineEntity;\n}\n\n``\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/workflow-line-entity.mdx",
    "content": "# WorkflowLineEntity (free)\n\n自由布局线条实体\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowLineEntity.html)\n\n"
  },
  {
    "path": "apps/docs/src/zh/api/core/workflow-lines-manager.mdx",
    "content": "# WorkflowLinesManager (free)\n\n自由布局线条管理, 目前挂在自由布局 document 下边\n\n[> API Detail](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowLinesManager.html)\n\n```\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\n\nconst ctx = useClientContext();\nconsole.log(ctx.document.linesManager)\n```\n\n## getAllLines\n\n获取所有线条的实体\n\n```ts pure\nconst allLines = ctx.document.linesManager.getAllLines()\n\n```\n\n## createLine\n\n创建线条\n```ts pure\n// from和 to 为对应要连线的节点id， fromPort, toPort 为 端口 id, 如果节点为单个端口可以不指定\nconst line = ctx.document.linesManager.createLine({ from, to, fromPort, toPort })\n```\n\n## toJSON\n\n导出线条数据\n\n```ts pure\nconst json = ctx.document.linesManager.toJSON()\n```\n\n## onAvailableLinesChange\n\n监听所有线条的连线变化\n\n```ts pure\nimport { useEffect } from 'react'\nimport { useClientContext, useRefresh } from '@flowgram.ai/free-layout-editor'\n\n\nfunction SomeReact() {\n  const refresh = useRefresh()\n  const linesManager = useClientContext().document.linesManager\n  useEffect(() => {\n      const dispose = linesManager.onAvailableLinesChange(() => refresh())\n      return () => dispose.dispose()\n  }, [])\n  console.log(ctx.document.linesManager.getAllLines())\n}\n```\n\n## 自定义箭头渲染器\n\nWorkflowLinesManager 支持通过渲染器注册表自定义箭头样式。详细使用方法请参考 [线条配置指南](/zh/guide/free-layout/line#4自定义箭头渲染器) 文档。\n\n```tsx\n// 简单示例：注册自定义箭头\nconst editorProps = {\n  materials: {\n    components: {\n      'arrow-renderer': MyCustomArrow,\n    },\n  },\n};\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/hooks/use-client-context.mdx",
    "content": "# useClientContext\n\n提供在 react 内部访问画布的上下文, 目前固定布局和 自由布局有一定区别\n\n## 固定布局\n\n- Return: [FixedLayoutPluginContext](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/FixedLayoutPluginContext.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor'\nconst ctx = useClientContext()\nconsole.log(ctx.operation) // FlowOperationService 操作服务\nconsole.log(ctx.document) // FlowDocument 数据文档\nconsole.log(ctx.playground) // Playground 画布\nconsole.log(ctx.history) // HistoryService 历史记录\nconsole.log(ctx.clipboard) // ClipboardService 剪贴板\nconsole.log(ctx.container) // Inversify IOC 容器\nconsole.log(ctx.get(MyService)) // 获取任意的 IOC 模块，详细见 自定义 Service\n```\n\n## 自由布局\n\n- Return: [FreeLayoutPluginContext](https://flowgram.ai/auto-docs/free-layout-editor/interfaces/FreeLayoutPluginContext.html)\n\n```ts pure\nimport { useClientContext } from '@flowgram.ai/free-layout-editor'\nconst ctx = useClientContext()\nconsole.log(ctx.document) // WorkflowDocument 数据文档\nconsole.log(ctx.playground) // Playground 画布\nconsole.log(ctx.history) // HistoryService 历史记录\nconsole.log(ctx.selection) // SelectionService 选择器服务\nconsole.log(ctx.clipboard) // ClipboardService 剪贴板\nconsole.log(ctx.container) // Inversify IOC 容器\nconsole.log(ctx.get(MyService)) // 获取任意的 IOC 模块，详细见 自定义 Service\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/hooks/use-node-render.mdx",
    "content": "# useNodeRender\n\n提供节点渲染相关的方法, 返回结果的 form 等价于 [getNodeForm](/api/utils/get-node-form.html)\n\n## 固定布局\n\n- Return: [NodeRenderReturnType](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/NodeRenderReturnType.html)\n\n```tsx pure\n\nimport { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';\n\nexport const BaseNode = ({ node }: { node: FlowNodeEntity }) => {\n  /**\n   * 提供节点渲染相关的方法\n   */\n  const nodeRender = useNodeRender();\n  /**\n   * 只有在节点引擎开启时候才能使用表单\n   */\n  const form = nodeRender.form;\n\n  return (\n    <div\n      className=\"demo-fixed-node\"\n      /*\n       * onMouseEnter 加到固定布局节点主要是为了监听 分支线条的 hover 高亮\n       **/\n      onMouseEnter={nodeRender.onMouseEnter}\n      onMouseLeave={nodeRender.onMouseLeave}\n      onMouseDown={e => {\n        // trigger drag node\n        nodeRender.startDrag(e);\n        e.stopPropagation();\n      }}\n      style={{\n        /**\n         * 用于精确控制分支节点的样式\n         * isBlockIcon: 整个 condition 分支的 头部节点\n         * isBlockOrderIcon: 分支的第一个节点\n         */\n        ...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? { width: 260 } : {}),\n      }}\n    >\n      {form?.render()}\n    </div>\n  );\n};\n\n```\n\n## 自由布局\n\n- Return: [NodeRenderReturnType](https://flowgram.ai/auto-docs/free-layout-core/interfaces/NodeRenderReturnType.html)\n\n```tsx pure\nimport { WorkflowNodeRenderer, useNodeRender } from '@flowgram.ai/free-layout-editor';\nexport const BaseNode = () => {\n  const { form, node } = useNodeRender()\n  return (\n    <WorkflowNodeRenderer className=\"demo-free-node\" node={node}>\n      {form?.render()}\n    </WorkflowNodeRenderer>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/hooks/use-playground-tools.mdx",
    "content": "# usePlaygroundTools\n\n画布工具方法\n\n## 固定布局\n\n- Return: [PlaygroundTools](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/PlaygroundTools.html)\n```tsx pure\nimport { useEffect, useState } from 'react'\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 16, display: 'flex', gap: 8 }}>\n    <button onClick={() => tools.zoomin()}>ZoomIn</button>\n    <button onClick={() => tools.zoomout()}>ZoomOut</button>\n    <button onClick={() => tools.fitView()}>Fitview</button>\n    <button onClick={() => tools.changeLayout()}>ChangeLayout</button>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n    <span>{Math.floor(tools.zoom * 100)}%</span>\n  </div>\n}\n```\n\n\n## 自由布局\n\n- Return: [PlaygroundTools](https://flowgram.ai/auto-docs/free-layout-editor/interfaces/PlaygroundTools.html)\n\n```tsx pure\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}>\n    <button onClick={() => tools.zoomin()}>ZoomIn</button>\n    <button onClick={() => tools.zoomout()}>ZoomOut</button>\n    <button onClick={() => tools.fitView()}>Fitview</button>\n    <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n    <span>{Math.floor(tools.zoom * 100)}%</span>\n  </div>\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/hooks/use-refresh.mdx",
    "content": "# useRefresh\n\n## Source Code\n\n```ts\nimport { useCallback, useState } from 'react';\n\nexport function useRefresh(defaultValue?: any): (v?: any) => void {\n  const [, update] = useState<any>(defaultValue);\n  return useCallback((v?: any) => update(v !== undefined ? v : {}), []);\n}\n```\n\n## Usage\n\n```tsx pure\nimport { useRefresh } from '@flowgram.ai/fixed-layout-editor';\n\nfunction Demo() {\n  const refresh = useRefresh();\n  return (\n    <div>\n      <button onClick={() => refresh()}>Refresh</button>\n    </div>\n  )\n}\n\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/api/hooks/use-service.mdx",
    "content": "# useService\n\n获取底层 [IOC](/guide/concepts/ioc.html) 的所有单例模块\n\n```ts pure\n\nconst playground = useService<Playground>(Playground)\nconst flowDocument = useService<FlowDocument>(FlowDocument)\nconst historyService = useService<HistoryService>(HistoryService)\n\n// 等价\nconst playground1 = useClientContext().playground\n\n// 等价\nconst playground3 = useClientContext().get<Playground>(Playground)\n\n```\n\n\n\n## 自定义 Service\n\n```tsx pure\n/**\n *  inversify: https://github.com/inversify/InversifyJS\n */\nimport { injectable, inject } from 'inversify'\nimport { FlowDocument } from '@flowgram.ai/fixed-layout-editor'\n\n@injectable()\nclass MyService {\n  // 依赖注入单例模块\n  @inject(FlowDocument) flowDocument: FlowDocument\n  // ...\n}\n\nimport { useMemo } from 'react';\nimport { type FixedLayoutProps } from '@flowgram.ai/fixed-layout-editor';\n\nfunction BaseNode() {\n  const mySerivce = useService<MyService>(MyService)\n}\n\nexport function useEditorProps(\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      // ....other props\n      onBind: ({ bind }) => {\n        bind(MyService).toSelf().inSingletonScope()\n      },\n      materials: {\n        renderDefaultNode: BaseNode\n      }\n    }),\n    [],\n  );\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/index.mdx",
    "content": "---\n# API Overview\ntitle: API 预览\noverview: true\n---\n"
  },
  {
    "path": "apps/docs/src/zh/api/plugins.mdx",
    "content": "---\noverview: true\noverviewHeaders: [2]\n---\n这里是官网 api 配置，demo 用。\n"
  },
  {
    "path": "apps/docs/src/zh/api/services/clipboard-service.mdx",
    "content": "# ClipboardService\n\n剪贴板服务\n\n[> API Detail](https://flowgram.ai/auto-docs/core/interfaces/ClipboardService.html)\n"
  },
  {
    "path": "apps/docs/src/zh/api/services/command-service.mdx",
    "content": "# CommandService\n\n指令服务，需要和 [Shortcuts](/guide/advanced/shortcuts.html) 一起使用\n\n[> API Detail](https://flowgram.ai/auto-docs/command/interfaces/CommandService.html)\n\n\n```typescript pure\n\nctx.get(CommandService).execCommand('selectAll')\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/services/flow-operation-service.mdx",
    "content": "# FlowOperationService\n\n节点操作服务, 目前用于固定布局，自由布局现阶段可通过 WorkflowDocument 直接操作, 后续也会抽象出 operation\n\n[> API Detail](https://flowgram.ai/auto-docs/fixed-layout-editor/interfaces/FlowOperationService.html)\n\n```typescript pure\nconst operationService = useService<FlowOperationService>(FlowOperationService)\noperationService.addNode({ id: 'xxx', type: 'custom', data: {} })\n\n// or\nconst ctx = useClientContext();\nctx.operation.addNode({ id: 'xxx', type: 'custom', data: {} })\n\n\n```\n\n## Interface\n\n```typescript pure\n\nexport interface FlowOperationBaseService extends Disposable {\n  /**\n   * 执行操作\n   * @param operation 可序列化的操作\n   * @returns 操作返回\n   */\n  apply(operation: FlowOperation): any;\n\n  /**\n   * 添加节点，如果节点已经存在则不会重复创建\n   * @param nodeJSON 节点数据\n   * @param config 配置\n   * @returns 成功添加的节点\n   */\n  addNode(nodeJSON: FlowNodeJSON, config?: AddNodeConfig): FlowNodeEntity;\n\n  /**\n   * 基于某一个起始节点往后面添加\n   * @param fromNode 起始节点\n   * @param nodeJSON 添加的节点JSON\n   */\n  addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity;\n\n  /**\n   * 删除节点\n   * @param node 节点\n   * @returns\n   */\n  deleteNode(node: FlowNodeEntityOrId): void;\n\n  /**\n   * 批量删除节点\n   * @param nodes\n   */\n  deleteNodes(nodes: FlowNodeEntityOrId[]): void;\n\n  /**\n   * 添加块（分支）\n   * @param target 目标\n   * @param blockJSON 块数据\n   * @param config 配置\n   * @returns\n   */\n  addBlock(\n    target: FlowNodeEntityOrId,\n    blockJSON: FlowNodeJSON,\n    config?: AddBlockConfig,\n  ): FlowNodeEntity;\n\n  /**\n   * 移动节点\n   * @param node 被移动的节点\n   * @param config 移动节点配置\n   */\n  moveNode(node: FlowNodeEntityOrId, config?: MoveNodeConfig): void;\n\n  /**\n   * 拖拽节点\n   * @param param0\n   * @returns\n   */\n  dragNodes({ dropNode, nodes }: { dropNode: FlowNodeEntity; nodes: FlowNodeEntity[] }): void;\n\n  /**\n   * 添加节点的回调\n   */\n  onNodeAdd: Event<OnNodeAddEvent>;\n}\n\nexport interface FlowOperationService extends FlowOperationBaseService {\n  /**\n   * 创建分组\n   * @param nodes 节点列表\n   */\n  createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined;\n  /**\n   * 取消分组\n   * @param groupNode\n   */\n  ungroup(groupNode: FlowNodeEntity): void;\n  /**\n   * 开始事务\n   */\n  startTransaction(): void;\n  /**\n   * 结束事务\n   */\n  endTransaction(): void;\n  /**\n   * 修改表单数据\n   * @param node 节点\n   * @param path 属性路径\n   * @param value 值\n   */\n  setFormValue(node: FlowNodeEntityOrId, path: string, value: unknown): void;\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/services/history-service.mdx",
    "content": "## HistoryService\n\n[> API Detail](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryService.html)\n\n## Redo/Undo\n\n```tsx pure\nimport { useEffect, useState } from 'react'\nimport { useClientContext } from '@flowgram.ai/fixed-layout-editor';\n\nexport function Tools() {\n  const { history } = useClientContext();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return <div>\n    <button onClick={() => history.undo()} disabled={!canUndo}>Undo</button>\n    <button onClick={() => history.redo()} disabled={!canRedo}>Redo</button>\n  </div>\n}\n```\n\n## 渲染历史记录\n\n```tsx pure\nimport { useEffect } from 'react'\nimport { useRefresh, useClientContext } from '@flowgram.ai/fixed-layout-editor'\n\nfunction HistoryListRender() {\n  const refresh = useRefresh()\n  const ctx = useClientContext()\n  useEffect(() => {\n    ctx.history.onApply(() => refresh())\n  }, [ctx])\n  return (\n    <div>\n      {ctx.history.historyManager.historyStack.items.map((record) => <HistoryOperations key={record.id} operations={record.operations} />)}\n    </div>\n  )\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/services/selection-service.mdx",
    "content": "# SelectionService\n\n用于控制选择的节点\n\n[> API Detail](https://flowgram.ai/auto-docs/core/classes/SelectionService.html)\n\n## Usage\n```tsx pure\n// Listen Selection Change\nctx.selection.onSelectionChanged((nodes) => {\n})\n// Select All Nodes\nctx.selection.selection = ctx.document.getAllNodes()\n```\n"
  },
  {
    "path": "apps/docs/src/zh/api/utils/disposable-collection.mdx",
    "content": "# DisposableCollection\n\n## Usage\n\n\n```ts pure\n\nimport { DisposableCollection, Disposable } from '@flowgram.ai/utils'\nconst disposable1: Disposable = {\n  dispose() {\n    console.log(1)\n  },\n};\nconst disposable2: Disposable = {\n  dispose() {\n    console.log(2)\n  },\n};\nconst dc = new DisposableCollection();\ndc.onDispose(() => {\n  console.log('end')\n});\n\ndc.pushAll([disposable1, disposable2]);\ndc.dispose(); // Log: 1, 2, dispose end\n\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/disposable.ts\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/api/utils/disposable.mdx",
    "content": "# Disposable\n\n## Interface\n\n```ts\n/**\n * An object that performs a cleanup operation when `.dispose()` is called.\n *\n * Some examples of how disposables are used:\n *\n * - An event listener that removes itself when `.dispose()` is called.\n * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.\n */\nexport interface Disposable {\n  dispose(): void;\n}\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/disposable.ts\n"
  },
  {
    "path": "apps/docs/src/zh/api/utils/emitter.mdx",
    "content": "# Emitter\n\n事件模块\n\n\n## Usage\n\n```tsx pure\nimport { Emitter } from '@flowgram.ai/utils'\n\nclass Doc {\n  private _content = ''\n  private _onContentChangeEmitter = new Emitter<string>()\n  readonly onContentChange = this._onContentChangeEmitter.event\n  setContent(content: string) {\n    this._content = content\n    this._onContentChangeEmitter.fire(content)\n  }\n  get content() {\n    return this._content\n  }\n}\n\nfunction App() {\n  const doc1 = useMemo(() => new Doc(), [])\n  const [content, updateContent] = useState(doc1.content)\n  useEffect(() => {\n    const toDispose = doc1.onContentChange((content) => {\n      updateContent(content)\n    })\n    return () => toDispose.dispose()\n  }, [doc1])\n  return <div>{content}</div>\n}\n\n\n```\n\n## Source Code\n\nhttps://github.com/bytedance/flowgram.ai/blob/main/packages/common/utils/src/event.ts\n\n"
  },
  {
    "path": "apps/docs/src/zh/api/utils/get-node-form.mdx",
    "content": "# getNodeForm\n\n获取节点的表单能力，需要开启 节点引擎才能使用\n\n[> API Detail](https://flowgram.ai/auto-docs/editor/functions/getNodeForm.html)\n\n\n## Usage\n\n```tsx pure\n\n// 1. BaseNode\nfunction BaseNode({ node }) {\n  const form = getNodeForm(node);\n  console.log(form.getValueIn('title'))\n  return <div>{form?.render()}</div>\n}\n\n// 2. useNodeRender\nfunction BaseNode() {\n  const { form } = useNodeRender();\n  console.log(form.getValueIn('title'))\n  return <div>{form?.render()}</div>\n}\n\n```\n\n## Return Inteface\n\n```ts pure\n\nexport interface NodeFormProps<TValues> {\n  /**\n   * The initialValues of the form.\n   */\n  initialValues: TValues;\n  /**\n   * Form values. Returns a deep copy of the data in the store.\n   */\n  values: TValues;\n  /**\n   * Form state\n   */\n  state: FormState;\n  /**\n   * Get value in certain path\n   * @param name path\n   */\n  getValueIn<TValue = FieldValue>(name: FieldName): TValue;\n\n  /**\n   * Set value in certain path.\n   * It will trigger the re-rendering of the Field Component if a Field is related to this path\n   * @param name path\n   */\n  setValueIn<TValue>(name: FieldName, value: TValue): void;\n  /**\n   * set form values\n   */\n  updateFormValues(values: any): void;\n  /**\n   * Render form\n   */\n  render: () => React.ReactNode;\n  /**\n   * Form value change event\n   */\n  onFormValuesChange: Event<OnFormValuesChangePayload>;\n  /**\n   * Trigger form validate\n   */\n  validate: () => Promise<boolean>;\n  /**\n   * Form validate event\n   */\n  onValidate: Event<FormState>;\n  /**\n   * Form field value change event\n   */\n  onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(\n    name: FieldName,\n    callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void\n  ): Disposable;\n}\n\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/examples/_meta.json",
    "content": "[\n  {\n    \"type\": \"file\",\n    \"name\": \"index\",\n    \"label\": \"体验环境\"\n  },\n  \"playground\",\n  {\n    \"type\": \"dir\",\n    \"name\": \"fixed-layout\",\n    \"label\": \"固定布局\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"free-layout\",\n    \"label\": \"自由布局\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"node-form\",\n    \"label\": \"节点表单\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/zh/examples/fixed-layout/_meta.json",
    "content": "[\n  \"fixed-layout-simple\",\n  \"fixed-composite-nodes\",\n  \"fixed-feature-overview\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/examples/fixed-layout/fixed-composite-nodes.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 复合节点\n\nimport { CompositeNodesPreview } from '../../../../components';\n\n<CompositeNodesPreview cellHeight={500}/>\n\n## 安装\n\n```bash\nnpx @flowgram.ai/create-app@latest fixed-layout-simple\n```\n\n## 源码\n\n- jsonData: https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout-simple/src/data\n- nodeRegistries: https://github.com/bytedance/flowgram.ai/tree/main/packages/canvas-engine/fixed-layout-core/src/activities\n"
  },
  {
    "path": "apps/docs/src/zh/examples/fixed-layout/fixed-feature-overview.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 最佳实践\n\nimport { FixedFeatureOverview } from '../../../../components';\n\n<FixedFeatureOverview />\n\n\n## 安装\n\n```shell\nnpx @flowgram.ai/create-app@latest fixed-layout\n```\n\n## 源码\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout\n\n\n## 项目概览\n\n### 核心技术栈\n- **前端框架**: React 18 + TypeScript\n- **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)\n- **样式方案**: Less + Styled Components + CSS Variables\n- **UI 组件库**: Semi Design (@douyinfe/semi-ui)\n- **状态管理**: 基于 Flowgram 自研的编辑器框架\n- **依赖注入**: Inversify\n\n\n### 核心依赖包\n\n- **@flowgram.ai/fixed-layout-editor**: 固定布局编辑器核心依赖\n- **@flowgram.ai/fixed-semi-materials**: Semi Design 物料库\n- **@flowgram.ai/form-materials**: 表单物料库\n- **@flowgram.ai/group-plugin**: 分组插件\n- **@flowgram.ai/minimap-plugin**: 缩略图插件\n\n## 代码说明\n\n```\nsrc/\n├── app.tsx                    # 应用入口组件\n├── editor.tsx                 # 主编辑器组件\n├── index.ts                   # 模块导出入口\n├── initial-data.ts            # 初始化数据配置\n├── type.d.ts                  # 全局类型声明\n│\n├── assets/                    # 静态资源\n│   ├── icon-mouse.tsx         # 鼠标图标组件\n│   └── icon-pad.tsx           # 触控板图标组件\n│\n├── components/                # 通用组件库\n│   ├── index.ts               # 组件导出入口\n│   ├── node-list.tsx          # 节点列表组件\n│   │\n│   ├── agent-adder/           # Agent 添加器组件\n│   │   └── index.tsx\n│   ├── agent-label/           # Agent 标签组件\n│   │   └── index.tsx\n│   ├── base-node/             # 基础节点组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── branch-adder/          # 分支添加器组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── drag-node/             # 拖拽节点组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── node-adder/            # 节点添加器组件\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   └── utils.ts\n│   ├── selector-box-popover/  # 选择框弹出层组件\n│   │   └── index.tsx\n│   ├── sidebar/               # 侧边栏组件\n│   │   ├── index.tsx\n│   │   ├── sidebar-node-renderer.tsx\n│   │   ├── sidebar-provider.tsx\n│   │   └── sidebar-renderer.tsx\n│   └── tools/                 # 工具栏组件群\n│       ├── index.tsx\n│       ├── styles.tsx\n│       ├── fit-view.tsx       # 适应视图工具\n│       ├── minimap-switch.tsx # 缩略图开关\n│       ├── minimap.tsx        # 缩略图组件\n│       ├── readonly.tsx       # 只读模式切换\n│       ├── run.tsx            # 运行工具\n│       ├── save.tsx           # 保存工具\n│       ├── switch-vertical.tsx # 垂直布局切换\n│       └── zoom-select.tsx    # 缩放选择器\n│\n├── context/                   # React Context 状态管理\n│   ├── index.ts               # Context 导出入口\n│   ├── node-render-context.ts # 节点渲染上下文\n│   └── sidebar-context.ts     # 侧边栏上下文\n│\n├── form-components/           # 表单组件库\n│   ├── index.ts               # 表单组件导出入口\n│   ├── feedback.tsx           # 反馈组件\n│   │\n│   ├── form-content/          # 表单内容组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-header/           # 表单头部组件\n│   │   ├── index.tsx\n│   │   ├── styles.tsx\n│   │   ├── title-input.tsx\n│   │   └── utils.tsx\n│   ├── form-inputs/           # 表单输入组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   ├── form-item/             # 表单项组件\n│   │   ├── index.css\n│   │   └── index.tsx\n│   ├── form-outputs/          # 表单输出组件\n│   │   ├── index.tsx\n│   │   └── styles.tsx\n│   └── properties-edit/       # 属性编辑组件\n│       ├── index.tsx\n│       ├── property-edit.tsx\n│       └── styles.tsx\n│\n├── hooks/                     # 自定义 React Hooks\n│   ├── index.ts               # Hooks 导出入口\n│   ├── use-editor-props.ts    # 编辑器属性 Hook\n│   ├── use-is-sidebar.ts      # 侧边栏状态 Hook\n│   └── use-node-render-context.ts # 节点渲染上下文 Hook\n│\n├── nodes/                     # 流程节点定义\n│   ├── index.ts               # 节点注册表\n│   ├── default-form-meta.tsx  # 默认表单元数据\n│   │\n│   ├── agent/                 # Agent 节点类型\n│   │   ├── index.ts\n│   │   ├── agent.ts\n│   │   ├── agent-llm.ts\n│   │   ├── agent-memory.ts\n│   │   ├── agent-tools.ts\n│   │   ├── memory.ts\n│   │   └── tool.ts\n│   ├── break-loop/            # 跳出循环节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case/                  # Case 分支节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── case-default/          # 默认 Case 节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── catch-block/           # 异常捕获块节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── end/                   # 结束节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── if/                    # 条件判断节点\n│   │   └── index.ts\n│   ├── if-block/              # 条件块节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── llm/                   # LLM 节点\n│   │   └── index.ts\n│   ├── loop/                  # 循环节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── start/                 # 开始节点\n│   │   ├── index.ts\n│   │   └── form-meta.tsx\n│   ├── switch/                # Switch 分支节点\n│   │   └── index.ts\n│   └── trycatch/              # Try-Catch 节点\n│       ├── index.ts\n│       └── form-meta.tsx\n│\n├── plugins/                   # 插件系统\n│   ├── index.ts               # 插件导出入口\n│   │\n│   ├── clipboard-plugin/      # 剪贴板插件\n│   │   └── create-clipboard-plugin.ts\n│   ├── group-plugin/          # 分组插件\n│   │   ├── index.ts\n│   │   ├── group-box-header.tsx\n│   │   ├── group-node.tsx\n│   │   ├── group-note.tsx\n│   │   ├── group-tools.tsx\n│   │   ├── icons/\n│   │   │   └── index.tsx\n│   │   └── multilang-textarea-editor/\n│   │       ├── index.css\n│   │       ├── index.tsx\n│   │       └── base-textarea.tsx\n│   └── variable-panel-plugin/ # 变量面板插件\n│       ├── index.ts\n│       ├── variable-panel-layer.tsx\n│       ├── variable-panel-plugin.ts\n│       └── components/\n│           ├── full-variable-list.tsx\n│           ├── global-variable-editor.tsx\n│           └── variable-panel.tsx\n│\n├── services/                  # 服务层\n│   ├── index.ts\n│   └── custom-service.ts      # 自定义服务\n│\n├── shortcuts/                 # 快捷键系统\n│   ├── index.ts\n│   ├── constants.ts           # 快捷键常量\n│   └── utils.ts               # 快捷键工具函数\n│\n└── typings/                   # 类型定义\n    ├── index.ts               # 类型导出入口\n    ├── json-schema.ts         # JSON Schema 类型\n    └── node.ts                # 节点类型定义\n```\n\n## 架构设计分析\n\n### 整体架构模式\n\n该项目采用了**分层架构**和**模块化设计**相结合的架构模式：\n\n1. **表现层 (Presentation Layer)**\n- 组件层：负责 UI 渲染和用户交互\n- 工具层：提供编辑器工具功能\n\n2. **业务逻辑层 (Business Logic Layer)**\n- 节点系统：定义各种流程节点的行为和属性\n- 插件系统：提供可扩展的功能模块\n- 服务层：处理业务逻辑和数据操作\n\n3. **数据层 (Data Layer)**\n- Context 状态管理：管理应用全局状态\n- 类型系统：确保数据结构的一致性\n\n### 核心设计模式\n\n#### 1. 提供者模式 (Provider Pattern)\n```typescript\n// 主编辑器组件使用多层 Provider 嵌套\n<FixedLayoutEditorProvider {...editorProps}>\n  <SidebarProvider>\n    <EditorRenderer />\n    <DemoTools />\n    <SidebarRenderer />\n  </SidebarProvider>\n</FixedLayoutEditorProvider>\n```\n\n**应用场景**:\n- `FixedLayoutEditorProvider`: 提供编辑器核心功能和状态\n- `SidebarProvider`: 管理侧边栏的显示状态和选中节点\n\n#### 2. 注册表模式 (Registry Pattern)\n```typescript\nexport const FlowNodeRegistries: FlowNodeRegistry[] = [\n  StartNodeRegistry,\n  EndNodeRegistry,\n  SwitchNodeRegistry,\n  LLMNodeRegistry,\n  // ... 更多节点类型\n];\n```\n\n**设计优势**:\n- 支持动态节点类型注册\n- 易于扩展新的节点类型\n- 实现了节点类型的解耦\n\n#### 3. 插件模式 (Plugin Pattern)\n```typescript\nplugins: () => [\n  createMinimapPlugin({...}),\n  createGroupPlugin({...}),\n  createClipboardPlugin(),\n  createVariablePanelPlugin({}),\n]\n```\n\n**插件系统特点**:\n- **缩略图插件**: 提供画布缩略图功能\n- **分组插件**: 支持节点分组管理\n- **剪贴板插件**: 实现复制粘贴功能\n- **变量面板插件**: 提供变量管理界面\n\n#### 4. 工厂模式 (Factory Pattern)\n在节点创建和配置中广泛使用：\n```typescript\ngetNodeDefaultRegistry(type) {\n  return {\n    type,\n    meta: {\n      defaultExpanded: true,\n    },\n  };\n}\n```\n\n#### 5. 观察者模式 (Observer Pattern)\n通过历史记录系统实现：\n```typescript\nhistory: {\n  enable: true,\n  enableChangeNode: true,\n  onApply: debounce((ctx, opt) => {\n    console.log('auto save: ', ctx.document.toJSON());\n  }, 100),\n}\n```\n\n#### 6. 策略模式 (Strategy Pattern)\n在材料系统中体现：\n```typescript\nmaterials: {\n  components: {\n    ...defaultFixedSemiMaterials,\n    [FlowRendererKey.ADDER]: NodeAdder,\n    [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n    // 可根据 key 替换不同的渲染策略\n  }\n}\n```\n\n### 状态管理架构\n\n#### Context 系统设计\n项目采用了多个专用的 Context 来管理不同领域的状态：\n\n1. **SidebarContext**: 管理侧边栏状态\n```typescript\nexport const SidebarContext = React.createContext<{\n  visible: boolean;\n  nodeId?: string;\n  setNodeId: (node: string | undefined) => void;\n}>({ visible: false, setNodeId: () => {} });\n```\n\n2. **NodeRenderContext**: 管理节点渲染相关状态\n3. **IsSidebarContext**: 简单的布尔状态管理\n\n#### 自定义 Hooks 设计\n- `useEditorProps`: 集中管理编辑器的所有配置属性\n- `useIsSidebar`: 判断当前是否在侧边栏环境中\n- `useNodeRenderContext`: 获取节点渲染上下文\n\n### 组件架构设计\n\n#### 组件分层结构\n1. **基础组件层**\n- `BaseNode`: 所有节点的基础渲染组件\n- `DragNode`: 拖拽状态下的节点组件\n\n2. **功能组件层**\n- 添加器组件: `NodeAdder`, `BranchAdder`, `AgentAdder`\n- 工具组件: 缩放、保存、运行等功能组件\n\n3. **容器组件层**\n- `Sidebar`: 侧边栏容器及其子组件\n- `Tools`: 工具栏容器\n\n### 数据流架构\n\n#### 初始数据结构\n项目定义了完整的初始流程数据，包含多种节点类型的示例：\n- **Start 节点**: 流程起始点，定义输出参数\n- **Agent 节点**: 包含 LLM、Memory、Tools 子组件\n- **LLM 节点**: 大语言模型处理节点\n- **Switch 节点**: 条件分支节点\n- **Loop 节点**: 循环处理节点\n- **TryCatch 节点**: 异常处理节点\n- **End 节点**: 流程结束点\n\n#### 数据转换机制\n```typescript\nfromNodeJSON(node, json) {\n  return json; // 数据导入时的转换逻辑\n},\ntoNodeJSON(node, json) {\n  return json; // 数据导出时的转换逻辑\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/examples/fixed-layout/fixed-layout-simple.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 基础用法\n\nimport { FixedLayoutSimplePreview } from '../../../../components';\n\n<FixedLayoutSimplePreview />\n\n## 安装\n\n```bash\nnpx @flowgram.ai/create-app@latest fixed-layout-simple\n```\n\n## 源码\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-fixed-layout-simple\n"
  },
  {
    "path": "apps/docs/src/zh/examples/free-layout/_meta.json",
    "content": "[\n  \"free-layout-simple\",\n  \"free-feature-overview\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/examples/free-layout/free-feature-overview.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 最佳实践\n\nimport { FreeFeatureOverview } from '../../../../components';\n\n<FreeFeatureOverview />\n\n## 安装\n\n```shell\nnpx @flowgram.ai/create-app@latest free-layout\n```\n\n## 源码\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout\n\n## 项目概览\n\n### 核心技术栈\n- **前端框架**: React 18 + TypeScript\n- **构建工具**: Rsbuild (基于 Rspack 的现代构建工具)\n- **样式方案**: Less + Styled Components + CSS Variables\n- **UI 组件库**: Semi Design (@douyinfe/semi-ui)\n- **状态管理**: 基于 Flowgram 自研的编辑器框架\n- **依赖注入**: Inversify\n\n### 核心依赖包\n\n- **@flowgram.ai/free-layout-editor**: 自由布局编辑器核心依赖\n- **@flowgram.ai/free-snap-plugin**: 自动对齐及辅助线插件\n- **@flowgram.ai/free-lines-plugin**: 连线渲染插件\n- **@flowgram.ai/free-node-panel-plugin**: 节点添加面板渲染插件\n- **@flowgram.ai/minimap-plugin**: 缩略图插件\n- **@flowgram.ai/free-container-plugin**: 子画布插件\n- **@flowgram.ai/free-group-plugin**: 分组插件\n- **@flowgram.ai/form-materials**: 表单物料\n- **@flowgram.ai/runtime-interface**: 运行时接口\n- **@flowgram.ai/runtime-js**: js 运行时模块\n- **@flowgram.ai/panel-manager-plugin**:  侧边栏面板管理\n\n## 代码说明\n\n### 目录结构\n```\nsrc/\n├── app.tsx                  # 应用入口文件\n├── editor.tsx               # 编辑器主组件\n├── initial-data.ts          # 初始化数据配置\n├── assets/                  # 静态资源\n├── components/              # 组件库\n│   ├── index.ts\n│   ├── add-node/            # 添加节点组件\n│   ├── base-node/           # 基础节点组件\n│   ├── comment/             # 注释组件\n│   ├── group/               # 分组组件\n│   ├── line-add-button/     # 连线添加按钮\n│   ├── node-menu/           # 节点菜单\n│   ├── node-panel/          # 节点添加面板\n│   ├── selector-box-popover/ # 选择框弹窗\n│   ├── sidebar/             # 侧边栏\n│   ├── testrun/             # 测试运行组件\n│   │   ├── hooks/           # 测试运行钩子\n│   │   ├── node-status-bar/ # 节点状态栏\n│   │   ├── testrun-button/  # 测试运行按钮\n│   │   ├── testrun-form/    # 测试运行表单\n│   │   ├── testrun-json-input/ # JSON输入组件\n│   │   └── testrun-panel/   # 测试运行面板\n│   └── tools/               # 工具组件\n├── context/                 # React Context\n│   ├── node-render-context.ts # 当前渲染节点 Context\n│   ├── sidebar-context        # 侧边栏 Context\n├── form-components/         # 表单组件库\n│   ├── form-content/        # 表单内容\n│   ├── form-header/         # 表单头部\n│   ├── form-inputs/         # 表单输入\n│   └── form-item/           # 表单项\n│   └── feedback.tsx         # 表单校验错误渲染\n├── hooks/\n│   ├── index.ts\n│   ├── use-editor-props.tsx # 编辑器属性钩子\n│   ├── use-is-sidebar.ts    # 侧边栏状态钩子\n│   ├── use-node-render-context.ts # 节点渲染上下文钩子\n│   └── use-port-click.ts    # 端口点击钩子\n├── nodes/                    # 节点定义\n│   ├── index.ts\n│   ├── constants.ts         # 节点常量定义\n│   ├── default-form-meta.ts # 默认表单元数据\n│   ├── block-end/           # 块结束节点\n│   ├── block-start/         # 块开始节点\n│   ├── break/               # 中断节点\n│   ├── code/                # 代码节点\n│   ├── comment/             # 注释节点\n│   ├── condition/           # 条件节点\n│   ├── continue/            # 继续节点\n│   ├── end/                 # 结束节点\n│   ├── group/               # 分组节点\n│   ├── http/                # HTTP节点\n│   ├── llm/                 # LLM节点\n│   ├── loop/                # 循环节点\n│   ├── start/               # 开始节点\n│   └── variable/            # 变量节点\n├── plugins/                 # 插件系统\n│   ├── index.ts\n│   ├── context-menu-plugin/ # 右键菜单插件\n│   ├── runtime-plugin/      # 运行时插件\n│   │   ├── client/          # 客户端\n│   │   │   ├── browser-client/ # 浏览器客户端\n│   │   │   └── server-client/  # 服务器客户端\n│   │   └── runtime-service/ # 运行时服务\n│   └── variable-panel-plugin/ # 变量面板插件\n│       └── components/      # 变量面板组件\n├── services/                 # 服务层\n│   ├── index.ts\n│   └── custom-service.ts    # 自定义服务\n├── shortcuts/                # 快捷键系统\n│   ├── index.ts\n│   ├── constants.ts         # 快捷键常量\n│   ├── shortcuts.ts         # 快捷键定义\n│   ├── type.ts              # 类型定义\n│   ├── collapse/            # 折叠快捷键\n│   ├── copy/                # 复制快捷键\n│   ├── delete/              # 删除快捷键\n│   ├── expand/              # 展开快捷键\n│   ├── paste/               # 粘贴快捷键\n│   ├── select-all/          # 全选快捷键\n│   ├── zoom-in/             # 放大快捷键\n│   └── zoom-out/            # 缩小快捷键\n├── styles/                   # 样式文件\n├── typings/                  # 类型定义\n│   ├── index.ts\n│   ├── json-schema.ts       # JSON Schema类型\n│   └── node.ts              # 节点类型定义\n└── utils/                    # 工具函数\n    ├── index.ts\n    └── on-drag-line-end.ts  # 拖拽连线结束处理\n```\n\n### 关键目录功能说明\n\n#### 1. `/components` - 组件库\n- **base-node**: 所有节点的基础渲染组件\n- **testrun**: 完整的测试运行功能模块，包含状态栏、表单、面板等\n- **sidebar**: 侧边栏组件，提供工具和属性面板\n- **node-panel**: 节点添加面板，支持拖拽添加新节点\n\n#### 2. `/nodes` - 节点系统\n每个节点类型都有独立的目录，包含：\n- 节点注册信息 (`index.ts`)\n- 表单元数据定义 (`form-meta.ts`)\n- 节点特定的组件和逻辑\n\n#### 3. `/plugins` - 插件系统\n- **runtime-plugin**: 支持浏览器和服务器两种运行模式\n- **context-menu-plugin**: 右键菜单功能\n- **variable-panel-plugin**: 变量管理面板\n\n#### 4. `/shortcuts` - 快捷键系统\n完整的快捷键支持，包括：\n- 基础操作：复制、粘贴、删除、全选\n- 视图操作：放大、缩小、折叠、展开\n- 每个快捷键都有独立的实现模块\n\n## 应用架构设计\n\n### 核心设计模式\n\n#### 1. 插件化架构 (Plugin Architecture)\n应用采用高度模块化的插件系统，每个功能都作为独立插件存在：\n\n```typescript\nplugins: () => [\n  createFreeLinesPlugin({ renderInsideLine: LineAddButton }),\n  createMinimapPlugin({ /* 配置 */ }),\n  createFreeSnapPlugin({ /* 对齐配置 */ }),\n  createFreeNodePanelPlugin({ renderer: NodePanel }),\n  createContainerNodePlugin({}),\n  createFreeGroupPlugin({ groupNodeRender: GroupNodeRender }),\n  createContextMenuPlugin({}),\n  createRuntimePlugin({ mode: 'browser' }),\n  createVariablePanelPlugin({})\n]\n```\n\n#### 2. 节点注册系统 (Node Registry Pattern)\n通过注册表模式管理不同类型的工作流节点：\n\n```typescript\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  ConditionNodeRegistry,    // 条件节点\n  StartNodeRegistry,        // 开始节点\n  EndNodeRegistry,          // 结束节点\n  LLMNodeRegistry,          // LLM节点\n  LoopNodeRegistry,         // 循环节点\n  CommentNodeRegistry,      // 注释节点\n  HTTPNodeRegistry,         // HTTP节点\n  CodeNodeRegistry,         // 代码节点\n  // ... 更多节点类型\n];\n```\n\n#### 3. 依赖注入模式 (Dependency Injection)\n使用 Inversify 框架实现服务的依赖注入：\n\n```typescript\nonBind: ({ bind }) => {\n  bind(CustomService).toSelf().inSingletonScope();\n}\n```\n\n## 核心功能分析\n\n### 1. 编辑器配置系统\n\n`useEditorProps` 是整个编辑器的配置中心，包含：\n\n```typescript\nexport function useEditorProps(\n  initialData: FlowDocumentJSON,\n  nodeRegistries: FlowNodeRegistry[]\n): FreeLayoutProps {\n  return useMemo<FreeLayoutProps>(() => ({\n    background: true,                    // 背景网格\n    readonly: false,                     // 是否只读\n    initialData,                         // 初始数据\n    nodeRegistries,                      // 节点注册表\n\n    // 核心功能配置\n    playground: { preventGlobalGesture: true /* 阻止 mac 浏览器手势翻页 */ },\n    nodeEngine: { enable: true },\n    variableEngine: { enable: true },\n    history: { enable: true, enableChangeNode: true },\n\n    // 业务逻辑配置\n    canAddLine: (ctx, fromPort, toPort) => { /* 连线规则 */ },\n    canDeleteLine: (ctx, line) => { /* 删除连线规则 */ },\n    canDeleteNode: (ctx, node) => { /* 删除节点规则 */ },\n    canDropToNode: (ctx, params) => { /* 拖拽规则 */ },\n\n    // 插件配置\n    plugins: () => [/* 插件列表 */],\n\n    // 事件处理\n    onContentChange: debounce((ctx, event) => { /* 自动保存 */ }, 1000),\n    onInit: (ctx) => { /* 初始化 */ },\n    onAllLayersRendered: (ctx) => { /* 渲染完成 */ }\n  }), []);\n}\n```\n\n### 2. 节点类型系统\n\n应用支持多种工作流节点类型：\n\n```typescript\nexport enum WorkflowNodeType {\n  Start = 'start',           // 开始节点\n  End = 'end',               // 结束节点\n  LLM = 'llm',               // 大语言模型节点\n  HTTP = 'http',             // HTTP请求节点\n  Code = 'code',             // 代码执行节点\n  Variable = 'variable',     // 变量节点\n  Condition = 'condition',   // 条件判断节点\n  Loop = 'loop',             // 循环节点\n  BlockStart = 'block-start', // 子画布开始节点\n  BlockEnd = 'block-end',    // 子画布结束节点\n  Comment = 'comment',       // 注释节点\n  Continue = 'continue',     // 继续节点\n  Break = 'break',           // 中断节点\n}\n```\n\n每个节点都遵循统一的注册模式：\n\n```typescript\nexport const StartNodeRegistry: FlowNodeRegistry = {\n  type: WorkflowNodeType.Start,\n  meta: {\n    isStart: true,\n    deleteDisable: true,        // 不可删除\n    copyDisable: true,          // 不可复制\n    nodePanelVisible: false,    // 不在节点面板显示\n    defaultPorts: [{ type: 'output' }],\n    size: { width: 360, height: 211 }\n  },\n  info: {\n    icon: iconStart,\n    description: '工作流的起始节点，用于设置启动工作流所需的信息。'\n  },\n  formMeta,                     // 表单配置\n  canAdd() { return false; }    // 不允许添加多个开始节点\n};\n```\n\n### 3. 插件化架构\n\n应用的功能通过插件系统实现模块化：\n\n#### 核心插件列表\n1. **FreeLinesPlugin** - 连线渲染和交互\n2. **MinimapPlugin** - 缩略图导航\n3. **FreeSnapPlugin** - 自动对齐和辅助线\n4. **FreeNodePanelPlugin** - 节点添加面板\n5. **ContainerNodePlugin** - 容器节点（如循环节点）\n6. **FreeGroupPlugin** - 节点分组功能\n7. **ContextMenuPlugin** - 右键菜单\n8. **RuntimePlugin** - 工作流运行时\n9. **VariablePanelPlugin** - 变量管理面板\n\n### 4. 运行时系统\n\n应用支持两种运行模式：\n\n```typescript\ncreateRuntimePlugin({\n  mode: 'browser',              // 浏览器模式\n  // mode: 'server',            // 服务器模式\n  // serverConfig: {\n  //   domain: 'localhost',\n  //   port: 4000,\n  //   protocol: 'http',\n  // },\n})\n```\n\n## 设计理念与架构优势\n\n### 1. 高度模块化\n- **插件化架构**: 每个功能都是独立插件，易于扩展和维护\n- **节点注册系统**: 新节点类型可以轻松添加，无需修改核心代码\n- **组件化设计**: UI组件高度复用，职责清晰\n\n### 2. 类型安全\n- **完整的TypeScript支持**: 从配置到运行时的全链路类型保护\n- **JSON Schema集成**: 节点数据结构通过Schema验证\n- **强类型的插件接口**: 插件开发有明确的类型约束\n\n### 3. 用户体验优化\n- **实时预览**: 支持工作流的实时运行和调试\n- **丰富的交互**: 拖拽、缩放、对齐、快捷键等完整的编辑体验\n- **可视化反馈**: 缩略图、状态指示、连线动画等视觉反馈\n\n### 4. 扩展性设计\n- **开放的插件系统**: 第三方可以轻松开发自定义插件\n- **灵活的节点系统**: 支持自定义节点类型和表单配置\n- **多运行时支持**: 浏览器和服务器双模式运行\n\n### 5. 性能优化\n- **按需加载**: 组件和插件支持按需加载\n- **防抖处理**: 自动保存等高频操作的性能优化\n\n## 技术亮点\n\n### 1. 自研编辑器框架\n基于 `@flowgram.ai/free-layout-editor` 自研框架，提供：\n- 自由布局的画布系统\n- 完整的撤销/重做功能\n- 节点和连线的生命周期管理\n- 变量引擎和表达式系统\n\n### 2. 先进的构建配置\n使用 Rsbuild 作为构建工具：\n\n```typescript\nexport default defineConfig({\n  plugins: [pluginReact(), pluginLess()],\n  source: {\n    entry: { index: './src/app.tsx' },\n    decorators: { version: 'legacy' }  // 支持装饰器\n  },\n  tools: {\n    rspack: {\n      ignoreWarnings: [/Critical dependency/]  // 忽略特定警告\n    }\n  }\n});\n```\n\n### 3. 国际化支持\n内置多语言支持：\n\n```typescript\ni18n: {\n  locale: navigator.language,\n  languages: {\n    'zh-CN': {\n      'Never Remind': '不再提示',\n      'Hold {{key}} to drag node out': '按住 {{key}} 可以将节点拖出',\n    },\n    'en-US': {},\n  }\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/examples/free-layout/free-layout-simple.mdx",
    "content": "---\noutline: true\n---\n\n# 基础用法\n\nimport { FreeLayoutSimplePreview } from '../../../../components';\n\n<FreeLayoutSimplePreview />\n\n## 功能介绍\n\nFree Layout 是 Flowgram.ai 提供的自由布局编辑器组件，允许用户创建和编辑流程图、工作流和各种节点连接图表。核心功能包括：\n\n- 节点自由拖拽与定位\n- 节点连接与边缘管理\n- 可配置的节点注册与自定义渲染\n- 内置撤销/重做历史记录\n- 支持插件扩展（如缩略图、自动对齐等）\n\n## 从零构建自由布局编辑器\n\n本节将带你从零开始构建一个自由布局编辑器应用，完整演示如何使用 @flowgram.ai/free-layout-editor 包构建一个可交互的流程编辑器。\n\n### 1. 环境准备\n\n首先，我们需要创建一个新的项目：\n\n```bash\n# 使用脚手架快速创建项目\nnpx @flowgram.ai/create-app@latest free-layout-simple\n\n# 进入项目目录\ncd free-layout-simple\n\n# 安装依赖\nnpm install\n```\n\n### 2. 项目结构\n\n创建完成后，项目结构如下：\n\n```\nfree-layout-simple/\n├── src/\n│   ├── components/            # 组件目录\n│   │   ├── node-add-panel.tsx # 节点添加面板\n│   │   ├── tools.tsx          # 工具栏组件\n│   │   └── minimap.tsx        # 缩略图组件\n│   ├── hooks/\n│   │   └── use-editor-props.tsx # 编辑器配置\n│   ├── initial-data.ts        # 初始数据定义\n│   ├── node-registries.ts     # 节点类型注册\n│   ├── editor.tsx             # 编辑器主组件\n│   ├── app.tsx                # 应用入口\n│   ├── index.tsx              # 渲染入口\n│   └── index.css              # 样式文件\n├── package.json\n└── ...其他配置文件\n```\n\n### 3. 开发流程\n\n#### 步骤一：定义初始数据\n\n首先，我们需要定义画布的初始数据结构，包括节点和连线：\n\n```tsx\n// src/initial-data.ts\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: '开始节点',\n        content: '这是开始节点'\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: '自定义节点',\n        content: '这是自定义节点'\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: '结束节点',\n        content: '这是结束节点'\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n```\n\n#### 步骤二：注册节点类型\n\n接下来，我们需要定义不同类型节点的行为和外观：\n\n```tsx\n// src/node-registries.ts\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\n/**\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // 开始节点标记\n      deleteDisable: true, // 开始节点不能被删除\n      copyDisable: true, // 开始节点不能被 copy\n      defaultPorts: [{ type: 'output' }], // 定义 input 和 output 端口，开始节点只有 output 端口\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }], // 结束节点只有 input 端口\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口\n  },\n];\n```\n\n#### 步骤三：创建编辑器配置\n\n使用 React hook 封装编辑器配置：\n\n```tsx\n// src/hooks/use-editor-props.tsx\nimport { useMemo } from 'react';\nimport {\n  FreeLayoutProps,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n  Field,\n  useNodeRender,\n} from '@flowgram.ai/free-layout-editor';\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';\nimport { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';\n\nimport { nodeRegistries } from '../node-registries';\nimport { initialData } from '../initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      // 启用背景网格\n      background: true,\n      // 非只读模式\n      readonly: false,\n      // 初始数据\n      initialData,\n      // 节点类型注册\n      nodeRegistries,\n      // 默认节点注册\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            // 节点表单渲染\n            render: () => (\n              <>\n                <Field<string> name=\"title\">\n                  {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n                </Field>\n                <div className=\"demo-free-node-content\">\n                  <Field<string> name=\"content\">\n                    <input />\n                  </Field>\n                </div>\n              </>\n            ),\n          },\n        };\n      },\n      // 节点渲染\n      materials: {\n        renderDefaultNode: (props: WorkflowNodeProps) => {\n          const { form } = useNodeRender();\n          return (\n            <WorkflowNodeRenderer className=\"demo-free-node\" node={props.node}>\n              {form?.render()}\n            </WorkflowNodeRenderer>\n          );\n        },\n      },\n      // 内容变更回调\n      onContentChange(ctx, event) {\n        console.log('数据变更: ', event, ctx.document.toJSON());\n      },\n      // 启用节点表单引擎\n      nodeEngine: {\n        enable: true,\n      },\n      // 启用历史记录\n      history: {\n        enable: true,\n        enableChangeNode: true, // 监听节点引擎数据变化\n      },\n      // 初始化回调\n      onInit: (ctx) => {},\n      // 渲染完成回调\n      onAllLayersRendered(ctx) {\n        ctx.document.fitView(false); // 适应视图\n      },\n      // 销毁回调\n      onDispose() {\n        console.log('编辑器已销毁');\n      },\n      // 插件配置\n      plugins: () => [\n        // 缩略图插件\n        createMinimapPlugin({\n          disableLayer: true,\n          canvasStyle: {\n            canvasWidth: 182,\n            canvasHeight: 102,\n            canvasPadding: 50,\n            canvasBackground: 'rgba(245, 245, 245, 1)',\n            canvasBorderRadius: 10,\n            viewportBackground: 'rgba(235, 235, 235, 1)',\n            viewportBorderRadius: 4,\n            viewportBorderColor: 'rgba(201, 201, 201, 1)',\n            viewportBorderWidth: 1,\n            viewportBorderDashLength: 2,\n            nodeColor: 'rgba(255, 255, 255, 1)',\n            nodeBorderRadius: 2,\n            nodeBorderWidth: 0.145,\n            nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n            overlayColor: 'rgba(255, 255, 255, 0)',\n          },\n        }),\n        // 自动对齐插件\n        createFreeSnapPlugin({\n          edgeColor: '#00B2B2',\n          alignColor: '#00B2B2',\n          edgeLineWidth: 1,\n          alignLineWidth: 1,\n          alignCrossWidth: 8,\n        }),\n      ],\n    }),\n    []\n  );\n```\n\n#### 步骤四：创建节点添加面板\n\n```tsx\n// src/components/node-add-panel.tsx\nimport React from 'react';\nimport { WorkflowDragService, useService } from '@flowgram.ai/free-layout-editor';\n\nconst nodeTypes = ['自定义节点1', '自定义节点2'];\n\nexport const NodeAddPanel: React.FC = () => {\n  const dragService = useService<WorkflowDragService>(WorkflowDragService);\n\n  return (\n    <div className=\"demo-free-sidebar\">\n      {nodeTypes.map(nodeType => (\n        <div\n          key={nodeType}\n          className=\"demo-free-card\"\n          onMouseDown={e => dragService.startDragCard(nodeType, e, {\n            data: {\n              title: nodeType,\n              content: '拖拽创建的节点'\n            }\n          })}\n        >\n          {nodeType}\n        </div>\n      ))}\n    </div>\n  );\n};\n```\n\n#### 步骤五：创建工具栏和缩略图\n\n```tsx\n// src/components/tools.tsx\nimport React from 'react';\nimport { useEffect, useState } from 'react';\nimport { usePlaygroundTools, useClientContext } from '@flowgram.ai/free-layout-editor';\n\nexport const Tools: React.FC = () => {\n  const { history } = useClientContext();\n  const tools = usePlaygroundTools();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const disposable = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => disposable.dispose();\n  }, [history]);\n\n  return (\n    <div\n      style={{ position: 'absolute', zIndex: 10, bottom: 16, left: 226, display: 'flex', gap: 8 }}\n    >\n      <button onClick={() => tools.zoomin()}>ZoomIn</button>\n      <button onClick={() => tools.zoomout()}>ZoomOut</button>\n      <button onClick={() => tools.fitView()}>Fitview</button>\n      <button onClick={() => tools.autoLayout()}>AutoLayout</button>\n      <button onClick={() => history.undo()} disabled={!canUndo}>\n        Undo\n      </button>\n      <button onClick={() => history.redo()} disabled={!canRedo}>\n        Redo\n      </button>\n      <span>{Math.floor(tools.zoom * 100)}%</span>\n    </div>\n  );\n};\n\n// src/components/minimap.tsx\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\nexport const Minimap = () => {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 226,\n        bottom: 51,\n        zIndex: 100,\n        width: 198,\n      }}\n    >\n      <MinimapRender\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </div>\n  );\n};\n```\n\n#### 步骤六：组装编辑器主组件\n\n```tsx\n// src/editor.tsx\nimport { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';\n\nimport { useEditorProps } from './hooks/use-editor-props';\nimport { Tools } from './components/tools';\nimport { NodeAddPanel } from './components/node-add-panel';\nimport { Minimap } from './components/minimap';\nimport '@flowgram.ai/free-layout-editor/index.css';\nimport './index.css';\n\nexport const Editor = () => {\n  const editorProps = useEditorProps();\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <div className=\"demo-free-container\">\n        <div className=\"demo-free-layout\">\n          <NodeAddPanel />\n          <EditorRenderer className=\"demo-free-editor\" />\n        </div>\n        <Tools />\n        <Minimap />\n      </div>\n    </FreeLayoutEditorProvider>\n  );\n};\n```\n\n#### 步骤七：创建应用入口\n\n```tsx\n// src/app.tsx\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { Editor } from './editor';\n\nReactDOM.render(<Editor />, document.getElementById('root'))\n```\n\n#### 步骤八：添加样式\n\n```css\n/* src/index.css */\n.demo-free-node {\n    display: flex;\n    min-width: 300px;\n    min-height: 100px;\n    flex-direction: column;\n    align-items: flex-start;\n    box-sizing: border-box;\n    border-radius: 8px;\n    border: 1px solid var(--light-usage-border-color-border, rgba(28, 31, 35, 0.08));\n    background: #fff;\n    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.1);\n}\n\n.demo-free-node-title {\n    background-color: #93bfe2;\n    width: 100%;\n    border-radius: 8px 8px 0 0;\n    padding: 4px 12px;\n}\n.demo-free-node-content {\n    padding: 4px 12px;\n    flex-grow: 1;\n    width: 100%;\n}\n.demo-free-node::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    z-index: -1;\n    background-color: white;\n    border-radius: 7px;\n}\n\n.demo-free-node:hover:before {\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-node.activated:before,\n.demo-free-node.selected:before {\n    outline: 2px solid var(--light-usage-primary-color-primary, #4d53e8);\n    -webkit-filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n    filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.3)) drop-shadow(0 4px 14px rgba(0, 0, 0, 0.1));\n}\n\n.demo-free-sidebar {\n    height: 100%;\n    overflow-y: auto;\n    padding: 12px 16px 0;\n    box-sizing: border-box;\n    background: #f7f7fa;\n    border-right: 1px solid rgba(29, 28, 35, 0.08);\n}\n\n.demo-free-right-top-panel {\n    position: fixed;\n    right: 10px;\n    top: 70px;\n    width: 300px;\n    z-index: 999;\n}\n\n.demo-free-card {\n    width: 140px;\n    height: 60px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 20px;\n    background: #fff;\n    border-radius: 8px;\n    box-shadow: 0 6px 8px 0 rgba(28, 31, 35, 0.03);\n    cursor: -webkit-grab;\n    cursor: grab;\n    line-height: 16px;\n    margin-bottom: 12px;\n    overflow: hidden;\n    padding: 16px;\n    position: relative;\n    color: black;\n}\n\n.demo-free-layout {\n    display: flex;\n    flex-direction: row;\n    flex-grow: 1;\n}\n\n.demo-free-editor {\n    flex-grow: 1;\n    position: relative;\n    height: 100%;\n}\n\n.demo-free-container {\n    position: absolute;\n    left: 0;\n    top: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    flex-direction: column;\n}\n\n```\n\n### 4. 运行项目\n\n完成上述步骤后，你可以运行项目查看效果：\n\n```bash\nnpm run dev\n```\n\n项目将在本地启动，通常访问 http://localhost:3000 即可看到效果。\n\n## 核心概念\n\n### 1. 数据结构\n\nFree Layout 使用标准化的数据结构来描述节点和连接：\n\n```tsx\n// 工作流数据结构\nconst initialData: WorkflowJSON = {\n  // 节点定义\n  nodes: [\n    {\n      id: 'start_0',        // 节点唯一ID\n      type: 'start',        // 节点类型（对应 nodeRegistries 中的注册）\n      meta: {\n        position: { x: 0, y: 0 }, // 节点位置\n      },\n      data: {\n        title: 'Start',     // 节点数据（可自定义）\n        content: 'Start content'\n      },\n    },\n    // 更多节点...\n  ],\n  // 连线定义\n  edges: [\n    {\n      sourceNodeID: 'start_0', // 源节点ID\n      targetNodeID: 'node_0',  // 目标节点ID\n    },\n    // 更多连线...\n  ],\n};\n```\n\n### 2. 节点注册\n\n使用 `nodeRegistries` 定义不同类型节点的行为和外观：\n\n```tsx\n// 节点注册\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  // 开始节点定义\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // Mark as start\n      deleteDisable: true, // The start node cannot be deleted\n      copyDisable: true, // The start node cannot be copied\n      defaultPorts: [{ type: 'output' }], // Used to define the input and output ports, the start node only has the output port\n    },\n  },\n  // 更多节点类型...\n];\n```\n\n### 3. 编辑器组件\n\n```tsx\n// 核心编辑器容器与渲染器\nimport {\n  FreeLayoutEditorProvider,\n  EditorRenderer\n} from '@flowgram.ai/free-layout-editor';\n\n// 编辑器配置示例\nconst editorProps = {\n  background: true,       // 启用背景网格\n  readonly: false,        // 非只读模式，允许编辑\n  initialData: {...},     // 初始化数据：节点和边的定义\n  nodeRegistries: [...],  // 节点类型注册\n  nodeEngine: {\n    enable: true,         // 启用节点表单引擎\n  },\n  history: {\n    enable: true,         // 启用历史记录\n    enableChangeNode: true, // 监听节点数据变化\n  }\n};\n\n// 完整编辑器渲染\n<FreeLayoutEditorProvider {...editorProps}>\n  <div className=\"container\">\n    <NodeAddPanel />              {/* 节点添加面板 */}\n    <EditorRenderer />            {/* 核心编辑器渲染区域 */}\n    <Tools />                     {/* 工具栏 */}\n    <Minimap />                   {/* 缩略图 */}\n  </div>\n</FreeLayoutEditorProvider>\n```\n\n### 4. 核心钩子函数\n\n在组件中可以使用多种钩子函数获取和操作编辑器：\n\n```tsx\n// 获取拖拽服务\nconst dragService = useService<WorkflowDragService>(WorkflowDragService);\n// 开始拖拽节点\ndragService.startDragCard('nodeType', event, { data: {...} });\n\n// 获取编辑器上下文\nconst { document, playground } = useClientContext();\n// 操作画布\ndocument.fitView();                 // 适应视图\nplayground.config.zoomin();               // 缩放画布\ndocument.fromJSON(newData);         // 更新数据\n```\n\n### 5. 插件扩展\n\nFree Layout 支持通过插件机制扩展功能：\n\n```tsx\nplugins: () => [\n  // 缩略图插件\n  createMinimapPlugin({\n    canvasStyle: {\n      canvasWidth: 180,\n      canvasHeight: 100,\n      canvasBackground: 'rgba(245, 245, 245, 1)',\n    }\n  }),\n  // 自动对齐插件\n  createFreeSnapPlugin({\n    edgeColor: '#00B2B2',     // 对齐线颜色\n    alignColor: '#00B2B2',    // 辅助线颜色\n    edgeLineWidth: 1,         // 线宽\n  }),\n],\n```\n\n## 安装\n\n```bash\nnpx @flowgram.ai/create-app@latest free-layout-simple\n```\n\n## 源码\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout-simple\n"
  },
  {
    "path": "apps/docs/src/zh/examples/index.mdx",
    "content": "---\noverview: true\ntitle: 预览\n---\n"
  },
  {
    "path": "apps/docs/src/zh/examples/node-form/_meta.json",
    "content": "[\n  \"basic\",\n  \"effect\",\n  \"array\",\n  \"dynamic\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/examples/node-form/array.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 数组\n\nimport { NodeFormArrayPreview } from '../../../../components/node-form/array/preview';\n\n以下例子展示了数组的基本用法，包含：\n- 基本写法（渲染、增删）。\n- 如何对数组每项配置校验逻辑。 此处的校验规则为每项最大长度不超过8个英文字符。\n- 如何对数组每项配置副作用。 此处的副作用为每项在初始化时控制台输出 `${name} value init to ${value}`, 值变更时输出 `${name} value changed to ${value}`\n- 数组项如何做交换。\n\n<NodeFormArrayPreview />\n"
  },
  {
    "path": "apps/docs/src/zh/examples/node-form/basic.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 基础用法\n\nimport { NodeFormBasicPreview } from '../../../../components';\n\n<div>\n  该例子展示了表单的几个基础用法\n   - 表单组件渲染\n   - 必填校验\n   - 默认值设置\n</div>\n<NodeFormBasicPreview />\n"
  },
  {
    "path": "apps/docs/src/zh/examples/node-form/dynamic.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 联动\n\nimport { NodeFormDynamicPreview } from '../../../../components';\n\n当前例子展示了如何通过 `deps` 字段来声明表单项之间的联动更新关系。\n\n例子说明：当 `Country` 有值时才会显示 `City` 字段。\n\n你也可以将`form.getValueIn('country')` 作为 city `Field` 下组件的入参，来控制组件内的行为, 如筛选当前country下的cities。\n\n<NodeFormDynamicPreview />\n"
  },
  {
    "path": "apps/docs/src/zh/examples/node-form/effect.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 副作用\n\nimport { NodeFormEffectPreview } from '../../../../components';\n\n以下例子展示了表单副作用的配置方式。举了两个个例子，行为描述如下\n1. Basic effect(基础例子)：当表单项值变更时，控制台会打印表单当前值。\n2. Control other fields (控制其他表单项的值)：当前表单项数据变更时要同时改变另一个表单项的值。\n\n<NodeFormEffectPreview />\n"
  },
  {
    "path": "apps/docs/src/zh/examples/playground.mdx",
    "content": "---\noutline: false\npageType: doc-wide\n---\n\n\n# 无限画布\n\nPlaygroundReact 是固定布局和自由布局的底层模块，可以单独使用, 功能：\n\n- 无限拖动画布\n- 鼠标/触摸板手势缩放\n- 内置背景插件\n- 内置快捷键插件\n\nimport { InfiniteCanvasPreview } from '../../../components';\n\n<InfiniteCanvasPreview />\n\n## 安装\n\n```bash\nnpx @flowgram.ai/create-app@latest playground\n```\n\n## 源码\n\nhttps://github.com/bytedance/flowgram.ai/tree/main/apps/demo-playground\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/_meta.json",
    "content": "[\n  {\n    \"type\": \"dir\",\n    \"name\": \"getting-started\",\n    \"label\": \"开始\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"free-layout\",\n    \"label\": \"自由布局\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"fixed-layout\",\n    \"label\": \"固定布局\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"form\",\n    \"label\": \"表单\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"variable\",\n    \"label\": \"变量\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"plugin\",\n    \"label\": \"插件\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"advanced\",\n    \"label\": \"进阶\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"concepts\",\n    \"label\": \"概念\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"runtime\",\n    \"label\": \"运行时接入\"\n  },\n  \"contributing\",\n  \"contact-us\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/_meta.json",
    "content": "[\n  \"zoom-scroll\",\n  \"history\",\n  \"shortcuts\",\n  \"custom-service\",\n  \"custom-layer\",\n  \"custom-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/custom-layer.mdx",
    "content": "#  自定义 Layer\n\n我们将画布拆分成多个 Layer，实现交互分层的思想，便于插件化管理，详细见 [画布引擎](/guide/concepts/canvas-engine.html)\n\n1. 通过 `observeEntityDatas` `observeEntities` `observeEntity` 监听画布节点任意数据模块的更新\n2. 通过 `onZoom` `onScroll` `onViewportChange` 等监听画布的缩放或者滚动\n3. 通过 `render` 往画布中插入 react 元素, 如绘制 svg 线条\n\n![Layer](@/public/layer-uml.jpg)\n\n## 创建 Layer\n\n```tsx pure\nimport { FreeLayoutPluginContext, inject, injectable, FlowNodeEntity, FlowNodeTransformData, FlowNodeFormData } from '@flowgram.ai/free-layout-editor'\n\n@injectable()\nexport class MyLayer extends Layer {\n  @inject(FreeLayoutPluginContext) ctx: FreeLayoutPluginContext\n  // 可以监听节点的宽高位置变化\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) transformDatas: FlowNodeTransformData[];\n  // 可以监听表单数据变化，连线数据可以存在表单里\n  @observeEntityDatas(FlowNodeEntity, FlowNodeFormData) formDatas: FlowNodeFormData[];\n  onReady() {\n    // 也可以添加样式\n    // zIndex可以控制是否要盖在节点, 节点默认是 10，大于 10 则在节点上边\n    this.node.style.zIndex = 11;\n  }\n  onZoom(scale) {\n    // 跟着画布缩放\n    this.node.style.transform = `scale(${scale})`;\n  }\n  render() {\n    return <div>{...}</div>\n  }\n}\n\n```\n\n## 加入到画布\n\n- 通过 use-editor-props\n\n```ts pure\n{\n  onInit: (ctx) => {\n    ctx.playground.registerLayer(MyLayer)\n  }\n}\n```\n\n- 通过插件添加\n\n```tsx pure\nimport { FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor'\n\nexport const createMyPlugin = definePluginCreator<{}, FreeLayoutPluginContext>({\n  onInit: (ctx, opts) => {\n    ctx.playground.registerLayer(MyLayer)\n  },\n});\n```\n\n## Layer 生命周期说明\n\n```ts\ninterface Layer {\n    /**\n     * 初始化时候触发\n     */\n    onReady?(): void;\n\n    /**\n     * playground 大小变化时候会触发\n     */\n    onResize?(size: PipelineDimension): void;\n\n    /**\n     * playground focus 时候触发\n     */\n    onFocus?(): void;\n\n    /**\n     * playground blur 时候触发\n     */\n    onBlur?(): void;\n\n    /**\n     * 监听缩放\n     */\n    onZoom?(scale: number): void;\n\n    /**\n     * 监听滚动\n     */\n    onScroll?(scroll: { scrollX: number; scrollY: number }): void;\n\n    /**\n     * viewport 更新触发\n     */\n    onViewportChange?(): void;\n\n    /**\n     * readonly 或 disable 状态变化\n     * @param state\n     */\n    onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;\n\n    /**\n   * 数据更新自动触发react render，如果不提供则不会调用react渲染\n   */\n    render?(): JSX.Element\n }\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/custom-plugin.mdx",
    "content": "# 自定义插件\n\n\n## 插件的生命周期说明\n\n```tsx pure\n\n/**\n  * from: https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/core/src/plugin/plugin.ts\n */\nimport { ContainerModule, interfaces } from 'inversify';\n\nexport interface PluginBindConfig {\n  bind: interfaces.Bind;\n  unbind: interfaces.Unbind;\n  isBound: interfaces.IsBound;\n  rebind: interfaces.Rebind;\n}\nexport interface PluginConfig<Opts, CTX extends PluginContext = PluginContext> {\n  /**\n   * 插件 IOC 注册, 等价于 containerModule\n   * @param ctx\n   */\n  onBind?: (bindConfig: PluginBindConfig, opts: Opts) => void;\n  /**\n   * 画布注册阶段\n   */\n  onInit?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布准备阶段，一般用于 dom 事件注册等\n   */\n  onReady?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布销毁阶段\n   */\n  onDispose?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布所有 layer 渲染结束\n   */\n  onAllLayersRendered?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * IOC 模块，用于更底层的插件扩展\n   */\n  containerModules?: interfaces.ContainerModule[];\n}\n\n```\n\n## 创建插件\n\n```tsx pure\n/**\n * 如果希望插件固定布局和自由布局都能使用， 请只用\n*  import { definePluginCreator } from '@flowgram.ai/core'\n */\nimport { definePluginCreator, FixedLayoutPluginContext } from '@flowgram.ai/fixed-layout-editor'\n\nexport interface MyPluginOptions {\n  opt1: string;\n}\n\nexport const createMyPlugin = definePluginCreator<MyPluginOptions, FixedLayoutPluginContext>({\n  onBind: (bindConfig, opts) => {\n    // 注册 IOC 模块, Service 如何定义 见 自定义 Service\n    bindConfig.bind(MyService).toSelf().inSingletonScope()\n  },\n  onInit: (ctx, opts) => {\n    // 插件配置\n    console.log(opts.opt1)\n    // ctx 对应 FixedLayoutPluginContext 或者 FreeLayoutPluginContext\n    console.log(ctx.document)\n    console.log(ctx.playground)\n    console.log(ctx.get<MyService>(MyService)) // 获取 IOC 模块\n  },\n});\n```\n\n## 添加插件\n\n```tsx pure title=\"use-editor-props.ts\"\n\n// EditorProps\n{\n  plugins: () => [\n    createMyPlugin({\n      opt1: 'xxx'\n    })\n  ]\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/custom-service.mdx",
    "content": "#  自定义 Service\n\n业务中需要抽象出单例服务便于插件化管理\n\n```tsx pure\nimport { useMemo } from 'react';\nimport { FlowDocument, type FixedLayoutProps, inject, injectable } from '@flowgram.ai/fixed-layout-editor'\n\n/**\n * Docs: https://inversify.io/docs/introduction/getting-started/\n * Warning: Use decorator legacy\n *   // rsbuild.config.ts\n *   {\n *     source: {\n *       decorators: {\n *         version: 'legacy'\n *       }\n *     }\n *   }\n * Usage:\n *  1.\n *    const myService = useService(MyService)\n *    myService.save()\n *  2.\n *    const myService = useClientContext().get(MyService)\n *  3.\n *    const myService = node.getService(MyService)\n */\n@injectable()\nclass MyService {\n  // 依赖注入单例模块\n  @inject(FlowDocument) flowDocument: FlowDocument\n  // ...\n}\n\nfunction BaseNode() {\n  const mySerivce = useService<MyService>(MyService)\n}\n\nexport function useEditorProps(\n): FixedLayoutProps {\n  return useMemo<FixedLayoutProps>(\n    () => ({\n      // ....other props\n      onBind: ({ bind }) => {\n        bind(MyService).toSelf().inSingletonScope()\n      },\n      materials: {\n        renderDefaultNode: BaseNode\n      }\n    }),\n    [],\n  );\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/history.mdx",
    "content": "# 历史记录\n\nUndo/Redo 是 FlowGram.AI 的一个插件，在 @flowgram.ai/fixed-layout-editor 和 @flowgram.ai/free-layout-editor 两种模式的编辑器中均有提供该功能。\n\n\n## 1. 快速开始\n\n[> Demo Detail](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout/src/hooks/use-editor-props.ts#L125)\n\n### 1.1. 开启 history\n使用 Undo/Redo 功能前需要先引入编辑器，以固定布局编辑器为例。\n\n1. package.json 添加依赖\n```tsx pure title=\"use-editor-props.tsx\" {4}\nexport function useEditorProps() {\n  return useMemo(\n    () => ({\n      history: {\n        enable: true,\n        enableChangeNode: true // Listen Node engine data change\n      }\n    })\n  )\n}\n```\n\n开启之后将获得以下能力：\n\n<table className=\"rs-table\">\n  <tr>\n    <td>简介</td>\n    <td>描述</td>\n    <td>自由布局</td>\n    <td>固定布局</td>\n  </tr>\n  <tr>\n    <td rowSpan={2}>Undo/Redo 快捷键</td>\n    <td>画布上使用 Cmd/Ctrl + Z 触发 Undo</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>画布上使用 Cmd/Ctrl + Shift + Z 触发 Redo</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td rowSpan={7}>画布节点操作支持undo/redo</td>\n    <td>增删节点 </td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>增删连线</td>\n    <td>✅</td>\n    <td>❌</td>\n  </tr>\n  <tr>\n    <td>移动节点</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>增删分支</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>移动分支</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>添加分组</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>取消分组</td>\n    <td>❌</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td rowSpan={2}>画布批量操作</td>\n    <td>删除节点</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n  <tr>\n    <td>移动节点</td>\n    <td>✅</td>\n    <td>✅</td>\n  </tr>\n\n</table>\n\n### 1.2. 关闭 history\n如果某些系统触发的数据变更不希望被undo redo监听到，可以主动关掉 历史服务 操作完数据再重新启动\n\n```tsx pure\nconst { history } = useClientContext();\n\nhistory.stop()\n// 做一些不希望被捕获的操作， 这些变更不会被记录到操作栈\n...\nhistory.start()\n```\n\n### 1.3. History Undo/Redo 合并\n\n```tsx pure\n\nconst { history } = useClientContext();\n\nhistory.startTransaction();\n\n// 这里的 任意操作都会被合并成一个\n...\n\nhistory.endTransaction();\n\n```\n\n### 1.4. Undo/Redo 调用\n一般 Undo/Redo 会在界面上提供两个按钮入口，点击了能触发 Undo 和 Redo，按钮本身需要有是否可以 Undo/Redo 的状态。\n\n```tsx pure\nexport function useUndoRedo(): UndoRedo {\n  const { history } = useClientContext();\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const toDispose = history.undoRedoService.onChange(() => {\n      setCanUndo(history.canUndo());\n      setCanRedo(history.canRedo());\n    });\n    return () => {\n      toDispose.dispose();\n    };\n  }, []);\n\n  return {\n    canUndo,\n    canRedo,\n    undo: () => history.undo(),\n    redo: () => history.redo(),\n  };\n}\n```\n\n## 2. 功能扩展\n### 2.1. 操作注册\n操作通过 operationMetas 去注册操作\n\n```tsx pure title=\"use-editor-props.tsx\"\n...\nhistory={{\n  enable: true,\n  operationMetas: [\n    {\n        type: 'addNode',\n        apply: () => { console.log('addNode')},\n        inverse: (op) => ({ type: 'deleteNode', value: op.value })\n    }\n  ]\n}}\n```\n`OperationMeta` 核心定义如下\n  - `type` 是操作的唯一标识\n  - `inverse` 是一个函数，该函数返回当前操作的逆操作\n  - `apply` 是操作被触发的时候执行的逻辑\n\n```tsx pure\nexport interface OperationMeta {\n  /**\n   * 操作类型 需要唯一\n   */\n  type: string;\n  /**\n   * 将一个操作转换成另一个逆操作， 如insert转成delete\n   * @param op 操作\n   * @returns 逆操作\n   */\n  inverse: (op: Operation) => Operation;\n  /**\n   * 执行操作\n   * @param operation 操作\n   */\n  apply(operation: Operation, source: any): void | Promise<void>;\n}\n```\n\n假设我要做增删节点支持 Undo/Redo 的功能，我就需要添加两个操作\n\n<div style={{marginTop: 16, display: 'flex', gap: 8 }}>\n  <div>\n    <div>\n      ```tsx pure\n      {\n        type: 'addNode',\n        inverse: op => ({ ...op, type: 'deleteNode' }),\n        apply(op, ctx) {\n          document = ctx.get(Document)\n          document.addNode(op.value)\n        },\n      }\n      ```\n    </div>\n  </div>\n  <div>\n    <div>\n      ```tsx pure\n      {\n        type: 'deleteNode',\n        inverse: op => ({ ...op, type: 'addNode' }),\n        apply(op, ctx) {\n          document = ctx.get(Document)\n          document.deleteNode(op.value.id)\n        },\n      }\n      ```\n    </div>\n  </div>\n</div>\n\n### 2.2. 操作合并\noperationMeta 支持 shouldMerge 来自定义合并策略，如果频繁触发的操作可以进行合并\n\n:::warning shouldMerge 返回\n- 返回 false 代表不合并\n- 返回 true 代表合并进一个操作栈元素\n- 返回 Operation 代表合并成一个操作\n\n:::\n\n以下示例是一个合并 500ms 内对同一个字段编辑进行合并\n\n```tsx pure\n{\n  type: 'changeData',\n  inverse: op => ({ ...op, type: 'changeData' }),\n  apply(op, ctx) {},\n  shouldMerge: (op, prev, element) => {\n    // 合并500ms内的操作\n    if (Date.now() - element.getTimestamp() < 500) {\n      if (\n        op.type === prev.type && // 相同类型\n        op.value.id === prev.value.id && // 相同节点\n        op.value?.path === prev.value?.path // 相同路径\n      ) {\n        return {\n          type: op.type,\n          value: {\n            ...op.value,\n            value: op.value.value,\n            oldValue: prev.value.oldValue,\n          },\n        };\n      }\n    }\n    return false;\n  }\n}\n```\n\n### 2.3. 操作执行\n1. 单操作执行\n\n通过 pushOperation 触发, 如下示例使用方在业务中触发刚刚定义的操作\n\n```tsx pure\nfunction handleAddNode () {\n   const { history } = useClientContext()\n   history.pushOperation({\n       type: 'addNode',\n       value: {\n          name: 'xx'\n          id: 'xxx'\n       }\n   })\n}\n```\n\n2. 批量执行\n通过 transact 调用的函数中所有执行的操作都会被合并进一个栈元素， undo/redo 的时候会被一起执行\n如下是实现了一个批量删除的例子：\n\n```tsx pure\nfunction deleteNodes(nodes: FlowNodeEntity[]) {\n  const { history } = useClientContext()\n  history.transact(() => {\n    nodes.forEach(node => {\n      history.pushOperation({\n        type: OperationType.deleteNode,\n        value: {\n          fromId: fromNode.id,\n          data: node.data,\n        },\n      });\n    });\n  });\n}\n```\n\n### 2.4. 撤销重做\n1. 撤销重做\n撤销执行 history.undo 方法\n重做执行 history.redo 方法\n\n```tsx pure\nfunction undo() {\n    const { history } = useClientContext();\n    history.undo();\n}\n\nfunction redo() {\n    const { history } = useClientContext();\n    history.redo();\n}\n```\n\n2. 监听撤销重做\n监听 undoRedoService.onChange 的 onChange 事件即可\n如下是一个 undo/redo 触发后路由对应操作的uri（选中对应节点或表单项）\n```tsx pure\nfunction listenHistoryChange() {\n  const { history } = useClientContext();\n  history.undoRedoService.onChange(\n    ({ type, element }) => {\n      if (type === UndoRedoChangeType.PUSH) {\n        return;\n      }\n      const op = element.getLastOperation();\n      if (!op) {\n        return;\n      }\n      if (op.uri) {\n        // goto somewhere\n      }\n    },\n  )\n}\n```\n\n### 2.5. 操作历史\n1. 查看刷新\n可以通过 HistoryStack.items 获得历史记录， 通过监听 HistoryStack.onChange 事件来刷新界面\n\n```tsx pure\nimport React from 'react';\n\nexport function HistoryList() {\n  const { historyStack } = useService<HistoryManager>(HistoryManager)\n  const { refresh } = useRefresh()\n  let items = historyManager.historyStack.items;\n\n  useEffect(() => {\n      const disposable = historyStack.onChange(() => {\n          refresh()\n      ])\n\n      return () => {\n          disposable.dispose()\n      }\n  }, [])\n\n  return (\n      <ul>\n        {items.map((item, index) => (\n          <li key={index}>\n            <div>\n              {item.type}({item.id}):\n              {item.operations.map((o, index) => (\n                <Tooltip\n                  key={index}\n                  title={(o.description || '') + `----uri: ${o.uri?.displayName}`}\n                >\n                  {o.label || o.type}\n                </Tooltip>\n              ))}\n            </div>\n\n          </li>\n        ))}\n      </ul>\n  );\n}\n```\n2. 持久化\n持久化是通过 history-storage 插件实现\n- databaseName： 数据库名称\n- resourceStorageLimit： 资源存储限制数量\n\n引入 @flowgram.ai/history-storage 包后，可使用该插件\n\n```tsx pure\nimport { createHistoryStoragePlugin } from '@flowgram.ai/history-storage';\n\ncreateHistoryStoragePlugin({\n    databaseName: 'your-history',\n    resourceStorageLimit: 50,\n}),\n```\n\n通过 useStorageHistoryItems 查询数据库列表\n\n```tsx pure\nimport {\n  useStorageHistoryItems,\n} from '@flowgram.ai/history-storage';\n\nexport const HistoryList = () => {\n  const { uri } = useCurrentWidget();\n\n  const { items } = useStorageHistoryItems(\n    storage,\n    uri.withoutQuery().toString(),\n  );\n\n  return <>\n    { JSON.stringify(items) }\n  </>\n}\n```\n\n## 3. API 列表\n### 3.1. [OperationMeta](https://flowgram.ai/auto-docs/fixed-history-plugin/interfaces/OperationMeta.html)\n操作元数据，用以定义一个操作\n\n### 3.2. [Operation](https://flowgram.ai/auto-docs/fixed-history-plugin/interfaces/Operation.html)\n操作数据，通过 type 和 OperationMeta 关联\n\n### 3.3. [OperationService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/OperationService.html)\n\n[onApply](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/OperationService.html#onapply)\n想监听某个触发的操作可以使用onApply\n\n```tsx pure\nuseService(OperationService).onApply((op: Operation) => {\n    console.log(op)\n    // 此处可以根据type执行自己的业务逻辑\n})\n```\n\n### 3.4. [HistoryService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryService.html)\nHistory 模块核心 API 暴露的Service\n\n### 3.5. [UndoRedoService](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/UndoRedoService.html)\n管理 UndoRedo 栈的服务\n\n### 3.6. [HistoryStack](https://flowgram.ai/auto-docs/fixed-history-plugin/classes/HistoryStack.html)\n历史栈，监听所有 push undo redo 操作，并记录到栈里面\n\n### 3.7. [HistoryDatabase](https://flowgram.ai/auto-docs/history-storage/classes/HistoryDatabase.html)\n持久化数据库操作\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/shortcuts.mdx",
    "content": "# 快捷键\n\n## 自定义快捷键\n\n```ts pure\n// 添加到 EditorProps\n{\n  shortcuts(shortcutsRegistry, ctx) {\n      // 按住 cmmand + a，选中所有节点\n      shortcutsRegistry.addHandlers({\n        commandId: 'selectAll',\n        shortcuts: ['meta a', 'ctrl a'],\n        isEnabled: (...args) => true,\n        execute(...args) {\n          const allNodes = ctx.document.getAllNodes();\n          ctx.playground.selectionService.selection = allNodes;\n        },\n      });\n  },\n}\n\n```\n\n## 通过 CommandService 调用快捷键\n\n```ts pure\nconst commandService = useService(CommandService)\n/**\n * 调用命令服务, args 参数会透传给 execute 和 isEnabled\n */\ncommandService.executeCommand('selectAll', ...args)\n\n// OR\nctx.get(CommandService).executeCommand('selectAll', ...args)\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/advanced/zoom-scroll.mdx",
    "content": "# 画布滚动和缩放\n\n[> 详细用法参考 Playground](/api/core/playground.html)\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/_meta.json",
    "content": "[\n  \"canvas-engine\",\n  \"node-engine\",\n  \"ecs\",\n  \"ioc\",\n  \"reactflow\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/canvas-engine.mdx",
    "content": "\n# 画布引擎\n\n## Playground\n画布引擎底层会提供一套自己的坐标系, 主要由 Playground 驱动\n\n```ts\ninterface Playground {\n   node: HTMLDivElement // 画布挂载的dom节点\n   toReactComponent() // 渲染为react 节点\n   readonly: boolean // 只读模式\n   config: PlaygroundConfigEntity // 包含 zoom，scroll 等画布数据\n}\n// hook 快速获取\nconst { playground } = useClientContext()\n```\n\n## Layer\n\n:::warning P.S.\n- 渲染层在底层建立了一套自己的坐标系，基于这个坐标系实现模拟滚动、缩放等逻辑，在算viewport时候节点也需要转换到该坐标系上\n- 渲染按画布被拆分成多个层 (Layer)，分层设计是基于ECS的数据切割思想，不同 Layer 只监听自己想要的数据，独立渲染不干扰，Layer 可以理解为ECS的 System，即最终Entity数据消费的地方\n- Layer 实现了类mobx的observer响应式动态依赖收集，数据更新会触发 autorun或render\n\n:::\n\n![切面编程](@/public/layer-uml.jpg)\n\n- Layer 生命周期\n\n```ts\ninterface Layer {\n    /**\n     * 初始化时候触发\n     */\n    onReady?(): void;\n\n    /**\n     * playground 大小变化时候会触发\n     */\n    onResize?(size: PipelineDimension): void;\n\n    /**\n     * playground focus 时候触发\n     */\n    onFocus?(): void;\n\n    /**\n     * playground blur 时候触发\n     */\n    onBlur?(): void;\n\n    /**\n     * 监听缩放\n     */\n    onZoom?(scale: number): void;\n\n    /**\n     * 监听滚动\n     */\n    onScroll?(scroll: { scrollX: number; scrollY: number }): void;\n\n    /**\n     * viewport 更新触发\n     */\n    onViewportChange?(): void;\n\n    /**\n     * readonly 或 disable 状态变化\n     * @param state\n     */\n    onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;\n\n    /**\n   * 数据更新自动触发react render，如果不提供则不会调用react渲染\n   */\n    render?(): JSX.Element\n }\n```\n\nLayer的定位其实和 Unity 游戏引擎 提供的 [MonoBehaviour](https://docs.unity3d.com/ScriptReference/MonoBehaviour.html) 类似， Unity 游戏引擎的脚本扩展都是基于这个，可以认为是最核心的设计，底层也是基于 C# 提供的反射 (Reflection) 能力的依赖注入\n\n```c#\nusing System.Collections;\nusing System.Collections.Generic;\nusing UnityEngine;\npublic class MyMonoBehavior : MonoBehaviour\n{\n    void Awake()\n    {\n        Debug.Log(\"Awake method is always called before application starts.\");\n    }\n    void Start()\n    {\n        Debug.Log(\"Start method is always called after Awake().\");\n    }\n    void Update()\n    {\n        Debug.Log(\"Update method is called in every frame.\");\n    }\n}\n```\n\n- Layer 的响应式更新\n\n```ts\nexport class DemoLayer extends Layer {\n    // 任意的inversify模块 的注入\n    @inject(FlowDocument) document: FlowDocument\n    // 监听单个Entity\n    @observeEntity(SomeEntity) entity: SomeEntity\n    // 监听多个Entity\n    @observeEntities(SomeEntity) entities: SomeEntity[]\n    // 监听 Entity的数据块（ECS - Component）变化\n    @observeEntityDatas(SomeEntity, SomeEntityData) transforms: SomeEntityData[]\n    autorun() {}\n    render() {\n      return <div></div>\n    }\n}\n```\n\n## FlowNodeEntity\n\n- 节点是一颗树, 包含子节点 (blocks) 和父亲节点, 节点采用 ECS 架构\n```ts\ninteface FlowNodeEntity {\n    id： string\n    blocks: FlowNodeEntity[]\n    pre?: FlowNodeEntity\n    next?: FlowNodeEntity\n    parent?: FlowNodeEntity\n    collapsed: boolean // 是否展开\n    getData(dataRegistry): NodeEntityData\n    addData(dataRegistry)\n}\n```\n\n## FlowNodeTransformData 节点的位置及大小数据\n\n```ts\nclass FlowNodeTransformData {\n    localTransform: Matrix, // 相对偏移, 只相对于同一个Block的上一个Sibling节点的偏移\n    worldTransform: Matrix, // 绝对偏移, 相对于Parent和Sibling节点叠加后的偏移\n    delta：Point // 居中居左偏移, 和Matrix独立，每个节点自己控制\n    getSize(): Size, // 由自己(独立节点) 或者 子分支节点宽高间距计算得出\n    getBounds(): Rectangle // 由worldMatix及 size 计算得出, 用于最终渲染，该范围也可用于确定高亮选中区域\n    inputPoint(): Point // 输入点位置，一般是Block的第一个节点的中上位置(居中布局)\n    outputPoint(): Point // 输出点位置，默认是节点中下位置，但条件分支，是由内置结束节点等具体逻辑判断得出\n   // ...others\n}\n```\n\n## FlowNodeRenderData 节点内容渲染数据\n\n```ts\nclass FlowNodeRenderData {\n  node: HTMLDivElement // 当前节点的dom\n  expanded：boolean // 是否展开\n  activated： boolean // 是否激活\n  hidden： boolean // 是否隐藏\n  // ...others\n}\n```\n\n## FlowDocument\n\n```ts\ninterface FLowDocument {\n    root: FlowNodeEntity // 画布的根节点\n    fromJSON(data): void // 导入数据\n    toJSON(): FlowDocumentJSON // 导出数据\n    addNode(type: string, meta: any): FlowNodeEntity // 添加节点\n    travese(fn: (node: flowNodeEntity) => void, startNode = this.root) // 遍历\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/ecs.mdx",
    "content": "# ECS\n\n## 为什么需要 ECS\n\n:::warning ECS （Entity-Component-System）\n适合解耦大的数据对象，常用于游戏，游戏的每个角色（Entity）数据都非常庞大，需要拆分成如物理引擎相关数据、皮肤相关、角色属性等 (多个 Component)，供不同的子系统（System）消费。流程的数据结构复杂，很适合用ECS做拆解\n\n:::\n\n<img loading=\"lazy\"  className=\"invert-img\" src=\"/ecs.png\"/>\n\n\n## 方案对比\n\n我们对比两个数据方案：\n\n### 1. ReduxStore 方案\n\n```jsx pure\nconst store = () => ({\n  nodes: [{\n    position: any\n    form: any\n    data3: any\n\n  }],\n  edges: []\n})\n\nfunction Playground() {\n  const { nodes } = useStore(store)\n\n  return nodes.map(node => <Node data={node} />)\n}\n```\n\n优点：\n- 中心化数据管理使用简单\n\n缺点：\n- 中心化数据管理无法精确更新，带来性能瓶颈\n- 扩展性差，节点新增一个数据，都耦合到一个 大JSON 里\n\n### 2. ECS 方案\n\n备注：\n- NodeData 对应的是 ECS - Component\n- Layer 对应 ECS - System\n\n```jsx pure\n\n/**\n  *  画布文档数据\n  */\nclass FlowDocument {\n  /**\n*  * 节点数据定义, 节点创建时候会把数据实例化\n    */\n  nodeDefines: [\n    NodePositionData,\n    NodeFormData,\n    NodeLineData\n  ]\n  nodeEntities: Entity[] = []\n}\n\n/**\n *  节点\n */\nclass FlowNodeEntity {\n  id: string // 只有id 不带数据\n  getData: (dataId: string) => EntityData\n}\n\n// 渲染线条\nclass LinesLayer {\n  /**\n   *  内部通过 node.getData(NodeLineData) 获取对应的数据，下同\n   */\n  @observeEntityData(FlowNodeEntity, NodeLineData) lines: NodeLineData[]\n  render() {\n    // 渲染线条\n    return this.lines.map(line => <Line data={line} />)\n  }\n}\n\n// 渲染节点位置\nclass NodePositionsLayer {\n  @observeEntityData(FlowNodeEntity, NodePositionData) positions: NodePositionData[]\n  render() {\n    // 渲染位置及排版\n  }\n}\n\n// 渲染节点表单\nclass  NodeFormsLayer {\n  @observeEntityData(FlowNodeEntity, NodeFormData) contents: NodeFormData[]\n  render() {\n    // 渲染节点内容\n  }\n}\n\n\n/**\n * 画布实例，通过 Layer 分层渲染\n */\nclass Playground {\n  layers: [\n    LinesLayer, // 线条渲染\n    NodePositionsLayer, // 位置渲染\n    NodeFormsLayer // 内容渲染\n  ]，\n  render() {\n    // 画布分层渲染\n    return this.layers.map(layer => layer.render())\n  }\n}\n```\n优点：\n\n- 节点数据拆开来单独控制渲染，性能可做到精确更新\n- 扩展性强，新增一个节点数据，则新增一个 XXXData + XXXLayer\n\n缺点：\n- 有一定学习成本\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/index.mdx",
    "content": "# 概念\n\n![FlowGramAI 架构](@/public/canvas-engine.png)\n\n- CanvasEngine：画布引擎负责绘制“点-线”构成的图, 保障大规模节点时的流畅性\n- NodeEngine: 节点引擎提供 渲染、校验、数据修改等表单能力\n- VariableEngine: 变量引擎引入作用域模型, 抽象各业务场景的变量\n- Material: 物料库包含默认 ICON 等 UI, 业务接入后可覆盖扩展\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/ioc.mdx",
    "content": "# IOC\n\n## 为什么需要 IOC\n\n:::warning 几个概念\n\n- 控制反转： Inversion of Control， 是面向对象中的一种设计原则，可以用来降低代码模块之间的耦合度，其中最常见的方式叫做依赖注入（Dependency Injection，简称DI）\n- 领域逻辑：Domain Logic，也可以叫 业务逻辑（Business Logic），这些业务逻辑与特定的产品功能相关\n- 面向切面编程：AOP （Aspect-Oriented Programming），最核心的设计原则是将软件系统拆分为公用逻辑 (横切，有贯穿的意味) 和 领域逻辑 （纵切）的多个个方面 (Aspect)，横切部分可以被所有的 纵切 部分 “按需消费”\n\n:::\n\n回答这个问题之前先了解切面编程，切面编程目的是将领域逻辑的粒度拆的更细，横切部分可被纵切 “按需消费” ，横切和纵切的连接也叫 织入 (Weaving)，而 IOC 就是扮演 Weaving 注入到纵切的角色\n\n![切面编程](@/public/weaving.png)\n\n理想的切面编程\n\n```ts\n- myAppliation 提供业务逻辑\n  - service 特定的业务逻辑服务\n     - customDomainLogicService\n  - contributionImplement 钩子的注册实例化\n    - MyApplicationContributionImpl\n  - component 业务组件\n\n- core 提供通用逻辑\n  - model 通用模型\n  - contribution 钩子接口\n     - LifecycleContribution 应用的生命周期\n     - CommandContribution\n  - service 公用的service的服务\n     - CommandService\n     - ClipboardService\n  - component 公用的组件\n  ```\n\n  ```ts\n  // IOC 的注入\n@injectable()\nexport class CustomDomainLogicService {\n  @inject(FlowContextService) protected flowContextService: FlowContextService;\n  @inject(CommandService) protected commandService: CommandService;\n  @inject(SelectionService) protected selectionService: SelectionService;\n}\n// IOC 的接口声明\ninterface LifecycleContribution {\n   onInit(): void\n   onStart(): void\n   onDispose(): void\n}\n// IOC 的接口实现\n@injectable()\nexport class MyApplicationContributionImpl implements LifecycleContribution {\n    onStart(): void {\n      // 特定的业务逻辑代码\n    }\n}\n\n// 手动挂在到生命周期钩子\nbind(LifecycleContribution).toService(MyApplicationContributionImpl)\n```\n\n\n:::warning IOC是切面编程的一种手段，引入后，底层模块可以以接口形式暴露给外部注册，带来的好处：\n- 实现微内核 + 插件化的设计，实现插件的可插拔按需消费\n- 可以让包拆得更干净，实现 feature 式的拆包\n\n:::\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/node-engine.mdx",
    "content": "# 节点引擎\n\n节点引擎 NodeEngine 是一个流程节点逻辑的书写框架，让业务专注于业务自身的渲染与数据逻辑，无需关注画布以及节点间联动的底层api。与此同时，节点引擎沉淀了最佳的节点书写范式，帮助业务解决流程业务中可能遇到的各种问题， 如数据逻辑与渲染耦合等。\n节点引擎是可选启用的。如果你不存在以下这些复杂的节点逻辑，可以选择不启用节点引擎，自己维护节点数据与渲染。复杂节点逻辑如：1）节点不渲染也能校验或触发数据副作用；2）节点间联动丰富；3）redo/undo; 等等。\n\n## 基础概念\n\nFlowNodeEntity\n流程节点模型。\n\nFlowNodeRegistry\n流程节点的静态配置。\n\nFormMeta\n节点引擎的静态配置。 配置在 FlowNodeRegistry 中的 formMeta 字段。\n\nForm\n节点引擎中的表单。它维护节点的数据并提供渲染、校验、副作用等能力。他的模型 FormModel 提供节点数据的访问和修改及触发校验等能力。\n\nField\n节点表单中的某个渲染字段。注意， Form 已经提供了数据层的逻辑，Field 更多是一个渲染层的模型，它仅在表单字段渲染后才存在。\n\nvalidate\n表单校验。通常有对单个字段的校验也有整体表单校验。\n\neffect\n表单数据的副作用。通常指在表单数据发生一些事件时要触发特定逻辑。 如在某字段的数据变更时要同步一些信息到某个store，这个可以被称为一个effect。\n\nFormPlugin\n表单插件。可以配置在formMeta 中，插件可以对表单进行一系列深度操作。如变量插件。\n"
  },
  {
    "path": "apps/docs/src/zh/guide/concepts/reactflow.mdx",
    "content": "\n# 对比 ReactFlow\n\n[Reactflow](https://reactflow.dev/) 是很优秀的开源项目，架构及代码清晰，但偏流程渲染引擎的底层架构 (Node、Edge、Handle)，需要在上层开发大量功能才能适配复杂场景(如 固定布局，需要对数据建模写布局算法), 高级功能收费。\n\n相比 Reactflow，FlowGram 的目标是提供流程编辑一整套开箱即用的解决方案。\n\n- 下边是 Reactflow 官方提供的 pro 收费能力\n\n| 付费功能                         | FlowGram 是否支持 | 未来计划支持 |\n|----------------------------------|------------------------|--------------|\n| 分组                             | 支持                   |              |\n| redo/undo                        | 支持                   |              |\n| copy/paste                       | 支持                   |              |\n| HelpLines 辅助线                | 支持                   |              |\n| 自定义节点及形状                 | 支持                   |              |\n| 自定义线条                       | 支持                   |              |\n| AutoLayout，自动布局整理         | 支持                   |              |\n| ForceLayout，节点排斥效果        | 不支持                 | No           |\n| Expand/Collapse                  | 支持                   |              |\n| Collaborative 多人协同           | 不支持                 | Yes          |\n| WorkflowBuilder 相当于固定布局完整案例 | 支持                   |              |\n\n- Reactflow 事件都是绑定在原子化的 dom 节点上，且内置，交互定制成本高，需要理解它的源码才能深度开发，如下，在画布缩放很小时候无法选到点位\n\n<table>\n  <tr>\n    <td>\n      <div className=\"rs-tip\">由于 事件是绑定在 svg 上，svg 在缩放后很容易点不到</div>\n      <img loading=\"lazy\" src=\"/reactflow/reactflow-render.gif\"/>\n    </td>\n    <td>\n      <div className=\"rs-tip\">FlowGram 的事件是一种全局监听 mousemove 变化，并通过计算及 Threshold  大致确定位置，即使缩放很小也能点到, 同时支持线条重连</div>\n      <img loading=\"lazy\" src=\"/reactflow/reactflow-interaction.gif\"/>\n    </td>\n  </tr>\n</table>\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/contact-us.mdx",
    "content": "# 联系我们\n\n- Issues: [Issues](https://github.com/bytedance/flowgram.ai/issues)\n- Discord: https://discord.gg/SwDWdrgA9f\n- Lark: 通过 [注册飞书](https://www.feishu.cn/en/) 并扫描下边的二维码加入飞书群\n\n<img src=\"/lark-group.png\" width=\"200\"/>\n"
  },
  {
    "path": "apps/docs/src/zh/guide/contributing.mdx",
    "content": "# 贡献指南\n\n本文帮助你快速在本仓库完成开发、测试与提 PR。仓库采用 Rush + pnpm 的 Monorepo 管理方式，文档站基于 Rspress 构建。\n\n## 构建开发环境\n\n1. **安装 Node.js 18+**（推荐 LTS/Hydrogen）\n\n```bash\nnvm install lts/hydrogen\nnvm alias default lts/hydrogen # 设为默认 Node 版本\nnvm use lts/hydrogen\n```\n\n2. **克隆仓库到本地**\n\n```bash\ngit clone git@github.com:bytedance/flowgram.ai.git\n```\n\n3. **安装全局依赖**\n\n```bash\nnpm i -g pnpm@10.6.5 @microsoft/rush@5.150.0\n```\n\n4. **安装项目依赖**\n\n```bash\nrush install\n```\n\n5. **构建项目**\n\n```bash\nrush build\n```\n\n6. **运行文档或示例**\n\n```bash\nrush dev:docs                  # 在 apps/docs 启动文档站（含增量构建）\nrush dev:demo-fixed-layout     # 运行固定布局示例\nrush dev:demo-free-layout      # 运行自由布局示例\n```\n\n## 常用命令（Rush 自定义）\n\n```bash\nrush build            # 构建所有包\nrush build:watch      # 增量构建并监听\nrush lint             # 运行 ESLint 检查\nrush lint:fix         # 自动修复 ESLint 问题\nrush ts-check         # TypeScript 类型检查\nrush test             # 运行各包的测试脚本（按包聚合）\nrush e2e:test         # 运行所有 e2e 测试\nrush e2e:update-screenshot # 更新 e2e 快照\nrush dep-check        # 自动检查依赖健康度\n```\n\n## 分支与提交规范\n\n- 分支命名：\n  - `feat/描述`（新功能）\n  - `fix/描述`（问题修复）\n  - `docs/描述`（文档变更）\n  - `chore/描述`（维护/杂项）\n- 提交信息（Conventional Commits）：\n  - 格式：`type(scope): subject`，例如：\n\n```text\nfeat(editor): 支持节点批量对齐\n```\n\n  - 常用类型：`feat`、`fix`、`docs`、`style`、`refactor`、`test`、`chore`\n  - 仓库已启用 commitlint 校验（commit-msg 钩子），提交信息将被自动检查；同时 pre-commit 会运行 lint-staged（自动更新许可证头、eslint 修复）与 `rush check` 校验。\n\n## 开发与质量保障\n\n- 本地开发建议：\n  - 先执行 `rush build:watch`，再在对应 demo 或 docs 目录运行开发命令（如 `rush dev:docs`）。\n  - 修改代码后，确保通过：`rush lint`、`rush ts-check`、`rush build`、`rush test`。\n- 测试说明：\n  - e2e 用例位于 `e2e/` 目录，可通过 `rush e2e:test` 运行，或更新快照 `rush e2e:update-screenshot`。\n\n## Pull Request 流程\n\n1. 从 `main` 创建你的工作分支（遵循分支命名规范）。\n2. 编码并补充测试/文档。\n3. 本地通过质量校验（lint、ts-check、build、test）。\n4. 提交 PR：填写说明、关联 Issue，并注意使用模板。\n5. 评审与 CI：维护者会进行代码评审，CI 全绿后即可合并。\n\n## 文档贡献\n\n- 文档位置：`apps/docs/src/zh/**`（中文）与 `apps/docs/src/en/**`（英文）。\n- 本地预览：执行 `rush dev:docs` 启动 Rspress 文档站。\n- 若需自动生成 API 文档，可在 `apps/docs` 目录执行 `rushx docs`（调用脚本生成）。\n\n## 常见问题\n\n- pnpm-lock 合并冲突：仓库已在 `post-checkout` 钩子中配置合并策略，通常可避免锁文件冲突。\n- Node 版本：请确保使用 Node 18+，否则可能出现依赖或构建失败。\n\n## 报告问题\n\n- 请在 GitHub 提交 Issue：https://github.com/bytedance/flowgram.ai/issues/new/choose\n  - 描述问题、复现步骤、期望与实际行为，必要时附代码示例。\n\n## 许可证\n\n- 本项目遵循 MIT 许可证。提交代码即默认同意相关条款。\n"
  },
  {
    "path": "apps/docs/src/zh/guide/fixed-layout/_meta.json",
    "content": "[\n  \"load\",\n  \"node\",\n  \"composite-nodes\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/fixed-layout/composite-nodes.mdx",
    "content": "# 复合节点\n\n复合节点由多个节点组合，并支持自定义线条，如 分支节点、Loop 节点、TryCatch 节点:\n\n## 使用\n\n```ts pure title=\"node-registries.ts\"\n\nimport { FlowNodeRegistry  } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 节点注册\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    type: 'yourCustomNodeType',\n    extend: 'dynamicSplit',\n  },\n];\n\n```\n\n## 内置的复合节点\n\n<div className=\"rs-tip\">\n  <a className=\"rs-link\" target=\"_blank\" href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/canvas-engine/fixed-layout-core/src/activities\">\n    Source Code\n  </a>\n</div>\n\nimport { CompositeNodesPreview } from '../../../../components';\n\n<CompositeNodesPreview />\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/fixed-layout/load.mdx",
    "content": "# 加载与保存\n\n画布的数据通过 [FlowDocument](/api/core/flow-document.html) 来存储\n\n## 画布数据格式\n\n画布文档数据采用树形结构，支持嵌套\n\n:::note 文档数据基本结构:\n\n- nodes `array` 节点列表, 支持嵌套\n\n:::\n\n:::note 节点数据基本结构:\n\n\n- id: `string` 节点唯一标识, 必须保证唯一\n- meta: `object` 节点的 ui 配置信息，如自由布局的 `position` 信息放这里\n- type: `string | number` 节点类型，会和 `nodeRegistries` 中的 `type` 对应\n- data: `object` 节点表单数据\n- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`\n\n:::\n\n```tsx pure title=\"initial-data.tsx\"\nimport { FlowDocumentJSON } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 配置流程数据，数据为 blocks 嵌套的格式\n */\nexport const initialData: FlowDocumentJSON = {\n  nodes: [\n    // 开始节点\n    {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'Start',\n        content: 'start content'\n      },\n      blocks: [],\n    },\n    // 分支节点\n    {\n      id: 'condition_0',\n      type: 'condition',\n      data: {\n        title: 'Condition'\n      },\n      blocks: [\n        {\n          id: 'branch_0',\n          type: 'block',\n          data: {\n            title: 'Branch 0',\n            content: 'branch 1 content'\n          },\n          blocks: [\n            {\n              id: 'custom_0',\n              type: 'custom',\n              data: {\n                title: 'Custom',\n                content: 'custrom content'\n              },\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          type: 'block',\n          data: {\n            title: 'Branch 1',\n            content: 'branch 1 content'\n          },\n          blocks: [],\n        },\n      ],\n    },\n    // 结束节点\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'End',\n        content: 'end content'\n      },\n    },\n  ],\n};\n\n```\n\n## 加载\n\n- 通过 initialData 加载\n\n```tsx pure\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App({ data }) {\n  return (\n    <FixedLayoutEditorProvider initialData={data} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n\n- 通过 ref 动态加载\n\n```tsx pure\n\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    const data = await request('https://xxxx/getJSON')\n    ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // 加载后触发画布的 fitview 让节点自动居中\n      ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));\n    }, 100)\n  }, [])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n- 动态 reload 数据\n\n```tsx pure\n\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\n\nfunction App({ data }) {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    // 当 data 变化时候重新加载画布数据\n    await ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // 加载后触发画布的 fitview 让节点自动居中\n      ref.current.playground.config.fitView(ref.current.document.root.bounds.pad(30));\n    }, 100)\n  }, [data])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n```\n\n## 监听变化并自动保存\n\n```tsx pure\n\nimport { FixedLayoutEditorProvider, FixedLayoutPluginContext, EditorRenderer } from '@flowgram.ai/fixed-layout-editor'\nimport { debounce } from 'lodash-es'\n\nfunction App() {\n  const ref = useRef<FixedLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    // 监听画布变化 延迟 1 秒 保存数据, 避免画布频繁更新\n    const toDispose = ref.current.history.onApply(debounce(() => {\n        // 通过 toJSON 获取画布最新的数据\n        request('https://xxxx/save', {\n          data: ref.current.document.toJSON()\n        })\n    }, 1000))\n    return () => toDispose.dispose()\n  }, [])\n  return (\n    <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FixedLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/fixed-layout/node.mdx",
    "content": "# 节点\n\n节点通过 [FlowNodeEntity](/api/core/flow-node-entity.html) 定义\n\n\n## 节点核心 API\n\n- id: `string` 节点 id\n- flowNodeType: `string` | `number` 节点类型\n- bounds: `Rectangle` 获取节点的 x，y，width，height, 等价于 `transform.bounds`\n- blocks: `FlowNodeEntity[]` 获取子节点 (如 Loop)\n- collapsedChildren: `FlowNodeEntity[]` 获取子节点, 包含折叠的子节点\n- allCollapsedChildren: `FlowNodeEntity[]` 获取所有子节点，包括所有折叠的子节点\n- children: `FlowNodeEntity[]` 获取子节点, 不包含折叠的子节点\n- pre: `FlowNodeEntity | undefined` 获取上一个节点\n- next: `FlowNodeEntity | undefined` 获取下一个节点\n- parent: `FlowNodeEntity | undefined` 获取父节点\n- originParent: `FlowNodeEntity | undefined` 获取原始父节点, 这个用于固定布局分支的第一个节点(orderIcon) 找到整个虚拟分支\n- allChildren: `FlowNodeEntity[]` 获取所有子节点, 不包含折叠的子节点\n- document: [FlowDocument](/api/core/flow-document.html) 文档链接\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) 获取节点的 transform 矩阵数据\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) 获取节点的渲染数据, 包含渲染状态等\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) 获取节点的表单数据, 等价于 [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量作用域\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量私有作用域\n- getNodeRegistry(): [FlowNodeRegistry](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1) 获取节点注册器\n- getService(): 获取 [IOC](/guide/concepts/ioc.html) 服务，如 `node.getService(HistoryService)`\n- getExtInfo(): 获取节点扩展数据, 如 `node.getExtInfo<{ test: string }>()`\n- updateExtInfo(): 更新节点扩展数据, 如 `node.updateExtInfo<{ test: string }>({ test: 'test' })`\n- dispose(): 销毁节点\n- toJSON(): json 序列化\n\n## 节点数据\n\n通过 `node.toJSON()` 可以获取\n\n:::note 基本结构:\n\n- id: `string` 节点唯一标识, 必须保证唯一\n- meta: `object` 节点的 ui 配置信息，如自由布局的 `position` 信息放这里\n- type: `string | number` 节点类型，会和 `nodeRegistries` 中的 `type` 对应\n- data: `object` 节点表单数据, 业务可自定义\n- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`\n\n:::\n\n```ts pure\nconst nodeData: FlowNodeJSON = {\n  id: 'xxxx',\n  type: 'condition',\n  data: {\n    title: 'MyCondition',\n    desc: 'xxxxx'\n  },\n}\n```\n\n## 节点定义\n\n声明节点可以用于确定节点的类型及渲染方式\n\n```tsx pure\nimport { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 自定义节点注册\n */\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    /**\n     * 自定义节点类型\n     */\n    type: 'condition',\n    /**\n     * 自定义节点扩展:\n     *  - loop: 扩展为循环节点\n     *  - start: 扩展为开始节点\n     *  - dynamicSplit: 扩展为分支节点\n     *  - end: 扩展为结束节点\n     *  - tryCatch: 扩展为 tryCatch 节点\n     *  - default: 扩展为普通节点 (默认)\n     */\n    extend: 'dynamicSplit',\n    /**\n     * 节点配置信息\n     */\n    meta: {\n      // isStart: false, // 是否为开始节点\n      // isNodeEnd: false, // 是否为结束节点，结束节点后边无法再添加节点\n      // draggable: false, // 是否可拖拽，如开始节点和结束节点无法拖拽\n      // selectable: false, // 触发器等开始节点不能被框选\n      // deleteDisable: true, // 禁止删除\n      // copyDisable: true, // 禁止copy\n      // addDisable: true, // 禁止添加\n    },\n    /**\n     * 配置节点表单的校验及渲染,\n     * 注：validate 采用数据和渲染分离，保证节点即使不渲染也能对数据做校验\n     */\n    formMeta: {\n      validateTrigger: ValidateTrigger.onChange,\n      validate: {\n        title: ({ value }) => (value ? undefined : 'Title is required'),\n      },\n      /**\n       * Render form\n       */\n      render: () => (\n       <>\n          <Field name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field name=\"content\">\n            {({ field }) => <input onChange={field.onChange} value={field.value}/>}\n          </Field>\n        </>\n      )\n    },\n  },\n];\n```\n\n## 当前渲染节点获取\n\n通过 [useNodeRender](/api/hooks/use-node-render.html) 获取节点相关方法\n\n```tsx pure\nfunction BaseNode() {\n  const { id, type, data, updateData, node } = useNodeRender()\n}\n```\n\n## 创建节点\n\n通过 [FlowOperationService](/api/services/flow-operation-service.html) 创建\n\n- 添加节点\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addNode({\n  id: 'xxx', // 要保证画布内唯一\n  type: 'custom',\n  meta: {},\n  data: {}, // 表单相关数据\n  blocks: [], // 子节点\n  parent: someParent // 父亲节点，分支会用到\n})\n\n```\n- 在指定节点之后添加\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addFromNode(targetNode, {\n  id: 'xxx', // 要保证画布内唯一\n  type: 'custom',\n  meta: {},\n  data: {}, // 表单相关数据\n  blocks: [], // 子节点\n})\n\n```\n- 添加分支节点 （用于条件分支）\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.operation.addBlock(parentNode, {\n  id: 'xxx', // 要保证画布内唯一\n  type: 'block',\n  meta: {},\n  data: {}, // 表单相关数据\n  blocks: [], // 子节点\n})\n```\n\n## 删除节点\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const ctx = useClientContext()\n  function onClick() {\n    ctx.operation.deleteNode(node)\n  }\n  return (\n    <button onClick={onClick}>Delete</button>\n  )\n}\n```\n\n## 更新节点 data 数据\n\n- 通过 [useNodeRender](/api/hooks/use-node-render.html) 或 [node.form](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) 获取节点的 data 数据\n\n```tsx pure\nfunction BaseNode() {\n  const { node, form } = useNodeRender();\n  // 1. form.values 对应节点的 data 数据\n  // 2. form.setValueIn('title', 'xxxx') 修改 data.title\n  // 3. form.getValueIn('title') 获取 data.title\n  // 4. form.updateFormValues({ ... }) 更新表单所有数据\n\n  function onChange(e) {\n    form.setValueIn('title', e.target.value)\n  }\n  return <input value={form.values.title} onChange={onChange}/>\n}\n```\n- 通过 Field 更新表单数据, 详细见 [表单的使用](/guide/form/form.html)\n\n```tsx pure\n\nfunction FormRender() {\n  return (\n    <Field name=\"title\">\n      <Input />\n    </Field>\n  )\n}\n```\n\n## 更新节点的 extInfo 数据\n\nextInfo 用于存储 一些 ui 状态, 如果未开启节点引擎，节点的 data 数据会默认存到 extInfo 里\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const times = node.getExtInfo()?.times || 0\n  function onClick() {\n    node.updateExtInfo({ times: times ++ })\n  }\n  return (\n    <div>\n      <span>Click Times: {times}</span>\n      <button onClick={onClick}>Click</button>\n    </div>\n  )\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/form/_meta.json",
    "content": "[\n  \"form\",\n  \"without-form\",\n  \"form-materials\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/form/form-materials.mdx",
    "content": "# 官方表单物料\n\n文档已迁移到：[物料](/materials/introduction.html)\n"
  },
  {
    "path": "apps/docs/src/zh/guide/form/form.mdx",
    "content": "# 节点表单\n\n## 术语\n\n<table className=\"rs-table\">\n  <tr>\n    <td>节点表单</td>\n    <td>特指流程节点内的表单或点击节点展开的表单，关联节点数据。</td>\n  </tr>\n  <tr>\n    <td>节点引擎</td>\n    <td>FlowGram.ai 内置的引擎之一，它核心维护了节点数据的增删查改，并提供渲染、校验、副作用、画布或变量联动等能力， 除此之外，它还提供节点错误捕获渲染、无内容时的 placeholder 渲染等能力，见以下章节例子。</td>\n  </tr>\n</table>\n\n## 快速开始\n\n### 开启节点引擎\n\n[> API Detail](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/editor/src/preset/editor-props.ts#L54)\n\n```tsx pure title=\"use-editor-props.ts\" {3}\n\n// EditorProps\n{\n  nodeEngine: {\n    /**\n     * 需要开启节点引擎才能使用\n     */\n    enable: true;\n    materials: {\n      /**\n       * 节点内部报错的渲染组件\n       */\n      nodeErrorRender?: NodeErrorRender;\n      /**\n       * 节点无内容时的渲染组件\n       */\n      nodePlaceholderRender?: NodePlaceholderRender;\n    }\n  }\n}\n```\n\n### 配置表单\n\n `formMeta` 是节点表单唯一配置入口，配置在每个节点的NodeRegistry 上。\n\n[> node-registries.ts](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout-simple/src/node-registries.ts)\n\n```tsx pure title=\"node-registries.ts\"\nimport { FlowNodeRegistry, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';\n\nexport const nodeRegistries: FlowNodeRegistry[] = [\n  {\n    type: 'start',\n    /**\n     * 配置节点表单的校验及渲染\n     */\n    formMeta: {\n      /**\n       * 配置校验在数据变更时触发\n       */\n      validateTrigger: ValidateTrigger.onChange,\n      /**\n       * 配置校验规则， 'content' 为字段路径，以下配置值对该路径下的数据进行校验。\n       *\n       * 也可支持动态函数写法, 用于根据 values 生成校验器:\n       *  validate: (values, ctx) => ({ content: () => {}, })\n      */\n      validate: {\n        content: ({ value }) => (value ? undefined : 'Content is required'),\n      },\n      /**\n       * 配置表单渲染\n       */\n      render: () => (\n       <>\n          <Field<string> name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field<string> name=\"content\">\n            {({ field, fieldState }) => (\n              <>\n                <input onChange={field.onChange} value={field.value}/>\n                {fieldState?.invalid && <Feedback errors={fieldState?.errors}/>}\n              </>\n            )}\n          </Field>\n        </>\n      )\n    },\n  }\n]\n\n```\n\n[> 表单写法的基础例子](/examples/node-form/basic.html)\n\n### 渲染表单\n\n[> base-node.tsx](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-fixed-layout-simple/src/components/base-node.tsx)\n\n```tsx pure title=\"base-node.tsx\"\n\nexport const BaseNode = () => {\n  /**\n   * 提供节点渲染相关的方法\n   */\n  const { form } = useNodeRender()\n  return (\n    <div className=\"demo-free-node\" className={form?.state.invalid && \"error\"}>\n      {\n        // 表单渲染通过 formMeta 生成\n        form?.render()\n      }\n    </div>\n  )\n};\n\n```\n\n## 核心概念\n\n### FormMeta\n\n在 `NodeRegistry` 中，我们通过`formMeta` 来配置节点表单, 它遵循以下API。\n\n[> FormMeta API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/node/src/types.ts#L89)\n\n这里特别说明, 节点表单与通用表单有一个很大的区别，它的数据逻辑（如校验、数据变更后的副作用等）需要在表单不渲染的情况下依然生效，我们称 <span className=\"rs-red\">数据与渲染分离</span>\n。所以这些数据逻辑需要配置在formMeta 中的非render 字段中，保证不渲染情况下节点引擎也可以调用到这些逻辑, 而通用表单引擎（如react-hook-form）则没有这个限制, 校验可以直接写在react组件中。\n\n\n### FormMeta.render (渲染)\n`render` 字段用于配置表单的渲染逻辑\n\n`render: (props: FormRenderProps<any>) => React.ReactElement;`\n\n[> FormRenderProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/form.ts#L91)\n\n\n返回的 react 组件可使用以下表单组件和模型：\n\n#### Field (组件)\n\n`Field` 是表单字段的 React 高阶组件，封装了表单字段的通用逻辑，如数据与状态的注入，组件的刷新等。其核心必填参数为 `name`, 用于声明表单项的路径，在一个表单中具有唯一性。\n\n[> Field Props API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L106)\n\nField 的渲染部分，支持三种写法，如下:\n\n```tsx pure\nconst render = () => (\n  <div>\n    <Label> 1. 通过 children </Label>\n    {/* 该方式适用于简单场景，Field 会将  value onChange 等属性直接注入第一层children组件中  */}\n    <Field name=\"c\">\n      <Input />\n    </Field>\n    <Label> 2. 通过 Render Props  </Label>\n    {/* 该方式适用于复杂场景，当 return 的组件存在多层嵌套，用户可以主动将field 中的属性注入希望注入的组件中 */}\n    <Field name=\"a\">\n        {({ field, fieldState, formState }: FieldRenderProps<string>) => <div><Input {...field} /><Feedbacks errors={fieldState.errors}/></div>}\n    </Field>\n\n    <Label> 3. 通过传 render 函数</Label>\n    {/* 该方式类似方式2，但通过props 传入 */}\n    <Field name=\"b\" render={({ field }: FieldRenderProps<string>) => <Input {...field} />} />\n  </div>\n);\n```\n\n``` ts pure\ninterface FieldRenderProps<TValue> {\n  // Field 实例\n  field: Field<TValue>;\n  // Field 状态（响应式）\n  fieldState: Readonly<FieldState>;\n  // Form 状态\n  formState: Readonly<FormState>;\n}\n```\n[> FieldRenderProps API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L125)\n\n#### Field (模型)\n\n`Field` 实例通常通过render props 传入（如上例子），或通过 `useCurrentField` hook 获取。它包含表单字段在渲染层面的常见API。\n注意: `Field` 是一个渲染模型，仅提供一般组件需要的API, 如 `value` `onChange` `onFocus` `onBlur`，如果是数据相关的API 请使用 `Form` 模型实例，如 `form.setValueIn(name, value)` 设置某字段的值。\n\n[> Field 模型 API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L34)\n\n\n#### FieldArray (组件)\n\n`FieldArray` 是数组类型字段的 React 高阶组件，封装了数组类型字段的通用逻辑，如数据与状态的注入，组件的刷新，以及数组项的遍历等。其核心必填参数为 `name`, 用于声明该表单项的路径，在一个表单中具有唯一性。\n\n`FieldArray` 的基础用法可以参照以下例子:\n\n[> 数组例子](https://flowgram.ai/examples/node-form/array.html)\n\n#### FieldArray (模型)\n\n`FieldArray` 继承于 `Field` ，是数组类型字段在渲染层的模型，除了包含渲染层的常见API，还包含数组的基本操作如 `FieldArray.map`, `FieldArray.remove`, `FieldArray.append` 等。API 的使用方法也可见上述[数组例子](https://flowgram.ai/examples/node-form/array.html)。\n\n[> FieldArray 模型 API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L69)\n\n#### Form(组件)\n\n`Form` 组件是表单的最外层高阶组件，上述 `Field` `FieldArray` 等能力仅在该高阶组件下可以使用。节点表单的渲染已经将`<Form />` 封装到了引擎内部，所以用户无需关注，可以直接在`render` 返回的 react 组件中直接使用 `Field`。但如果用户需要独立使用表单引擎，或者在节点之外独立再渲染一次表单，需要自行在表单内容外包上`Form`组件。\n\n#### Form(模型)\n\n`Form` 实例可通过`render` 函数的入参获得， 也可通过 hook `useForm` 获取，见[例子](#useform)。它是表单核心模型门面，用户可以通过Form 实例操作表单数据、监听变更、触发校验等。\n\n[> Form 模型 API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/form.ts#L58)\n\n### 校验\n基于[FormMeta](#formmeta)章节中提到的\"数据与渲染分离\"概念，校验逻辑需配置在 `FormMeta` 全局, 并通过路径匹配方式声明校验逻辑所作用的表单项，如下例子。\n\n路径支持模糊匹配，见[路径](#路径)章节。\n\n<div className=\"rs-center\" >\n  <img loading=\"lazy\" src=\"/form-validate.gif\"  style={{ maxWidth: 600 }}/>\n</div>\n\n```tsx pure\nexport const renderValidateExample = ({ form }: FormRenderProps<FormData>) => (\n  <>\n    <Label> a (最大长度为 5)</Label>\n    <Field name=\"a\">\n      {({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n        <>\n          <Input value={value} onChange={onChange} />\n          <Feedback errors={fieldState?.errors} />\n        </>\n      )}\n    </Field>\n    <Label> b (如果a存在，b可以选填) </Label>\n    <Field\n      name=\"b\"\n      render={({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (\n        <>\n          <Input value={value} onChange={onChange} />\n          <Feedback errors={fieldState?.errors} />\n        </>\n  )}\n/>\n  </>\n);\n\nexport const VALIDATE_EXAMPLE: FormMeta = {\n  render: renderValidateExample,\n  // 校验时机配置\n  validateTrigger: ValidateTrigger.onChange,\n  /*\n   * 也可支持动态函数写法, 用于根据 values 生成校验器:\n   *   validate: (values, ctx) => ({ a: () => '', b: () => '', c, () => '' })\n  */\n  validate: {\n    // 单纯校验值\n    a: ({ value }) => (value.length > 5 ? '最大长度为5' : undefined),\n    // 校验依赖其他表单项的值\n    b: ({ value, formValues }) => {\n      if (formValues.a) {\n        return undefined;\n      } else {\n        return value ? 'undefined' : 'a 存在时 b 必填';\n      }\n    },\n    // 校验依赖节点或画布信息\n    c: ({ value, formValues, context }) => {\n      const { node， playgroundContext } = context;\n      // 此处逻辑省略\n    },\n  },\n};\n```\n#### 校验时机\n\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`ValidateTrigger.onChange`</td>\n    <td>表单数据变更时校验（不包含初始化数据）</td>\n  </tr>\n  <tr>\n    <td>`ValidateTrigger.onBlur`</td>\n    <td>表单项输入控件onBlur时校验。<br/>注意，这里有两个前提：一是表单项的输入控件需要支持 `onBlur` 入参，二是要将 `Field.onBlur` 传入该控件: <br/>```<Field>{({field})=><Input ... onBlur={field.onBlur}>}</Field>```</td>\n  </tr>\n</table>\n`validateTrigger` 建议配置 `ValidateTrigger.onChange` 即数据变更时校验，如果配置 `ValidateTrigger.onBlur`, 校验只会在组件blur事件触发时触发。那么当节点表单不渲染的情况下，就算是数据变更了，也不会触发校验。\n\n\n#### 主动触发校验\n\n1. 主动触发整个表单的校验\n\n```tsx pure\nconst form = useForm()\nform.validate()\n```\n\n2. 主动触发单个表单项校验\n\n```tsx pure\nconst validate = useFieldValidate(name)\nvalidate()\n```\n`name` 不传则默认获取当前 `<Field />` 标签下的 `Field` 的 `validate`, 通过传 `name` 可获取 `<Form />` 下任意 `Field`。\n\n\n### 路径\n\n1. 表单路径以`.`为层级分隔符， 如 `a.b.c` 指向数据 `{a:{b:{c:1}}}` 下的 `1`\n2. 路径支持模糊匹配，在校验和副作用配置中会使用到。如下例子。通常在数组场景中使用较多。\n\n<div className=\"rs-red\">\n  注意：* 仅代表下钻一级\n</div>\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`arr.*`</td>\n    <td>`arr` 字段的所有一级子项</td>\n  </tr>\n  <tr>\n    <td>`arr.x.*`</td>\n    <td>`arr.x` 的所有一级子项</td>\n  </tr>\n  <tr>\n    <td>`arr.*.x`</td>\n    <td>`arr` 所有一级子项下的 `x`</td>\n  </tr>\n</table>\n\n### 副作用 (effect)\n\n副作用是节点表单特有的概念，指在节点数据发生变更时需要执行的副作用。同样，遵循 \"数据与渲染分离\" 的原则，副作用和校验相似，也配置在 `FormMeta` 全局。\n- 通过 key value 形式配置，key 表示表单项路径匹配规则，支持模糊匹配，value 为作用在该路径上的effect。\n- value 为数组，即支持一个表单项有多个effect。\n\n\n```tsx pur\n\nexport const EFFECT_EXAMPLE: FormMeta = {\n  ...\n  effect: {\n    ['a.b']: [\n      {\n        event: DataEvent.onValueChange,\n        effect: ({ value }: EffectFuncProps<string, FormData>) => {\n          console.log('a.b value changed:', value);\n        },\n      },\n    ],\n    ['arr.*']:[\n      {\n        event: DataEvent.onValueInit,\n        effect: ({ value, name }: EffectFuncProps<string, FormData>) => {\n          console.log(name + ' value init:', value);\n        },\n      },\n    ]\n  }\n};\n```\n\n``` tsx pur\ninterface EffectFuncProps<TFieldValue = any, TFormValues = any> {\n  name: FieldName;\n  value: TFieldValue;\n  prevValue?: TFieldValue;\n  formValues: TFormValues;\n  form: IForm;\n  context: NodeContext;\n}\n```\n\n[Effect 相关 API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/node/src/types.ts#L54)\n\n#### 副作用时机\n\n<table className=\"rs-table\">\n  <tr>\n    <td>`DataEvent.onValueChange`</td>\n    <td>数据变更时触发</td>\n  </tr>\n  <tr>\n    <td>`DataEvent.onValueInit`</td>\n    <td>数据初始化时触发</td>\n  </tr>\n  <tr>\n    <td>`DataEvent.onValueInitOrChange`</td>\n    <td>数据初始化和变更时都会触发</td>\n  </tr>\n</table>\n\n### 联动\n\n[> 联动例子](/examples/node-form/dynamic.html)\n\n\n## hooks\n### 节点表单内\n以下hook 可在节点表单内部使用\n\n#### useCurrentField\n`() => Field`\n\n该 hook 需要在Field 标签内部使用\n\n```tsx pur\nconst field = useCurrentField()\n```\n[> Field 模型 API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L34)\n\n\n#### useCurrentFieldState\n`() => FieldState`\n\n该 hook 需要在Field 标签内部使用\n\n```tsx pur\nconst fieldState = useCurrentFieldState()\n```\n[> FieldState API](https://github.com/bytedance/flowgram.ai/blob/main/packages/node-engine/form/src/types/field.ts#L158)\n\n#### useFieldValidate\n`(name?: FieldName) => () => Promise<void>`\n\n如果需要主动触发字段的校验，可以使用该hook 获取到 Field 的 validate 函数。\n\n`name` 为 Field 的路径，不传则默认获取当前 `<Field />` 下的validate\n\n```tsx pur\nconst validate = useFieldValidate()\nvalidate()\n```\n\n#### useForm\n`() => Form`\n\n用于获取 Form 实例。\n\n注意，该hook 在 `render` 函数第一层不生效，仅在 `render` 函数内的 react 组件内部才可使用。`render` 函数的入参中已经传入了 `form: Form`, 可以直接使用。\n\n1. 在 render 函数第一层直接使用 `props.form`\n```tsx pur\nconst formMeta = {\n  render: ({form}) =>\n  <div>\n    {form.getValueIn('my.path')}\n  </div>\n}\n```\n\n2. 在组件内部可使用 `useForm`\n\n```tsx pur\n\nconst formMeta = {\n  render: () =>\n    <div>\n      <Field name={'my.path'}>\n        <MySelect />\n      </Field>\n    </div>\n}\n\n// MySelect.tsx\n...\nconst form = useForm()\nconst valueNeeded = form.getValueIn('my.other.path')\n...\n```\n\n<span className=\"rs-red\">注意：Form 的 api 不具备任何响应式能力，若需监听某字段值，可使用 [useWatch](#usewatch) </span>\n\n\n#### useWatch\n`<TValue = FieldValue>(name: FieldName) => TValue`\n\n该 hook 和上述 `useForm` 相似, 在 `render` 函数返回组件的第一层不生效，仅在封装过的组件内部可用。如果需要在 `render` 根级别使用，可以对 `render` 返回的内容做一层组件封装。\n\n```tsx pur\n{\n  render: () =>\n    <div>\n      <Field name={'a'}><A /></Field>\n      <Field name={'b'}><B /></Field>\n    </div>\n}\n\n// A.tsx\n...\nconst b = useWatch('b')\n// do something with b\n...\n```\n\n\n### 节点表单外\n以下 hook 用于在节点表单外部，如画布全局、相邻节点上需要去监听某个节点表单的数据或状态。通常需要传入 `node: FlowNodeEntity` 作为参数\n\n#### useWatchFormValues\n监听 node 内整个表单的值\n\n`<TFormValues = any>(node: FlowNodeEntity) => TFormValues | undefined`\n\n```tsx pur\nconst values = useWatchFormValues(node)\n```\n\n#### useWatchFormValueIn\n\n监听 node 内某个表单项的值\n\n`<TValue = any>(node: FlowNodeEntity，name: string) => TFormValues | undefined`\n\n```tsx pur\nconst value = useWatchFormValueIn(node, name)\n```\n\n#### useWatchFormState\n\n监听 node 内表单的状态\n\n`(node: FlowNodeEntity) => FormState | undefined`\n\n```tsx pur\nconst formState = useWatchFormState(node)\n```\n\n#### useWatchFormErrors\n\n监听 node 内表单的 Errors\n\n`(node: FlowNodeEntity) => Errors | undefined`\n\n```tsx pur\nconst errors = useWatchFormErrors(node)\n```\n\n#### useWatchFormWarnings\n\n监听 node 内表单的 Warnings\n\n`(node: FlowNodeEntity) => Warnings | undefined`\n\n```tsx pur\nconst warnings = useWatchFormErrors(node)\n```\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/form/without-form.mdx",
    "content": "# 不使用表单\n\n当节点引擎不开启，节点的 data 数据会存在 `node.getExtInfo` 中, 如下\n\n```tsx pure\n\nexport const useEditorProps = () => {\n  return {\n    // ...\n    nodeEngine: {\n      enable: false, // 不开启节点引擎，则无法使用 form\n    },\n    history: {\n      enable: true,\n      enableChangeNode: false // 不再监听表单数据变化\n    },\n    materials: {\n      /**\n       * Render Node\n       */\n      renderDefaultNode: ({ node }: WorkflowNodeProps) => {\n        return (\n          <WorkflowNodeRenderer className=\"demo-free-node\" node={node}>\n            <input value={node.getExtInfo()?.title} onChange={e => node.updateExtInfo({ title: e.target.value})}/>\n          </WorkflowNodeRenderer>\n        );\n      },\n    },\n    // /...\n  }\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/_meta.json",
    "content": "[\n  \"load\",\n  \"node\",\n  \"line\",\n  \"port\",\n  \"sub-canvas\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/line.mdx",
    "content": "# 线条\n\n- [WorkflowLinesManager](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/workflow-lines-manager.ts) 管理所有的线条\n- [WorkflowNodeLinesData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-lines-data.ts) 节点上连接的线条管理\n- [WorkflowLineEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts) 线条实体\n\n\n## 获取所有线条的实体\n\n```ts pure\nconst allLines = ctx.document.linesManager.getAllLines() // 线条实体 包含 from/to 分别代表线条连接的节点\n\n```\n\n## 创建/删除线条\n\n```ts pure\n// from和 to 为对应要连线的节点id， fromPort, toPort 为 端口 id, 如果节点为单个端口可以不指定\nconst line = ctx.document.linesManager.createLine({ from, to, fromPort, toPort })\n\n// 删除线条\nline.dispose()\n\n```\n\n## 导出线条数据\n\n:::note 线条基本结构:\n\n- sourceNodeID: `string` 开始节点 id\n- targetNodeID: `string` 目标节点 id\n- sourcePortID?: `string | number` 开始端口 id, 缺省则采用开始节点的默认端口\n- targetPortID?: `string | number` 目标端口 id, 缺省则采用目标节点的默认端口\n\n:::\n```ts pure\nconst json = ctx.document.linesManager.toJSON()\n```\n\n## 获取当前节点的输入/输出节点或线条\n\n```ts pure\n// 获取当前节点的输入节点（通过连接线计算）\nnode.lines.inputNodes\n// 获取所有输入节点 （会往上递归获取所有）\nnode.lines.allInputNodes\n// 获取输出节点\nnode.lines.outputNodes\n// 获取所有输出节点\nnode.lines.allOutputNodes\n// 输入线条 (包含 isDrawing 或 isHidden 的线条)\nnode.lines.inputLines\n// 输出线条 (包含 isDrawing 或 isHidden 的线条)\nnode.lines.outputLines\n// 所有线条 (不包含 isDrawing 或 isHidden 的线条)\nnode.lines.availableLines\n\n```\n\n## 线条配置\n\n我们提供丰富的线条配置参数, 给 `FreeLayoutEditorProvider`, 详细见 [FreeLayoutProps](https://github.com/bytedance/flowgram.ai/blob/main/packages/client/free-layout-editor/src/preset/free-layout-props.ts)\n\n```tsx pure\ninterface FreeLayoutProps {\n    /**\n     * 线条颜色配置\n     */\n    lineColor?: LineColor;\n    /**\n     * 判断线条是否标红\n     * @param ctx\n     * @param fromPort\n     * @param toPort\n     * @param lines\n     */\n    isErrorLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity | undefined, lines: WorkflowLinesManager) => boolean;\n    /**\n     * 判断端口是否标红\n     * @param ctx\n     * @param port\n     */\n    isErrorPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n    /**\n     * 判断端口是否禁用\n     * @param ctx\n     * @param port\n     */\n    isDisabledPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n    /**\n     * 判断线条箭头是否反转\n     * @param ctx\n     * @param line\n     */\n    isReverseLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * 判断线条是否隐藏箭头\n     * @param ctx\n     * @param line\n     */\n    isHideArrowLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * 判断线条是否展示流动效果\n     * @param ctx\n     * @param line\n     */\n    isFlowingLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * 判断线条是否禁用\n     * @param ctx\n     * @param line\n     */\n    isDisabledLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n    /**\n     * 拖拽线条结束\n     * @param ctx\n     * @param params\n     */\n    onDragLineEnd?: (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => Promise<void>;\n    /**\n     * 设置线条渲染器类型\n     * @param ctx\n     * @param line\n     */\n    setLineRenderType?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => LineRenderType | undefined;\n    /**\n     * 设置线条样式\n     * @param ctx\n     * @param line\n     */\n    setLineClassName?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => string | undefined;\n    /**\n     * 是否允许创建线条\n     * @param ctx\n     * @param fromPort - 开始点\n     * @param toPort - 目标点\n     */\n    canAddLine?: (ctx: FreeLayoutPluginContext, fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, lines: WorkflowLinesManager, silent?: boolean) => boolean;\n    /**\n     *\n     * 是否允许删除线条\n     * @param ctx\n     * @param line - 目标线条\n     * @param newLineInfo - 新的线条信息\n     * @param silent - 如果为false，可以加 toast 弹窗\n     */\n    canDeleteLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity, newLineInfo?: Required<WorkflowLinePortInfo>, silent?: boolean) => boolean;\n    /**\n     * 是否允许重置线条\n     * @param ctx\n     * @param oldLine - 原来的线条\n     * @param newLineInfo - 新的线条信息\n     * @param lines - 线条管理器\n     */\n    canResetLine?: (ctx: FreeLayoutPluginContext, oldLine: WorkflowLineEntity, newLineInfo: WorkflowLinePortInfo, lines: WorkflowLinesManager ) => boolean;\n}\n```\n\n### 1. 线条 ui 属性配置\n\n- 修改 UIState\n\n```tsx pure\n/**\n *  more: https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts#L41\n */\nline.updateUIState({\n  lockedColor: 'blue',\n  strokeWidth: 2,\n  strokeWidthSelected: 3,\n  className: 'xxx',\n  style: {}\n})\n```\n\n- 不同线条指定特定的颜色 (优先级最高)\n\n```tsx pure\n\nctx.document.linesManager.getAllLines().forEach(line => {\n  if (line.from.flowNodeType === 'start') {\n    line.lockedColor = 'blue'\n  } else if (line.to.flowNodeType === 'end') {\n    line.lockedColor = 'yellow'\n  }\n})\n\n```\n\n- 全局颜色配置\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      lineColor: {\n        hidden: 'transparent',\n        default: '#4d53e8',\n        drawing: '#5DD6E3',\n        hovered: '#37d0ff',\n        selected: '#37d0ff',\n        error: 'red',\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n\n### 2.让单个输出端口只能连一条线\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }} className=\"invert-img\" src=\"/free-layout/line-limit.gif\"/>\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      /*\n       * Check whether the line can be added\n       * 判断是否连线\n       */\n      canAddLine(ctx, fromPort, toPort) {\n        // not the same node\n        if (fromPort.node === toPort.node) {\n          return false;\n        }\n        // 控制线条添加数目\n        if (fromPort.availableLines.length >= 1) {\n          return false\n        }\n        return true;\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n\n### 3.连接到空白地方添加节点\n\n代码见自由布局最佳实践\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }}  className=\"invert-img\" src=\"/free-layout/line-add-panel.gif\"/>\n\n```tsx pure\n\nfunction App() {\n  const editorProps: FreeLayoutProps = {\n      /**\n       * Drag the end of the line to create an add panel (feature optional)\n       * 拖拽线条结束需要创建一个添加面板 （功能可选）\n       */\n      async onDragLineEnd(ctx, params) {\n        const { fromPort, toPort, mousePos, line, originLine } = params;\n        if (originLine || !line) {\n          return;\n        }\n        if (toPort) {\n          return;\n        }\n        // 这里可以根据 mousePos 打开添加面板\n        await ctx.get(WorkflowNodePanelService).call({\n          fromPort,\n          toPort: undefined,\n          panelPosition: mousePos,\n          enableBuildLine: true,\n          panelProps: {\n            enableNodePlaceholder: true,\n            enableScrollClose: true,\n          },\n        });\n      },\n      // ...others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n\n### 4.自定义箭头渲染器\n\n你可以通过注册自定义的箭头渲染器来完全自定义线条的箭头样式。\n\n```tsx pure\nimport {\n  FlowRendererKey,\n  type ArrowRendererProps,\n  useEditorProps\n} from '@flowgram.ai/free-layout-editor';\n\n// 1. 创建自定义箭头组件\nfunction CustomArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide }: ArrowRendererProps) {\n  if (hide) return null;\n\n  const size = 8;\n  const rotation = reverseArrow\n    ? (vertical ? 270 : 180)\n    : (vertical ? 90 : 0);\n\n  return (\n    <g\n      id={id}\n      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}\n    >\n      <path\n        d={`M0,0 L${-size},-${size/2} L${-size},${size/2} Z`}\n        fill=\"currentColor\"\n        strokeWidth={strokeWidth}\n        stroke=\"currentColor\"\n      />\n    </g>\n  );\n}\n\n// 2. 在编辑器中注册自定义箭头\nfunction App() {\n  const materials = {\n    components: {\n      [FlowRendererKey.ARROW_RENDERER]: CustomArrowRenderer,\n    },\n  };\n\n  const editorProps = useEditorProps({\n    materials,\n    // ...其他配置\n  });\n\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  );\n}\n```\n\n**高级用法**：根据线条状态动态渲染不同的箭头样式：\n\n```tsx pure\nfunction AdvancedArrowRenderer({ id, pos, reverseArrow, strokeWidth, vertical, hide, line }: ArrowRendererProps) {\n  if (hide) return null;\n\n  const size = 8;\n  const rotation = reverseArrow\n    ? (vertical ? 270 : 180)\n    : (vertical ? 90 : 0);\n\n  // 根据线条状态选择不同的箭头样式\n  let arrowPath: string;\n  let fillColor: string;\n\n  if (line?.hasError) {\n    // 错误状态：红色感叹号箭头\n    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;\n    fillColor = '#ff4d4f';\n  } else if (line?.processing) {\n    // 处理中状态：蓝色圆形箭头\n    arrowPath = `M0,0 m-${size/2},0 a${size/2},${size/2} 0 1,0 ${size},0 a${size/2},${size/2} 0 1,0 -${size},0`;\n    fillColor = '#1890ff';\n  } else {\n    // 默认状态：标准三角形箭头\n    arrowPath = `M0,0 L${-size},-${size/2} L${-size},${size/2} Z`;\n    fillColor = 'currentColor';\n  }\n\n  return (\n    <g\n      id={id}\n      transform={`translate(${pos.x}, ${pos.y}) rotate(${rotation})`}\n    >\n      <path\n        d={arrowPath}\n        fill={fillColor}\n        strokeWidth={strokeWidth}\n        stroke={fillColor}\n      />\n    </g>\n  );\n}\n```\n\n**ArrowRendererProps 接口**：\n\n```ts pure\ninterface ArrowRendererProps {\n  id: string;                    // 箭头唯一标识符\n  pos: { x: number; y: number }; // 箭头位置\n  reverseArrow: boolean;         // 是否反转箭头方向\n  strokeWidth: number;           // 线条粗细\n  vertical: boolean;             // 是否为垂直线条\n  hide: boolean;                 // 是否隐藏箭头\n  line?: WorkflowLineEntity;     // 线条实体（可用于获取状态）\n}\n```\n\n## 在线条上添加 Label\n\n代码见自由布局最佳实践\n\n<img loading=\"lazy\" style={{ width: 500, margin: '0 auto' }}  className=\"invert-img\" src=\"/free-layout/line-add-button.gif\"/>\n\n```ts pure\n\nimport { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';\n\nconst editorProps = {\n  plugins: () => [\n      /**\n       * Line render plugin\n       * 连线渲染插件\n       */\n      createFreeLinesPlugin({\n        renderInsideLine: LineAddButton,\n      }),\n  ]\n}\n```\n\n## 节点监听自身的连线变化并刷新\n\n```tsx pure\n\nimport {\n  useRefresh,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-editor';\n\nfunction NodeRender({ node }) {\n  const refresh = useRefresh()\n  const linesData = node.get(WorkflowNodeLinesData)\n  useEffect(() => {\n    const dispose = linesData.onDataChange(() => refresh())\n    return () => dispose.dispose()\n  }, [])\n  return <div>xxxx</div>\n}\n\n```\n\n## 监听所有线条的连线变化\n\n这个场景用于当希望在外部组件监听线条连接情况\n\n```ts pure\nimport { useEffect } from 'react'\nimport { WorkflowLinesManager, useRefresh } from '@flowgram.ai/free-layout-editor'\n\n\nfunction SomeReact() {\n  const refresh = useRefresh()\n  const linesManager = useService(WorkflowLinesManager)\n  useEffect(() => {\n      const dispose = linesManager.onAvailableLinesChange(() => refresh())\n      return () => dispose.dispose()\n  }, [])\n  console.log(ctx.document.linesManager.getAllLines())\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/load.mdx",
    "content": "# 加载与保存\n\n画布的数据通过 [WorkflowDocument](/api/core/workflow-document.html) 来存储\n\n## 画布数据\n\n:::note 文档数据基本结构:\n\n- nodes `array` 节点列表, 支持嵌套\n- edges `array` 边列表\n\n:::\n\n:::note 节点数据基本结构:\n\n- id: `string` 节点唯一标识, 必须保证唯一\n- meta: `object` 节点的 ui 配置信息，如自由布局的 `position` 信息放这里\n- type: `string | number` 节点类型，会和 `nodeRegistries` 中的 `type` 对应\n- data: `object` 节点表单数据, 业务可自定义\n- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming`, 目前会存子画布的节点\n- edges: `array` 子画布的边数据\n\n:::\n\n:::note 边数据基本结构:\n\n- sourceNodeID: `string` 开始节点 id\n- targetNodeID: `string` 目标节点 id\n- sourcePortID?: `string | number` 开始端口 id, 缺省则采用开始节点的默认端口\n- targetPortID?: `string | number` 目标端口 id, 缺省则采用目标节点的默认端口\n\n:::\n\n\n```tsx pure title=\"initial-data.ts\"\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'Start',\n        content: 'Start content'\n      },\n    },\n    {\n      id: 'node_0',\n      type: 'custom',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: {\n        title: 'Custom',\n        content: 'Custom node content'\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'End',\n        content: 'End content'\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'node_0',\n    },\n    {\n      sourceNodeID: 'node_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n\n```\n## 加载\n\n- 通过 initialData 加载\n\n```tsx pure\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App({ data }) {\n  return (\n    <FreeLayoutEditorProvider initialData={data} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n\n- 通过 ref 动态加载\n\n```tsx pure\n\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(async () => {\n    const data = await request('https://xxxx/getJSON')\n    ref.current.document.fromJSON(data)\n    setTimeout(() => {\n      // 加载后触发画布的 fitview 让节点自动居中\n      ref.current.document.fitView()\n    }, 100)\n  }, [])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n- 动态重载画布所有数据 (会产生 redo/undo/onContentChange )\n\n```tsx pure\n\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\n\nfunction App({ data }) {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    // ctx.operation 支持 diff 重新加载画布数据并可通过 undo 撤销\n    ref.current.operation.fromJSON(data)\n    setTimeout(() => {\n      // 加载后触发画布的 fitview 让节点自动居中\n      ref.current.document.fitView()\n    }, 10)\n  }, [data])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n- 批量添加节点和线条\n\n```tsx pure\nctx.document.batchAddFromJSON({\n  nodes: [...],\n  edges: [...]\n}, {\n  // 可选，如果要添加到子画布中用这个\n  parent: loopNode\n})\n```\n\n## 监听变化并自动保存\n\n```tsx pure\n\nimport { FreeLayoutEditorProvider, FreeLayoutPluginContext, EditorRenderer } from '@flowgram.ai/free-layout-editor'\nimport { debounce } from 'lodash-es'\n\nfunction App() {\n  const ref = useRef<FreeLayoutPluginContext | undefined>();\n\n  useEffect(() => {\n    // 监听画布变化 延迟 1 秒 保存数据, 避免画布频繁更新\n    const toDispose = ref.current.document.onContentChange(debounce(() => {\n        // 通过 toJSON 获取画布最新的数据\n        request('https://xxxx/save', {\n          data: ref.current.document.toJSON()\n        })\n    }, 1000))\n    return () => toDispose.dispose()\n  }, [])\n  return (\n    <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/node.mdx",
    "content": "# 节点\n\n节点通过 [FlowNodeEntity](/api/core/flow-node-entity.html) 定义\n\n## 节点核心 API\n\n- id: `string` 节点 id\n- flowNodeType: `string` | `number` 节点类型\n- bounds: `Rectangle` 获取节点的 x，y，width，height, 等价于 `transform.bounds`\n- blocks: `FlowNodeEntity[]` 获取子节点 (如 Loop)\n- parent: `FlowNodeEntity | undefined` 获取父节点\n- document: [WorkflowDocument](/api/core/workflow-document.html) 文档链接\n- transform: [FlowNodeTransformData](https://flowgram.ai/auto-docs/document/classes/FlowNodeTransformData.html) 获取节点的 transform 矩阵数据\n- renderData: [FlowNodeRenderData](https://flowgram.ai/auto-docs/document/classes/FlowNodeRenderData.html) 获取节点的渲染数据, 包含渲染状态等\n- form: [NodeFormProps](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) 获取节点的表单数据, 等价于 [getNodeForm](/api/utils/get-node-form.html)\n- scope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量作用域\n- privateScope: [FlowNodeScope](https://flowgram.ai/auto-docs/editor/interfaces/FlowNodeScope) 变量私有作用域\n- lines: [WorkflowNodeLinesData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodeLinesData.html) 自由布局线条数据\n- ports: [WorkflowNodePortsData](https://flowgram.ai/auto-docs/free-layout-core/classes/WorkflowNodePortsData.html) 自由布局端口数据\n- getNodeRegistry(): [WorkflowNodeRegistry](https://flowgram.ai/auto-docs/free-layout-core/interfaces/WorkflowNodeRegistry) 获取节点注册器\n- getService(): 获取 [IOC](/guide/concepts/ioc.html) 服务，如 `node.getService(HistoryService)`\n- getExtInfo(): 获取节点扩展数据, 如 `node.getExtInfo<{ test: string }>()`\n- updateExtInfo(): 更新节点扩展数据, 如 `node.updateExtInfo<{ test: string }>({ test: 'test' })`\n- dispose(): 销毁节点\n- toJSON(): json 序列化\n\n## 节点数据\n\n通过 `node.toJSON()` 可以获取\n\n:::note 基本结构:\n\n- id: `string` 节点唯一标识, 必须保证唯一\n- meta: `object` 节点的 ui 配置信息，如自由布局的 `position` 信息放这里\n- type: `string | number` 节点类型，会和 `nodeRegistries` 中的 `type` 对应\n- data: `object` 节点表单数据, 业务可自定义\n- blocks: `array` 节点的分支, 采用 `block` 更贴近 `Gramming` 自由布局布局场景会用在子画布的子节点\n- edges: `array` 子画布的边数据\n\n:::\n\n```ts pure\nconst nodeData: FlowNodeJSON = {\n  id: 'xxxx',\n  type: 'condition',\n  data: {\n    title: 'MyCondition',\n    desc: 'xxxxx'\n  },\n}\n```\n\n## 节点定义\n\n在自由布局场景，节点定义用于声明节点的初始化位置/大小，端口，表单渲染等。\n\n\n```tsx pure title=\"node-registries.tsx\"\nimport { WorkflowNodeRegistry, ValidateTrigger } from '@flowgram.ai/free-layout-editor';\n\n/**\n * You can customize your own node registry\n * 你可以自定义节点的注册器\n */\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true, // 标记为开始节点\n      deleteDisable: true, // 开始节点不能删除\n      copyDisable: true, // 开始节点不能复制\n      defaultPorts: [{ type: 'output' }], // 用于定义节点的输入和输出端口, 开始节点只有输出端口\n      // useDynamicPort: true, // 用于动态端口，会寻找 data-port-id 和 data-port-type 属性的 dom 作为端口\n    },\n    /**\n     * 配置节点表单的校验及渲染,\n     * 注：validate 采用数据和渲染分离，保证节点即使不渲染也能对数据做校验\n     */\n    formMeta: {\n      validateTrigger: ValidateTrigger.onChange,\n      validate: {\n        title: ({ value }) => (value ? undefined : 'Title is required'),\n      },\n      /**\n       * Render form\n       */\n      render: () => (\n       <>\n          <Field name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field name=\"content\">\n            {({ field }) => <input onChange={field.onChange} value={field.value}/>}\n          </Field>\n        </>\n      )\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n    formMeta: {\n      // ...\n    }\n  },\n  {\n    type: 'custom',\n    meta: {\n    },\n    formMeta: {\n      // ...\n    },\n    defaultPorts: [{ type: 'output' }, { type: 'input' }], // 普通节点有两个端口\n  },\n];\n\n```\n\n\n\n\n## 当前渲染节点获取\n\n通过 [useNodeRender](/api/hooks/use-node-render.html) 获取节点相关方法\n\n```tsx pure\nfunction BaseNode() {\n  const { id, type, data, updateData, node } = useNodeRender()\n}\n```\n\n## 获取当前节点的输入/输出节点或线条\n\n```ts pure\n// 获取当前节点的输入节点（通过连接线计算）\nnode.lines.inputNodes\n// 获取所有输入节点 （会往上递归获取所有）\nnode.lines.allInputNodes\n// 获取输出节点\nnode.lines.outputNodes\n// 获取所有输出节点\nnode.lines.allOutputNodes\n// 输入线条 (包含 isDrawing 或 isHidden 的线条)\nnode.lines.inputLines\n// 输出线条 (包含 isDrawing 或 isHidden 的线条)\nnode.lines.outputLines\n// 所有线条 (不包含 isDrawing 或 isHidden 的线条)\nnode.lines.availableLines\n\n```\n\n## 创建节点\n\n- 通过 [WorkflowDocument](/api/core/workflow-document.html) 创建\n\n```ts pure\nconst ctx = useClientContext()\n\nctx.document.createWorkflowNode({\n  id: 'xxx', // 要保证画布内唯一\n  type: 'custom',\n  meta: {\n    /**\n     * 如果不传入，则默认在画布中间创建\n     * 如果要通过鼠标位置获取 position (如点击画布任意位置创建节点)，可通过 `ctx.playground.config.getPosFromMouseEvent(mouseEvent)` 转换\n     */\n    position: { x: 100, y: 100 } //\n  },\n  data: {}, // 表单相关数据\n  blocks: [], // 子画布的节点\n  edges: [] // 子画布的边\n})\n\n```\n- 通过 WorkflowDragService 创建, 见[自由布局基础用法](/examples/free-layout/free-layout-simple.html)\n\n```ts pure\nconst dragService = useService<WorkflowDragService>(WorkflowDragService);\n\n// 这里的 mouseEvent 会自动转成 画布的 position\ndragService.startDragCard(nodeType, mouseEvent, {\n  id: 'xxxx',\n  data: {}, // 表单相关数据\n  blocks: [], // 子画布的节点\n  edges: [] // 子画布的边\n})\n\n```\n\n## 删除节点\n\n通过 `node.dispose` 删除节点\n\n```tsx pure\nfunction BaseNode({ node }) {\n  function onClick() {\n    node.dispose()\n  }\n  return (\n    <button onClick={onClick}>Delete</button>\n  )\n}\n```\n\n## 更新节点 data 数据\n\n- 通过 [useNodeRender](/api/hooks/use-node-render.html) 或 [node.form](https://flowgram.ai/auto-docs/editor/interfaces/NodeFormProps.html) 获取节点的 data 数据\n\n```tsx pure\nfunction BaseNode() {\n  const { node, form } = useNodeRender();\n  // 1. form.values 对应节点的 data 数据\n  // 2. form.setValueIn('title', 'xxxx') 修改 data.title\n  // 3. form.getValueIn('title') 获取 data.title\n  // 4. form.updateFormValues({ ... }) 更新表单所有数据\n\n  function onChange(e) {\n    form.setValueIn('title', e.target.value)\n  }\n  return <input value={form.values.title} onChange={onChange}/>\n}\n```\n- 通过 Field 更新表单数据, 详细见 [表单的使用](/guide/form/form.html)\n\n```tsx pure\n\nfunction FormRender() {\n  return (\n    <Field name=\"title\">\n      <Input />\n    </Field>\n  )\n}\n```\n\n## 更新节点的 extInfo 数据\n\nextInfo 用于存储 一些 ui 状态, 如果未开启节点引擎，节点的 data 数据会默认存到 extInfo 里\n\n```tsx pure\nfunction BaseNode({ node }) {\n  const times = node.getExtInfo()?.times || 0\n  function onClick() {\n    node.updateExtInfo({ times: times ++ })\n  }\n  return (\n    <div>\n      <span>Click Times: {times}</span>\n      <button onClick={onClick}>Click</button>\n    </div>\n  )\n}\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/port.mdx",
    "content": "# 端口\n\n- [WorkflowNodePortsData](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-ports-data.ts) 管理节点的所有端口信息\n- [WorkflowPortEntity](https://github.com/bytedance/flowgram.ai/blob/main/packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts) 端口实例\n- [WorkflowPortRender](https://github.com/bytedance/flowgram.ai/blob/main/packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx) 端口渲染组件\n\n\n## 定义端口\n\n节点声明添加 `defaultPorts` , 如 `{ type: 'input', location: 'left' }`, 则会在节点左侧加入输入端口\n\n\n```ts pure\n// Port 接口\nexport interface WorkflowPort {\n  /**\n   * 没有代表 默认连接点，默认 input 类型 为最左边中心，output 类型为最右边中心\n   */\n  portID?: string | number;\n  /**\n   * 输入或者输出点\n   */\n  type: 'input' | 'output';\n  /**\n   * 端口位置\n   */\n  location?: 'left' | 'top' | 'right' | 'bottom';\n  /**\n   *  端口位置配置\n   * @example\n   *  // bottom-center\n   *  {\n   *    left: '50%',\n   *    bottom: 0\n   *  }\n   *  // right-center\n   *  {\n   *    right: 0,\n   *    top: '50%'\n   *  }\n   */\n  locationConfig?: { left?: string | number, top?: string | number, right?: string | number, bottom?: string | number}\n  /**\n   * 相对于 location 的偏移\n   */\n  offset?: IPoint;\n  /**\n   * 端口热区大小\n   */\n  size?: { width: number; height: number };\n  /**\n   * 禁用端口\n   */\n  disabled?: boolean;\n}\n```\n\n```ts pure title=\"node-registries.ts\"\n{\n  type: 'start',\n  meta: {\n    defaultPorts: [{ type: 'output', location: 'right' }, { type: 'input', location: 'left' }]\n  },\n}\n```\n\n## 动态端口\n\n节点声明添加 `useDynamicPort` , 当设置为 true 则会到节点dom 上寻找 data-port-id 和 data-port-type 属性的 dom 作为端口\n\n\n```ts pure title=\"node-registries.ts\"\n{\n  type: 'condition',\n  meta: {\n    defaultPorts: [{ type: 'input'}]\n    useDynamicPort: true\n  },\n}\n\n```\n\n```tsx pure\n\n/**\n*  动态端口通过 querySelectorAll('[data-port-id]') 查找端口位置\n */\nfunction BaseNode() {\n  return (\n    <div>\n      <div data-port-id=\"condition-if-0\" data-port-type=\"output\" data-port-location=\"right\"></div>\n      <div data-port-id=\"condition-if-1\" data-port-type=\"output\" data-port-location=\"right\" ></div>\n      {/* others */}\n    </div>\n  )\n}\n```\n\n## 垂直端口\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/vertical-ports.png\"/>\n\n```typescript pure\nexport const nodeRegsistries = [\n  {\n    type: 'chain',\n    meta: {\n      defaultPorts: [\n        { type: 'input' },\n        { type: 'output' },\n        {\n          portID: 'p4',\n          location: 'bottom',\n          locationConfig: { left: '33%', bottom: 0 },\n          type: 'output',\n        },\n        {\n          portID: 'p5',\n          location: 'bottom',\n          locationConfig: { left: '66%', bottom: 0 },\n          type: 'output',\n        },\n      ],\n    },\n  },\n  {\n    type: 'tool',\n    meta: {\n      defaultPorts: [{ location: 'top', type: 'input' }],\n    },\n  },\n]\n```\n\n## 更新端口数据\n\n- 静态端口更新\n```ts pure\n// 可以根据表单数据调用这个方法更新静态端口数据\nnode.ports.updateAllPorts([\n    { type: 'output', location: 'right', locationConfig: { left: '33%', bottom: 0 }},\n    { type: 'input', location: 'left', locationConfig: { left: '66%', bottom: 0 }}\n])\n```\n- 动态端口更新\n\n```ts pure\n// 刷新并同步节点 dom 中的端口数据\nnode.ports.updateDynamicPorts()\n```\n\n## 监听表单变化并更新端口数据\n\n下边 condition 节点通过 [表单effect](/guide/form/form.html) 监听 `portKeys` 数据并更新端口数据, 详细见 [Demo](/examples/free-layout/free-layout-simple.html)\n\n<img loading=\"lazy\" className=\"invert-img\" height=\"200\" src=\"/free-layout/auto-update-ports.gif\"/>\n\n```tsx pure title=\"node-registries.ts\"\nimport {\n  Field,\n  DataEvent,\n  EffectFuncProps,\n  WorkflowPorts\n} from '@flowgram.ai/free-layout-editor';\n\nconst CONDITION_ITEM_HEIGHT = 30\nconst conditionNodeRegistry =  {\n    type: 'condition',\n    meta: {\n      defaultPorts: [{ type: 'input' }],\n    },\n    formMeta: {\n      effect: {\n        /**\n         * Listen for \"portsKeys\" changes and update ports\n         */\n        portKeys: [{\n          event: DataEvent.onValueInitOrChange,\n          effect: ({ value, context }: EffectFuncProps<Array<string>, FormData>) => {\n            const { node } = context\n            const defaultPorts: WorkflowPorts = [{ type: 'input'}]\n            const newPorts: WorkflowPorts = value.map((portID: string, i: number) => ({\n              type: 'output',\n              portID,\n              location: 'right',\n              locationConfig: {\n                right: 0,\n                top: (i + 1) * CONDITION_ITEM_HEIGHT\n              }\n            }))\n            node.ports.updateAllPorts([...defaultPorts, ...newPorts])\n          },\n        }],\n      },\n      render: () => (\n        <>\n          <Field<string> name=\"title\">\n            {({ field }) => <div className=\"demo-free-node-title\">{field.value}</div>}\n          </Field>\n          <Field<Array<string>> name=\"portKeys\">\n            {({ field: { value, onChange }, }) => {\n              return (\n                <div className=\"demo-free-node-content\" style={{\n                  width: 160,\n                  height: value.length * CONDITION_ITEM_HEIGHT,\n                  minHeight: 2 * CONDITION_ITEM_HEIGHT\n                }}>\n                  <div>\n                    <button onClick={() => onChange(value.concat(`if_${value.length}`))}>Add Port</button>\n                  </div>\n                  <div style={{ marginTop: 8 }}>\n                    <button onClick={() => onChange(value.filter((v, i, arr) => i !== arr.length - 1))}>Delete Port\n                    </button>\n                  </div>\n                </div>\n              )\n            }}\n          </Field>\n        </>\n      ),\n    },\n  }\n```\n## 端口渲染\n\n端口最终通过 `WorkflowPortRender` 组件渲染，支持自定义 style, 或者业务基于源码重新实现该组件, 参考 [自由布局最佳实践 - 节点渲染](https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/base-node/node-wrapper.tsx)\n\n## 自定义端口颜色\n\n可以通过向 `WorkflowPortRender` 传递颜色 props 来自定义端口颜色：\n\n- `primaryColor` - 激活状态颜色（linked/hovered）\n- `secondaryColor` - 默认状态颜色\n- `errorColor` - 错误状态颜色\n- `backgroundColor` - 背景颜色\n\n```tsx pure\n\nimport { WorkflowPortRender, useNodeRender } from '@flowgram.ai/free-layout-editor';\n\nfunction BaseNode() {\n  const { ports } = useNodeRender();\n  return (\n    <div>\n      <div data-port-id=\"condition-if-0\" data-port-type=\"output\"></div>\n      <div data-port-id=\"condition-if-1\" data-port-type=\"output\"></div>\n      {ports.map((p) => (\n        <WorkflowPortRender\n          key={p.id}\n          entity={p}\n          className=\"xxx\"\n          style={{ /* custom style */}}\n          // 自定义端口颜色\n          primaryColor=\"#4d53e8\"        // 激活状态颜色（linked/hovered）\n          secondaryColor=\"#9197f1\"      // 默认状态颜色\n          errorColor=\"#ff4444\"          // 错误状态颜色\n          backgroundColor=\"#ffffff\"     // 背景颜色\n        />\n      ))}\n    </div>\n  )\n}\n```\n\n## 获取端口数据\n\n```ts pure\nconst { ports } = node\n\nconsole.log(ports.inputPorts) // 获取当前节点的所有输入端口\nconsole.log(ports.outputPorts) // 获取当前节点的所有输出端口\n\nconsole.log(ports.inputPorts.map(port => port.availableLines)) // 通过端口找到连接的线条\n\nports.updateDynamicPorts() // 当动态端口修改了 dom 结构或位置，可以通过该方法手动刷新端口位置(dom 渲染有延迟，最好在 useEffect 或者 setTimeout 执行)\n```\n\n## 端口双向连接\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/two-way-connection.gif\"/>\n\n```ts pure title=\"node-registries.ts\"\n  {\n    type: 'twoway',\n    meta: {\n      defaultPorts: [\n        // input 和 output 端口 可以叠加\n        { type: 'input', portID: 'input-left', location: 'left' },\n        { type: 'output', portID: 'output-left', location: 'left' },\n        { type: 'input', portID: 'input-right', location: 'right' },\n        { type: 'output', portID: 'output-right', location: 'right' },\n      ],\n    },\n  },\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/free-layout/sub-canvas.mdx",
    "content": "# 子画布\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/loop2.png\"/>\n\n详细代码见 [自由布局最佳实践](/examples/free-layout/free-feature-overview.html)\n\n## 添加子画布插件\n\n```tsx pure\n\nimport { createContainerNodePlugin } from '@flowgram.ai/free-container-plugin';\n\nfunction App() {\n  const editorProps = {\n    plugins: () => [\n      createContainerNodePlugin({}),\n    ]\n    // ..others\n  }\n  return (\n    <FreeLayoutEditorProvider {...editorProps}>\n      <EditorRenderer className=\"demo-editor\" />\n    </FreeLayoutEditorProvider>\n  )\n}\n```\n\n## 定义子画布节点\n\n```tsx pure\nimport { SubCanvasRender } from '@flowgram.ai/free-container-plugin';\n\nexport const LoopNodeRegistry: FlowNodeRegistry = {\n  type: 'loop',\n  info: {\n    icon: iconLoop,\n    description:\n      'Used to repeatedly execute a series of tasks by setting the number of iterations and logic.',\n  },\n  meta: {\n    /**\n     * 子画布标记\n     */\n    isContainer: true,\n    /**\n    * 子画布默认大小设置\n     */\n    size: {\n      width: 560,\n      height: 400,\n    },\n    /**\n    * 子画布 padding 设置\n     */\n    padding: () => ({ // 容器 padding 设置\n      top: 150,\n      bottom: 100,\n      left: 100,\n      right: 100,\n    }),\n    /**\n      * 控制子画布内的节点选中状态\n      */\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      // 鼠标开始时所在位置不包括当前节点时才可选中\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n  },\n  formMeta: {\n    render: () => (\n      <div>\n        { /* others */ }\n        <SubCanvasRender />\n      </div>\n    )\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/getting-started/_meta.json",
    "content": "[\n  \"introduction\",\n  \"quick-start\",\n  \"free-layout\",\n  \"fixed-layout\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/getting-started/fixed-layout.mdx",
    "content": "# 固定布局\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\nimport { FixedLayoutCodePreview } from '@components/code-preview';\nimport step1 from '@components/fixed-examples/step-1.tsx?raw';\nimport step2 from '@components/fixed-examples/step-2.tsx?raw';\nimport step3 from '@components/fixed-examples/step-3.tsx?raw';\nimport step4 from '@components/fixed-examples/step-4.tsx?raw';\nimport step5App from '@components/fixed-examples/step-5/app.tsx?raw';\nimport step5UseEditorProps from '@components/fixed-examples/step-5/use-editor-props.tsx?raw';\nimport step5InitialData from '@components/fixed-examples/step-5/initial-data.ts?raw';\nimport step5NodeRegistries from '@components/fixed-examples/step-5/node-registries.tsx?raw';\nimport step5NodeRender from '@components/fixed-examples/step-5/node-render.tsx?raw';\nimport step5Adder from '@components/fixed-examples/step-5/adder.tsx?raw';\nimport step6App from '@components/fixed-examples/step-6/app.tsx?raw';\nimport step6UseEditorProps from '@components/fixed-examples/step-6/use-editor-props.tsx?raw';\nimport step6InitialData from '@components/fixed-examples/step-6/initial-data.ts?raw';\nimport step6NodeRegistries from '@components/fixed-examples/step-6/node-registries.tsx?raw';\nimport step6NodeRender from '@components/fixed-examples/step-6/node-render.tsx?raw';\nimport step6Adder from '@components/fixed-examples/step-6/adder.tsx?raw';\nimport step7App from '@components/fixed-examples/step-7/app.tsx?raw';\nimport step7UseEditorProps from '@components/fixed-examples/step-7/use-editor-props.tsx?raw';\nimport step7InitialData from '@components/fixed-examples/step-7/initial-data.ts?raw';\nimport step7NodeRegistries from '@components/fixed-examples/step-7/node-registries.tsx?raw';\nimport step7NodeRender from '@components/fixed-examples/step-7/node-render.tsx?raw';\nimport step7Adder from '@components/fixed-examples/step-7/adder.tsx?raw';\nimport step7Tools from '@components/fixed-examples/step-7/tools.tsx?raw';\nimport step7Minimap from '@components/fixed-examples/step-7/minimap.tsx?raw';\n\n\n\n## Step.0 - 安装依赖\n\n1. 安装编辑器包\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"pnpm\": \"pnpm add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"yarn\": \"yarn add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n  \"bun\": \"bun add @flowgram.ai/fixed-layout-editor @flowgram.ai/fixed-semi-materials\",\n}} />\n\n2. 安装 styled-components（若尚未安装）\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install styled-components\",\n  \"pnpm\": \"pnpm add styled-components\",\n  \"yarn\": \"yarn add styled-components\",\n  \"bun\": \"bun add styled-components\",\n}} />\n\n## Step.1 - 引入画布组件\n\n1. 引入样式文件，确保基础样式生效：\n   ```tsx\n   import '@flowgram.ai/fixed-layout-editor/index.css';\n   ```\n\n2. 使用 `FixedLayoutEditorProvider` 提供编辑器上下文，`EditorRenderer` 负责渲染画布；并通过 `materials.components` 引入默认的固定布局 Semi 组件集：\n   ```tsx\n   const FlowGramApp = () => (\n     <FixedLayoutEditorProvider\n       materials={{ components: defaultFixedSemiMaterials }}\n     >\n       <EditorRenderer />\n     </FixedLayoutEditorProvider>\n   );\n   ```\n\n3. 其余文件保持默认导出即可。\n\n> 预期效果：页面加载后仅展示一个空白画布，无任何节点或连线。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step1\n}} />\n\n\n## Step.2 - 实现节点组件\n\n1. 导入节点渲染相关 API：\n   - `useNodeRender`：获取节点上下文（如拖拽、悬停、高亮、表单等）。\n   - `FlowNodeEntity`：节点实体类型，用于声明 `NodeRender` 的 props。\n\n2. 创建 `NodeRender` 组件，自定义节点尺寸与样式，并接入拖拽与表单渲染：\n   ```tsx\n   import { useNodeRender, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';\n\n   export const NodeRender = ({ node }: { node: FlowNodeEntity }) => {\n     const { onMouseEnter, onMouseLeave, startDrag, form, dragging, activated } = useNodeRender();\n     return (\n       <div\n         onMouseEnter={onMouseEnter}\n         onMouseLeave={onMouseLeave}\n         onMouseDown={(e) => { startDrag(e); e.stopPropagation(); }}\n         style={{ width: 280, minHeight: 88, background: '#fff', borderRadius: 8, opacity: dragging ? 0.3 : 1, /* ... */ }}\n       >\n         {form?.render()}\n       </div>\n     );\n   };\n   ```\n\n3. 在 `FixedLayoutEditorProvider` 中注册：\n   - `materials.renderDefaultNode` 指定默认节点渲染器为 `NodeRender`。\n   - `nodeRegistries` 声明可用节点类型（示例为 `custom`）。\n   - `initialData` 提供一个初始节点，类型为 `custom`。\n\n> 预期效果：画布中出现一个可拖拽的自定义样式节点。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step2\n}} />\n\n\n## Step.3 - 自定义添加节点组件与删除节点按钮\n\n1. 为节点添加删除按钮\n   - 在 `NodeRender` 中通过 `useClientContext()` 获取 `ctx`，点击按钮调用 `ctx.operation.deleteNode(node)` 删除当前节点。\n   - 注意阻止事件冒泡：`e.stopPropagation()`，避免干扰画布的选择/拖拽行为。\n   ```tsx\n   const ctx = useClientContext();\n   <button onClick={(e) => { e.stopPropagation(); ctx.operation.deleteNode(node); }}>×</button>\n   ```\n\n2. 自定义添加节点组件 `Adder`\n   - 使用 `useService(FlowOperationService)` 与 `usePlayground()` 封装 `handleAdd` 方法：在指定节点之后插入一个新节点，并滚动到视图中心。\n   - 基于 `hoverActivated` 切换 UI：悬停时显示加号与更大的点击区域；只读模式下不显示。\n   ```tsx\n   const { handleAdd } = useAddNode();\n   const Adder = ({ from, hoverActivated }) => (\n     <div onClick={() => handleAdd({ type: 'custom', id: `custom_${Date.now()}` }, from)}>\n       {hoverActivated ? <span>+</span> : null}\n     </div>\n   );\n   ```\n\n3. 在 `materials.components` 中注册 `Adder`\n   - 通过 `FlowRendererKey.ADDER` 覆盖默认的“添加节点”渲染器。\n   ```tsx\n   materials={{\n     renderDefaultNode: NodeRender,\n     components: { ...defaultFixedSemiMaterials, [FlowRendererKey.ADDER]: Adder },\n   }}\n   ```\n\n4. 初始化数据与视图适配\n   - `initialData` 提供基础流程：`start -> custom -> end`。\n   - 在 `onAllLayersRendered` 中调用 `fitView`，让画布自动适配内容：\n   ```tsx\n   onAllLayersRendered={(ctx) => {\n     setTimeout(() => {\n       ctx.playground.config.fitView(ctx.document.root.bounds.pad(30));\n     }, 10);\n   }}\n   ```\n\n> 预期效果：\n>\n> • 画布展示一个由 `start`、`custom`、`end` 组成的基础流程，视图自动居中/缩放到合适范围。\n>\n> • 鼠标悬停到可添加位置时出现圆形加号按钮，点击即可在当前节点之后新增一个 `custom` 节点，并自动滚动到新节点。\n>\n> • 每个节点右上角显示“×”删除按钮，点击可删除该节点（只读模式下隐藏添加组件）。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step3\n}} />\n\n## Step.4 - 引入插件\n\n:::info\n\n- `@flowgram.ai/minimap-plugin`：迷你地图插件，提供画布的小地图视图。\n\n:::\n\n1. 安装插件依赖\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/minimap-plugin\",\n  \"pnpm\": \"pnpm add @flowgram.ai/minimap-plugin\",\n  \"yarn\": \"yarn add @flowgram.ai/minimap-plugin\",\n  \"bun\": \"bun add @flowgram.ai/minimap-plugin\",\n}} />\n\n2. 从对应包导入插件创建函数：\n  - `createMinimapPlugin` 用于生成画布缩略图。\n\n3. 在 `FixedLayoutEditorProvider` 的 `plugins` 属性中注册插件：\n  ```tsx\n  plugins={() => [\n    createMinimapPlugin({\n      enableDisplayAllNodes: true,\n    })\n  ]}\n  ```\n\n> 预期效果:\n>\n> • 画布右上角出现可拖拽/缩放的迷你地图，点击或拖拽缩略图可快速定位主画布。\n>\n> • 启用 `enableDisplayAllNodes: true` 后，迷你地图会显示所有节点，方便在流程较长时快速导航。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step4\n}} />\n\n\n## Step.5 - 拆分文件\n\n为避免单个文件代码行数过长，我们需要将原本集中在一个组件中的编辑器配置、节点渲染、初始化数据等拆分为独立文件，便于维护、复用与协作。\n\n```sh\n- use-editor-props.tsx # 画布配置（集中管理 Provider 的 props）\n- node-render.tsx      # 节点渲染（含删除按钮）\n- initial-data.ts      # 初始化数据（start/custom/end）\n- node-registries.tsx  # 节点注册（示例仅注册 'custom'）\n- adder.tsx            # 自定义添加节点组件（点击新增 custom 节点）\n- App.tsx              # 画布入口（挂载 EditorRenderer）\n```\n\n文件职责说明\n\n- `use-editor-props.tsx`：集中管理 FixedLayoutEditorProvider 的所有 props（插件、视图适配、材料、节点注册与初始数据）：\n  - `plugins`：注册迷你地图插件 `createMinimapPlugin({ enableDisplayAllNodes: true })`。\n  - `onAllLayersRendered`：渲染完成后调用 `fitView`，让画布自动适配内容。\n  - `materials`：\n    - `renderDefaultNode` 指定默认节点渲染器为 `NodeRender`。\n    - `components` 合并 `defaultFixedSemiMaterials`，并用 `[FlowRendererKey.ADDER]: Adder` 覆盖默认添加器。\n  - `nodeRegistries` 与 `initialData` 分别来自独立文件。\n\n- `node-render.tsx`：定义自定义节点渲染器 `NodeRender`，设置节点外观并通过 `form?.render()` 渲染内部表单；同时在右上角提供“×”删除按钮（`useClientContext().operation.deleteNode(node)`）。\n\n- `initial-data.ts`：提供基础流程的初始数据，包含 `start -> custom -> end` 三个节点。\n\n- `node-registries.tsx`：声明节点类型集合（示例为仅注册 `'custom'`）。\n\n- `adder.tsx`：实现自定义添加节点组件 `Adder`，悬停时显示加号；点击时通过 `FlowOperationService.addFromNode` 在当前节点之后新增一个 `custom` 节点，并调用 `scrollToView` 自动定位新节点。\n\n- `App.tsx`：应用入口，从 `useEditorProps` 获取配置并挂载 `EditorRenderer`。\n\n> 预期效果：通过拆分文件，代码结构更清晰、职责更明确，后续扩展与团队协作更容易；界面效果与上一节一致（基础流程、删除按钮与添加节点组件可用，且含迷你地图与自动视图适配）。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step5App,\n    '/use-editor-props.tsx': step5UseEditorProps,\n    '/initial-data.ts': step5InitialData,\n    '/node-registries.tsx': step5NodeRegistries,\n    '/node-render.tsx': step5NodeRender,\n    '/adder.tsx': step5Adder,\n}} />\n\n## Step.6 - 接入表单与历史记录\n\n1. 节点注册与扩展\n\n- `condition`：通过 `extend: 'dynamicSplit'` 将其扩展为“分支节点”，并在 `onAdd()` 中返回默认 `blocks`（多分支）。\n- `custom`：普通节点，在 `onAdd()` 中为其设置默认 `data.title` 与 `data.content`。\n- 可选的 `meta` 配置项可控制节点行为（是否可拖拽、选择、删除、复制、添加等）。\n\n2. 启用表单与历史\n\n在 `use-editor-props.tsx` 中：\n- `nodeEngine.enable = true`：开启节点引擎，允许为节点类型配置 `formMeta`。\n- `history.enable = true` 与 `history.enableChangeNode = true`：启用撤销/重做，并监听节点数据变化（例如表单字段变更）。\n- `history.onApply(ctx)`：在应用历史记录后触发，可用于自动保存（示例中打印 `ctx.document.toJSON()`）。\n- `getNodeDefaultRegistry(type)`：为未显式注册的类型提供默认配置：\n  - `meta.defaultExpanded = true`：默认展开节点的内部内容区域。\n  - `formMeta.render`：渲染表单。本示例通过 `<Field<string> name=\"title\">` 与 `<Field<string> name=\"content\">` 分别展示标题与一个可编辑输入框。\n  ```tsx\n  getNodeDefaultRegistry(type) {\n    return {\n      type,\n      meta: { defaultExpanded: true },\n      formMeta: {\n        render: () => (\n          <>\n            <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n            <Field<string> name=\"content\">\n              <input />\n            </Field>\n          </>\n        ),\n      },\n    };\n  }\n  ```\n\n3. 初始化数据与渲染\n\n- 在 `initial-data.ts` 中，包含一个 `start` 节点、一个带三条分支的 `condition` 节点（其中一条分支包含 `custom`，另一条为 `break`，还有一条为空），以及一个 `end` 节点。\n- 各节点携带 `data.title` 与 `data.content`，`NodeRender` 中的 `form?.render()` 会将这些表单字段渲染到节点外壳内。\n- `Adder` 组件点击后新增 `custom` 节点，默认 `title: 'New Custom Node'` 与 `content: 'Custom Node Content'`。\n\n> 预期效果:\n>\n> • 画布包含 `start`、`condition`（三分支）与 `end`，各节点显示其 `title` 与 `content`；可通过 `Adder` 在流程中快速新增 `custom` 节点。\n>\n> • 撤销/重做快捷键可用，节点移动、添加、删除以及表单输入变更都会纳入历史记录；触发历史应用时将执行示例中的自动保存回调。\n>\n> • 分支节点的展开区域默认打开，便于查看与编辑内部内容。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step6App,\n    '/use-editor-props.tsx': step6UseEditorProps,\n    '/initial-data.ts': step6InitialData,\n    '/node-registries.tsx': step6NodeRegistries,\n    '/node-render.tsx': step6NodeRender,\n    '/adder.tsx': step6Adder,\n}} />\n\n## Step.7 - 创建工具栏\n\n1. 引入工具栏与迷你地图组件\n\n- 在 `App.tsx` 中引入并渲染 `<Tools />` 与自定义 `<Minimap />`，与 `<EditorRenderer />` 同级放置于 `FixedLayoutEditorProvider` 内，使其能够访问编辑器上下文与画布操作方法。\n\n2. 通过工具方法操控画布\n\n- 使用 `usePlaygroundTools()` 获取画布操作方法：`zoomin/zoomout`、`fitView`、`changeLayout` 等。\n- 实时显示缩放比例：通过 `tools.zoom` 读取当前画布缩放并展示百分比。\n\n3. 接入撤销/重做状态\n\n- 使用 `useClientContext()` 获取 `history`，并监听 `history.undoRedoService.onChange` 更新工具栏中的 `Undo/Redo` 可用态。\n- 在 `use-editor-props.tsx` 中确保开启历史：`history.enable = true` 与 `history.enableChangeNode = true`，使撤销/重做对节点数据与布局变更生效。\n\n4. 自定义迷你地图（可选）\n\n- 使用 `MinimapRender` 并自定义容器样式，将缩略图固定在左下角，避免遮挡主要交互区域。\n- 在 `use-editor-props.tsx` 中为迷你地图插件传入：`disableLayer: true` 与 `canvasStyle`（`canvasWidth/canvasHeight/canvasPadding`），获得紧凑的缩略图尺寸与边距。\n\n5. 保持前述功能\n\n- `NodeRender` 继续通过 `form?.render()` 渲染表单字段，并提供删除按钮；`Adder` 支持一键新增 `custom` 节点；`node-registries.tsx` 中维持 `condition/custom` 类型注册；`initial-data.ts` 中包含 `start → condition(三分支) → end` 的示例流程。\n\n> 预期效果:\n>\n> • 画面左下角出现工具栏，支持 `ZoomIn/ZoomOut`、`FitView`、`ChangeLayout` 等常用操作，并实时显示缩放比例；`Undo/Redo` 按钮会根据历史状态联动更新。\n>\n> • 左下角同时展示自定义样式的迷你地图，可点击/拖拽缩略图快速定位主画布；结合工具栏形成完整的编辑工具区，操作更高效。\n\n<FixedLayoutCodePreview files={{\n    '/App.tsx': step7App,\n    '/use-editor-props.tsx': step7UseEditorProps,\n    '/initial-data.ts': step7InitialData,\n    '/node-registries.tsx': step7NodeRegistries,\n    '/node-render.tsx': step7NodeRender,\n    '/adder.tsx': step7Adder,\n    '/tools.tsx': step7Tools,\n    '/minimap.tsx': step7Minimap,\n}} />\n\n## Step.8 - 了解更多\n\n<div style={{\n  display: \"grid\",\n  gridTemplateColumns: \"1fr 1fr\",\n  gap: \"2rem\",\n  marginTop: \"1rem\",\n}}>\n  <div>\n  了解更多固定布局用法\n  - [加载与保存](/guide/fixed-layout/load)\n  - [节点](/guide/fixed-layout/node)\n  - [复合节点](/guide/fixed-layout/composite-nodes)\n  </div>\n  <div>\n  了解 FlowGram.AI 更多功能\n  - [表单](/guide/form/form)\n  - [变量](/guide/variable/basic)\n  - [物料](/materials/introduction)\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/zh/guide/getting-started/free-layout.mdx",
    "content": "# 自由布局\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\nimport { CodePreview } from '@components/code-preview';\nimport step1 from '@components/free-examples/step-1.tsx?raw';\nimport step2 from '@components/free-examples/step-2.tsx?raw';\nimport step3 from '@components/free-examples/step-3.tsx?raw';\nimport step4 from '@components/free-examples/step-4.tsx?raw';\nimport step5App from '@components/free-examples/step-5/app.tsx?raw';\nimport step5InitialData from '@components/free-examples/step-5/initial-data.ts?raw';\nimport step5UseEditorProps from '@components/free-examples/step-5/use-editor-props.tsx?raw';\nimport step5NodeRender from '@components/free-examples/step-5/node-render.tsx?raw';\nimport step5NodeRegistries from '@components/free-examples/step-5/node-registries.tsx?raw';\nimport step6App from '@components/free-examples/step-6/app.tsx?raw';\nimport step6InitialData from '@components/free-examples/step-6/initial-data.ts?raw';\nimport step6UseEditorProps from '@components/free-examples/step-6/use-editor-props.tsx?raw';\nimport step6NodeRender from '@components/free-examples/step-6/node-render.tsx?raw';\nimport step6NodeRegistries from '@components/free-examples/step-6/node-registries.tsx?raw';\nimport step7App from '@components/free-examples/step-7/app.tsx?raw';\nimport step7InitialData from '@components/free-examples/step-7/initial-data.ts?raw';\nimport step7UseEditorProps from '@components/free-examples/step-7/use-editor-props.tsx?raw';\nimport step7NodeRender from '@components/free-examples/step-7/node-render.tsx?raw';\nimport step7NodeRegistries from '@components/free-examples/step-7/node-registries.tsx?raw';\nimport step7Tools from '@components/free-examples/step-7/tools.tsx?raw';\nimport step7AddNode from '@components/free-examples/step-7/add-node.tsx?raw';\nimport step7Minimap from '@components/free-examples/step-7/minimap.tsx?raw';\n\n## Step.0 - 安装依赖\n\n1. 安装编辑器包\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/free-layout-editor\",\n  \"pnpm\": \"pnpm add @flowgram.ai/free-layout-editor\",\n  \"yarn\": \"yarn add @flowgram.ai/free-layout-editor\",\n  \"bun\": \"bun add @flowgram.ai/free-layout-editor\",\n}} />\n\n2. 安装 styled-components（若尚未安装）\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install styled-components\",\n  \"pnpm\": \"pnpm add styled-components\",\n  \"yarn\": \"yarn add styled-components\",\n  \"bun\": \"bun add styled-components\",\n}} />\n\n## Step.1 - 引入画布组件\n\n1. 引入样式文件，确保基础样式生效：\n   ```tsx\n   import '@flowgram.ai/free-layout-editor/index.css';\n   ```\n\n2. 使用 `FreeLayoutEditorProvider` 提供编辑器上下文，`EditorRenderer` 负责渲染画布：\n   ```tsx\n   const FlowGramApp = () => (\n     <FreeLayoutEditorProvider>\n       <EditorRenderer />\n     </FreeLayoutEditorProvider>\n   );\n   ```\n\n3. 其余文件保持默认导出即可。\n\n> 预期效果：页面加载后仅展示一个空白画布，无任何节点或连线。\n\n<CodePreview files={{\n    '/App.tsx': step1\n}} />\n\n## Step.2 - 实现节点组件并注册\n\n1. 导入节点渲染相关 Hook 与组件：\n   - `useNodeRender`：获取节点上下文（如表单）。\n   - `WorkflowNodeProps` & `WorkflowNodeRenderer`：定义并渲染节点外壳。\n\n2. 创建 `NodeRender` 组件，自定义节点尺寸与样式：\n   ```tsx\n   const NodeRender = (props: WorkflowNodeProps) => {\n     const { form } = useNodeRender();\n     return (\n       <WorkflowNodeRenderer\n         style={{ width: 280, height: 88, background: '#fff', borderRadius: 8, ... }}\n         node={props.node}\n       >\n         {form?.render()}\n       </WorkflowNodeRenderer>\n     );\n   };\n   ```\n\n3. 在 `FreeLayoutEditorProvider` 中注册：\n   - `materials.renderDefaultNode` 指定默认节点渲染器。\n   - `nodeRegistries` 声明可用节点类型（示例为 `custom`）。\n   - `initialData` 提供一个初始节点，位置 `{ x: 250, y: 100 }`。\n\n> 预期效果：画布中出现一个可拖拽的自定义样式节点。\n\n<CodePreview files={{\n    '/App.tsx': step2\n}} />\n\n## Step.3 - 添加多节点与连线\n\n1. 新增 `onAllLayersRendered` 回调，在所有图层渲染完成后调用 `ctx.tools.fitView(false)`，让画布自动适配内容。\n\n2. 新增 `canDeleteNode` & `canDeleteLine` 回调，返回 `true` 允许删除节点与连线。\n\n3. 扩展 `initialData`：\n   - 再增加一个同类型节点，位置 `{ x: 400, y: 0 }`。\n   - 在 `edges` 数组中添加一条连线，连接节点 `1` 与节点 `2`。\n\n> 预期效果：\n>\n> • 画布展示两个相连节点，并自动居中/缩放到合适视图。\n>\n> • 选中任意节点或连线，键盘删除键可删除选中元素。\n\n<CodePreview files={{\n    '/App.tsx': step3\n}} />\n\n## Step.4 - 引入插件\n\n:::info\n\n- `@flowgram.ai/free-snap-plugin`：节点对齐插件，使节点在网格上对齐。\n- `@flowgram.ai/minimap-plugin`：迷你地图插件，提供画布的小地图视图。\n\n:::\n\n1. 安装插件依赖\n\n<PackageManagerTabs command={{\n  \"npm\": \"npm install @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"pnpm\": \"pnpm add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"yarn\": \"yarn add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n  \"bun\": \"bun add @flowgram.ai/free-snap-plugin @flowgram.ai/minimap-plugin\",\n}} />\n\n2. 从对应包导入插件创建函数：\n  - `createFreeSnapPlugin` 用于节点网格对齐。\n  - `createMinimapPlugin` 用于生成画布缩略图。\n\n3. 在 `FreeLayoutEditorProvider` 的 `plugins` 属性中注册插件：\n  ```tsx\n  plugins={() => [\n    createMinimapPlugin({}),\n    createFreeSnapPlugin({})\n  ]}\n  ```\n\n> 预期效果:\n>\n> • 画布右上角出现可拖拽/缩放的迷你地图，点击或拖拽缩略图可快速定位主画布。\n>\n> • 拖拽节点时，节点会自动吸附到附近节点，便于快速对齐。\n\n<CodePreview files={{\n    '/App.tsx': step4\n}} />\n\n\n## Step.5 - 拆分文件\n\n为避免单个文件代码行数过长，我们需要将原本集中在一个组件中的编辑器配置、节点渲染、初始化数据等拆分为独立文件，便于维护、复用与协作。\n\n```sh\n- use-editor-props.ts # 画布配置\n- node-render.tsx # 节点渲染\n- initial-data.ts # 初始化数据\n- node-registries.ts # 节点配置\n- App.tsx # 画布入口\n```\n\n文件职责说明\n\n- `use-editor-props.tsx`：集中管理 FreeLayoutEditorProvider 的所有 props（插件、视图适配、材料、节点注册与初始数据）。\n- `node-render.tsx`：定义自定义节点渲染器 NodeRender，负责外观与内部表单渲染。\n- `initial-data.ts`：提供初始节点与连线。当前示例包含 5 个 custom 节点及多条连接关系。\n- `node-registries.tsx`：声明节点类型集合（示例为仅注册 'custom'）。\n- `App.tsx`：应用入口，从 useEditorProps 获取配置并挂载 EditorRenderer。\n\n> 预期效果: 通过拆分文件，代码结构更清晰、职责更明确，后续代码更易扩展\n\n<CodePreview files={{\n    '/App.tsx': step5App,\n    '/use-editor-props.tsx': step5UseEditorProps,\n    '/initial-data.ts': step5InitialData,\n    '/node-registries.tsx': step5NodeRegistries,\n    '/node-render.tsx': step5NodeRender,\n}} />\n\n## Step.6 - 接入表单与历史记录\n\n1. 节点注册与端口配置\n\n- `start`：起始节点，不可删除，默认只有输出端口。\n- `end`：结束节点，不可删除，默认只有输入端口。\n- `custom`：普通节点，默认同时拥有输入与输出端口。\n\n2. 启用表单与历史记录\n\n在 `useEditorProps.tsx` 中：\n- `nodeEngine.enable = true`：开启节点引擎，允许为节点类型配置 `formMeta`。\n- `history.enable = true` 与 `history.enableChangeNode = true`：启用撤销/重做，并监听节点数据变化（例如表单变更）。\n- `getNodeDefaultRegistry(type)`：为未显式注册的类型提供默认配置：\n  - `meta.defaultExpanded = true`：默认展开节点的内部内容区域。\n  - `formMeta.render`：渲染表单。本示例通过 `<Field<string> name=\"title\">` 渲染标题字段。\n\n3. 初始化数据与渲染\n\n- 在 `initial-data.ts` 中，为每个节点设置 `data.title`（如 `Start Node`、`Custom Node A/B/C`、`End Node`）。\n- `NodeRender` 中的 `form?.render()` 会将表单内容渲染进节点外壳，展示各节点的标题。\n\n> 预期效果:\n>\n> • 画布包含 `start`、多个 `custom` 与 `end` 节点，连接关系与初始数据一致。\n>\n> • 每个节点显示其 `title`；选中节点时可扩展为展示更多表单字段与交互。\n>\n> • 撤销/重做快捷键可用，可通过删除、移动节点操作进行验证。\n\n<CodePreview activeFile=\"/use-editor-props.tsx\" files={{\n    '/App.tsx': step6App,\n    '/use-editor-props.tsx': step6UseEditorProps,\n    '/initial-data.ts': step6InitialData,\n    '/node-registries.tsx': step6NodeRegistries,\n    '/node-render.tsx': step6NodeRender,\n}} />\n\n## Step.7 - 创建工具栏\n\n1. 引入工具栏组件\n\n- 在 `App.tsx` 中引入 `<Tools />`，与 `<EditorRenderer />` 同级放置于 `FreeLayoutEditorProvider` 内，使其能够访问编辑器上下文与工具方法。\n\n2. 通过工具方法操控画布\n\n- 使用 `usePlaygroundTools()` 获取画布操作方法：`zoomin/zoomout`、`fitView`、`autoLayout`、`switchLineType` 等。\n- 切换连线样式：通过 `switchLineType` 在 `LineType.BEZIER`（贝塞尔）与 `LineType.LINE_CHART`（折线）之间切换。\n- 实时显示缩放比例：读取 `tools.zoom`，展示当前画布缩放百分比。\n\n3. 接入撤销/重做状态\n\n- 使用 `useClientContext()` 获取 `history`，并监听 `history.undoRedoService.onChange` 更新 `canUndo/canRedo` 按钮状态。\n- 在 `use-editor-props.tsx` 中确保开启历史：`history.enable = true` 与 `history.enableChangeNode = true`，使撤销/重做对节点数据变化生效。\n\n5. 扩展组件（可选）\n\n- Minimap：通过自定义 `MinimapRender` 容器样式将缩略图固定在右下角，提升定位效率。\n- AddNode：提供快速新增节点按钮，通过 `WorkflowDocument.createWorkflowNodeByType` 在画布中心创建并选中节点。\n\n> 预期效果:\n>\n> • 页面右下角出现工具栏，支持 ZoomIn/ZoomOut、FitView、AutoLayout 等常用操作，实时显示缩放比例。\n>\n> • 可切换连线样式（贝塞尔/折线），撤销/重做按钮会随历史状态自动启用/禁用。\n>\n> • 搭配 Minimap 与 AddNode 组件，形成完整的编辑工具区，操作更高效。\n\n<CodePreview activeFile=\"/tools.tsx\" files={{\n    '/App.tsx': step7App,\n    '/use-editor-props.tsx': step7UseEditorProps,\n    '/initial-data.ts': step7InitialData,\n    '/node-registries.tsx': step7NodeRegistries,\n    '/tools.tsx': step7Tools,\n    '/add-node.tsx': step7AddNode,\n    '/minimap.tsx': step7Minimap,\n    '/node-render.tsx': step7NodeRender,\n}} />\n\n## Step.8 - 了解更多\n\n<div style={{\n  display: \"grid\",\n  gridTemplateColumns: \"1fr 1fr\",\n  gap: \"2rem\",\n  marginTop: \"1rem\",\n}}>\n  <div>\n  了解更多自由布局用法：\n  - [加载与保存](/guide/free-layout/load)\n  - [节点](/guide/free-layout/node)\n  - [线条](/guide/free-layout/line)\n  - [端口](/guide/free-layout/port)\n  - [子画布](/guide/free-layout/sub-canvas)\n  </div>\n  <div>\n  了解 FlowGram.AI 更多功能：\n  - [表单](/guide/form/form)\n  - [变量](/guide/variable/basic)\n  - [物料](/materials/introduction)\n  - [运行时](/guide/runtime/introduction)\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/zh/guide/getting-started/introduction.mdx",
    "content": "# 简介\n\nFlowGram 是一个工作流开发框架与工具集。帮助开发者以更快、更简单的方式搭建 AI 工作流平台。\nFlowGram 内置开箱开箱即用的工作流开发能力：可视化流程画布、节点配置表单、变量作用域链，以及开箱即用的物料。\n使用 FlowGram 构建你自己的 AI 工作流平台吧。\n\n## 为什么选择 FlowGram\n\nFlowGram 的诞生源于字节跳动内部构建多样化 AI 工作流平台的需求。\n这些平台通常具有复杂的业务逻辑和流程，从零开始构建不仅耗时，而且开发和维护成本极高。\n\n许多开发者最初尝试使用业界主流的图形可视化库来搭建工作流平台。\n然而，这些通用库无法解决工作流场景下的核心问题，开发者仍需自行处理节点数据管理、动态表单、数据校验、变量作用域链等一系列难题，这导致开发效率低下且后期维护困难。\n\n为了解决这些痛点，我们推出了 FlowGram，一个专为工作流场景设计的开发框架，旨在帮助开发者提升工作流平台的开发效率、缩短开发周期。\nFlowGram 提供了以下核心功能：\n\n- **流程画布**：提供可视化的节点、边的编排能力，同时支持自由布局和固定布局，可以轻松构建复杂的流程图。\n- **表单**：表单引擎维护节点数据的增删查改，并提供渲染、校验、副作用、联动、错误捕获等能力，简化了节点配置的开发。\n- **变量**：变量引擎支持作用域约束、变量结构透视、类型推导等能力，方便管理流程中的数据流转。\n- **物料**：提供开箱即用的组件、副作用、校验器等物料，开发者可以快速复用和扩展，提升开发效率。\n\n借助这些功能，开发者可以将精力聚焦于业务逻辑的实现，从而快速构建出功能完善、性能卓越的 AI 工作流平台。\n\n## 下一步\n\n请阅读 [快速上手](/guide/getting-started/quick-start) 来开始使用 FlowGram。\n\n欢迎到通过 [注册飞书](https://www.feishu.cn/en/) 并扫描下边的二维码加入飞书群，来与我们交流\n\n<img src=\"/lark-group.png\" width=\"200\"/>\n\n## 附：交互体验\n\nFlowGram 提供一套交互的最佳实践，让操作流程更加丝滑\n\n<table className=\"rs-table\">\n  <tr>\n    <td>Motion 过渡动画</td>\n    <td>\n      <p>\n        Motion 动画在 Web 端应用可追溯到 Material Design，里边提到元素的变化如宽高或位置需要一个过渡过程，画布引擎会把线条和节点拆分单独绘制，使实现 Motion 过渡动画成本大大降低\n      </p>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/common/motion.gif\" />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>触摸板手势缩放 + 空格自由拖动画布</td>\n    <td>\n      <p>\n        手势指在 Mac 触摸板两指展开/合并可以实现画布放大/缩小，或者按住空格拖动画布，交互借鉴 Sketch、Figma\n      </p>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/common/touch-pad.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>缩略图</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/minimap.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>撤销/重做</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/redo-undo.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>复制/粘贴(支持快捷键)</td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/copypaste.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>\n        <div>框选 + 拖拽</div>\n        <div>(固定)</div>\n      </div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <div className=\"rs-center\">\n          <img loading=\"lazy\" src=\"/fixed-layout/dragdrop.gif\"  />\n        </div>\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>水平/垂直布局切换</div>\n      <div>(固定)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/layout-change.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>分支折叠</div>\n      <div>(固定)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/fold.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      <div>分组</div>\n      <div>(固定)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/fixed-layout/group.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      自动整理\n      <div>(自由)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/autolayout.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      吸附对齐 + 参考线\n      <div>(自由)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/snap.gif\"  />\n      </div>\n    </td>\n  </tr>\n  <tr>\n    <td>\n      Coze Loop 子画布\n      <div>(自由)</div>\n    </td>\n    <td>\n      <div className=\"rs-center\">\n        <img loading=\"lazy\" src=\"/free-layout/loop.gif\"  />\n      </div>\n    </td>\n  </tr>\n</table>\n"
  },
  {
    "path": "apps/docs/src/zh/guide/getting-started/quick-start.mdx",
    "content": "# 快速上手\n\nimport {\n  PackageManagerTabs\n  // @ts-ignore\n} from '@theme';\n\n:::info\n快速体验 FlowGram.AI，你可以直接 [在 CodeSandbox 中打开](https://codesandbox.io/p/github/louisyoungx/flowgram-demo/main) 或者 [在 StackBlitz 中打开](https://stackblitz.com/~/github.com/louisyoungx/flowgram-demo)\n:::\n\n选择开始方式：\n- 方式一：使用官方模板脚手架搭建新项目（⭐️ 推荐用于快速入门）。\n- 方式二：通过安装编辑器包集成到现有项目中。\n\n## 方式一：通过官方模版创建 FlowGram.AI 应用\n\n1. 使用 FlowGram CLI 搭建一个可运行的演示\n\n<PackageManagerTabs command={{\n  npm: \"npx @flowgram.ai/create-app@latest\",\n  pnpm: \"pnpm dlx @flowgram.ai/create-app@latest\",\n  yarn: \"yarn dlx @flowgram.ai/create-app@latest\",\n  bun: \"bunx @flowgram.ai/create-app@latest\",\n}} />\n\n2. 在提示时选择一个模板（推荐选择 `Free Layout Demo` 用于快速入门）\n\n```text\n- Free Layout Demo            # 自由布局最佳实践 (⭐️ 推荐)\n- Free Layout Demo Simple     # 自由布局基本用法\n- Fixed Layout Demo           # 固定布局最佳实践\n- Fixed Layout Demo Simple    # 固定布局基本用法\n```\n\n<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(320px, 1fr))', gap: 16, marginTop: 12 }}>\n  <div>\n    <p><strong>Free Layout Demo</strong> [查看在线演示](/examples/free-layout/free-layout-simple.html)</p>\n    <img src=\"/examples/example-free-layout.png\" alt=\"自由布局预览\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Fixed Layout Demo</strong> [查看在线演示](/examples/fixed-layout/fixed-layout-simple.html)</p>\n    <img src=\"/examples/example-fixed-layout.png\" alt=\"固定布局预览\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Free Layout Demo Simple</strong> [查看在线演示](/examples/free-layout/free-layout-simple.html)</p>\n    <img src=\"/examples/example-free-layout-simple.png\" alt=\"自由布局简单预览\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <p><strong>Fixed Layout Demo Simple</strong> [查看在线演示](/examples/fixed-layout/fixed-layout-simple.html)</p>\n    <img src=\"/examples/example-fixed-layout-simple.png\" alt=\"固定布局简单预览\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n</div>\n\n3. 查看安装的目录名称\n\n- 使用 Free Layout Demo 模板创建的项目，目录名称为 `demo-free-layout`\n- 使用 Free Layout Demo Simple 模板创建的项目，目录名称为 `demo-free-layout-simple`\n- 使用 Fixed Layout Demo 模板创建的项目，目录名称为 `demo-fixed-layout`\n- 使用 Fixed Layout Demo Simple 模板创建的项目，目录名称为 `demo-fixed-layout-simple`\n\n4. 进入项目目录\n\n```sh\ncd [project-name]\n```\n\n5. 安装依赖\n\n<PackageManagerTabs command={{\n  npm: \"npm install\",\n  pnpm: \"pnpm install\",\n  yarn: \"yarn install\",\n  bun: \"bun install\",\n}} />\n\n6. 启动开发服务器\n\n<PackageManagerTabs command={{\n  npm: \"npm run dev\",\n  pnpm: \"pnpm dev\",\n  yarn: \"yarn dev\",\n  bun: \"bun dev\",\n}} />\n\n## 方式二：直接安装编辑器包\n\n:::tip\n此方法适用于对 FlowGram 项目有一定了解的开发者。\n\n如果初次接触 FlowGram，我们建议优先选择方式一，先对 FlowGram 项目进行熟悉，然后再将所需代码逐步整合到现有的工程中。\n:::\n\n如果你需要将包添加到现有项目中，选择一个布局类型：\n\n<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>\n  <div>\n    <strong>自由布局</strong>\n    <p>节点可以在画布上任意拖动，可以使用边在节点之间进行连接，从而建立节点之间的逻辑关系</p>\n    <p>下一步：[创建自由布局画布](/guide/getting-started/free-layout)</p>\n    <img src=\"/free-layout/free-layout-demo.gif\" alt=\"自由布局演示\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n  <div>\n    <strong>固定布局</strong>\n    <p>节点在图中的位置代表了节点之间的逻辑关系</p>\n    <p>下一步：[创建固定布局画布](/guide/getting-started/fixed-layout)</p>\n    <img src=\"/fixed-layout/fixed-layout-demo.gif\" alt=\"固定布局演示\" style={{ width: '100%', borderRadius: 8 }} />\n  </div>\n</div>\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/_meta.json",
    "content": "[\n  \"background-plugin\",\n  \"minimap-plugin\",\n  \"export-plugin\",\n  \"panel-manager-plugin\",\n  \"free-auto-layout-plugin\",\n  \"free-stack-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/background-plugin.mdx",
    "content": "# @flowgram.ai/background-plugin\n\n背景插件用于自定义画布的背景效果，支持点阵背景、Logo显示和新拟态(Neumorphism)视觉效果。\n\n## 背景配置\n\n背景插件通过 `@flowgram.ai/background-plugin`(内置) 提供，配置项包括：\n\n### 基础配置\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-color.png\"/>\n\n```ts pure\n{\n  // 背景颜色\n  backgroundColor: '#1a1a1a',\n\n  // 点的颜色\n  dotColor: '#ffffff',\n\n  // 点的大小(像素)\n  dotSize: 1,\n\n  // 网格间距(像素)\n  gridSize: 20,\n\n  // 点的透明度(0-1)\n  dotOpacity: 0.5,\n\n  // 点的填充颜色\n  dotFillColor: '#ffffff'\n}\n```\n\n### Logo配置\n\n支持文本和图片两种Logo类型：\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-logo.png\"/>\n\n```ts pure\n{\n  logo: {\n    // Logo文本\n    text: 'FLOWGRAM.AI',\n\n    // 图片URL (可选，优先级高于文本)\n    imageUrl: 'https://example.com/logo.png',\n\n    // 位置：'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'\n    position: 'center',\n\n    // 大小\n    size: 200,\n\n    // 透明度(0-1)\n    opacity: 0.25,\n\n    // 颜色\n    color: '#ffffff',\n\n    // 字体\n    fontFamily: 'Arial, sans-serif',\n\n    // 字体粗细\n    fontWeight: 'bold',\n\n    // 自定义偏移量\n    offset: { x: 0, y: 0 }\n  }\n}\n```\n\n### 新拟态效果\n\n新拟态(Neumorphism)是一种现代化的视觉设计风格，通过双层柔和阴影创造立体感：\n\n<img loading=\"lazy\" className=\"invert-img\" src=\"/free-layout/background-neumorphism.png\"/>\n\n```ts pure\n{\n  logo: {\n    neumorphism: {\n      // 启用新拟态效果\n      enabled: true,\n\n      // 文字颜色\n      textColor: '#E0E0E0',\n\n      // 高光阴影颜色\n      lightShadowColor: 'rgba(255,255,255,0.9)',\n\n      // 暗色阴影颜色\n      darkShadowColor: 'rgba(0,0,0,0.15)',\n\n      // 阴影偏移距离\n      shadowOffset: 6,\n\n      // 阴影模糊半径\n      shadowBlur: 12,\n\n      // 阴影强度\n      intensity: 0.6,\n\n      // 凸起效果(true=凸起, false=凹陷)\n      raised: true\n    }\n  }\n}\n```\n\n## 使用示例\n\n```tsx pure\n// 在编辑器配置中直接使用 background 属性\nconst editorProps = {\n  // 背景配置\n  background: {\n    // 深色主题背景\n    backgroundColor: '#1a1a1a',\n    dotColor: '#ffffff',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.3,\n\n    // 品牌Logo\n    logo: {\n      text: 'FLOWGRAM.AI',\n      position: 'center',\n      size: 200,\n      opacity: 0.25,\n      color: '#ffffff',\n      fontFamily: 'Arial, sans-serif',\n      fontWeight: 'bold',\n\n      // 新拟态效果\n      neumorphism: {\n        enabled: true,\n        textColor: '#E0E0E0',\n        lightShadowColor: 'rgba(255,255,255,0.9)',\n        darkShadowColor: 'rgba(0,0,0,0.15)',\n        shadowOffset: 6,\n        shadowBlur: 12,\n        intensity: 0.6,\n        raised: true\n      }\n    }\n  }\n}\n```\n\n## 预设样式\n\n### 经典黑色主题\n\n```tsx pure\nconst editorProps = {\n  background: {\n    backgroundColor: '#1a1a1a',\n    dotColor: '#ffffff',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.3,\n    logo: {\n      text: '您的品牌',\n      position: 'center',\n      size: 200,\n      opacity: 0.25,\n      color: '#ffffff',\n      neumorphism: {\n        enabled: true,\n        textColor: '#E0E0E0',\n        lightShadowColor: 'rgba(255,255,255,0.9)',\n        darkShadowColor: 'rgba(0,0,0,0.15)',\n        shadowOffset: 6,\n        shadowBlur: 12,\n        intensity: 0.6,\n        raised: true\n      }\n    }\n  }\n}\n```\n\n### 简约白色主题\n\n```tsx pure\nconst editorProps = {\n  background: {\n    backgroundColor: '#ffffff',\n    dotColor: '#000000',\n    dotSize: 1,\n    gridSize: 20,\n    dotOpacity: 0.1,\n    logo: {\n      text: '您的品牌',\n      position: 'center',\n      size: 200,\n      opacity: 0.1,\n      color: '#000000'\n    }\n  }\n}\n```\n\n## 注意事项\n\n1. **颜色搭配**：确保Logo颜色与背景色有足够的对比度\n2. **透明度设置**：Logo透明度不宜过高，以免影响内容可读性\n3. **新拟态效果**：需要合理调整阴影参数，过强的效果可能分散注意力\n4. **性能考虑**：复杂的阴影效果可能影响渲染性能，建议在低端设备上适当简化\n\n## 类型定义\n\n```ts\ninterface BackgroundLayerOptions {\n  /** 网格间距，默认 20px */\n  gridSize?: number;\n  /** 点的大小，默认 1px */\n  dotSize?: number;\n  /** 点的颜色，默认 \"#eceeef\" */\n  dotColor?: string;\n  /** 点的透明度，默认 0.5 */\n  dotOpacity?: number;\n  /** 背景颜色，默认透明 */\n  backgroundColor?: string;\n  /** 点的填充颜色，默认与stroke颜色相同 */\n  dotFillColor?: string;\n  /** Logo 配置 */\n  logo?: {\n    /** Logo 文本内容 */\n    text?: string;\n    /** Logo 图片 URL */\n    imageUrl?: string;\n    /** Logo 位置，默认 'center' */\n    position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n    /** Logo 大小，默认 'medium' */\n    size?: 'small' | 'medium' | 'large' | number;\n    /** Logo 透明度，默认 0.1 */\n    opacity?: number;\n    /** Logo 颜色（仅文本），默认 \"#cccccc\" */\n    color?: string;\n    /** Logo 字体家族（仅文本），默认 'Arial, sans-serif' */\n    fontFamily?: string;\n    /** Logo 字体粗细（仅文本），默认 'normal' */\n    fontWeight?: 'normal' | 'bold' | 'lighter' | number;\n    /** 自定义偏移 */\n    offset?: { x: number; y: number };\n    /** 新拟态效果配置 */\n    neumorphism?: {\n      /** 启用新拟态效果 */\n      enabled: boolean;\n      /** 文字颜色 */\n      textColor?: string;\n      /** 高光阴影颜色 */\n      lightShadowColor?: string;\n      /** 暗色阴影颜色 */\n      darkShadowColor?: string;\n      /** 阴影偏移距离 */\n      shadowOffset?: number;\n      /** 阴影模糊半径 */\n      shadowBlur?: number;\n      /** 阴影强度 */\n      intensity?: number;\n      /** 凸起效果(true=凸起, false=凹陷) */\n      raised?: boolean;\n    };\n  };\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/export-plugin.mdx",
    "content": "# @flowgram.ai/export-plugin\n\nimport { PackageManagerTabs } from '@theme';\n\n<PackageManagerTabs command={{\n  npm: \"npm install @flowgram.ai/export-plugin\"\n}} />\n\n导出插件提供了将工作流导出为图片（PNG、JPEG、SVG）或数据文件（JSON、YAML）的功能。\n\n## 快速开始\n\n1. 安装\n\n<PackageManagerTabs command=\"install @flowgram.ai/export-plugin\" />\n\n2. 注册插件\n\n```tsx pure\nimport { createDownloadPlugin } from '@flowgram.ai/export-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [createDownloadPlugin({})]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n## 配置项\n\n```ts pure\ncreateDownloadPlugin({\n  // 自定义导出文件名\n  // 默认: `flowgram-${nanoid(5)}.${format}`\n  getFilename: (format) => `my-workflow.${format}`,\n\n  // 导出图片时添加的水印 SVG\n  // 水印会显示在图片右下角\n  watermarkSVG: '<svg>...</svg>',\n})\n```\n\n## 支持的导出格式\n\n```ts pure\nimport { FlowDownloadFormat } from '@flowgram.ai/export-plugin'\n\n// 图片格式\nFlowDownloadFormat.PNG   // PNG 图片\nFlowDownloadFormat.JPEG  // JPEG 图片\nFlowDownloadFormat.SVG   // SVG 矢量图\n\n// 数据格式\nFlowDownloadFormat.JSON  // JSON 数据\nFlowDownloadFormat.YAML  // YAML 数据\n```\n\n## 使用 FlowDownloadService\n\n在组件中使用 `FlowDownloadService` 来触发导出：\n\n```tsx pure\nimport { useService } from '@flowgram.ai/free-layout-editor';\nimport { FlowDownloadService, FlowDownloadFormat } from '@flowgram.ai/export-plugin';\n\nexport const ExportButton = () => {\n  const downloadService = useService(FlowDownloadService);\n\n  const handleExportPNG = async () => {\n    await downloadService.download({ format: FlowDownloadFormat.PNG });\n  };\n\n  const handleExportJSON = async () => {\n    await downloadService.download({ format: FlowDownloadFormat.JSON });\n  };\n\n  return (\n    <div>\n      <button onClick={handleExportPNG} disabled={downloadService.downloading}>\n        导出 PNG\n      </button>\n      <button onClick={handleExportJSON} disabled={downloadService.downloading}>\n        导出 JSON\n      </button>\n    </div>\n  );\n};\n```\n\n## 监听下载状态\n\n```tsx pure\nimport { useEffect, useState } from 'react';\nimport { useService } from '@flowgram.ai/free-layout-editor';\nimport { FlowDownloadService } from '@flowgram.ai/export-plugin';\n\nexport const DownloadStatus = () => {\n  const downloadService = useService(FlowDownloadService);\n  const [downloading, setDownloading] = useState(false);\n\n  useEffect(() => {\n    const disposer = downloadService.onDownloadingChange((value) => {\n      setDownloading(value);\n    });\n    return () => disposer.dispose();\n  }, [downloadService]);\n\n  return downloading ? <span>正在导出...</span> : null;\n};\n```\n\n## 类型定义\n\n```ts pure\nimport { FlowDownloadFormat } from '@flowgram.ai/export-plugin';\n\ninterface DownloadServiceOptions {\n  // 自定义导出文件名的函数\n  getFilename?: (format: FlowDownloadFormat) => string;\n\n  // 导出图片时的水印 SVG 字符串\n  watermarkSVG?: string;\n}\n\ninterface WorkflowDownloadParams {\n  // 导出格式\n  format: FlowDownloadFormat;\n}\n\nenum FlowDownloadFormat {\n  JSON = 'json',\n  YAML = 'yaml',\n  PNG = 'png',\n  JPEG = 'jpeg',\n  SVG = 'svg',\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/free-auto-layout-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/free-auto-layout-plugin\n\n基于 Dagre 算法的自动布局插件，为自由布局画布提供智能的节点排列功能。\n\n## 功能\n\n- 基于 Dagre 有向图布局算法，自动计算节点的最优位置\n- 支持多种布局方向（从左到右、从上到下等）\n- 可配置节点间距、边距等布局参数\n- 支持嵌套容器的递归布局\n- 提供动画效果和视图自适应功能\n- 与历史记录系统集成，支持撤销/重做操作\n\n![Preview](@/public/plugin/auto-layout.gif)\n\n## 快速开始\n\n1. 安装\n\n<PackageManagerTabs command=\"install @flowgram.ai/free-auto-layout-plugin\" />\n\n2. 注册插件\n\n插件的注册方法和 flowgram 的其他插件基本相同，只需要保证不要重复创建以及最终传入到对应的 FreeLayoutEditorProvider 即可\n\n```tsx\nimport { createFreeAutoLayoutPlugin } from '@flowgram.ai/free-auto-layout-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeAutoLayoutPlugin({\n      layoutConfig: {\n        rankdir: 'LR', // 布局方向：从左到右\n        nodesep: 100,  // 节点间距\n        ranksep: 100,  // 层级间距\n      }\n    })\n  ]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n\n3. 在 React 组件中使用\n\n也可以通过工具类在组件中触发自动布局：\n\n```tsx\nimport { WorkflowAutoLayoutTool } from '@flowgram.ai/free-layout-editor';\n\nconst AutoLayoutButton = () => {\n  const tools = usePlaygroundTools();\n  const playground = usePlayground();\n\n  const handleAutoLayout = async () => {\n    await tools.autoLayout({\n      enableAnimation: true,      // 启用动画效果\n      animationDuration: 1000,     // 动画持续时间\n      disableFitView: false,      // 布局后自动适应视图\n    });\n  }\n\n  return (\n    <button onClick={handleAutoLayout}>\n      自动布局\n    </button>\n  );\n};\n```\n\n4. 使用自动布局服务\n\n通过依赖注入获取 AutoLayoutService 实例来执行布局：\n\n```tsx\nimport { AutoLayoutService } from '@flowgram.ai/free-auto-layout-plugin';\n\nclass MyLayoutService {\n  @inject(AutoLayoutService)\n  private autoLayoutService: AutoLayoutService;\n\n  async performAutoLayout() {\n    await this.autoLayoutService.layout({\n      enableAnimation: true,      // 启用动画效果\n      animationDuration: 1000,     // 动画持续时间\n      disableFitView: false,      // 布局后自动适应视图\n    });\n  }\n}\n```\n\n## 配置选项\n\n### LayoutConfig\n\n布局算法的配置参数：\n\n```typescript\ninterface LayoutConfig {\n  /** 布局方向 */\n  rankdir?: 'TB' | 'BT' | 'LR' | 'RL';\n  /** 对齐方式 */\n  align?: 'UL' | 'UR' | 'DL' | 'DR';\n  /** 同层节点间距 */\n  nodesep?: number;\n  /** 边的间距 */\n  edgesep?: number;\n  /** 层级间距 */\n  ranksep?: number;\n  /** 水平边距 */\n  marginx?: number;\n  /** 垂直边距 */\n  marginy?: number;\n  /** 环路处理算法 */\n  acyclicer?: string;\n  /** 排序算法 */\n  ranker?: 'network-simplex' | 'tight-tree' | 'longest-path';\n}\n```\n\n### LayoutOptions\n\n布局执行时的选项：\n\n```typescript\ninterface LayoutOptions {\n  /** 容器节点，默认为根节点 */\n  containerNode?: WorkflowNodeEntity;\n  /** 获取跟随节点的函数 */\n  getFollowNode?: GetFollowNode;\n  /** 禁用自动适应视图 */\n  disableFitView?: boolean;\n  /** 启用动画效果 */\n  enableAnimation?: boolean;\n  /** 动画持续时间（毫秒） */\n  animationDuration?: number;\n  /** 节点过滤函数，用于控制哪些节点参与布局计算 */\n  filterNode?: (params: { node: WorkflowNodeEntity; parent?: WorkflowNodeEntity }) => boolean;\n}\n```\n\n## 布局算法\n\n### Dagre 算法\n\n插件基于 Dagre 库实现，这是一个专门用于有向图布局的 JavaScript 库。算法特点：\n\n- **分层布局**：将节点按照依赖关系分为不同层级\n- **最小交叉**：尽量减少连线的交叉\n- **均匀分布**：在满足约束的前提下均匀分布节点\n\n### 布局流程\n\n1. **图构建**：将工作流节点和连线转换为 Dagre 图结构\n2. **层级计算**：根据节点依赖关系计算层级（rank）\n3. **顺序优化**：在每个层级内优化节点顺序以减少交叉\n4. **位置计算**：计算每个节点的最终坐标位置\n5. **动画执行**：如果启用动画，平滑过渡到新位置\n\n## 高级用法\n\n### 自定义跟随节点\n\n可以通过 `getFollowNode` 函数自定义节点的跟随关系：\n\n```typescript\nconst layoutOptions: LayoutOptions = {\n  getFollowNode: (node: LayoutNode) => {\n    // 返回应该跟随当前节点的节点ID\n    if (node.flowNodeType === 'comment') {\n      return getNearestNode(node);\n    }\n    return undefined;\n  }\n};\nawait tools.autoLayout(layoutOptions);\n```\n\n### 节点过滤\n\n可以通过 `filterNode` 函数控制哪些节点参与布局计算：\n\n```typescript\nconst layoutOptions: LayoutOptions = {\n  filterNode: ({ node, parent }) => {\n    // 过滤掉特定类型的节点\n    if (node.flowNodeType === 'comment') {\n      return false;\n    }\n\n    // 根据父节点条件过滤\n    if (parent && parent.flowNodeType.type === 'group') {\n      return false;\n    }\n\n    return true; // 默认包含所有节点\n  }\n};\n\n// 使用过滤选项执行布局\nawait tools.autoLayout(layoutOptions);\n```\n\n### 仅对指定容器布局\n\n插件支持对指定容器进行递归布局，会自动处理容器内部的节点排列：\n\n```typescript\n// 对特定容器执行布局\nawait autoLayoutService.layout({\n  containerNode: specificContainerNode,\n  enableAnimation: true,\n});\n```\n\n## 常见问题\n\n### Q: 如何在初始化时触发自动布局?\n\nA: 在画布渲染完成后调用 `ctx.tool.autoLayout()` 方法即可触发自动布局。\n```typescript\nconst editorProps = useMemo(() => ({\n  onAllLayersRendered: (ctx) => {\n    ctx.tool.autoLayout({\n      enableAnimation: false, // 初始化时自动布局禁用动画，可优化用户体验\n    }\n    );\n  }\n}), []);\n```\n\n### Q: 如何实现自定义布局方向？\n\nA: 方法一：在 EditorProps 中注册 AutoLayout 插件，通过 `rankdir` 参数控制布局方向：\n\n```typescript\n\nimport { createFreeAutoLayoutPlugin } from '@flowgram.ai/free-auto-layout-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeAutoLayoutPlugin({\n      layoutConfig: {\n        rankdir: 'TB', // 从上到下\n        // rankdir: 'LR', // 从左到右（默认）\n        // rankdir: 'RL', // 从右到左\n        // rankdir: 'BT', // 从下到上\n      }\n    })\n  ]\n}), []);\n```\n\n方法二：在调用 `autoLayout` 方法时，通过 `layoutConfig` 参数传递布局配置：\n\n```typescript\nconst tools = usePlaygroundTools();\nconst playground = usePlayground();\n\nconst handleAutoLayout = async () => {\n  await tools.autoLayout({\n    layoutConfig: {\n      rankdir: 'TB', // 从上到下\n      // rankdir: 'LR', // 从左到右（默认）\n      // rankdir: 'RL', // 从右到左\n      // rankdir: 'BT', // 从下到上\n    }\n  });\n}\n```\n\n### Q: 布局动画卡顿怎么优化？\n\nA: 对于复杂工作流，建议禁用动画或减少动画时长：\n\n```typescript\nlayoutOptions: {\n  enableAnimation: false, // 禁用动画\n  // 或者\n  animationDuration: 150, // 减少动画时长\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/free-stack-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/free-stack-plugin\n\n层级管理插件，为自由布局画布提供节点和连线的 z-index 层级控制功能。\n\n## 功能\n\n- 智能计算节点和连线的层级关系，避免遮挡问题\n- 支持选中节点自动置顶显示\n- 支持悬停节点和连线的高亮显示\n- 可自定义节点排序规则，控制同层级节点的渲染顺序\n- 自动处理父子节点的层级关系\n- 支持连线的智能层级管理，确保连线可见性\n- 实时响应节点选择、悬停和实体变化事件\n\n## 快速开始\n\n1. 安装\n\n<PackageManagerTabs command=\"install @flowgram.ai/free-stack-plugin\" />\n\n2. 注册插件\n\n插件的注册方法和 flowgram 的其他插件基本相同，只需要保证不要重复创建以及最终传入到对应的 FreeLayoutEditorProvider 即可\n\n```tsx\nimport { createFreeStackPlugin } from '@flowgram.ai/free-stack-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeStackPlugin()\n  ]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n3. 自定义节点排序\n\n可以通过 `sortNodes` 函数自定义同层级节点的排序规则：\n\n```tsx\nimport { createFreeStackPlugin } from '@flowgram.ai/free-stack-plugin';\nimport { WorkflowNodeType } from './nodes/constants';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [\n    createFreeStackPlugin({\n      sortNodes: (nodes) => {\n        const commentNodes = [];\n        const otherNodes = [];\n\n        // 将注释节点和其他节点分开\n        nodes.forEach((node) => {\n          if (node.flowNodeType === WorkflowNodeType.Comment) {\n            commentNodes.push(node);\n          } else {\n            otherNodes.push(node);\n          }\n        });\n\n        // 注释节点渲染在底层，其他节点在上层\n        return [...commentNodes, ...otherNodes];\n      },\n    })\n  ]\n}), []);\n```\n\n## 配置选项\n\n### FreeStackPluginOptions\n\n插件的配置选项：\n\n```typescript\ninterface FreeStackPluginOptions {\n  /** 自定义节点排序函数 */\n  sortNodes?: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n}\n```\n\n### sortNodes 函数\n\n用于自定义同层级节点的排序规则：\n\n```typescript\ntype SortNodesFunction = (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n```\n\n**参数说明：**\n- `nodes`: 需要排序的节点数组\n- **返回值**: 排序后的节点数组\n\n**使用场景：**\n- 将特定类型的节点（如注释）放在底层\n- 按照业务优先级排序节点\n- 按照创建时间或其他属性排序\n\n## 层级管理算法\n\n### 基础层级计算\n\n插件使用智能算法计算每个节点和连线的层级：\n\n1. **基础层级**：从 `BASE_Z_INDEX`（默认为 8）开始计算\n2. **节点层级**：根据节点的嵌套关系和排序规则计算\n3. **连线层级**：确保连线不被节点遮挡，同时处理特殊情况\n\n### 层级提升规则\n\n以下情况会触发层级提升：\n\n- **选中节点**：选中的节点会被提升到顶层\n- **悬停元素**：悬停的节点或连线会被高亮显示\n- **正在绘制的连线**：绘制中的连线会置于顶层\n- **父子关系连线**：父子节点间的连线会优先显示\n\n### 层级计算流程\n\n1. **初始化**：清除缓存，计算基础参数\n2. **节点索引**：建立节点索引映射\n3. **选中节点处理**：标记选中节点的父级关系\n4. **层级分配**：递归处理节点层级\n5. **连线处理**：计算连线层级，确保可见性\n6. **样式应用**：将计算结果应用到 DOM 元素\n\n## 高级用法\n\n### 复杂排序规则\n\n可以实现复杂的节点排序逻辑：\n\n```typescript\nconst sortNodes = (nodes: WorkflowNodeEntity[]) => {\n  return nodes.sort((a, b) => {\n    // 1. 按节点类型优先级排序\n    const typeOrder = {\n      [WorkflowNodeType.Comment]: 0,\n      [WorkflowNodeType.Start]: 1,\n      [WorkflowNodeType.End]: 2,\n      // ... 其他类型\n    };\n\n    const aOrder = typeOrder[a.flowNodeType] ?? 999;\n    const bOrder = typeOrder[b.flowNodeType] ?? 999;\n\n    if (aOrder !== bOrder) {\n      return aOrder - bOrder;\n    }\n\n    // 2. 按创建时间排序\n    return a.createTime - b.createTime;\n  });\n};\n```\n\n## 常见问题\n\n### Q: 如何让特定类型的节点始终在底层？\n\nA: 通过 `sortNodes` 函数将这些节点排在数组前面：\n\n```typescript\nconst sortNodes = (nodes) => {\n  const backgroundNodes = nodes.filter(node =>\n    node.flowNodeType === WorkflowNodeType.Comment\n  );\n  const foregroundNodes = nodes.filter(node =>\n    node.flowNodeType !== WorkflowNodeType.Comment\n  );\n\n  return [...backgroundNodes, ...foregroundNodes];\n};\n```\n\n### Q: 如何禁用自动层级管理？\n\nA: 目前插件没有提供禁用选项，如果需要完全自定义层级管理，建议不使用此插件，直接在节点组件中设置 z-index。\n\n### Q: 性能优化建议？\n\nA: 插件已经内置了性能优化：\n- 使用防抖机制减少计算频率\n- 只在必要时重新计算层级\n- 使用 Map 数据结构提高查找效率\n\n对于大型画布（超过 1000 个节点），建议：\n- 简化 `sortNodes` 函数的逻辑\n- 避免在排序函数中进行复杂计算\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/minimap-plugin.mdx",
    "content": "# @flowgram.ai/minimap-plugin\n\nimport { PackageManagerTabs } from '@theme';\n\n<PackageManagerTabs command={{\n  npm: \"npm install @flowgram.ai/minimap-plugin\"\n}} />\n\n\n## EditorProps\n\n```ts pure\nimport { createMinimapPlugin } from '@flowgram.ai/minimap-plugin'\n\n\n{\n  plugins: () => [\n    /**\n     * Minimap plugin\n     */\n    createMinimapPlugin({\n      disableLayer: true,\n      enableDisplayAllNodes: true,\n      canvasStyle: {\n        canvasWidth: 182,\n        canvasHeight: 102,\n        canvasPadding: 50,\n        canvasBackground: 'rgba(245, 245, 245, 1)',\n        canvasBorderRadius: 10,\n        viewportBackground: 'rgba(235, 235, 235, 1)',\n        viewportBorderRadius: 4,\n        viewportBorderColor: 'rgba(201, 201, 201, 1)',\n        viewportBorderWidth: 1,\n        viewportBorderDashLength: 2,\n        nodeColor: 'rgba(255, 255, 255, 1)',\n        nodeBorderRadius: 2,\n        nodeBorderWidth: 0.145,\n        nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n        overlayColor: 'rgba(255, 255, 255, 0)',\n      },\n    }),\n  ]\n}\n```\n\n## 缩略图组件\n\n```tsx pure\nimport { MinimapRender } from '@flowgram.ai/minimap-plugin';\n\n\nexport const Minimap = () => {\n  return (\n    <div\n      style={{\n        position: 'absolute',\n        left: 16,\n        bottom: 51,\n        zIndex: 100,\n        width: 182,\n      }}\n    >\n      <MinimapRender\n        containerStyles={{\n          pointerEvents: 'auto',\n          position: 'relative',\n          top: 'unset',\n          right: 'unset',\n          bottom: 'unset',\n          left: 'unset',\n        }}\n        inactiveStyle={{\n          opacity: 1,\n          scale: 1,\n          translateX: 0,\n          translateY: 0,\n        }}\n      />\n    </div>\n  );\n};\n\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/plugin/panel-manager-plugin.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# @flowgram.ai/panel-manager-plugin\n\n管理各类面板的插件。\n\n## 功能\n\n- 基于该插件可以低成本的在画布的右侧和底部以 React 组件形式接入自定义面板\n\n- 无需复杂的样式适配，底层自动计算面板的限位和布局\n\n- 自动管理面板队列的出入\n\n![Preview](@/public/plugin/panel-manager-1.png)\n\n## 快速开始\n\n1. 安装\n\n<PackageManagerTabs command=\"install @flowgram.ai/panel-manager-plugin\" />\n\n2. 注册插件\n\n插件的注册方法和 flowgram 的其他插件基本相同，只需要保证不要重复创建以及最终传入到对应的 LayoutEditorProvider 即可\n\n```tsx\nimport { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';\n\nconst editorProps = useMemo(() => ({\n  plugins: () => [createPanelManagerPlugin({})]\n}), []);\n\nreturn (\n  <FreeLayoutEditorProvider {...editorProps}>\n    <EditorRenderer />\n  </FreeLayoutEditorProvider>\n)\n```\n\n3. 注册面板组件\n\n面板的注册需要唯一的 `key` 以及返回 ReactNode 的渲染函数 `render`\n\n这里以节点表单面板为例：\n\n```tsx pure\nimport { type PanelFactory } from '@flowgram.ai/panel-manager-plugin';\n\nexport const NODE_FORM_PANEL = 'node-form-panel';\nexport const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {\n  key: NODE_FORM_PANEL,\n  defaultSize: 400,\n  render: (props: NodeFormPanelProps) => <NodeFormPanel {...props} />\n}\n```\n\n将定义好的对象传入插件中：\n\n```ts pure\ncreatePanelManagerPlugin({\n  factories: [nodeFormPanelFactory],\n  getPopupContainer: (ctx) => ctx.playground.node.parentNode,\n  autoResize: true\n})\n```\n\n4. 打开/关闭面板\n\n面板的打开关闭通过 PanelManager 的实例控制：\n\n```ts\nimport { PanelManager } from '@flowgram.ai/panel-manager-plugin';\n\nclass NodeFormService {\n  @inject(PanelManager): panelManager: PanelManager;\n\n  openPanel(nodeId: string) {\n    this.panelManager.open(NODE_FORM_PANEL, 'right', {\n      props: {\n        nodeId\n      }\n    })\n  }\n  closePanel() {\n    this.panelManager.close(NODE_FORM_PANEL)\n  }\n}\n```\n\n除此之外也可以在 react 组件中以 hook 的方式获取实例：\n\n```tsx\nimport { usePanelManager } from '@flowgram.ai/panel-manager-plugin';\n\nconst panelManager = usePanelManager();\n\n<button\n  onClick={() => panelManager.open(TEST_RUN_FORM_PANEL, 'right')}\n>\n  试运行\n</button>\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/question.mdx",
    "content": "# 注意事项及常见问题\n\n# 注意事项\n\n1. 画布的 editor 版本及导入的插件版本必须一致，如果出现以下错误都是这个问题造成\n\n\n2. 画布的事件监听，在 react 中使用都要配套写销毁逻辑，防止无限注册导致内存泄漏\n\n```tsx pure\nfunction SomeReactComp() {\n  useEffect(() => {\n    const toDispose = ctx.document.onContentChange(() => {\n      // DO Something\n    })\n    return () => toDispose.dispose() // Destroy Event\n  }, [])\n}\n```\n\n\n\n# 常见问题\n\n\n## 运行报报错\n\n\n## 如何修改节点的数据\n\n\n## 是否支持 vue\n\n\n##\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/_meta.json",
    "content": "[\n  \"introduction\",\n  \"quick-start\",\n  \"schema\",\n  \"node\",\n  \"api\",\n  \"source-code-guide\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/api.mdx",
    "content": "---\ntitle: 运行时 API\ndescription: FlowGram Runtime API\nsidebar_position: 2\n---\n\n# FlowGram Runtime API 参考\n\nFlowGram Runtime 提供了五个核心 API，用于工作流的验证、运行、监控、结果获取和取消。本文档详细介绍了这些 API 的使用方法、参数和返回值。\n\n## TaskRun API\n\n### 功能描述\n\nTaskRun API 用于启动一个工作流任务，接收工作流 schema 和初始输入，返回任务 ID。\n\n### 参数说明\n\nTaskRun API 接收一个 `TaskRunInput` 对象作为参数：\n\n| 参数名 | 类型 | 必填 | 描述 |\n| ------ | ---- | ---- | ---- |\n| schema | string | 是 | 工作流 schema 的 JSON 字符串，定义了工作流的节点和边 |\n| inputs | object | 否 | 工作流的初始输入参数，可以为空 |\n\n`schema` 参数是一个 JSON 字符串，定义了工作流的结构，包括节点和边的信息。schema 的基本结构如下：\n\n```typescript\ninterface WorkflowSchema {\n  nodes: WorkflowNodeSchema[];\n  edges: WorkflowEdgeSchema[];\n}\n\ninterface WorkflowNodeSchema {\n  id: string;\n  type: FlowGramNode;\n  name?: string;\n  meta: {\n    position: {\n      x: number;\n      y: number;\n    };\n  };\n  data: any;\n  blocks?: WorkflowNodeSchema[];\n  edges?: WorkflowEdgeSchema[];\n}\n\ninterface WorkflowEdgeSchema {\n  sourceNodeID: string;\n  sourcePort: string;\n  targetNodeID: string;\n  targetPort: string;\n}\n```\n\n### 返回值说明\n\nTaskRun API 返回一个 `TaskRunOutput` 对象：\n\n| 字段名 | 类型 | 描述 |\n| ------ | ---- | ---- |\n| taskID | string | 任务的唯一标识符，用于后续查询任务状态和结果 |\n\n### 错误处理\n\nTaskRun API 可能会抛出以下错误：\n\n- **Schema 解析错误**：当提供的 schema 不是有效的 JSON 字符串时\n- **Schema 结构错误**：当 schema 结构不符合预期格式时\n- **节点类型错误**：当 schema 中包含不支持的节点类型时\n- **初始化错误**：当工作流初始化失败时\n\n### 使用示例\n\n```javascript\nimport { TaskRunAPI } from '@flowgram.ai/runtime-js';\n\nconst schema = JSON.stringify({\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      meta: { position: { x: 0, y: 0 } },\n      data: {}\n    },\n    {\n      id: 'llm',\n      type: 'llm',\n      meta: { position: { x: 200, y: 0 } },\n      data: {\n        modelName: 'gpt-3.5-turbo',\n        temperature: 0.7,\n        systemPrompt: '你是一个助手',\n        prompt: '介绍一下自己'\n      }\n    },\n    {\n      id: 'end',\n      type: 'end',\n      meta: { position: { x: 400, y: 0 } },\n      data: {}\n    }\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start',\n      sourcePort: 'out',\n      targetNodeID: 'llm',\n      targetPort: 'in'\n    },\n    {\n      sourceNodeID: 'llm',\n      sourcePort: 'out',\n      targetNodeID: 'end',\n      targetPort: 'in'\n    }\n  ]\n});\n\nconst inputs = {\n  userInput: '请介绍一下自己'\n};\n\nasync function runWorkflow() {\n  try {\n    const result = await TaskRunAPI({\n      schema,\n      inputs\n    });\n    console.log('Task ID:', result.taskID);\n    return result.taskID;\n  } catch (error) {\n    console.error('启动工作流失败:', error);\n  }\n}\n```\n\n### 注意事项\n\n- schema 必须是有效的 JSON 字符串，且符合 WorkflowSchema 的结构\n- 工作流必须包含一个起始节点（type: 'start'）和一个结束节点（type: 'end'）\n- 节点之间的连接必须通过边（edges）正确定义\n- 任务启动后会异步执行，可以通过 TaskReport API 和 TaskResult API 获取执行状态和结果\n\n## TaskReport API\n\n### 功能描述\n\nTaskReport API 用于获取工作流任务的执行报告，包括任务状态和各节点的执行状态。\n\n### 参数说明\n\nTaskReport API 接收一个 `TaskReportInput` 对象作为参数：\n\n| 参数名 | 类型 | 必填 | 描述 |\n| ------ | ---- | ---- | ---- |\n| taskID | string | 是 | 任务的唯一标识符，由 TaskRun API 返回 |\n\n### 返回值说明\n\nTaskReport API 返回一个 `TaskReportOutput` 对象，包含任务的执行报告：\n\n| 字段名 | 类型 | 描述 |\n| ------ | ---- | ---- |\n| workflow | WorkflowStatus | 工作流整体状态 |\n| nodes | `Record<string, NodeStatus>` | 各节点的执行状态 |\n\n`WorkflowStatus` 结构如下：\n\n```typescript\ninterface WorkflowStatus {\n  status: 'idle' | 'processing' | 'success' | 'fail' | 'canceled';\n  terminated: boolean;\n}\n```\n\n`NodeStatus` 结构如下：\n\n```typescript\ninterface NodeStatus {\n  status: 'idle' | 'processing' | 'success' | 'fail' | 'canceled';\n  startTime?: number;\n  endTime?: number;\n}\n```\n\n### 错误处理\n\nTaskReport API 可能会遇到以下错误情况：\n\n- **任务不存在**：当提供的 taskID 不存在时，返回 undefined\n- **报告生成错误**：当报告生成过程中出现错误时\n\n### 使用示例\n\n```javascript\nimport { TaskReportAPI } from '@flowgram.ai/runtime-js';\n\nasync function getTaskReport(taskID) {\n  try {\n    const report = await TaskReportAPI({ taskID });\n\n    if (!report) {\n      console.log('任务不存在或报告未生成');\n      return;\n    }\n\n    console.log('工作流状态:', report.workflow.status);\n    console.log('工作流是否终止:', report.workflow.terminated);\n\n    // 打印各节点状态\n    for (const [nodeId, nodeStatus] of Object.entries(report.nodes)) {\n      console.log(`节点 ${nodeId} 状态:`, nodeStatus.status);\n      if (nodeStatus.startTime) {\n        console.log(`节点 ${nodeId} 开始时间:`, new Date(nodeStatus.startTime).toLocaleString());\n      }\n      if (nodeStatus.endTime) {\n        console.log(`节点 ${nodeId} 结束时间:`, new Date(nodeStatus.endTime).toLocaleString());\n      }\n    }\n\n    return report;\n  } catch (error) {\n    console.error('获取任务报告失败:', error);\n  }\n}\n```\n\n### 注意事项\n\n- 任务报告是实时的，可以多次调用 TaskReport API 来获取最新的执行状态\n- 如果工作流尚未终止（`workflow.terminated` 为 false），则工作流仍在执行中\n- 节点状态可能为 'idle'（未开始）、'processing'（执行中）、'success'（成功）、'fail'（失败）或 'canceled'（已取消）\n- 建议定期轮询任务报告，以监控工作流的执行进度\n\n## TaskCancel API\n\n### 功能描述\n\nTaskCancel API 用于取消正在执行的工作流任务。\n\n### 参数说明\n\nTaskCancel API 接收一个 `TaskCancelInput` 对象作为参数：\n\n| 参数名 | 类型 | 必填 | 描述 |\n| ------ | ---- | ---- | ---- |\n| taskID | string | 是 | 任务的唯一标识符，由 TaskRun API 返回 |\n\n### 返回值说明\n\nTaskCancel API 返回一个 `TaskCancelOutput` 对象：\n\n| 字段名 | 类型 | 描述 |\n| ------ | ---- | ---- |\n| success | boolean | 表示任务是否成功取消 |\n\n### 错误处理\n\nTaskCancel API 可能会遇到以下错误情况：\n\n- **任务不存在**：当提供的 taskID 不存在时，返回 `{ success: false }`\n- **任务已完成**：当任务已经完成或已经取消时，无法再次取消\n\n### 使用示例\n\n```javascript\nimport { TaskCancelAPI } from '@flowgram.ai/runtime-js';\n\nasync function cancelTask(taskID) {\n  try {\n    const result = await TaskCancelAPI({ taskID });\n\n    if (result.success) {\n      console.log('任务已成功取消');\n    } else {\n      console.log('任务取消失败，可能任务不存在或已完成');\n    }\n\n    return result.success;\n  } catch (error) {\n    console.error('取消任务失败:', error);\n    return false;\n  }\n}\n```\n\n### 注意事项\n\n- 任务取消是异步的，取消请求成功后，任务可能需要一些时间才能完全停止\n- 已经完成的任务无法取消\n- 取消任务后，可以通过 TaskReport API 查看任务的最终状态，已取消的任务状态将变为 'canceled'\n- 取消任务不会清除任务的中间结果，仍然可以通过 TaskResult API 获取已执行部分的结果\n\n## TaskResult API\n\n### 功能描述\n\nTaskResult API 用于获取工作流任务的最终结果。\n\n### 参数说明\n\nTaskResult API 接收一个 `TaskResultInput` 对象作为参数：\n\n| 参数名 | 类型 | 必填 | 描述 |\n| ------ | ---- | ---- | ---- |\n| taskID | string | 是 | 任务的唯一标识符，由 TaskRun API 返回 |\n\n### 返回值说明\n\nTaskResult API 返回一个 `WorkflowOutputs` 对象，包含工作流的输出结果：\n\n```typescript\ntype WorkflowOutputs = Record<string, any>;\n```\n\n返回的对象结构取决于工作流的具体实现和输出定义。\n\n### 错误处理\n\nTaskResult API 可能会遇到以下错误情况：\n\n- **任务不存在**：当提供的 taskID 不存在时，返回 undefined\n- **任务未完成**：当任务尚未终止时，返回 undefined\n- **结果获取错误**：当获取结果过程中出现错误时\n\n### 使用示例\n\n```javascript\nimport { TaskResultAPI } from '@flowgram.ai/runtime-js';\n\nasync function getTaskResult(taskID) {\n  try {\n    const result = await TaskResultAPI({ taskID });\n\n    if (!result) {\n      console.log('任务不存在或尚未完成');\n      return;\n    }\n\n    console.log('任务结果:', result);\n    return result;\n  } catch (error) {\n    console.error('获取任务结果失败:', error);\n  }\n}\n\n// 使用示例：等待任务完成并获取结果\nasync function waitForResult(taskID, pollingInterval = 1000, timeout = 60000) {\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeout) {\n    // 获取任务报告\n    const report = await TaskReportAPI({ taskID });\n\n    // 如果任务已终止，获取结果\n    if (report && report.workflow.terminated) {\n      return await TaskResultAPI({ taskID });\n    }\n\n    // 等待一段时间后再次检查\n    await new Promise(resolve => setTimeout(resolve, pollingInterval));\n  }\n\n  throw new Error('等待任务结果超时');\n}\n```\n\n### 注意事项\n\n- 只有当任务已经终止（完成、失败或取消）时，才能获取到结果\n- 如果任务尚未完成，TaskResult API 将返回 undefined\n- 建议先通过 TaskReport API 检查任务是否已终止，再调用 TaskResult API 获取结果\n- 对于已取消的任务，可能只能获取到部分结果或没有结果\n- 结果的具体结构取决于工作流的定义，需要根据实际工作流的输出进行解析\n\n## TaskValidate API\n\n### 功能描述\n\nTaskValidate API 用于验证工作流 schema 和输入参数的有效性，在实际运行工作流之前检查配置是否正确。这个 API 可以帮助您在启动工作流之前发现潜在的配置错误。\n\n### 参数说明\n\nTaskValidate API 接收一个 `TaskValidateInput` 对象作为参数：\n\n| 参数名 | 类型 | 必填 | 描述 |\n| ------ | ---- | ---- | ---- |\n| schema | string | 是 | 工作流 schema 的 JSON 字符串，定义了工作流的节点和边 |\n| inputs | object | 否 | 工作流的初始输入参数，用于验证输入是否符合 schema 要求 |\n\n### 返回值说明\n\nTaskValidate API 返回一个 `TaskValidateOutput` 对象：\n\n| 字段名 | 类型 | 描述 |\n| ------ | ---- | ---- |\n| valid | boolean | 表示验证是否通过，true 表示验证成功，false 表示验证失败 |\n| errors | string[] | 可选字段，当验证失败时包含具体的错误信息列表 |\n\n### 验证内容\n\nTaskValidate API 会对以下内容进行验证：\n\n- **Schema 结构验证**：检查 schema 是否符合 WorkflowSchema 的格式要求\n- **节点类型验证**：验证 schema 中的节点类型是否被支持\n- **边连接验证**：检查节点之间的连接是否正确\n- **输入参数验证**：验证提供的 inputs 是否符合 schema 中定义的输入要求\n- **工作流完整性验证**：检查工作流是否包含必要的起始和结束节点\n\n### 错误处理\n\nTaskValidate API 可能会返回以下类型的验证错误：\n\n- **Schema 解析错误**：当提供的 schema 不是有效的 JSON 字符串时\n- **Schema 结构错误**：当 schema 结构不符合预期格式时\n- **节点配置错误**：当节点配置不完整或不正确时\n- **连接错误**：当节点之间的连接存在问题时\n- **输入参数错误**：当输入参数不符合要求时\n\n### 使用示例\n\n```javascript\nimport { TaskValidateAPI } from '@flowgram.ai/runtime-js';\n\nconst schema = JSON.stringify({\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      meta: { position: { x: 0, y: 0 } },\n      data: {\n        outputs: {\n          type: 'object',\n          properties: {\n            userInput: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            }\n          },\n          required: ['userInput'],\n        },\n      }\n    },\n    {\n      id: 'llm',\n      type: 'llm',\n      meta: { position: { x: 200, y: 0 } },\n      data: {\n        title: 'LLM_0',\n        inputsValues: {\n          prompt: {\n            type: 'ref',\n            content: ['start_0', 'userInput'],\n          }\n        },\n        inputs: {\n          type: 'object',\n          required: ['editor'],\n          properties: {\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'end',\n      type: 'end',\n      meta: { position: { x: 400, y: 0 } },\n      data: {}\n    }\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start',\n      sourcePort: 'out',\n      targetNodeID: 'llm',\n      targetPort: 'in'\n    },\n    {\n      sourceNodeID: 'llm',\n      sourcePort: 'out',\n      targetNodeID: 'end',\n      targetPort: 'in'\n    }\n  ]\n});\n\nconst inputs = {\n  userInput: '请介绍一下自己'\n};\n\nasync function validateWorkflow() {\n  try {\n    const result = await TaskValidateAPI({\n      schema,\n      inputs\n    });\n\n    if (result.valid) {\n      console.log('工作流验证通过，可以安全运行');\n      return true;\n    } else {\n      console.error('工作流验证失败:');\n      result.errors?.forEach(error => {\n        console.error('- ' + error);\n      });\n      return false;\n    }\n  } catch (error) {\n    console.error('验证过程中发生错误:', error);\n    return false;\n  }\n}\n\n// 在运行工作流之前先进行验证\nasync function safeRunWorkflow() {\n  const isValid = await validateWorkflow();\n  if (isValid) {\n    // 验证通过后再运行工作流\n    const runResult = await TaskRunAPI({ schema, inputs });\n    console.log('工作流已启动，任务 ID:', runResult.taskID);\n  } else {\n    console.log('工作流验证失败，请修复错误后重试');\n  }\n}\n```\n\n### 注意事项\n\n- 建议在调用 TaskRun API 之前先调用 TaskValidate API 进行验证\n- 验证通过并不保证工作流运行时不会出错，但可以提前发现大部分配置问题\n- TaskValidate API 的执行速度通常比 TaskRun API 快，适合用于实时验证\n- 验证结果中的错误信息可以帮助快速定位和修复问题\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/introduction.mdx",
    "content": "---\ntitle: 介绍\ndescription: FlowGram Runtime 的基本概念和设计理念\nsidebar_position: 2\n---\n\n# FlowGram Runtime 介绍\n\n\n<div className=\"rs-highlight\">\n**⚠️ 目前为早期开发阶段**\n\n- API 接口可能会有变更，不保证向后兼容\n- 目前只支持 nodejs 运行时，且只支持自由布局\n</div>\n\n本文档将介绍 FlowGram Runtime 的基本概念、设计理念和核心功能，帮助业务接入方开发者了解和使用这个工作流运行时引擎的参考实现。\n\n## 什么是 FlowGram Runtime\n\nFlowGram Runtime 是一个工作流运行时引擎的参考实现，旨在为业务接入方开发者提供运行时参考。能够解析和执行基于图结构的工作流，支持多种节点类型，包括 Start、End、LLM、Condition、Loop 等。\n\n### 项目定位与目标\n\nFlowGram Runtime **定位为 demo 而非 SDK**，主要目标是：\n\n- 提供工作流运行时的设计和实现参考\n- 展示如何构建和扩展工作流引擎\n- 为开发者提供可以直接学习和修改的代码基础\n- 支持快速原型开发和概念验证\n\n作为参考实现，FlowGram Runtime 不会作为包发布，开发者需要 fork 代码库后，根据自己的业务场景和需求进行修改和扩展。\n\n## 核心概念\n\n### 工作流 (Workflow)\n\n工作流是由节点和边组成的有向图，描述了一系列任务的执行顺序和逻辑关系。在 FlowGram Runtime 中，工作流使用 JSON 格式定义，包含节点和边两部分。\n\n工作流定义示例：\n```json\n{\n  \"nodes\": [\n    { \"id\": \"start\", \"type\": \"Start\", \"meta\": {}, \"data\": {} },\n    { \"id\": \"llm\", \"type\": \"LLM\", \"meta\": {}, \"data\": { \"systemPrompt\": \"你是助手\", \"userPrompt\": \"{{start.input}}\" } },\n    { \"id\": \"end\", \"type\": \"End\", \"meta\": {}, \"data\": {} }\n  ],\n  \"edges\": [\n    { \"sourceNodeID\": \"start\", \"targetNodeID\": \"llm\" },\n    { \"sourceNodeID\": \"llm\", \"targetNodeID\": \"end\" }\n  ]\n}\n```\n\n### 节点 (Node)\n\n节点是工作流中的基本执行单元，每个节点代表一个特定的操作或任务。FlowGram Runtime 支持多种节点类型，包括：\n\n- **Start 节点**：工作流的起点，提供工作流输入\n- **End 节点**：工作流的终点，收集工作流输出\n- **LLM 节点**：调用大型语言模型，支持系统提示词和用户提示词\n- **Condition 节点**：根据条件选择不同执行分支，支持多种比较操作符\n- **Loop 节点**：对数组中的每个元素执行相同操作，支持子工作流\n\n每个节点包含 ID、类型、元数据和数据等信息，不同类型的节点具有不同的配置选项和行为。\n\n### 边 (Edge)\n\n边定义了节点之间的连接关系，表示数据和控制流的传递方向。每条边包含源节点、目标节点和可选的源端口信息。\n\n边的定义决定了工作流的执行路径和数据流向，是构建复杂工作流逻辑的基础。\n\n### 执行引擎 (Engine)\n\n执行引擎负责解析工作流定义，按照定义的逻辑顺序执行各个节点，并处理节点间的数据流转。是 FlowGram Runtime 的核心组件，管理整个工作流的生命周期。\n\n## 技术架构\n\nFlowGram Runtime 采用领域驱动设计（DDD）架构，将系统分为多个领域：\n\n- **文档 (Document)**：工作流定义的数据结构，包括节点和边的模型\n- **引擎 (Engine)**：工作流执行的核心逻辑，负责工作流的解析和调度\n- **执行器 (Executor)**：负责执行各类节点的具体逻辑，如 LLM 调用、条件判断等\n- **状态 (State)**：维护工作流执行过程中的状态信息，包括执行历史和当前状态\n- **变量 (Variable)**：管理工作流执行过程中的变量数据，支持变量的存储和访问\n\n### 技术栈\n\nFlowGram Runtime JS 版本基于以下技术栈构建：\n\n- **TypeScript**：提供类型安全和现代 JavaScript 特性\n- **LangChain**：集成大型语言模型和相关工具\n- **OpenAI API**：提供 AI 模型调用能力\n- **Fastify**：高性能的 Web 框架，用于 HTTP API 服务\n- **tRPC**：类型安全的 API 框架\n\n### 模块组成\n\n项目由三个核心模块组成：\n\n1. **js-core**：核心运行时库，包含工作流引擎、节点执行器和状态管理\n2. **interface**：接口定义，定义了 API 和数据模型\n3. **nodejs**：NodeJS 服务实现，提供 HTTP API 和服务管理\n\n## 当前开发状态和限制 ⚠️\n\nFlowGram Runtime 目前处于早期开发阶段，有以下状态和限制：\n\n### 开发状态\n\n- 核心功能已经实现，包括工作流引擎、基本节点类型和主要 API\n- 基本的 LLM 集成已完成，支持与 OpenAI 和 LangChain 的集成\n- 提供了基本的错误处理和状态管理机制\n- 包含测试用例和示例工作流，但文档相对有限\n\n### 已知限制\n\n- **API 不稳定**：API 接口可能会有变更，不保证向后兼容\n- **功能不完善**：部分功能尚未完全实现，如 ServerInfo API 和 Validation API\n- **错误处理**：错误处理机制不够完善，某些边缘情况可能导致异常\n- **存储机制**：当前存储机制较为简单，不适合生产环境的持久化需求\n- **安全机制**：缺乏完善的安全机制，如认证、授权和输入验证\n\n## 未来开发计划\n\nFlowGram Runtime 的未来发展计划包括：\n\n### 多语言支持\n\n目前只有 JavaScript/TypeScript 版本，计划开发：\n- **Python 版本**：适用于数据科学和机器学习场景\n- **Go 版本**：适用于高性能服务端场景\n\n### 功能增强\n\n- 支持固定布局\n- 增加更多节点类型：代码节点、意图识别节点、批处理节点、终止循环节点、继续循环节点、HTTP节点\n- 完善错误处理和异常恢复机制\n- 完善的服务端校验接口，包括 schema 校验和输入校验等\n- 支持 `Docker` 运行\n\n### 试运行优化\n\n- 试运行支持输入表单\n- 试运行输入参数校验\n- 单节点调试\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/node.mdx",
    "content": "# 内置节点\n\n本文档详细介绍FlowGram Runtime中的节点系统，包括节点的基本概念、现有节点类型及其用法，以及如何创建自定义节点。\n\n现有节点：\n\n- 开始节点\n- 结束节点\n- LLM节点\n- 条件节点\n- 循环节点\n\n> 后续会支持代码节点、意图识别节点、批处理节点、终止循环节点、继续循环节点、HTTP节点\n\n## 节点概述\n\n### 节点在FlowGram Runtime中的作用\n\n节点是FlowGram工作流的基本执行单元，每个节点代表一个特定的操作或功能。FlowGram工作流本质上是由多个节点通过边连接形成的有向图，描述了任务的执行流程。节点系统的核心职责包括：\n\n1. **执行特定操作**：每种类型的节点都有其特定的功能，如启动工作流、调用LLM模型、执行条件判断等\n2. **处理输入输出**：节点接收输入数据，执行操作后产生输出数据\n3. **控制执行流程**：通过条件节点和循环节点控制工作流的执行路径\n\n### INodeExecutor接口介绍\n\n所有节点执行器都必须实现`INodeExecutor`接口，该接口定义了节点执行器的基本结构：\n\n```typescript\ninterface INodeExecutor {\n  // 节点类型，用于标识不同种类的节点\n  type: string;\n\n  // 执行方法，处理节点的具体逻辑\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n```\n\n其中：\n- `type`：节点类型标识符，如'start'、'end'、'llm'等\n- `execute`：节点执行方法，接收执行上下文，返回执行结果\n\n### 节点执行流程\n\n节点的执行流程如下：\n\n1. **准备阶段**：\n   - 从执行上下文中获取节点的输入数据\n   - 验证输入数据是否符合要求\n\n2. **执行阶段**：\n   - 执行节点特定的业务逻辑\n   - 处理可能出现的异常情况\n\n3. **完成阶段**：\n   - 生成节点的输出数据\n   - 更新节点状态\n   - 返回执行结果\n\n工作流引擎会根据节点间的连接关系，按顺序调度节点的执行。对于特殊节点（如条件节点和循环节点），引擎会根据节点的执行结果决定下一步的执行路径。\n\n## 现有节点详细介绍\n\nFlowGram Runtime目前实现了五种类型的节点：Start、End、LLM、Condition和Loop。下面将详细介绍每种节点的功能、配置和使用示例。\n\n### 开始节点 `Start`\n\n#### 功能\n\nStart节点是工作流的起始节点，用于接收工作流的输入数据并开始工作流的执行。每个工作流必须有且只有一个Start节点。\n\n#### 配置选项\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| outputs | JSONSchema | 是 | 定义工作流的输入数据结构 |\n\n#### 使用示例\n\n```json\n{\n  \"id\": \"start_0\",\n  \"type\": \"start\",\n  \"data\": {\n    \"title\": \"开始节点\",\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"prompt\": {\n          \"type\": \"string\",\n          \"description\": \"用户输入的提示词\"\n        }\n      },\n      \"required\": [\"prompt\"]\n    }\n  }\n}\n```\n\n在这个例子中，Start节点定义了工作流需要一个名为`prompt`的字符串类型输入。\n\n### 结束节点 `End`\n\n#### 功能\n\nEnd节点是工作流的结束节点，用于收集工作流的输出数据并结束工作流的执行。每个工作流必须有至少一个End节点。\n\n#### 配置选项\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| inputs | JSONSchema | 是 | 定义工作流的输出数据结构 |\n| inputsValues | `Record<string, ValueSchema>` | 是 | 定义工作流的输出数据值，可以是引用或常量 |\n\n#### 使用示例\n\n```json\n{\n  \"id\": \"end_0\",\n  \"type\": \"end\",\n  \"data\": {\n    \"title\": \"结束节点\",\n    \"inputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"result\": {\n          \"type\": \"string\",\n          \"description\": \"工作流的输出结果\"\n        }\n      }\n    },\n    \"inputsValues\": {\n      \"result\": {\n        \"type\": \"ref\",\n        \"content\": [\"llm_0\", \"result\"]\n      }\n    }\n  }\n}\n```\n\n在这个例子中，End节点定义了工作流的输出包含一个名为`result`的字符串，其值引用自ID为`llm_0`的节点的`result`输出。\n\n### LLM节点 `LLM`\n\n#### 功能\n\nLLM节点用于调用大型语言模型执行自然语言处理任务，是FlowGram工作流中最常用的节点类型之一。\n\n#### 配置选项\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| modelName | string | 是 | 模型名称，如\"gpt-3.5-turbo\" |\n| apiKey | string | 是 | API密钥 |\n| apiHost | string | 是 | API主机地址 |\n| temperature | number | 是 | 温度参数，控制输出的随机性 |\n| systemPrompt | string | 否 | 系统提示词，设置AI助手的角色和行为 |\n| prompt | string | 是 | 用户提示词，即向AI提出的问题或请求 |\n\n#### 使用示例\n\n```json\n{\n  \"id\": \"llm_0\",\n  \"type\": \"llm\",\n  \"data\": {\n    \"title\": \"LLM节点\",\n    \"inputsValues\": {\n      \"modelName\": {\n        \"type\": \"constant\",\n        \"content\": \"gpt-3.5-turbo\"\n      },\n      \"apiKey\": {\n        \"type\": \"constant\",\n        \"content\": \"sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n      },\n      \"apiHost\": {\n        \"type\": \"constant\",\n        \"content\": \"https://api.openai.com/v1\"\n      },\n      \"temperature\": {\n        \"type\": \"constant\",\n        \"content\": 0.7\n      },\n      \"systemPrompt\": {\n        \"type\": \"constant\",\n        \"content\": \"你是一个有帮助的助手。\"\n      },\n      \"prompt\": {\n        \"type\": \"ref\",\n        \"content\": [\"start_0\", \"prompt\"]\n      }\n    },\n    \"inputs\": {\n      \"type\": \"object\",\n      \"required\": [\"modelName\", \"apiKey\", \"apiHost\", \"temperature\", \"prompt\"],\n      \"properties\": {\n        \"modelName\": { \"type\": \"string\" },\n        \"apiKey\": { \"type\": \"string\" },\n        \"apiHost\": { \"type\": \"string\" },\n        \"temperature\": { \"type\": \"number\" },\n        \"systemPrompt\": { \"type\": \"string\" },\n        \"prompt\": { \"type\": \"string\" }\n      }\n    },\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"result\": { \"type\": \"string\" }\n      }\n    }\n  }\n}\n```\n\n在这个例子中，LLM节点使用gpt-3.5-turbo模型，温度参数为0.7，系统提示词设置为\"你是一个有帮助的助手\"，用户提示词引用自Start节点的输入。\n\n### 条件节点 `Condition`\n\n#### 功能\n\nCondition节点用于根据条件选择不同的执行分支，实现工作流的条件逻辑。\n\n#### 配置选项\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| conditions | Array | 是 | 条件数组，每个条件包含key和value |\n\n条件value的结构：\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| left | ValueSchema | 是 | 左值，可以是引用或常量 |\n| operator | string | 是 | 操作符，如\"eq\"、\"gt\"等 |\n| right | ValueSchema | 是 | 右值，可以是引用或常量 |\n\n支持的操作符：\n\n| 操作符 | 描述 | 适用类型 |\n|--------|------|----------|\n| eq | 等于 | 所有类型 |\n| neq | 不等于 | 所有类型 |\n| gt | 大于 | 数字、字符串 |\n| gte | 大于等于 | 数字、字符串 |\n| lt | 小于 | 数字、字符串 |\n| lte | 小于等于 | 数字、字符串 |\n| includes | 包含 | 字符串、数组 |\n| startsWith | 以...开始 | 字符串 |\n| endsWith | 以...结束 | 字符串 |\n\n#### 使用示例\n\n```json\n{\n  \"id\": \"condition_0\",\n  \"type\": \"condition\",\n  \"data\": {\n    \"title\": \"条件节点\",\n    \"conditions\": [\n      {\n        \"key\": \"if_true\",\n        \"value\": {\n          \"left\": {\n            \"type\": \"ref\",\n            \"content\": [\"start_0\", \"value\"]\n          },\n          \"operator\": \"gt\",\n          \"right\": {\n            \"type\": \"constant\",\n            \"content\": 10\n          }\n        }\n      },\n      {\n        \"key\": \"if_false\",\n        \"value\": {\n          \"left\": {\n            \"type\": \"ref\",\n            \"content\": [\"start_0\", \"value\"]\n          },\n          \"operator\": \"lte\",\n          \"right\": {\n            \"type\": \"constant\",\n            \"content\": 10\n          }\n        }\n      }\n    ]\n  }\n}\n```\n\n在这个例子中，条件节点定义了两个分支：当Start节点的value输出大于10时走\"if_true\"分支，否则走\"if_false\"分支。\n\n### 循环节点 `Loop`\n\n#### 功能\n\nLoop节点用于对数组中的每个元素执行相同的操作，实现工作流的循环逻辑。\n\n#### 配置选项\n\n| 选项 | 类型 | 必填 | 描述 |\n|------|------|------|------|\n| loopFor | ValueSchema | 是 | 要迭代的数组，通常是一个引用 |\n| loopOutputs | `Record<string, ValueSchema>` | 是 | 循环输出，引用子节点的变量 |\n| blocks | `Array<NodeSchema>` | 是 | 循环体内的节点数组 |\n\n#### 使用示例\n\n```json\n{\n  \"id\": \"loop_0\",\n  \"type\": \"loop\",\n  \"data\": {\n    \"title\": \"循环节点\",\n    \"loopFor\": {\n      \"type\": \"ref\",\n      \"content\": [\"start_0\", \"items\"]\n    },\n    \"loopOutputs\": {\n      \"results\": {\n        \"type\": \"ref\",\n        \"content\": [\"llm_1\", \"result\"]\n      }\n    }\n  },\n  \"blocks\": [\n    {\n      \"id\": \"llm_1\",\n      \"type\": \"llm\",\n      \"data\": {\n        \"inputsValues\": {\n          \"prompt\": {\n            \"type\": \"ref\",\n            \"content\": [\"loop_0_locals\", \"item\"]\n          }\n        }\n      }\n    }\n  ]\n}\n```\n\n在这个例子中，循环节点对Start节点的items输出（假设是一个数组）进行迭代，对每个元素调用一个LLM节点。在循环体内，可以通过`loop_0_locals.item`引用当前迭代的元素，循环输出可引用LLM节点输出作为循环节点的输出。\n\n## 如何新增自定义节点\n\nFlowGram Runtime设计为可扩展的，允许开发者添加自定义节点类型。以下是实现和注册自定义节点的步骤。\n\n### 实现INodeExecutor接口的步骤\n\n1. **创建节点执行器类**：\n\n```typescript\nimport { ExecutionContext, ExecutionResult, INodeExecutor } from '@flowgram.ai/runtime-interface';\n\nexport class CustomNodeExecutor implements INodeExecutor {\n  // 定义节点类型\n  public type = 'custom';\n\n  // 实现execute方法\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // 1. 从上下文中获取输入\n    const inputs = context.inputs as CustomNodeInputs;\n\n    // 2. 验证输入\n    if (!inputs.requiredParam) {\n      throw new Error('必需参数缺失');\n    }\n\n    // 3. 执行节点逻辑\n    const result = await this.processCustomLogic(inputs);\n\n    // 4. 返回输出\n    return {\n      outputs: {\n        result: result\n      }\n    };\n  }\n\n  // 自定义处理逻辑\n  private async processCustomLogic(inputs: CustomNodeInputs): Promise<string> {\n    // 实现自定义逻辑\n    return `处理结果: ${inputs.requiredParam}`;\n  }\n}\n\n// 定义输入接口\ninterface CustomNodeInputs {\n  requiredParam: string;\n  optionalParam?: number;\n}\n```\n\n2. **处理异常情况**：\n\n```typescript\npublic async execute(context: ExecutionContext): Promise<ExecutionResult> {\n  try {\n    const inputs = context.inputs as CustomNodeInputs;\n\n    // 验证输入\n    if (!inputs.requiredParam) {\n      throw new Error('必需参数缺失');\n    }\n\n    // 执行节点逻辑\n    const result = await this.processCustomLogic(inputs);\n\n    return {\n      outputs: {\n        result: result\n      }\n    };\n  } catch (error) {\n    // 处理异常\n    console.error('节点执行失败:', error);\n    throw error; // 或者返回特定的错误输出\n  }\n}\n```\n\n### 注册自定义节点的方法\n\n将自定义节点执行器添加到FlowGram Runtime的节点执行器注册表中：\n\n```typescript\nimport { WorkflowRuntimeNodeExecutors } from './nodes';\nimport { CustomNodeExecutor } from './nodes/custom';\n\n// 注册自定义节点执行器\nWorkflowRuntimeNodeExecutors.push(new CustomNodeExecutor());\n```\n\n### 自定义节点开发最佳实践\n\n1. **明确节点职责**：\n   - 每个节点应该有明确的单一职责\n   - 避免在一个节点中实现多个不相关的功能\n\n2. **输入验证**：\n   - 在执行节点逻辑前验证所有必需的输入\n   - 提供清晰的错误信息，便于调试\n\n3. **异常处理**：\n   - 捕获并处理可能的异常情况\n   - 避免让未处理的异常导致整个工作流崩溃\n\n4. **性能考虑**：\n   - 对于耗时操作，考虑实现超时机制\n   - 避免阻塞主线程的长时间同步操作\n\n5. **可测试性**：\n   - 设计节点时考虑单元测试的便利性\n   - 将核心逻辑与外部依赖分离，便于模拟测试\n\n6. **文档和注释**：\n   - 为自定义节点提供详细的文档\n   - 在代码中添加必要的注释，特别是复杂逻辑部分\n\n### 自定义节点示例\n\n下面是一个完整的自定义HTTP请求节点示例，用于发送HTTP请求并处理响应：\n\n```typescript\nimport { ExecutionContext, ExecutionResult, INodeExecutor } from '@flowgram.ai/runtime-interface';\nimport axios from 'axios';\n\n// 定义HTTP节点的输入接口\ninterface HTTPNodeInputs {\n  url: string;\n  method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n  headers?: Record<string, string>;\n  body?: any;\n  timeout?: number;\n}\n\n// 定义HTTP节点的输出接口\ninterface HTTPNodeOutputs {\n  status: number;\n  data: any;\n  headers: Record<string, string>;\n}\n\nexport class HTTPNodeExecutor implements INodeExecutor {\n  // 定义节点类型\n  public type = 'http';\n\n  // 实现execute方法\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // 1. 从上下文中获取输入\n    const inputs = context.inputs as HTTPNodeInputs;\n\n    // 2. 验证输入\n    if (!inputs.url) {\n      throw new Error('URL参数缺失');\n    }\n\n    if (!inputs.method) {\n      throw new Error('请求方法参数缺失');\n    }\n\n    // 3. 执行HTTP请求\n    try {\n      const response = await axios({\n        url: inputs.url,\n        method: inputs.method,\n        headers: inputs.headers || {},\n        data: inputs.body,\n        timeout: inputs.timeout || 30000\n      });\n\n      // 4. 处理响应\n      const outputs: HTTPNodeOutputs = {\n        status: response.status,\n        data: response.data,\n        headers: response.headers as Record<string, string>\n      };\n\n      // 5. 返回输出\n      return {\n        outputs\n      };\n    } catch (error) {\n      if (axios.isAxiosError(error)) {\n        // 处理Axios错误\n        if (error.response) {\n          // 服务器返回了错误状态码\n          return {\n            outputs: {\n              status: error.response.status,\n              data: error.response.data,\n              headers: error.response.headers as Record<string, string>\n            }\n          };\n        } else if (error.request) {\n          // 请求已发送但未收到响应\n          throw new Error(`请求超时或无响应: ${error.message}`);\n        } else {\n          // 请求配置有误\n          throw new Error(`请求配置错误: ${error.message}`);\n        }\n      } else {\n        // 处理非Axios错误\n        throw error;\n      }\n    }\n  }\n}\n\n// 注册HTTP节点执行器\nimport { WorkflowRuntimeNodeExecutors } from './nodes';\nWorkflowRuntimeNodeExecutors.push(new HTTPNodeExecutor());\n```\n\n使用示例：\n\n```json\n{\n  \"id\": \"http_0\",\n  \"type\": \"http\",\n  \"data\": {\n    \"title\": \"HTTP请求节点\",\n    \"inputsValues\": {\n      \"url\": {\n        \"type\": \"constant\",\n        \"content\": \"https://api.example.com/data\"\n      },\n      \"method\": {\n        \"type\": \"constant\",\n        \"content\": \"GET\"\n      },\n      \"headers\": {\n        \"type\": \"constant\",\n        \"content\": {\n          \"Authorization\": \"Bearer token123\"\n        }\n      }\n    },\n    \"inputs\": {\n      \"type\": \"object\",\n      \"required\": [\"url\", \"method\"],\n      \"properties\": {\n        \"url\": { \"type\": \"string\" },\n        \"method\": { \"type\": \"string\", \"enum\": [\"GET\", \"POST\", \"PUT\", \"DELETE\"] },\n        \"headers\": { \"type\": \"object\" },\n        \"body\": { \"type\": \"object\" },\n        \"timeout\": { \"type\": \"number\" }\n      }\n    },\n    \"outputs\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"status\": { \"type\": \"number\" },\n        \"data\": { \"type\": \"object\" },\n        \"headers\": { \"type\": \"object\" }\n      }\n    }\n  }\n}\n```\n\n通过以上步骤和示例，您可以根据自己的需求开发和注册自定义节点，扩展FlowGram Runtime的功能。\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/quick-start.mdx",
    "content": "---\ntitle: 快速开始\ndescription: 快速上手使用 FlowGram Runtime\nsidebar_position: 3\n---\n\n# 快速开始\n\n本文档将帮助您快速上手使用 FlowGram Runtime，包括环境准备、安装配置、创建和运行工作流等内容。通过本指南，您将能够在短时间内搭建起自己的工作流运行环境并运行第一个工作流示例。\n\n## 获取代码\n\n由于 FlowGram Runtime 定位为 demo 而非 SDK，不会作为 npm 包发布，您需要通过以下步骤来获取和使用：\n\n### 方式一：Fork 仓库（推荐）\n\n1. 访问 FlowGram Runtime 的代码仓库\n2. 点击 \"Fork\" 按钮创建您自己的仓库副本\n3. 克隆您 fork 的仓库到本地\n\n### 方式二：直接克隆 flowgram 仓库\n\n如果您只是想尝试使用而不需要提交更改，可以直接克隆原始仓库：\n\n```bash\ngit clone git@github.com:bytedance/flowgram.ai.git\ncd flowgram.ai\n```\n\n## 环境准备\n\n在开始使用 FlowGram Runtime 之前，请确保您的开发环境满足以下要求：\n\n- **Node.js**：版本 18.x 或更高版本（推荐使用 LTS 版本）\n\n```bash\nnvm install lts/hydrogen\nnvm alias default lts/hydrogen # set default node version\nnvm use lts/hydrogen\n```\n\n- **包管理器**：pnpm 9+ 与 rush 5+\n\n```bash\nnpm i -g pnpm@10.6.5 @microsoft/rush@5.150.0\n```\n\n## 安装依赖和项目设置\n\n获取代码后，需要安装依赖并进行基本设置：\n\n1. **安装项目依赖**\n\n```bash\nrush install\n```\n\n2. **构建项目**\n\n```bash\nrush build\n```\n\n## 启动服务\n\n1. **进入运行时目录**\n\n```bash\ncd packages/runtime/nodejs\n```\n\n2. **启动 nodejs 服务器**\n\n```bash\nnpm run dev\n```\n\n如果一切正常，你能在控制台看到以下输出：\n\n```\n> Listen Port: 4000\n> Server Address: http://localhost:4000\n> API Docs: http://localhost:4000/docs\n```\n\n3. **验证服务运行**\n\n在命令行支持 cURL 请求\n\n```bash\ncurl --location 'http://localhost:4000/api/task/run' \\\n--header 'Content-Type: application/json' \\\n--data '{\n  \"inputs\": {\n      \"test_input\": \"Hello FlowGram!\"\n  },\n  \"schema\": \"{\\\"nodes\\\":[{\\\"id\\\":\\\"start_0\\\",\\\"type\\\":\\\"start\\\",\\\"meta\\\":{\\\"position\\\":{\\\"x\\\":180,\\\"y\\\":0}},\\\"data\\\":{\\\"title\\\":\\\"Start\\\",\\\"outputs\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"test_input\\\":{\\\"key\\\":4,\\\"name\\\":\\\"test_input\\\",\\\"isPropertyRequired\\\":true,\\\"type\\\":\\\"string\\\",\\\"extra\\\":{\\\"index\\\":0}}},\\\"required\\\":[\\\"test_input\\\"]}}},{\\\"id\\\":\\\"end_0\\\",\\\"type\\\":\\\"end\\\",\\\"meta\\\":{\\\"position\\\":{\\\"x\\\":640,\\\"y\\\":0}},\\\"data\\\":{\\\"title\\\":\\\"End\\\",\\\"inputsValues\\\":{\\\"test_output\\\":{\\\"type\\\":\\\"ref\\\",\\\"content\\\":[\\\"start_0\\\",\\\"test_input\\\"]}},\\\"inputs\\\":{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{\\\"test_output\\\":{\\\"type\\\":\\\"string\\\"}}}}}],\\\"edges\\\":[{\\\"sourceNodeID\\\":\\\"start_0\\\",\\\"targetNodeID\\\":\\\"end_0\\\"}]}\"\n}'\n```\n\n此时服务所在的命令行应该会有如下输出：\n\n```\n> POST TaskRun - taskID:  xxxx-xxxx-xxxx-xxxx\n{ test_input: 'Hello FlowGram!' }\n> LOG Task finished:  xxxx-xxxx-xxxx-xxxx\n{ test_output: 'Hello FlowGram!' }\n```\n\n## 接入到 FlowGram 编辑器（Web 前端）\n\n在 `editorProps` 配置中修改 runtime 为 server 模式，并配置服务地址\n\n```ts\ncreateRuntimePlugin({\n  // mode: 'browser', // 移除这一行\n  mode: 'server',\n  serverConfig: {\n    domain: 'localhost',\n    port: 4000,\n    protocol: 'http',\n  },\n})\n```\n\n## 编译服务\n\n1. **进入运行时目录**\n\n```bash\ncd packages/runtime/nodejs\n```\n\n2. **编译服务**\n\n```bash\nnpm run build\n```\n\n2. **启动 nodejs 服务器**\n\n```bash\nnode dist/index.js\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/schema.mdx",
    "content": "---\ntitle: 认识 Schema\ndescription: FlowGram Schema 结构和配置的详细介绍\n---\n\n# Workflow Schema\n\n本文档详细介绍了 Workflow 的 Schema 结构。Workflow Schema 是定义工作流程的核心配置，描述了节点，以及边的配置。\n\n## 基本结构\n\n一个完整的 Workflow Schema 包含以下主要部分：\n\n```typescript\ninterface WorkflowSchema {\n  nodes: WorkflowNodeSchema[];\n  edges: WorkflowEdgeSchema[];\n}\n```\n\n## 节点 Schema\n\n节点是工作流中的基本单元，每个节点都有其特定的类型和配置：\n\n```typescript\ninterface WorkflowNodeSchema<T = string, D = any> {\n  id: string;           // 节点唯一标识符\n  type: T;              // 节点类型\n  meta: WorkflowNodeMetaSchema;  // 节点元数据\n  data: D & {          // 节点数据\n    title?: string;    // 节点标题\n    inputsValues?: Record<string, IFlowValue>;  // 输入值\n    inputs?: IJsonSchema;   // 输入模式定义\n    outputs?: IJsonSchema;  // 输出模式定义\n    [key: string]: any;     // 其他自定义数据\n  };\n  blocks?: WorkflowNodeSchema[];  // 子节点（用于复合节点）\n  edges?: WorkflowEdgeSchema[];   // 子节点之间的连接\n}\n```\n\n### 节点元数据\n\n```typescript\ninterface WorkflowNodeMetaSchema {\n  position: PositionSchema;        // 节点在画布中的位置\n}\n```\n\n## 边 Schema\n\n边定义了节点之间的连接关系：\n\n```typescript\ninterface WorkflowEdgeSchema {\n  sourceNodeID: string;    // 源节点ID\n  targetNodeID: string;    // 目标节点ID\n  sourcePortID?: string;   // 源节点端口ID（可选）\n  targetPortID?: string;   // 目标节点端口ID（可选）\n}\n```\n\n## 值类型\n\nWorkflow 支持多种值类型：\n\n```typescript\nenum WorkflowVariableType {\n  String = 'string',    // 字符串\n  Integer = 'integer',  // 整数\n  Number = 'number',    // 数字\n  Boolean = 'boolean',  // 布尔值\n  Object = 'object',    // 对象\n  Array = 'array',      // 数组\n  Null = 'null'         // 空值\n}\n```\n\n### 流程值\n\n在节点的输入值中，支持以下几种类型：\n\n```typescript\ntype IFlowValue =\n  | IFlowConstantValue     // 常量值\n  | IFlowRefValue          // 引用值\n  | IFlowExpressionValue   // 表达式值\n  | IFlowTemplateValue;    // 模板值\n```\n\n每种类型的具体定义：\n\n```typescript\ninterface IFlowConstantValue {\n  type: 'constant';\n  content?: string | number | boolean;\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n\ninterface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n}\n\ninterface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n}\n```\n\n## JSON Schema\n\n节点的输入输出定义使用 JSON Schema 格式：\n\n```typescript\ninterface IJsonSchema<T = string> {\n  type: T;                 // 数据类型\n  default?: any;           // 默认值\n  title?: string;          // 标题\n  description?: string;    // 描述\n  enum?: (string | number)[];  // 枚举值\n  properties?: Record<string, IJsonSchema<T>>;  // 对象属性\n  additionalProperties?: IJsonSchema<T>;        // 额外属性\n  items?: IJsonSchema<T>;                       // 数组项定义\n  required?: string[];                          // 必需字段\n  $ref?: string;                                // 引用\n  extra?: {                                     // 额外配置\n    index?: number;                             // 索引\n    weak?: boolean;                             // 弱类型比较\n    formComponent?: string;                     // 表单组件\n    [key: string]: any;\n  };\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/runtime/source-code-guide.mdx",
    "content": "---\ntitle: 源码导读\ndescription: FlowGram Runtime 源码结构和实现细节解析\n---\n\n# FlowGram Runtime 源码导读\n\n本文档旨在帮助开发者深入理解 FlowGram Runtime 的源码结构和实现细节，为后续的定制开发提供指导。由于 FlowGram Runtime 定位为参考实现而非直接使用的 SDK，因此了解其内部实现对于开发者来说尤为重要。\n\n## 项目结构概述\n\n### 目录结构\n\nFlowGram Runtime JS 项目的目录结构如下：\n\n```\npackages/runtime\n├── js-core/          # 核心运行时库\n│   ├── src/\n│   │   ├── application/    # 应用层，实现API接口\n│   │   ├── domain/         # 领域层，核心业务逻辑\n│   │   ├── infrastructure/ # 基础设施层，提供技术支持\n│   │   ├── nodes/          # 节点执行器实现\n│   │   └── index.ts        # 入口文件\n│   ├── package.json\n│   └── tsconfig.json\n├── interface/        # 接口定义\n│   ├── src/\n│   │   ├── api/       # API接口定义\n│   │   ├── domain/    # 领域模型接口定义\n│   │   ├── engine/    # 引擎接口定义\n│   │   ├── node/      # 节点接口定义\n│   │   └── index.ts   # 入口文件\n│   ├── package.json\n│   └── tsconfig.json\n└── nodejs/           # NodeJS 服务实现\n    ├── src/\n    │   ├── api/       # HTTP API实现\n    │   ├── server/    # 服务器实现\n    │   └── index.ts   # 入口文件\n    ├── package.json\n    └── tsconfig.json\n```\n\n### 模块组织\n\nFlowGram Runtime JS 采用模块化设计，主要分为三个核心模块：\n\n1. **interface**: 定义了系统的接口和数据结构，是其他模块的基础\n2. **js-core**: 实现了工作流引擎的核心功能，包括工作流解析、节点执行、状态管理等\n3. **nodejs**: 提供了基于 NodeJS 的 HTTP API 服务，使工作流引擎可以通过 HTTP 接口调用\n\n### 依赖关系\n\n模块之间的依赖关系如下：\n\n```mermaid\ngraph TD\n    nodejs --> js-core\n    js-core --> interface\n    nodejs -.-> interface\n```\n\n- **interface** 是基础模块，不依赖其他模块\n- **js-core** 依赖 interface 模块中定义的接口\n- **nodejs** 依赖 js-core 模块提供的功能，同时也使用 interface 模块中的接口定义\n\n主要的外部依赖包括：\n\n- **TypeScript**: 提供类型安全和面向对象编程支持\n- **LangChain**: 用于集成大型语言模型\n- **OpenAI API**: 提供 LLM 节点的默认实现\n- **fastify**: 用于实现 HTTP API 服务\n- **tRPC**: 用于类型安全的 API 定义和调用\n\n## 核心模块详解\n\n### js-core 模块\n\njs-core 模块是 FlowGram Runtime 的核心，实现了工作流引擎的主要功能。该模块采用领域驱动设计（DDD）架构，分为应用层、领域层和基础设施层。\n\n#### 应用层 (application)\n\n应用层负责协调领域对象，实现系统的用例。主要文件：\n\n- `application/workflow.ts`: 工作流应用服务，实现工作流的验证、执行、取消、查询等功能\n- `application/api.ts`: API 实现，包括 TaskValidate、TaskRun、TaskResult、TaskReport、TaskCancel 等\n\n#### 领域层 (domain)\n\n领域层包含业务核心逻辑和领域模型。主要目录和文件：\n\n- `domain/engine/`: 工作流执行引擎，负责工作流的解析和执行\n  - `engine.ts`: 工作流引擎实现，包含节点执行、状态管理等核心逻辑\n  - `validator.ts`: 工作流验证器，检查工作流定义的有效性\n- `domain/document/`: 工作流文档模型，表示工作流的结构\n  - `workflow.ts`: 工作流定义模型\n  - `node.ts`: 节点定义模型\n  - `edge.ts`: 边定义模型\n- `domain/executor/`: 节点执行器，负责执行具体节点的逻辑\n  - `executor.ts`: 节点执行器基类和工厂\n- `domain/variable/`: 变量管理，处理工作流中的变量存储和引用\n  - `manager.ts`: 变量管理器，负责变量的存储、获取和解析\n  - `store.ts`: 变量存储，提供变量的持久化\n- `domain/status/`: 状态管理，跟踪工作流和节点的执行状态\n  - `center.ts`: 状态中心，管理工作流和节点的状态\n- `domain/snapshot/`: 快照管理，记录工作流执行的中间状态\n  - `center.ts`: 快照中心，管理节点执行的快照\n- `domain/report/`: 报告生成，收集工作流执行的详细信息\n  - `center.ts`: 报告中心，生成工作流执行报告\n\n#### 基础设施层 (infrastructure)\n\n基础设施层提供技术支持，包括日志、事件、容器等。主要文件：\n\n- `infrastructure/logger.ts`: 日志服务，提供日志记录功能\n- `infrastructure/event.ts`: 事件服务，提供事件发布和订阅功能\n- `infrastructure/container.ts`: 依赖注入容器，管理对象的创建和生命周期\n- `infrastructure/error.ts`: 错误处理，定义系统中的错误类型和处理方式\n\n#### 节点执行器 (nodes)\n\nnodes 目录包含各种节点类型的执行器实现。主要文件：\n\n- `nodes/start.ts`: Start 节点执行器\n- `nodes/end.ts`: End 节点执行器\n- `nodes/llm.ts`: LLM 节点执行器，集成大型语言模型\n- `nodes/condition.ts`: Condition 节点执行器，实现条件分支\n- `nodes/loop.ts`: Loop 节点执行器，实现循环逻辑\n\n### interface 模块\n\ninterface 模块定义了系统的接口和数据结构，是其他模块的基础。主要目录和文件：\n\n- `api/`: API 接口定义\n  - `api.ts`: 定义系统提供的 API 接口\n  - `types.ts`: API 相关的数据类型定义\n- `domain/`: 领域模型接口定义\n  - `document.ts`: 工作流文档相关接口\n  - `engine.ts`: 工作流引擎相关接口\n  - `executor.ts`: 节点执行器相关接口\n  - `variable.ts`: 变量管理相关接口\n  - `status.ts`: 状态管理相关接口\n  - `snapshot.ts`: 快照管理相关接口\n  - `report.ts`: 报告生成相关接口\n- `engine/`: 引擎接口定义\n  - `types.ts`: 引擎相关的数据类型定义\n- `node/`: 节点接口定义\n  - `types.ts`: 节点相关的数据类型定义\n\n### nodejs 模块\n\nnodejs 模块提供了基于 NodeJS 的 HTTP API 服务，使工作流引擎可以通过 HTTP 接口调用。主要目录和文件：\n\n- `api/`: HTTP API 实现\n  - `router.ts`: API 路由定义\n  - `handlers.ts`: API 处理函数\n- `server/`: 服务器实现\n  - `server.ts`: HTTP 服务器实现\n  - `config.ts`: 服务器配置\n\n## 关键实现分析\n\n### 工作流引擎\n\n工作流引擎是 FlowGram Runtime 的核心，负责工作流的解析和执行。其主要实现位于 `js-core/src/domain/engine/engine.ts`。\n\n工作流引擎的主要功能包括：\n\n1. **工作流解析**：将工作流定义转换为内部模型\n2. **节点调度**：根据工作流定义的边，确定节点的执行顺序\n3. **节点执行**：调用节点执行器执行节点逻辑\n4. **状态管理**：跟踪工作流和节点的执行状态\n5. **变量管理**：处理节点间的数据传递\n6. **错误处理**：处理执行过程中的异常情况\n\n关键代码片段：\n\n```typescript\n// 工作流执行的核心方法\npublic async run(params: RunParams): Promise<RunResult> {\n  const { schema, inputs, options } = params;\n\n  // 创建工作流上下文\n  const context = this.createContext(schema, inputs, options);\n\n  try {\n    // 初始化工作流\n    await this.initialize(context);\n\n    // 执行工作流\n    await this.execute(context);\n\n    // 获取工作流结果\n    const result = await this.getResult(context);\n\n    return {\n      status: 'success',\n      outputs: result\n    };\n  } catch (error) {\n    // 错误处理\n    return {\n      status: 'fail',\n      error: error.message\n    };\n  }\n}\n\n// 执行工作流\nprivate async execute(context: IContext): Promise<void> {\n  // 获取开始节点\n  const startNode = context.workflow.getStartNode();\n\n  // 从开始节点开始执行\n  await this.executeNode({ context, node: startNode });\n\n  // 等待所有节点执行完成\n  await this.waitForCompletion(context);\n}\n\n// 执行节点\npublic async executeNode(params: { context: IContext; node: INode }): Promise<void> {\n  const { context, node } = params;\n\n  // 获取节点执行器\n  const executor = this.getExecutor(node.type);\n\n  // 准备节点输入\n  const inputs = await this.prepareInputs(context, node);\n\n  // 执行节点\n  const result = await executor.execute({\n    node,\n    inputs,\n    context\n  });\n\n  // 处理节点输出\n  await this.processOutputs(context, node, result.outputs);\n\n  // 调度下一个节点\n  await this.scheduleNextNodes(context, node);\n}\n```\n\n### 节点执行器\n\n节点执行器负责执行具体节点的逻辑。每种节点类型都有对应的执行器实现，位于 `js-core/src/nodes/` 目录。\n\n节点执行器的基本接口定义在 `interface/src/domain/executor.ts` 中：\n\n```typescript\nexport interface INodeExecutor {\n  type: string;\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n```\n\n以 LLM 节点执行器为例，其实现位于 `js-core/src/nodes/llm.ts`：\n\n```typescript\nexport class LLMExecutor implements INodeExecutor {\n  public type = 'llm';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = context.inputs as LLMExecutorInputs;\n\n    // 创建 LLM 提供商\n    const provider = this.createProvider(inputs);\n\n    // 准备提示词\n    const systemPrompt = inputs.systemPrompt || '';\n    const userPrompt = inputs.prompt || '';\n\n    // 调用 LLM\n    const result = await provider.call({\n      systemPrompt,\n      userPrompt,\n      options: {\n        temperature: inputs.temperature\n      }\n    });\n\n    // 返回结果\n    return {\n      outputs: {\n        result: result.content\n      }\n    };\n  }\n\n  private createProvider(inputs: LLMExecutorInputs): ILLMProvider {\n    // 根据模型名称创建不同的提供商\n    if (inputs.modelName.startsWith('gpt-')) {\n      return new OpenAIProvider({\n        apiKey: inputs.apiKey,\n        apiHost: inputs.apiHost,\n        modelName: inputs.modelName\n      });\n    }\n\n    throw new Error(`Unsupported model: ${inputs.modelName}`);\n  }\n}\n```\n\n### 变量管理\n\n变量管理是工作流执行的重要部分，负责处理节点间的数据传递。其主要实现位于 `js-core/src/domain/variable/` 目录。\n\n变量管理的核心是变量管理器和变量存储：\n\n- **变量管理器**：负责变量的解析、获取和设置\n- **变量存储**：提供变量的持久化存储\n\n关键代码片段：\n\n```typescript\n// 变量管理器\nexport class VariableManager implements IVariableManager {\n  constructor(private store: IVariableStore) {}\n\n  // 解析变量引用\n  public async resolve(ref: ValueSchema, scope?: string): Promise<any> {\n    if (ref.type === 'constant') {\n      return ref.content;\n    } else if (ref.type === 'ref') {\n      const path = ref.content as string[];\n      return this.get(path, scope);\n    }\n    throw new Error(`Unsupported value type: ${ref.type}`);\n  }\n\n  // 获取变量值\n  public async get(path: string[], scope?: string): Promise<any> {\n    const [nodeID, key, ...rest] = path;\n    const value = await this.store.get(nodeID, key, scope);\n\n    if (rest.length === 0) {\n      return value;\n    }\n\n    // 处理嵌套属性\n    return this.getNestedProperty(value, rest);\n  }\n\n  // 设置变量值\n  public async set(nodeID: string, key: string, value: any, scope?: string): Promise<void> {\n    await this.store.set(nodeID, key, value, scope);\n  }\n}\n```\n\n### 状态存储\n\n状态存储负责管理工作流和节点的执行状态。其主要实现位于 `js-core/src/domain/status/` 和 `js-core/src/domain/snapshot/` 目录。\n\n状态管理的核心组件包括：\n\n- **状态中心**：管理工作流和节点的状态\n- **快照中心**：记录节点执行的快照\n- **报告中心**：生成工作流执行报告\n\n关键代码片段：\n\n```typescript\n// 状态中心\nexport class StatusCenter implements IStatusCenter {\n  private workflowStatus: Record<string, WorkflowStatus> = {};\n  private nodeStatus: Record<string, Record<string, NodeStatus>> = {};\n\n  // 设置工作流状态\n  public setWorkflowStatus(workflowID: string, status: WorkflowStatus): void {\n    this.workflowStatus[workflowID] = status;\n  }\n\n  // 获取工作流状态\n  public getWorkflowStatus(workflowID: string): WorkflowStatus {\n    return this.workflowStatus[workflowID] || 'idle';\n  }\n\n  // 设置节点状态\n  public setNodeStatus(workflowID: string, nodeID: string, status: NodeStatus): void {\n    if (!this.nodeStatus[workflowID]) {\n      this.nodeStatus[workflowID] = {};\n    }\n    this.nodeStatus[workflowID][nodeID] = status;\n  }\n\n  // 获取节点状态\n  public getNodeStatus(workflowID: string, nodeID: string): NodeStatus {\n    return this.nodeStatus[workflowID]?.[nodeID] || 'idle';\n  }\n}\n```\n\n## 设计模式和架构决策\n\n### 领域驱动设计\n\nFlowGram Runtime 采用领域驱动设计（DDD）架构，将系统分为应用层、领域层和基础设施层。这种架构有助于分离关注点，使代码更加模块化和可维护。\n\n主要的领域概念包括：\n\n- **工作流**：表示一个完整的工作流定义\n- **节点**：工作流中的基本执行单元\n- **边**：连接节点的线，表示执行流程\n- **执行上下文**：工作流执行的环境\n- **变量**：工作流执行过程中的数据\n\n### 工厂模式\n\nFlowGram Runtime 使用工厂模式创建节点执行器，使系统能够根据节点类型动态创建对应的执行器。\n\n```typescript\n// 节点执行器工厂\nexport class NodeExecutorFactory implements INodeExecutorFactory {\n  private executors: Record<string, INodeExecutor> = {};\n\n  // 注册节点执行器\n  public register(executor: INodeExecutor): void {\n    this.executors[executor.type] = executor;\n  }\n\n  // 创建节点执行器\n  public create(type: string): INodeExecutor {\n    const executor = this.executors[type];\n    if (!executor) {\n      throw new Error(`No executor registered for node type: ${type}`);\n    }\n    return executor;\n  }\n}\n```\n\n### 策略模式\n\nFlowGram Runtime 使用策略模式处理不同类型的节点执行逻辑，每种节点类型都有对应的执行策略。\n\n```typescript\n// 节点执行器接口（策略接口）\nexport interface INodeExecutor {\n  type: string;\n  execute(context: ExecutionContext): Promise<ExecutionResult>;\n}\n\n// 具体策略实现\nexport class StartExecutor implements INodeExecutor {\n  public type = 'start';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // Start 节点的执行逻辑\n  }\n}\n\nexport class EndExecutor implements INodeExecutor {\n  public type = 'end';\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    // End 节点的执行逻辑\n  }\n}\n```\n\n### 观察者模式\n\nFlowGram Runtime 使用观察者模式实现事件系统，使组件能够发布和订阅事件。\n\n```typescript\n// 事件发布者\nexport class EventEmitter implements IEventEmitter {\n  private listeners: Record<string, Function[]> = {};\n\n  // 订阅事件\n  public on(event: string, listener: Function): void {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(listener);\n  }\n\n  // 发布事件\n  public emit(event: string, ...args: any[]): void {\n    const eventListeners = this.listeners[event];\n    if (eventListeners) {\n      for (const listener of eventListeners) {\n        listener(...args);\n      }\n    }\n  }\n}\n```\n\n### 依赖注入\n\nFlowGram Runtime 使用依赖注入管理组件之间的依赖关系，使组件更加松耦合和可测试。\n\n```typescript\n// 依赖注入容器\nexport class Container {\n  private static _instance: Container;\n  private registry: Map<any, any> = new Map();\n\n  public static get instance(): Container {\n    if (!Container._instance) {\n      Container._instance = new Container();\n    }\n    return Container._instance;\n  }\n\n  // 注册服务\n  public register<T>(token: any, instance: T): void {\n    this.registry.set(token, instance);\n  }\n\n  // 获取服务\n  public resolve<T>(token: any): T {\n    const instance = this.registry.get(token);\n    if (!instance) {\n      throw new Error(`No instance registered for token: ${token}`);\n    }\n    return instance;\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/_meta.json",
    "content": "[\n  \"basic\",\n  \"variable-output\",\n  \"variable-consume\",\n  \"concept\",\n  \"custom-scope-chain\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/basic.mdx",
    "content": "---\ndescription: 介绍什么是变量，以及变量引擎的作用\n---\n\n\n# 介绍\n\n:::warning\n\n本文档的变量引擎属于 FlowGram **设计态**，和运行态的变量引擎不同。\n\n:::\n\n## 阅读路径\n\n- 在这里先建立「变量是什么」以及「为什么需要变量引擎」的整体心智。\n- 接下来建议直接上手：[输出变量](./variable-output.mdx) → [消费变量](./variable-consume.mdx)，先学会如何在节点中产出变量，再去读取。\n- 当在实践中遇到作用域或类型等疑问时，再回到[核心概念](./concept.mdx)查阅详细术语。\n\n## 什么是变量？\n\n想象一下，你在搭建一个复杂的乐高模型，每个模块都需要精确地连接在一起。在工作流（Workflow）的世界里，**变量**就扮演着类似“连接件”的角色。它们是用来在不同节点之间传递信息的“信使”。\n\n简单来说，变量就是一个带名字的容器，你可以往里面装各种东西，比如用户的输入、计算结果，或者从某个地方获取的数据。\n\n一个变量通常由三部分组成：\n\n- **名字（唯一标识符）**：类似个人姓名，用于准确定位某个变量。例如 `userName`、`orderId`。\n- **值**：容器里装的东西。它可以是数字 `123`，文字 `\"Hello FlowGram!\"`，或者一个开关状态 `true` / `false`。\n- **类型**：规定了这个容器能装哪种东西。比如，有的只能装数字，有的只能装文字。\n\n---\n\n举个例子，在一个“智能问答”流程中：\n\n<div style={{display: 'flex', gap: '20px'}}>\n  <img style={{width: \"50%\"}} loading=\"lazy\" src=\"/variable/variable-biz-context-websearch-llm.png\" alt=\"智能问答流程\" />\n  <div>\n    <p style={{marginTop: 10}}>1. **`WebSearch` 节点**：负责上网搜索，然后把搜到的知识（比如“今天天气怎么样？”的答案）放进一个名为 `natural_language_desc` 的变量里。</p>\n    <p style={{marginTop: 5}}>2. **`LLM` 节点**：它会接过 `natural_language_desc` 这个“信使”，读取里面的内容，然后用更自然、更友好的方式回答用户。</p>\n    <p style={{marginTop: 5}}>3. 在这个过程中，`natural_language_desc` 的类型就是“字符串”，因为它装的是文字内容。</p>\n  </div>\n</div>\n\n### 设计态定义 vs. 运行态取值\n\n在**设计态**（绘制流程时），我们只需要确定变量的**定义**：名字、类型以及可选的元信息。变量引擎会把这些定义管理成结构化的数据。\n\n到了**运行态**，FlowGram 会依据这些定义为每次执行赋值。因此在设计时，先关注结构与约束；在运行时，所有节点就能依赖这些定义稳定地读写实际数据。\n\n## 为什么需要变量引擎？\n\n随着工作流复杂度的提升，变量的数量和管理难度也随之增加。\n\n为了应对这一挑战，FlowGram 提供了强大的**变量引擎**。\n\n它如同一位专业的“数据管家”，能够系统化地管理所有变量，确保数据流的清晰与稳定。\n\n启用变量引擎将为您带来以下核心优势：\n\n<div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: \"25px\" }}>\n  <div style={{ gridColumn: \"span 2\" }}>\n    <b>作用域约束：精准的数据访问控制</b>\n    <p className=\"rs-tip\">变量引擎能够精确控制每个变量的有效范围（即作用域）。如同为不同房间配置专属钥匙，它确保了变量只在预期的节点中被访问，从而有效避免了数据污染和意外的逻辑错误。</p>\n    <div style={{display: \"flex\", gap: \"25px\"}}>\n      <div>\n        <img loading=\"lazy\" src=\"/variable/variable-scope-feature-1.png\" alt=\"作用域约束示例 1\" />\n        <p style={{marginTop: '10px'}}>`Start` 节点定义的 `query` 变量，在它后面的 `LLM` 和 `End` 节点都能轻松访问。</p>\n      </div>\n      <div>\n        <img loading=\"lazy\" src=\"/variable/variable-scope-feature-2.png\" alt=\"作用域约束示例 2\" />\n        <p style={{marginTop: '10px'}}>`LLM` 节点处于 `Condition` 分支，等同于位于独立空间；外部的 `End` 节点无法访问其 `result` 变量。</p>\n      </div>\n    </div>\n  </div>\n  <div>\n    <b>变量结构透视：轻松洞悉复杂数据</b>\n    <p className=\"rs-tip\">当变量结构较复杂（例如包含多层嵌套的对象）时，变量引擎支持逐层展开，便于定位每一项数据。</p>\n    <img loading=\"lazy\" src=\"/variable/variable-tree-management.gif\" alt=\"变量结构透视\" />\n    <p style={{marginTop: '10px'}}>下图展示了所有节点的输出变量与层级关系，便于整体审视变量结构。</p>\n  </div>\n  <div>\n    <b>类型自动推导：心有灵犀一点通</b>\n    <p className=\"rs-tip\">无需为每个变量逐一指定类型，变量引擎可依据上下文自动完成推导。</p>\n    <img loading=\"lazy\" src=\"/variable/variable-batch-auto-infer.gif\" alt=\"类型自动推导\" />\n    <p style={{marginTop: '10px'}}>例如，当 `Start` 节点中 `arr` 变量的类型发生变更时，`Batch` 节点输出的 `item` 类型也会自动同步更新，确保了类型的一致性。</p>\n  </div>\n</div>\n\n:::tip\n\n建议先把变量读写跑通；当你需要深入理解作用域链、AST、声明、表达式等底层模型时，再阅读[变量概念](./concept.mdx)。\n\n:::\n\n## 如何开启变量引擎？\n\n您可以通过简单的配置来启用变量引擎，以体验其强大的功能。\n\n[> 查看 API 详情](https://flowgram.ai/auto-docs/editor/interfaces/VariablePluginOptions.html)\n\n```tsx pure title=\"use-editor-props.ts\" {3}\n// 在 EditorProps 中开启变量引擎\n{\n  variableEngine: {\n    // 设置为 true 即可开启变量引擎\n    enable: true\n  }\n}\n```\n\n## 接下来可以看看（推荐顺序）\n\n- [输出变量](./variable-output.mdx)：先学会在节点、插件、全局作用域中产出变量。\n- [消费变量](./variable-consume.mdx)：再学习如何在节点、UI 中安全读取变量。\n- [变量概念](./concept.mdx)：最后回顾作用域、AST、声明、类型等核心名词。\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/cases/_meta.json",
    "content": "[\n  \"case-batch-variable\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/cases/case-batch-variable.mdx",
    "content": "---\ndescription: 介绍如何实现批处理节点中的变量逻辑\n---\n\n# 批处理变量实现思路 (WIP)\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/concept.mdx",
    "content": "---\ndescription: 介绍变量引擎的核心概念\n---\n\n# 概念\n\n\n:::tip\n\n建议先完成[输出变量](./variable-output.mdx)→[消费变量](./variable-consume.mdx)的动手实践，再回到本文作为参考手册。我们通过 🌟 标记出可**优先掌握**的概念。\n\n:::\n\n## 阅读路径\n\n- 可以先快速浏览下方术语导航，确认自己要查的名词是否在其中。\n- 阅读「核心概念」关系图，建立变量、作用域、AST 的整体框架。\n- 按需跳转到对应小节，结合当前遇到的问题查阅细节，不必顺序阅读。\n\n:::info{title=\"📖 术语快速查询\"}\n\n- **概念本体**\n  - [变量](#变量) 🌟：流程设计阶段定义出来、运行时才求值的数据容器。\n  - [作用域](#作用域-) 🌟：变量的容器，同时维护与其他作用域的依赖关系。\n  - [AST](#ast-) 🌟：作用域内变量信息的结构化存储方式。\n- **AST 相关**\n  - [ASTNode](#astnode)：AST 树中的节点，表示一段变量信息。\n  - [ASTNodeJSON](#astnodejson)：ASTNode 的 JSON 序列化形式。\n  - [声明](#声明) 🌟：标识符 + 定义，变量引擎的最小信息单元。\n  - [类型](#类型) 🌟：用于约束变量值范围的定义。\n  - [表达式](#表达式)：输入若干变量后计算得到新变量。\n- **作用域关系**\n  - [作用域链](#作用域链)：决定一个作用域能引用哪些其他作用域。\n  - [依赖作用域](#依赖作用域)：当前作用域可读取的上游作用域集合。\n  - [覆盖作用域](#覆盖作用域)：可以访问当前作用域输出变量的下游集合。\n  - [节点作用域](#节点作用域) 🌟：节点公开的变量集合。\n  - [节点私有作用域](#节点私有作用域)：仅限节点自身与子节点访问的变量。\n  - [全局作用域](#全局作用域)：所有节点都可读的共享变量。\n\n:::\n\n\n\n## 核心概念\n\n变量引擎核心概念可以通过下图串起来理解：\n\n<img src=\"/variable/concept/concepts-zh.png\" alt=\"变量核心概念关系图\" width=\"600\" />\n\n:::info{title=\"读图重点\"}\n\n- 绿色节点代表「信息是什么」，如变量、类型、表达式。\n- 红色节点代表「信息怎么存」，即 AST 节点。\n- 紫色节点代表「信息放在哪」，即作用域。\n- 虚线节点及线条代表「信息怎么流动」，即作用域链。\n\n:::\n\n为了降低抽象程度，可以先记住一个真实案例：\n\n> 「批处理节点」读取「上游 HTTP 节点」的数组输出 → 遍历得到 `item` → 在子节点里继续使用 `item`。\n\n这个过程涉及的所有名词都在下文出现，阅读时可随时对照。\n\n### 变量\n\n变量是在设计态定义、在运行态求值的数据容器。进一步了解可参考 [变量介绍](./basic.mdx)。\n\n:::warning{title=\"⚠️ 变量在设计和运行中的关注点不同\"}\n\n**在流程设计中，变量只关注定义，不关注值**。变量的值在流程的[运行时](/guide/runtime/introduction)才会被动态计算。\n\n:::\n\n### 作用域 🌟\n\n作用域（Scope）是一种**容器**：容器内聚合了一系列**变量信息**，同时维护了**与其他作用域的依赖关系**。一句话概括：作用域决定「谁可以访问哪些变量」。\n\n作用域的范围可以根据业务场景的不同约定，常见的三类如下：\n\n| 场景 | 示例 |\n| :--- | :--- |\n| 流程里节点可以约定为作用域 | <img src=\"/variable/concept/scope-1.png\" alt=\"节点作用域\" width=\"600\" /> |\n| 全局变量侧边栏也可以约定为作用域 | <img src=\"/variable/concept/scope-2.png\" alt=\"全局作用域\" width=\"600\" /> |\n| 界面编辑里组件（含变量）可以约定为作用域 | <img src=\"/variable/concept/scope-3.png\" alt=\"组件作用域\" width=\"600\" /> |\n\n\n\n:::warning{title=\"为什么 FlowGram 要在节点之外，新抽象一个作用域的概念？\"}\n\n1. 节点 ≠ 作用域：同一个节点可能需要拆分成公开作用域与私有作用域。\n2. 存在与节点无关的作用域，如面向全局的变量抽屉。\n3. 部分节点需要多层作用域（例：循环的私有作用域），节点概念不足以描述。\n\n:::\n\n### AST 🌟\n\n作用域通过 `AST` 存储变量信息。可以把它当作「变量信息」的树形结构：每个节点描述一个声明、类型或表达式。\n\n:::tip\n\n通过 `scope.ast` 可以访问作用域内的 `AST` 树，从而对变量信息进行 CRUD 操作。\n\n:::\n\n\n#### ASTNode\n\n`ASTNode` 是变量引擎中用于**存储变量信息**的**基本信息单元**。它可以为各种**变量信息建模**：\n\n- **声明**：如 `VariableDeclaration` ，用于声明新变量。\n- **类型**：如 `StringType`，用于表示 String 类型。\n- **表达式**：如 `KeyPathExpression`，用于对变量的引用。\n\n:::info{title=\"ASTNode 具有以下特点\"}\n\n- **树状结构**: `ASTNode` 可以嵌套形成树（`AST`），表示复杂的变量结构。\n- **序列化**: `ASTNode` 可以与 JSON 格式（`ASTNodeJSON`）相互转换，以便存储或传输。\n- **可扩展**: 可以通过扩展 `ASTNode` 基类来添加新功能。\n- **响应式**: `ASTNode` 值的变化会触发事件，从而实现响应式编程模式。\n\n:::\n\n#### ASTNodeJSON\n\n`ASTNodeJSON` 是 `ASTNode` 的**纯 JSON 序列化**表示。通常我们会在设计端构造它，再交由变量引擎实例化。\n\n最关键的字段是 `kind`，用于表示 `ASTNode` 的类型：\n\n```tsx\n/**\n * 相当于 JavaScript 代码：\n * `var var_index: string`\n */\n{\n  kind: 'VariableDeclaration',\n  key: 'var_index',\n  type: { kind: 'StringType' },\n}\n```\n\n用户在使用变量引擎时，通过 `ASTNodeJSON` 描述变量信息，然后通过变量引擎**实例化**为 `ASTNode`，并将其添加到作用域中。\n\n```tsx\n/**\n * 通过 scope.setVar 方法，将 ASTNodeJSON 实例化为 ASTNode，并添加到作用域中\n */\nconst variableDeclaration: VariableDeclaration = scope.setVar({\n  kind: 'VariableDeclaration',\n  key: 'var_index',\n  type: { kind: 'StringType' },\n});\n\n/**\n * ASTNodeJSON 实例化为 ASTNode 之后，可以进行响应式监听\n */\nvariableDeclaration.onTypeChange((newType) => {\n  console.log('变量类型变化了', newType);\n})\n\n```\n\n:::info{title=\"概念比对\"}\n\n`ASTNodeJSON` 和 `ASTNode` 的关系，类似于 React 中 `JSX` 和 `VDOM` 的关系\n- `ASTNodeJSON` 通过变量引擎实例化为 `ASTNode`\n- `JSX` 通过 React 引擎实例化为 `VDOM`\n\n:::\n\n:::warning{title=\"❓ 为什么不用 Json Schema\"}\n\n[`Json Schema`](https://json-schema.org/) 是一种用于描述 JSON 数据结构的格式：\n\n- `Json Schema` 只描述了变量的类型信息，而 `ASTNodeJSON` 还可以包含变量的其他信息（如：变量的初始值）。\n- `ASTNodeJSON` 可以通过变量引擎实例化为 `ASTNode`，从而实现响应式监听等能力。\n- `Json Schema` 擅长描述 Json 的类型，而 `ASTNodeJSON` 可以通过自定义扩展定义行为更复杂的信息。\n\n在技术选型上，`变量引擎内核`需要更强大的扩展与表达能力，因此需要用 `ASTNodeJSON` 来描述更丰富更复杂的变量信息，如：通过定义变量的初始值，实现变量类型的动态推导 + 自动联动。\n\n不过 `Json Schema` 作为业界通用的 JSON 类型描述格式，在易用性、跨团队沟通以及生态（如 ajv、zod）上更有优势。因此我们在[**物料库**](/materials/introduction)中大量使用了 Json Schema，来降低大家的上手成本。\n\n:::\n\n:::tip\n\n变量引擎提供了 `ASTFactory`，可以**类型安全**地创建 `ASTNodeJSON`:\n\n```tsx\nimport { ASTFactory } from '@flowgram/editor';\n\n/**\n * 类型安全地创建 VariableDeclaration ASTNodeJSON\n *\n * 等价于：\n * {\n *   kind: 'VariableDeclaration',\n *   key: 'var_index',\n *   type: { kind: 'StringType' },\n * }\n */\nASTFactory.createVariableDeclaration({\n  key: 'var_index',\n  type: { kind: 'StringType' },\n});\n```\n:::\n\n\n\n\n### 声明 🌟\n\n声明 = 标识符（Key） + 定义（Definition）。在设计态中，声明是一种存储标识符与变量信息的 `ASTNode`，是变量系统的最小「可被引用」单元。\n\n- 标识符（Key）：访问声明的索引。\n- 定义（Definition）：声明定义的信息。如：变量的定义 = 类型 + 右值。\n\n\n:::info{title=\"举例：JavaScript 中的声明\"}\n\n**变量声明** = 标识符 + 变量定义（类型 + 初始值）\n\n```javascript\n/**\n * 标识符：some_var\n * 变量定义：类型为 number，初始值为 10\n */\nconst some_var: number = 10;\n```\n\n**函数声明** = 标识符 + 函数定义（函数入参出参 + 函数体实现）\n\n```javascript\n/**\n * 标识符：add\n * 函数定义：入参为两个 number 类型的变量 a, b，出参为 number 类型的变量\n */\nfunction add(a: number, b: number): number {\n  return a + b;\n}\n```\n\n**结构体声明** = 标识符 + 结构体定义（字段 + 类型）\n\n```javascript\n/**\n * 标识符：Point\n * 结构体定义：字段为 x, y，类型均为 number\n */\ninterface Point {\n  x: number;\n  y: number;\n}\n```\n\n:::\n\n\n:::tip{title=\"标识符的作用\"}\n\n- `标识符`是声明的**索引**，用于访问声明中的`定义`。\n- 举例：编程语言在编译时，通过`标识符`找到变量的类型`定义`，从而可以进行类型检查。\n\n:::\n\n\n变量引擎目前只提供了**变量字段声明**（`BaseVariableField`），并基于此扩展了**变量声明**（`VariableDeclaration`）和**属性声明**（`Property`）两种声明。\n\n- 变量字段声明（`BaseVariableField`）= 标识符 + 变量字段定义（类型 + 元信息 + 初始值）\n- 变量声明（`VariableDeclaration`）= **全局唯一**标识符 + 变量定义（类型 + 元信息 + 初始值 + 作用域内排序）\n- 属性声明（`Property`）= **Object 内唯一**标识符 + 属性定义（类型 + 元信息 + 初始值）\n\n\n\n### 类型 🌟\n\n类型用于**约束变量值的范围**。在设计态中，类型也是一种 `ASTNode`。理解类型有助于掌握「变量能装什么」以及「表达式返回什么」。\n\n变量引擎内置了 JSON 的**基础类型**：\n- `StringType`：字符串\n- `IntegerType`：整数\n- `NumberType`：浮点数\n- `BooleanType`：布尔值\n- `ObjectType`：对象，可下钻 `Property` 声明。\n- `ArrayType`：数组，可下钻其他类型。\n\n同时新增了：\n- `MapType`：键值对，键和值都可以进行类型定义。\n- `CustomType`：由用户进行自定义扩展，如日期、时间、文件类型等。\n\n### 表达式\n\n表达式**输入 0 个或者多个变量**，并通过特定方式进行计算，返回一个新的**变量**。设计态只描述「依赖了谁」和「推导出的类型」，运行态负责真正的值计算。\n\n```mermaid\ngraph LR\n\n输入变量_1 --输入--> 表达式\n输入变量_2 --输入--> 表达式\n\n表达式 --返回--> 输出变量\n\nstyle 表达式 stroke:#333,stroke-width:3px;\n```\n\n而在**设计态**中，表达式是一种 `ASTNode`，建模中我们只需关注：\n\n- 表达式**使用了哪些变量声明** ?\n- 表达式的**返回类型**是怎么推导的 ?\n\n```mermaid\ngraph LR\n\n变量声明_1 --输入--> 设计态中的表达式\n变量声明_2 --输入--> 设计态中的表达式\n设计态中的表达式 --推导--> 返回类型\n\nstyle 设计态中的表达式 stroke:#333,stroke-width:3px;\n\n```\n\n\n:::info{title=\"举例：设计态中表达式的推导\"}\n\n假设我们有一个用 JavaScript 代码描述的表达式 `ref_var + 1`\n\n表达式**使用了哪些变量声明** ?\n- `ref_var` 标识符对应的变量声明\n\n表达式的**返回类型**是怎么推导的 ?\n- `ref_var` 的类型为 `IntegerType`，则 `ref_var + 1` 的返回类型为 `IntegerType`\n- `ref_var` 的类型为 `NumberType`，则 `ref_var + 1` 的返回类型为 `NumberType`\n- `ref_var` 的类型为 `StringType`，则 `ref_var + 1` 的返回类型为 `StringType`\n\n:::\n\n:::info{title=\"举例：变量引擎如何实现类型推导 + 联动\"}\n\n<div style={{  }}>\n  <div style={{ width: 500 }}>\n     <img loading=\"lazy\" src=\"/variable/variable-batch-auto-infer.gif\" alt=\"类型自动推导\" style={{ width: \"100%\"}} />\n  </div>\n\n  <div style={{ minWidth: 500 }}>\n\n\n图中展示了一个常见的例子：批处理节点引用前序节点的输出变量，对其进行遍历处理，得到一个 item 变量。其中 item 的变量类型会随着前序节点输出变量的类型而自动变化。\n\n这个例子的 ASTNodeJSON 可表示为：\n\n```tsx\nASTFactory.createVariableDeclaration({\n  key: 'item',\n  initializer: ASTFactory.createEnumerateExpression({\n    enumerateFor: ASTFactory.createKeyPathExpression({\n      keyPath: ['start_0', 'arr']\n    })\n  })\n})\n```\n\n变量的推导链路如下：\n\n```mermaid\ngraph LR\n\n  Array_String[\"Array&lt;String&gt;\"]\n  Ref_Var[\"类型为 \\n Array&lt;String&gt; \\n 的变量\"]\n\n  VariableDeclaration --初始值--> EnumerateExpression\n  KeyPathExpression --引用--> Ref_Var\n  KeyPathExpression --返回类型--> Array_String\n  EnumerateExpression --遍历--> KeyPathExpression\n  EnumerateExpression --返回类型--> String\n  VariableDeclaration -.推导的类型.-> String\n  Array_String -.遍历提取子类型.-> String\n  Ref_Var -.类型.-> Array_String\n\n```\n  </div>\n</div>\n\n\n\n\n:::\n\n\n### 作用域链\n\n作用域链（Scope Chain）定义了**一个作用域可以引用哪些作用域的变量**。可以把它理解成「可读变量的白名单」。变量引擎提供了抽象类，具体业务可以根据实际编排形式实现自定义的作用域链。\n\n变量引擎内置了**自由布局作用域链**和**固定布局作用域链**两种作用域链实现。\n\n\n#### 依赖作用域\n\n`依赖作用域` = 当前作用域可以访问哪些作用域的输出变量。\n\n可以通过 `scope.depScopes` 访问作用域的`依赖作用域`。\n\n\n#### 覆盖作用域\n\n`覆盖作用域` = 哪些作用域可以访问当前作用域的输出变量。\n\n可以通过 `scope.coverScopes` 访问作用域的`覆盖作用域`。\n\n\n## 画布中的变量\n\nFlowGram 在画布中定义了以下几种特殊的作用域：\n\n### 节点作用域 🌟\n\n又称`节点公开作用域`，作用域可以访问**上游节点**的`节点作用域`的变量，同时其输出变量声明也可以被**下游节点**的`节点作用域`访问。\n\n`节点作用域` 可以通过 `node.scope` 来设置和获取，它的作用域链关系如下图所示：\n\n```mermaid\ngraph BT\n\n  subgraph 当前节点\n    子节点_1.scope\n    子节点_2.scope\n    当前节点.scope\n  end\n\n  子节点_1.scope -.依赖.-> 上游节点.scope\n  子节点_2.scope -.依赖.-> 上游节点.scope\n  当前节点.scope -.依赖.-> 上游节点.scope\n  下游节点.scope -.依赖.-> 上游节点.scope\n  下游节点.scope -.依赖.-> 当前节点.scope\n\n  下游节点.scope -.-|\"<font color=red>❌ 不可访问</font>\"| 子节点_1.scope\n  下游节点.scope -.-|\"<font color=red>❌ 不可访问</font>\"| 子节点_2.scope\n\n  linkStyle 5 stroke:red,stroke-width:2px\n  linkStyle 6 stroke:red,stroke-width:2px\n\n\n  style 当前节点.scope fill:#f9f,stroke:#333,stroke-width:3px\n```\n\n:::warning\n\n在默认的作用域逻辑中，子节点的 `节点作用域` 输出变量不可被**父节点的下游节点** 访问。\n\n:::\n\n\n### 节点私有作用域\n\n`节点私有作用域`的输出变量只能在**当前节点**的`节点作用域`及其**子节点**的`节点作用域`中访问。类似编程语言中`私有变量`的概念。\n\n`节点私有作用域` 可以通过 `node.privateScope` 来设置和获取，它的作用域链关系如下图所示：\n\n```mermaid\ngraph BT\n  subgraph 当前节点\n    子节点_1.scope -.依赖.-> 当前节点.privateScope\n    当前节点.scope -.依赖.-> 当前节点.privateScope\n    子节点_2.scope -.依赖.-> 当前节点.privateScope\n  end\n\n  当前节点 -.都依赖.-> 上游节点.scope\n  下游节点.scope -.依赖.-> 当前节点.scope\n  下游节点.scope -.依赖.-> 上游节点.scope\n\n\n  style 当前节点.privateScope fill:#f9f,stroke:#333,stroke-width:3px\n  style 当前节点.scope stroke:#333,stroke-width:3px\n```\n\n\n### 全局作用域\n\n`全局作用域`的变量能被**所有节点作用域和节点私有作用域**访问，但是**自身不依赖其他作用域**。适用于配置、常量、环境变量等公共信息。\n\n全局作用域的设置方式详见[输出全局变量](./variable-output#输出全局变量)，他的作用域链关系如下图所示：\n\n```mermaid\ngraph RL\n\n subgraph 当前节点\n    子节点_1.scope\n    子节点_2.scope\n    当前节点.scope\n    当前节点.privateScope\n  end\n\n  当前节点.scope -.依赖.-> 全局作用域\n  上游节点.scope -.依赖.-> 全局作用域\n  下游节点.scope -.依赖.-> 全局作用域\n  当前节点.privateScope -.依赖.-> 全局作用域\n  子节点_1.scope -.依赖.-> 全局作用域\n  子节点_2.scope -.依赖.-> 全局作用域\n\n  style 当前节点.scope stroke:#333,stroke-width:3px\n  style 全局作用域 fill:#f9f,stroke:#333,stroke-width:3px\n\n```\n\n\n\n## 整体架构\n\n![架构图](/variable/concept/arch-zh.png)\n\n变量引擎设计上遵循 DIP（依赖反转）原则，按照代码稳定性、抽象层次以及与业务的距离分为三层：\n\n### 变量抽象层\n\n抽象层是最稳定的一层，定义了 `ASTNode`、`Scope`、`ScopeChain` 等核心接口，为上层实现提供扩展约束。\n\n### 变量实现层\n\n这一层包含更贴近业务的实现，易随产品演化调整。引擎内置了一批稳定的 `ASTNode` 节点和 `ScopeChain` 实现；当业务需要时，可以通过依赖注入注册新的节点或覆盖已有实现。\n\n### 变量物料层\n\n最外层通过外观模式（Facade）提升易用性，将复杂能力封装成「物料」给使用者直接复用。\n\n- 变量物料的使用详见：[物料](/materials/introduction)\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/core-api.mdx",
    "content": "---\ndescription: 介绍变量引擎的底层 API 的设计与使用\n---\n\n# 底层 API (WIP)\n\n## Scope\n\n\n\n\n## ASTNode\n\n\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/core-ast.mdx",
    "content": "---\ndescription: 介绍变量引擎的内置 AST 节点\n---\n\n# 内置 AST(WIP)\n\n## 声明\n\n### BaseVariableField\n\n`BaseVariableField` 是所有**变量声明**和**属性声明**的 AST 节点的**抽象基类**，它定义了变量字段通用的 `key`、`type`、`initializer` 和 `meta` 属性。\n\n`BaseVariableField` 使用示例如下：\n\n```tsx\n// 通过 getByKeyPath 获取到变量字段\nconst varField = scope.available.getByKeyPath([\"custom_node_0\", \"query\"])\n\n// 获取变量字段上的信息\nconsole.log(varField.key); // query\nconsole.log(varField.keyPath); // [\"custom_node_0\", \"query\"]\nconsole.log(varField.meta); // { title: 'Query }\nconsole.log(varField.type.kind); // String\nconsole.log(varField.initializer.kind); // KeyPathExpression\n\n// 获取 ASTNodeJSON\nconsole.log(varField.toJSON()); // 输出变量声明的 JSON 结构\n\n// 获取下钻字段\nconsole.log(varField.getByKeyPath(['a', 'b', 'c']));\n\n// 更新类型\nvarField.updateType(ASTFactory.createNumber());\n\n// 更新 meta 元信息\nvarField.updateMeta({ title: 'Query 2' });\n\n// 更新 initializer\nvarField.updateInitializer(ASTFactory.createKeyPathExpression({\n  keyPath: ['start_0', 'query'],\n}));\n\n// 监听变量类型变化\nconst disposable = varField.onTypeChange((typeAST) => {\n  console.log(typeAST.kind);\n})\n```\n\n`VariableDeclaration` 和 `Property` 都继承自 `BaseVariableField`，两者的区别为：\n\n- `VariableDeclaration` 用于表示变量的声明和初始化，没有父 Field\n- `Property` 用于表示 Object 对象上的一个属性声明，有父 Field，父 Field 可以是其他 `Property` 或者 `VariableDeclaration`\n\n\n### VariableDeclaration\n\n`VariableDeclaration` 继承自 `BaseVariableField`，是作用域进行变量声明语句的 AST 节点，用于表示变量的声明和初始化。\n\n通过内置的 `ASTFactory` 快速创建 `VariableDeclaration`：\n\n```tsx\nimport { ASTFactory, ASTKind } from '@flowgram/editor';\n\n/**\n * 通过 ASTFactory 创建一个变量声明，类型为 String 类型\n *\n * Equals To Plain JSON:\n * ```json\n * {\n *   \"kind\": \"VariableDeclaration\",\n *   \"key\": \"my_unique_variable\",\n *   \"meta\": { \"title\": \"My Variable\" },\n *   \"order\": 0,\n *   \"type\": { \"kind\": \"String\" }\n * }\n * ```\n *\n * Similar to js code:\n * ```js\n * const my_unique_variable: string;\n * ```\n */\nscope.setVar(\n  ASTFactory.createVariableDeclaration({\n    key: 'my_unique_variable',\n    meta: { title: 'My Variable' },\n    type: ASTFactory.createString(),\n    order: 0,\n  })\n)\n\n/**\n * 通过 ASTFactory 创建一个变量声明，声明的右值为指向 start_0.query 的 keyPath 表达式\n *\n * Equals To Plain JSON:\n * ```json\n * {\n *   \"kind\": \"VariableDeclaration\",\n *   \"key\": \"my_unique_variable_2\",\n *   \"meta\": { \"title\": \"My Variable 2\" },\n *   \"order\": 1,\n *   \"initializer\": { \"kind\": \"KeyPathExpression\", \"keyPath\": [\"start_0\", \"query\"] }\n * }\n * ```\n *\n * Similar to js code:\n * ```js\n * const my_unique_variable_2 = start_0.query;\n * ```\n */\nscope_2.setVar(\n  ASTFactory.createVariableDeclaration({\n    key: 'my_unique_variable_2',\n    meta: { title: 'My Variable 2' },\n    initializer: ASTFactory.createKeyPathExpression({\n      keyPath: ['start_0', 'query'],\n    }),\n    order: 1,\n  })\n)\n```\n\n\n变量引擎将 `VariableDeclaration` 实例化后，可以获取变量声明上的信息，并进行更新、监听等操作：\n\n```tsx\nimport { ASTMatch } from \"@flowgram/editor\";\n\nconst varDecl = scope.getVar<VariableDeclaration>();\n\n// 判断 AST 节点是否为 VariableDeclaration\nif(!ASTMatch.isVariableDeclaration(varDecl)) {\n  throw new Error('AST 节点不是 VariableDeclaration 类型');\n}\n\n// 获取变量声明上的信息\nconsole.log(varDecl.key); // my_unique_variable_2\nconsole.log(varDecl.meta); // { title: 'My Variable 2' }\nconsole.log(varDecl.type.kind); // String\nconsole.log(varDecl.initializer); // KeyPathExpression\n\n// 获取 ASTNodeJSON\nconsole.log(varDecl.toJSON()); // 输出变量声明的 JSON 结构\n\n// 获取下钻字段\nconsole.log(varDecl.getByKeyPath(['a', 'b', 'c']));\n\n// 更新类型\nvarDecl.updateType(ASTFactory.createNumber());\n\n// 更新 initializer\nvarDecl.updateInitializer(ASTFactory.createKeyPathExpression({\n  keyPath: ['start_0', 'query'],\n}));\n\n// 监听变量类型变化\nconst disposable = varDecl.onTypeChange((typeAST) => {\n  console.log(typeAST.kind);\n})\n```\n\n\n### Property\n\n\n\n\n### VariableDeclarationList\n\n\n\n## 类型\n\n### BaseType\n\n\n### StringType\n\n\n### NumberType\n\n\n### IntegerType\n\n\n### ObjectType\n\n\n### ArrayType\n\n\n### MapType\n\n\n\n## 表达式\n\n### BaseExpression\n\n\n### EnumerateExpression\n\n\n### WrapArrayExpression\n\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/custom-scope-chain.mdx",
    "content": "---\ndescription: 介绍如何定制作用域链\n---\n\n# 作用域链\n\n:::info{title=\"阅读前提\"}\n\n- 建议先完成[输出变量](./variable-output.mdx)、[消费变量](./variable-consume.mdx)的实践。\n- 如果对作用域概念还不够熟悉，可先回顾[核心概念 - 作用域链](./concept#作用域链)。\n\n:::\n\n## 默认作用域链逻辑\n\n详见：[画布中的作用域](./concept#画布中的变量)\n\n\n## 在 `editor-props` 中定制\n\n作用域的定制逻辑，通常在 `editor-props` 中，通过 `variableEngine.chainConfig` 定制。\n\n```tsx pure title=\"use-editor-props.tsx\" {6-8}\n// ...\n{\n  // ...\n  variableEngine: {\n    enable: true,\n    chainConfig: {\n      // 作用域链逻辑定制\n    }\n  }\n  // ...\n}\n// ...\n```\n\n\n### 子节点能否被后续节点依赖\n\n**默认情况下，子节点是不可以被父节点的后续节点依赖的**。\n\n如果需要定制这个逻辑，需要在 `variableEngine.chainConfig.isNodeChildrenPrivate` 中配置。\n\n\n```tsx pure title=\"use-editor-props.tsx\" {8-15}\n{\n  variableEngine: {\n    enable: true,\n    chainConfig: {\n      /**\n       * 定制：子节点是否可以被父节点的后续节点依赖\n       */\n      isNodeChildrenPrivate(node) {\n        // 当命中用户某类自定义节点时，允许其子节点被后续节点依赖\n        if (node.flowNodeType === 'Your_Custom_Type') {\n          return false;\n        }\n        // 否则：默认不允许子节点被后续节点依赖\n        return true;\n      },\n    }\n  }\n}\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/variable-consume.mdx",
    "content": "---\ndescription: 介绍如何在 FlowGram 中消费变量引擎输出的变量\n---\n\n# 消费变量\n\n在 FlowGram 中，当一个节点想要使用到前序节点的变量，就需要消费变量。\n\n:::info{title=\"阅读提示\"}\n\n- 建议先完成[输出变量](./variable-output.mdx)，确保知道变量是如何被产出的；本篇作为第二站，专注于「如何拿到变量」。\n- 如果想快速看看变量选择器的效果，可先用用 [VariableSelector](#variableselector)；若需要在代码里拿到变量列表，再继续阅读后续 API 章节。\n- 本文示例默认使用「节点作用域」。涉及节点私有或全局作用域时，可随时参考[核心概念 - 画布中的变量](./concept#画布中的变量)补充理解。\n\n:::\n\n## `VariableSelector`\n\n为了让你能更轻松地在应用中集成变量选择的功能，官方物料提供了 `VariableSelector` 组件。\n\n详见文档： [VariableSelector](/materials/components/variable-selector)\n\n\n## 获取可访问的变量树\n\n在画布的节点中，我们常常需要获取**当前作用域下可用的变量**，并将它们以树形结构展示出来，方便用户进行选择和操作。\n\n:::info{title=\"常见需求拆解\"}\n\n- **仅需要展示变量列表** → `useAvailableVariables`\n- **需要展示并响应对象/数组的下钻字段** → `ASTMatch` + 递归渲染\n- **需要精准订阅指定变量/监听变化** → 直接操作 `scope.available`\n\n:::\n\n### `useAvailableVariables`\n\n`useAvailableVariables` 是一个轻量级的 Hook，它直接返回当前作用域可用的变量数组 (`VariableDeclaration[]`)。\n\n```tsx pure title=\"use-variable-tree.tsx\" {7}\nimport {\n  type BaseVariableField,\n  useAvailableVariables,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// .... 在 React 组件或 Hook 中\nconst availableVariables = useAvailableVariables();\n\nconst renderVariable = (variable: BaseVariableField) => {\n  // 这里可以根据你的需求渲染每个变量\n  // ....\n}\n\nreturn availableVariables.map(renderVariable);\n\n// ....\n```\n\n### 获取 Object 类型变量的下钻\n\n当变量的类型是 `Object` 时，我们往往需要能够“下钻”到它的内部，获取其属性。`ASTMatch.isObject` 方法可以帮助我们判断一个变量类型是否为对象。如果是，我们就可以递归地渲染它的 `properties`。\n\n:::tip\n\n变量树的每一层其实都是「声明」(`BaseVariableField`)。在对象场景下，`properties` 就是下一级声明数组。\n\n:::\n\n```tsx pure title=\"use-variable-tree.tsx\" {12}\nimport {\n  type BaseVariableField,\n  ASTMatch,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ....\n\nconst renderVariable = (variable: BaseVariableField) => ({\n  title: variable.meta?.title,\n  key: variable.key,\n  // 只有 Object 类型的变量才可以下钻\n  children: ASTMatch.isObject(variable.type) ? variable.type.properties.map(renderVariable) : [],\n});\n\n// ....\n\n```\n\n### 获取 Array 类型变量的下钻\n\n与 `Object` 类型类似，当遇到 `Array` 类型的变量时，我们也希望能展示它的内部结构。对于数组，我们通常关心的是其元素的类型。`ASTMatch.isArray` 可以判断变量类型是否为数组。值得注意的是，数组的元素类型可能是任意的，甚至可能是另一个数组。因此，我们需要一个递归的辅助函数 `getTypeChildren` 来处理这种情况。\n\n\n\n```tsx pure title=\"use-variable-tree.tsx\" {13,16}\nimport {\n  type BaseVariableField,\n  type BaseType,\n  ASTMatch,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ....\n\nconst getTypeChildren = (type?: BaseType): BaseVariableField[] => {\n  if (!type) return [];\n\n  // 获取 Object 的属性\n  if (ASTMatch.isObject(type)) return type.properties;\n\n  // 递归获取 Array 的元素类型\n  if (ASTMatch.isArray(type)) return getTypeChildren(type.items);\n\n  return [];\n};\n\nconst renderVariable = (variable: BaseVariableField) => ({\n  title: variable.meta?.title,\n  key: variable.key,\n  children: getTypeChildren(variable.type).map(renderVariable),\n});\n\n// ....\n\n```\n\n## `scope.available`\n\n`scope.available` 是变量系统的核心之一，可以对 **作用域内可用变量** 进行更加高级的变量获取和监听动作。\n\n:::info{title=\"何时直接用 scope.available？\"}\n\n- 需要通过 keyPath 精确读取或校验变量。\n- 需要在 Hook 之外（如插件、服务）操作变量可见性。\n- 需要订阅变量变化但不希望整个列表刷新。\n\n:::\n\n### `useScopeAvailable`\n\n`useScopeAvailable` 能够在 React 中直接返回 `scope.available`\n\n```tsx\nimport { useScopeAvailable } from '@flowgram.ai/free-layout-editor';\n\nconst available = useScopeAvailable();\n\n// available 对象上包含了变量列表和其他 API\nconsole.log(available.variables);\n\n// 获取单个变量\nconsole.log(available.getByKeyPath(['start_0', 'xxx']));\n\n// 监听单个变量的变化\navailable.trackByKeyPath(['start_0', 'xxx'], () => {\n  // ...\n})\n```\n\n:::info{title=\"与 useAvailableVariables 的主要区别\"}\n\n*   **返回值不同**：`useAvailableVariables` 直接返回变量数组，而 `useScopeAvailable` 返回的是一个包含了 `variables` 属性以及其他方法的 `ScopeAvailableData` 对象。\n*   **适用场景**：当你需要对变量进行更复杂的操作，比如通过 `trackByKeyPath` 追踪单个变量的变化时，`useScopeAvailable` 是你的不二之选。\n\n:::\n\n\n:::warning{title=\"useScopeAvailable 会在可用变量变化时自动刷新\"}\n\n如果不想自动刷新，可以通过 autoRefresh 参数关闭：\n\n```tsx\nuseScopeAvailable({ autoRefresh: false })\n```\n:::\n\n### `getByKeyPath`\n\n通过`getByKeyPath` 可以在当前作用域的可访问变量中获取特定变量字段（包括嵌套在 Object 或 Array 中的变量）\n\n```tsx {6,13-17}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect, useState } from 'react';\n\nfunction VariableDisplay({ keyPath }: { keyPath:string[] }) {\n  const available = useScopeAvailable();\n  const variableField = available.getByKeyPath(keyPath)\n\n  return <div>{variableField.meta?.title}</div>;\n}\n```\n\n`getByKeyPath` 也尝尝用于变量校验中，如：\n\n```tsx\nconst validateVariableInNode = (keyPath: string, node: FlowNodeEntity) => {\n  // 校验变量能否被当前节点所访问\n  return Boolean(node.scope.available.getByKeyPath(keyPath))\n}\n```\n\n\n### `trackByKeyPath`\n\n当你只关心某个特定变量字段（包括嵌套在 Object 或 Array 中的变量）的变化时，`trackByKeyPath` 能让你精准地“订阅”这个变量的更新，而不会因为其他不相关变量的变化导致组件重新渲染，从而实现更精细的性能优化。\n\n:::tip\n\n配合 `autoRefresh: false` 使用时，可以避免大范围刷新，只在订阅的变量变化时手动更新组件状态。\n\n:::\n\n```tsx {6,13-18}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect, useState } from 'react';\n\nfunction UserNameDisplay() {\n  // 关闭 autoRefresh 能力，防止任意变量变化触发重渲染\n  const available = useScopeAvailable({ autoRefresh: false });\n  const [userName, setUserName] = useState('');\n\n  useEffect(() => {\n    // 定义我们要追踪的变量路径\n    const keyPath = ['user', 'name'];\n\n    // 开始追踪！\n    const disposable = available.trackByKeyPath(keyPath, (nameField) => {\n      // 当 user.name 变量字段变化时，这个回调函数会被触发\n      // nameField 就是那个变化的变量字段，我们可以从中获取最新的默认值\n      setUserName(nameField?.meta.default || '');\n    });\n\n    // 组件卸载时取消追踪，避免内存泄漏\n    return () => disposable.dispose();\n  }, [available]); // 依赖项是 available 对象\n\n  return <div>User Name: {userName}</div>;\n}\n```\n\n### 整体监听 API\n\n除了 `trackByKeyPath`，`ScopeAvailableData` 还提供了一套整体变量变化的事件监听 API，让你能够更精细地控制变量变化的响应逻辑。\n\n这在处理一些复杂的、需要手动管理订阅的场景时非常有用。\n\n下面我们通过一个表格来详细对比这三个核心的监听 API：\n\n| API & 回调参数 | 触发时机 | 核心区别与适用场景 |\n| :--- | :--- | :--- |\n| `onVariableListChange: (variables: VariableDeclaration[]) => void` | 当可用变量的**列表结构**发生变化时。 | **只关心列表本身**。比如，上游节点新增/删除了一个输出变量，导致可用变量的总数或成员发生了变化。它不关心变量内部和下钻的改变。适用于需要根据变量列表的有无或数量来更新 UI 的场景。 |\n| `onAnyVariableChange: (changedVariable: VariableDeclaration) => void` | 当列表中**任意一个**变量的**类型，元数据和下钻字段**发生变化时。 | **只关心变量定义的更新**。比如，用户修改了一个输出变量的类型。它不关心列表结构的变化。适用于需要对任何一个变量的内容变化做出反应的场景。 |\n| `onListOrAnyVarChange: (variables: VariableDeclaration[]) => void` | 以上两种情况**任意一种**发生时。 | **最全面的监听**，是前两者的结合。无论是列表结构变化，还是任何一个变量的变化，都会触发。适用于需要对任何可能的变化都进行响应的“兜底”场景。 |\n\n让我们通过一个具体的例子来看看如何在组件中使用这些 API。\n\n```tsx {5,9-11,14-16,19-22}\nimport { useScopeAvailable } from '@flowgram.ai/fixed-layout-editor';\nimport { useEffect } from 'react';\n\nfunction AdvancedListenerComponent() {\n  const available = useScopeAvailable({ autoRefresh: false });\n\n  useEffect(() => {\n    // 1. 监听列表结构变化\n    const listChangeDisposable = available.onVariableListChange((variables) => {\n      console.log('可用变量列表的结构变了！新的列表长度是：', variables.length);\n    });\n\n    // 2. 监听任意变量的变化\n    const valueChangeDisposable = available.onAnyVariableChange((changedVariable) => {\n      console.log(`变量 '${changedVariable.keyPath.join('.')}' 的定义变了`);\n    });\n\n    // 3. 监听所有变化（结构或单个变量内部）\n    const allChangesDisposable = available.onListOrAnyVarChange((variables) => {\n      console.log('变量列表或其中某个变量发生了变化！');\n      // 注意：这里的回调参数是完整的变量列表，而不是单个变化的变量\n    });\n\n    // 在组件卸载时，务必清理所有的监听器，防止内存泄漏\n    return () => {\n      listChangeDisposable.dispose();\n      valueChangeDisposable.dispose();\n      allChangesDisposable.dispose();\n    };\n  }, [available]);\n\n  return <div>请在控制台查看变量变化的日志...</div>;\n}\n```\n\n:::warning\n\n这些 API 返回的都是一个 `Disposable` 对象。为了避免内存泄漏和不必要的计算，你必须在 `useEffect` 的清理函数中调用其 `dispose()` 方法来取消监听。\n\n:::\n\n\n## 获取当前作用域的输出变量\n\n### `useOutputVariables`\n\n\n`useOutputVariables` 可以获取**当前作用域的输出变量**，并在输出变量列表或者下钻变化时**自动触发刷新**。\n\n```tsx\nconst variables = useOutputVariables();\n```\n\n:::tip\n\nuseOutputVariables 在 flowgram@0.5.6 之后的版本提供，如果版本较早，可以通过以下代码实现获取：\n\n```tsx\nconst scope = useCurrentScope();\nconst refresh = useRefresh();\n\nuseEffect(() => {\n  const disposable = scope.output.onListOrAnyVarChange(() => {\n    refresh();\n  });\n\n  return () => disposable.dispose();\n}, [])\n\nconst variables = scope.variables;\n```\n:::\n\n\n## 其余 API\n\n### 获取当前作用域\n\n可以通过 [`useCurrentScope`](https://flowgram.ai/auto-docs/editor/functions/useCurrentScope) 获取当前的作用域。\n\n```tsx\nconst scope = useCurrentScope()\n\nscope.output.variables\n\nscope.available\n\n```\n\n### 设定当前作用域\n\n可以通过 [`ScopeProvider`](https://flowgram.ai/auto-docs/editor/functions/ScopeProvider) 设定当前作用域。\n\n```tsx\n// set the scope of current node\n<ScopeProvider scope={node.scope}>\n  <YourUI />\n</ScopeProvider>\n\n// set to private scope of current node\n<ScopeProvider scope={node.privateScope}>\n  <YourUI />\n</ScopeProvider>\n```\n"
  },
  {
    "path": "apps/docs/src/zh/guide/variable/variable-output.mdx",
    "content": "---\ndescription: 介绍如何在 FlowGram 中使用变量引擎输出变量\n---\n\n# 输出变量\n\n我们主要将输出变量分为三类：\n\n1. **输出节点变量**：通常作为该节点的产出，供后续节点使用。\n2. **输出节点私有变量**：输出变量仅限于节点内部（包括子节点），不能被外部节点访问。\n3. **输出全局变量**：贯穿整个流程，任何节点都可以读取，适合存放一些公共状态或配置。\n\n:::info{title=\"阅读指引\"}\n\n- 读完[变量介绍](./basic.mdx)后，可先从这里开始，通过实践搞清楚「变量是如何被产出的」。\n- 若你从表单配置出发，优先阅读「方式一：通过表单副作用同步」；若需要运行时动态创建变量，请看插件或 UI 相关章节。\n- 文中所有示例均使用了 `ASTFactory`，可先对照[变量概念 - AST 小节](./concept#ast-) 理解 AST 的概念。\n\n:::\n\n## 输出节点变量\n\n输出节点变量与当前节点的生命周期绑定：节点创建时变量诞生，节点删除时变量消失。（详见：[节点作用域](./concept#节点作用域)）\n\n我们通常有三种方式来输出节点变量：\n\n:::info{title=\"如何选择方式\"}\n\n- 变量定义源于表单配置 → 方式一。\n- 变量在运行时根据逻辑生成或批量同步 → 方式二。\n- UI 里直接写变量只作临时调试，正式环境请避免使用方式三。\n\n:::\n\n### 方式一：通过表单副作用同步\n\n[表单副作用](/guide/form/form#副作用-effect) 通常在节点的 `form-meta.ts` 文件中进行配置，是定义节点输出变量最常见的方式。\n\n:::info{title=\"适用场景\"}\n\n- 节点的变量模型可以从表单项推导出来。\n\n:::\n\n#### `provideJsonSchemaOutputs`\n\n若节点所需输出变量的结构与 [JSON Schema](https://json-schema.org/) 结构匹配，即可使用官方提供的 `provideJsonSchemaOutputs` 副作用物料。\n\n详见文档： [provideJsonSchemaOutputs](/materials/effects/provide-json-schema-outputs)\n\n\n#### `createEffectFromVariableProvider`\n\n\n`provideJsonSchemaOutputs` 只适配 `JsonSchema`。如果你想要定义自己的一套 Schema，那么就需要自定义表单的副作用。\n\n:::note\n\nFlowGram 提供了 `createEffectFromVariableProvider`，只需要定义一个 `parse`函数，就可以自定义自己的变量同步副作用：\n- `parse` 会在表单值初始化和更新时被调用\n- `parse` 的输入为当前字段的表单的值\n- `parse` 的输出为变量 AST\n\n:::\n\n下面这个例子中，我们为表单的两个字段 `path.to.value` 和 `path.to.value2` 分别创建了输出变量：\n\n```tsx pure title=\"form-meta.ts\" {26-37,40-56}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n  type ASTNodeJSON\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport function createTypeFromValue(typeValue: string): ASTNodeJSON | undefined {\n  switch (typeValue) {\n    case 'string':\n      return ASTFactory.createString();\n    case 'number':\n      return ASTFactory.createNumber();\n    case 'boolean':\n      return ASTFactory.createBoolean();\n    case 'integer':\n      return ASTFactory.createInteger();\n    default:\n      return;\n  }\n}\n\nexport const formMeta =  {\n  effect: {\n    // Create first variable\n    // = node.scope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value': createEffectFromVariableProvider({\n      // parse form value to variable\n      parse(v: string, { node }) {\n        return [{\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `uid_${node.id}`,\n          type: createTypeFromValue(v)\n        }]\n      }\n    }),\n    // Create second variable\n    // = node.scope.setVar('path.to.value2', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value2': createEffectFromVariableProvider({\n      // parse form value to variable\n      parse(v: { name: string; typeValue: string }[], { node }) {\n        return {\n          meta: {\n            title: `Second Output Variable For ${node.form.getValueIn(\"title\")}`,\n          },\n          key: `uid_${node.id}_2`,\n          type: ASTFactory.createObject({\n            properties: v.map(_item => ASTFactory.createProperty({\n              key: _item.name,\n              type: createTypeFromValue(_item.typeValue)\n            }))\n          })\n        }\n      }\n    }),\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n:::tip\n\n如果你的协议比较复杂，不知道如何解析为 AST，不妨参考下官方物料中 Json Schema 转换为 AST 的实现：[JsonSchemaUtils.schemaToAST](https://github.com/bytedance/flowgram.ai/blob/main/packages/variable-engine/json-schema/src/json-schema/utils.ts)\n\n:::\n\n:::warning\n\n使用 VariableSelector 官方物料进行变量选择时，**当前节点定义的每个输出变量都会作为独立的树节点显示**，而非默认按节点进行分组。参考 [VariableSelector 物料文档](/materials/components/variable-selector)\n\n:::\n\n#### 多个表单字段同步到一个变量上\n\n如果多个字段同步到一个变量，就需要用到 `createEffectFromVariableProvider` 的 `namespace` 字段，将多个字段的变量数据同步到同一个命名空间上。\n\n\n```tsx pure title=\"form-meta.ts\" {11}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 从 form 拿到多个字段的信息\n */\nconst variableSyncEffect = createEffectFromVariableProvider({\n  // 必须添加，确保不同字段的副作用，同步到同一个命名空间上\n  namespace: 'your_namespace',\n\n  // 将表单值解析为变量\n  parse(_, { form, node }) {\n    // 注意：form 字段要求 flowgram 版本 > 0.5.5, 之前的版本可以通过 node.form 获取\n    return [{\n      meta: {\n        title: `Title_${form.getValueIn('path.to.value')}_${form.getValueIn('path.to.value2')}`,\n      },\n      key: `uid_${node.id}`,\n      type: ASTFactory.createCustomType({ typeName: \"CustomVariableType\" })\n    }]\n  }\n})\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': variableSyncEffect,\n    'path.to.value2': variableSyncEffect,\n  },\n  render: () => (\n   // ...\n  )\n}\n```\n\n#### 在副作用中使用 `node.scope` API\n\n如果 `createEffectFromVariableProvider` 不能满足你的需求，你也可以直接在表单副作用中使用 `node.scope` API，进行更加灵活多变的变量操作。\n\n:::note\n\n`node.scope` 会返回一个节点的变量作用域（Scope）对象，这个对象上挂载了几个核心方法：\n\n- `setVar(variable)`: 设置一个变量。\n- `setVar(namespace, variable)`: 在指定的命名空间下设置一个变量。\n- `getVar()`: 获取所有变量。\n- `getVar(namespace)`: 获取指定命名空间下的变量。\n- `clearVar()`: 清空所有变量。\n- `clearVar(namespace)`: 清空指定命名空间下的变量。\n\n:::\n\n\n```tsx pure title=\"form-meta.tsx\" {10-18,29-38}\nimport { Effect } from '@flowgram.ai/editor';\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.scope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Title_${value}`,\n            },\n            key: `uid_${context.node.id}`,\n            type: ASTFactory.createString(),\n          })\n        )\n\n        console.log(\"查看生成的变量\", context.node.scope.getVar())\n\n      }) as Effect,\n    }],\n    'path.to.value2': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.scope.setVar(\n          'namespace_2',\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Title_${value}`,\n            },\n            key: `uid_${context.node.id}_2`,\n            type: ASTFactory.createNumber(),\n          })\n        )\n\n        console.log(\"查看生成的变量\", context.node.scope.getVar('namespace_2'))\n\n      }) as Effect,\n    }],\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n\n### 方式二：通过插件同步变量\n\n除了在表单中静态配置，我们还可以在插件（Plugin）中，通过 `node.scope` 更新自由动态地操作节点的变量。\n\n:::info{title=\"适用场景\"}\n\n- 需要跨多个节点批量创建或调整变量。\n- 需要在画布初始化阶段自动补齐默认变量。\n\n:::\n\n\n#### 指定节点的 Scope 进行更新\n\n下面的例子演示了如何在插件的 `onInit`生命周期中，获取开始节点的 `Scope`，并对它的变量进行一系列操作。\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {10-22}\nimport {\n  FlowDocument,\n  definePluginCreator,\n  PluginCreator,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      const startNode = ctx.get(FlowDocument).getNode('start_0');\n      const startScope =  startNode.scope!\n\n      // Set Variable For Start Scope\n      startScope.setVar(\n        ASTFactory.createVariableDeclaration({\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `uid`,\n          type: ASTFactory.createString(),\n        })\n      )\n    }\n  })\n```\n\n#### 在 onNodeCreate 同步变量\n\n下面的例子演示了如何通过 `onNodeCreate` 获取到新创建节点的 Scope，并通过监听 `node.form.onFormValuesChange` 实现变量的同步操作。\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {10,29}\nimport {\n  FlowDocument,\n  definePluginCreator,\n  PluginCreator,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createSyncVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      ctx.get(FlowDocument).onNodeCreate(({ node }) => {\n        const syncVariable = (title: string) => {\n          node.scope?.setVar(\n            ASTFactory.createVariableDeclaration({\n              key: `uid_${node.id}`,\n              meta: {\n                title,\n                icon: iconVariable,\n              },\n              type: ASTFactory.createString(),\n            })\n          );\n        };\n\n        if (node.form) {\n          // sync variable on init\n          syncVariable(node.form.getValueIn('title'));\n\n          // listen to form values change\n          node.form?.onFormValuesChange(({ values, name }) => {\n            // title field changed\n            if (name.match(/^title/)) {\n              syncVariable(values[name]);\n            }\n          });\n        }\n      });\n    }\n  })\n```\n\n### 方式三：在 UI 中同步变量（不推荐）\n\n:::warning\n直接在 UI 中同步变量（方式三）是一种 **非常不推荐** 的做法。它会打破**数据和渲染分离**的原则，会导致数据和渲染之间的紧密耦合，可能会导致：\n\n- 关闭节点侧边栏，就无法触发变量同步，导致数据和渲染之间的不一致。\n- 画布如果开启了只渲染视图内可见节点的性能优化，如果节点不在视图内，则联动逻辑会失效\n\n:::\n\n下面的例子演示了如何在 `formMeta.render` 中，通过 `useCurrentScope` 事件，同步更新变量。\n\n```tsx pure title=\"form-meta.ts\" {13}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\n/**\n * 从 form 拿到多个字段的信息\n */\nconst FormRender = () => {\n  /**\n   * 获取到当前作用域，用于后续设置变量\n   */\n  const scope = useCurrentScope()\n\n  return <>\n    <UserCustomForm\n      onValuesChange={(values) => {\n        scope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: values.title,\n            },\n            key: `uid`,\n            type: ASTFactory.createString(),\n          })\n        )\n      }}\n    />\n  </>\n}\n\nexport const formMeta = {\n  render: () => <FormRender />\n}\n```\n\n\n\n\n\n\n## 输出节点私有变量\n\n私有变量是指只能在当前节点及其子节点中访问的变量。（详见：[节点私有作用域](./concept#节点私有作用域)）\n\n:::tip\n\n判断是否要使用私有作用域的小诀窍：变量内容只为节点内部实现服务，并不希望暴露给下游节点时，就放到 `node.privateScope`。\n\n:::\n\n下面只列举其中两种方式，其他方式可以根据[输出节点变量](#输出节点变量)的方式类推。\n\n### 方式一：`createEffectFromVariableProvider`\n\n`createEffectFromVariableProvider` 提供了参数 `scope`，用于指定变量的作用域。\n- `scope` 设置为 `private` 时，变量的作用域为当前节点的私有作用域 `node.privateScope`\n- `scope` 设置为 `public` 时，变量的作用域为当前节点的作用域 `node.scope`\n\n```tsx pure title=\"form-meta.ts\" {11}\nimport {\n  createEffectFromVariableProvider,\n  ASTFactory,\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const formMeta =  {\n  effect: {\n    // Create variable in privateScope\n    // = node.privateScope.setVar('path.to.value', ASTFactory.createVariableDeclaration(parse(v)))\n    'path.to.value': createEffectFromVariableProvider({\n      scope: 'private',\n      // parse form value to variable\n      parse(v: string, { node }) {\n        return [{\n          meta: {\n            title: `Private_${v}`,\n          },\n          key: `uid_${node.id}_locals`,\n          type: ASTFactory.createBoolean(),\n        }]\n      }\n    }),\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n\n### 方式二：`node.privateScope`\n\n\n`node.privateScope` 的 API 设计得和节点作用域（`node.scope`）几乎一模一样，都提供了 `setVar`、`getVar`、`clearVar`等方法，并且同样支持命名空间（namespace）。详情可以参考 [`node.scope`](#在副作用中使用-nodescope-api)。\n\n\n```tsx pure title=\"form-meta.tsx\" {10-18}\nimport { Effect } from '@flowgram.ai/editor';\n\nexport const formMeta = {\n  effect: {\n    'path.to.value': [{\n      event: DataEvent.onValueInitOrChange,\n      effect: ((params) => {\n        const { context, value } = params;\n\n        context.node.privateScope.setVar(\n          ASTFactory.createVariableDeclaration({\n            meta: {\n              title: `Your Private Variable Title`,\n            },\n            key: `uid_${context.node.id}`,\n            type: ASTFactory.createInteger(),\n          })\n        )\n\n        console.log(\"查看生成的变量\", context.node.privateScope.getVar())\n\n      }) as Effect,\n    }],\n  },\n  render: () => (\n    // ...\n  )\n}\n```\n\n\n\n\n## 输出全局变量\n\n全局变量就像是整个流程的“共享内存”，任何节点、任何插件都可以访问和修改它。它非常适合用来存储一些贯穿始终的状态，比如用户信息、环境配置等等。（详见：[全局作用域](./concept#全局作用域)）\n\n:::info{title=\"何时选用全局作用域\"}\n\n- 变量在多个流程节点甚至插件中被复用。\n- 变量与具体节点解耦，例如环境配置、用户上下文。\n- 需要在流程初始化阶段就写入，后续节点只需读取。\n\n:::\n\n和节点变量类似，我们也有两种主要的方式来获取全局变量的作用域（`GlobalScope`）。\n\n### 方式一：在插件中获取\n\n在插件的上下文中（`ctx`），我们可以直接“注入”`GlobalScope` 的实例：\n\n\n```tsx pure title=\"global-variable-plugin.tsx\" {10-20}\nimport {\n  GlobalScope,\n  definePluginCreator,\n  PluginCreator\n} from '@flowgram.ai/fixed-layout-editor';\n\nexport const createGlobalVariablePlugin: PluginCreator<SyncVariablePluginOptions> =\n  definePluginCreator<SyncVariablePluginOptions, FixedLayoutPluginContext>({\n    onInit(ctx, options) {\n      const globalScope = ctx.get(GlobalScope)\n\n      globalScope.setVar(\n         ASTFactory.createVariableDeclaration({\n          meta: {\n            title: `Your Output Variable Title`,\n          },\n          key: `your_variable_global_unique_key`,\n          type: ASTFactory.createString(),\n        })\n      )\n    }\n  })\n\n```\n\n\n### 方式二：在 UI 中获取\n\n如果你想在画布的 React 组件中与全局变量交互，可以使用 `useService` 这个 Hook 来获取 `GlobalScope` 的实例：\n\n```tsx pure title=\"global-variable-component.tsx\" {7}\nimport {\n  GlobalScope,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\n\nfunction GlobalVariableComponent() {\n  const globalScope = useService(GlobalScope)\n\n  // ...\n\n  const handleChange = (v: string) => {\n    globalScope.setVar(\n      ASTFactory.createVariableDeclaration({\n        meta: {\n          title: `Your Output Variable Title`,\n        },\n        key: `uid_${v}`,\n        type: ASTFactory.createString(),\n      })\n    )\n  }\n\n  return <Input onChange={handleChange}/>\n}\n\n```\n\n\n\n### 全局作用域的 API\n\n`GlobalScope` 的 API 设计得和节点作用域（`node.scope`）几乎一模一样，都提供了 `setVar`、`getVar`、`clearVar` 等方法，并且同样支持命名空间（namespace）。详情可以参考 [`node.scope`](#在副作用中使用-nodescope-api)。\n\n下面是一个在插件中操作全局变量的综合示例：\n\n```tsx pure title=\"sync-variable-plugin.tsx\" {11-39}\nimport {\n  GlobalScope,\n} from '@flowgram.ai/fixed-layout-editor';\n\n// ...\n\nonInit(ctx, options) {\n  const globalScope = ctx.get(GlobalScope);\n\n  // 1. Create, Update, Read, Delete Variable in GlobalScope\n  globalScope.setVar(\n    ASTFactory.createVariableDeclaration({\n      meta: {\n        title: `Your Output Variable Title`,\n      },\n      key: `your_variable_global_unique_key`,\n      type: ASTFactory.createString(),\n    })\n  )\n\n  console.log(globalScope.getVar())\n\n  globalScope.clearVar()\n\n  // 2.  Create, Update, Read, Delete Variable in GlobalScope's namespace: 'namespace_1'\n    globalScope.setVar(\n      'namespace_1',\n      ASTFactory.createVariableDeclaration({\n        meta: {\n          title: `Your Output Variable Title 2`,\n        },\n        key: `uid_2`,\n        type: ASTFactory.createString(),\n      })\n  )\n\n  console.log(globalScope.getVar('namespace_1'))\n\n  globalScope.clearVar('namespace_1')\n\n  // ...\n}\n```\n\n详见：[Class: GlobalScope](https://flowgram.ai/auto-docs/editor/classes/GlobalScope.html)\n"
  },
  {
    "path": "apps/docs/src/zh/index.md",
    "content": "---\npageType: home\n\nhero:\n  name: FlowGram.AI\n  text: 工作流开发框架\n  tagline: 让搭建工作流平台更简单：画布、表单、变量、物料\n  actions:\n    - theme: brand\n      text: 快速开始\n      link: /guide/getting-started/introduction\n    - theme: alt\n      text: GitHub\n      link: https://github.com/bytedance/flowgram.ai\n  image:\n    src: /transparent-logo.svg\n    alt: Logo\nfeatures:\n  - title: 扣子\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-coze.png\" alt=\"扣子\"/></div>\n  - title: 飞书低代码平台工作流\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-apaas.png\" alt=\"飞书低代码平台工作流\"/></div>\n  - title: 飞书多维表格\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-bitable.png\" alt=\"飞书多维表格\"/></div>\n  - title: nndeploy\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-nndeploy.png\" alt=\"nndeploy\"/></div>\n  - title: Certimate\n    details: <div class=\"rspress-doc\" style=\"height&#58 180px; min-height&#58 0px\"><img class=\"medium-zoom-image\" style=\"border-radius&#58 8px;\" src=\"https://flowgram.ai/ref-certimate.png\" alt=\"Certimate\"/></div>\n---\n"
  },
  {
    "path": "apps/docs/src/zh/materials/_meta.json",
    "content": "[\n  \"introduction\",\n  \"cli\",\n  {\n    \"type\": \"dir\",\n    \"name\": \"components\",\n    \"label\": \"表单组件\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"effects\",\n    \"label\": \"表单副作用\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"form-plugins\",\n    \"label\": \"表单插件\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"validate\",\n    \"label\": \"表单校验器\"\n  },\n  {\n    \"type\": \"dir\",\n    \"name\": \"common\",\n    \"label\": \"通用逻辑\"\n  }\n]\n"
  },
  {
    "path": "apps/docs/src/zh/materials/cli.mdx",
    "content": "# 物料管理 CLI\n\nFlowgram 提供了专门的 CLI 命令行工具，帮助你管理项目中的官方物料。\n\n## 安装\n\n可使用 npx 直接运行：\n\n```bash\nnpx @flowgram.ai/cli@latest [command]\n```\n\n## 同步物料到项目\n\n使用 `materials` 命令将官方物料的源代码添加到你的项目中进行定制：\n\n```bash\n# 交互式选择物料\nnpx @flowgram.ai/cli@latest materials\n\n# 直接指定物料\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n\n# 指定多个物料（逗号分隔）\nnpx @flowgram.ai/cli@latest materials components/variable-selector,effect/provideJsonSchemaOutputs\n\n# 选择多个物料（交互式多选）\nnpx @flowgram.ai/cli@latest materials --select-multiple\n```\n\n运行后，CLI 会提示你选择要添加到项目中的物料：\n\n```console\n🚀 Welcome to @flowgram.ai form-materials CLI!\n📁 Project: /path/to/your/project\n🎯 Flowgram Version: 1.0.0\n  - Target material root: /path/to/your/project/src/form-materials\n\n🚀 The following materials will be added to your project\n📦 components/json-schema-editor\n📦 components/variable-selector\n📦 effect/provideJsonSchemaOutputs\n\n✅ These npm dependencies is added to your package.json\n- @semi-design/icons\n- lodash-es\n- classnames\n\n➡️ Please run npm install to install dependencies\n```\n\n### 高级选项\n\n`materials` 命令支持以下选项：\n\n| 选项 | 说明 | 示例 |\n|------|------|------|\n| `--refresh-project-imports` | 刷新项目中对复制物料的导入路径 | `npx @flowgram.ai/cli@latest materials components/json-schema-editor --refresh-project-imports` |\n| `--target-material-root-dir <dir>` | 指定物料复制的目标目录 | `npx @flowgram.ai/cli@latest materials --target-material-root-dir src/custom-materials` |\n| `--select-multiple` | 启用交互式多选模式 | `npx @flowgram.ai/cli@latest materials --select-multiple` |\n\n## 查找已使用的物料\n\n使用 `find-used-materials` 命令分析项目代码，找出所有已使用的官方物料：\n\n```bash\nnpx @flowgram.ai/cli@latest find-used-materials\n```\n\n输出示例：\n\n```console\n🚀 Welcome to @flowgram.ai form-materials CLI!\n📁 Project: /path/to/your/project\n🎯 Flowgram Version: 1.0.0\n\n👀 The exports of components/json-schema-editor is JsonSchemaEditor,JsonSchemaEditorProps\n👀 The exports of components/variable-selector is VariableSelector,VariableSelectorProps\n\n👀 Searching src/components/MyForm.tsx\n🔍 import { JsonSchemaEditor } from '@flowgram.ai/form-materials'\nimport components/json-schema-editor by JsonSchemaEditor\n\n👀 Searching src/pages/Settings.tsx\n🔍 import { VariableSelector } from '@flowgram.ai/form-materials'\nimport components/variable-selector by VariableSelector\n\n📦 All used materials:\ncomponents/json-schema-editor,components/variable-selector\n```\n\n这个命令会：\n- 扫描项目中所有的 TypeScript 文件\n- 分析从 `@flowgram.ai/form-materials` 的导入语句\n- 识别出使用的具体物料\n- 输出详细的使用位置信息\n\n## Case Run Down\n\n### 将项目中所有使用到的官方物料，代码同步到项目的 src/custom-materials 目录\n\n1. 使用 `find-used-materials` 命令查看项目中使用到的官方物料。\n\n```bash\nnpx @flowgram.ai/cli@latest find-used-materials\n```\n\n命令运行后，会输出项目中使用到的官方物料列表。\n\n```console\n📦 All used materials:\ncomponents/json-schema-editor,components/variable-selector\n```\n\n2. 使用 `materials` 命令将这些物料的源代码添加到项目的 src/custom-materials 目录。\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor,components/variable-selector \\\n  --target-material-root-dir src/custom-materials \\\n  --refresh-project-imports\n```\n\n- `--refresh-project-imports` 选项会刷新项目中对复制物料的导入路径，确保使用的是最新的定制版本。\n- `--target-material-root-dir src/custom-materials` 选项指定了物料复制的目标目录为 src/custom-materials。\n\n\n## 常见问题\n\n### Q: CLI 添加后找不到新增的依赖项目？\nA: 请检查是否运行了 `npm install` 安装新添加的依赖。\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/common/_meta.json",
    "content": "[\n  \"flow-value\",\n  \"json-schema-preset\",\n  \"inject-material\",\n  \"disable-declaration-plugin\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/materials/common/disable-declaration-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/common/disable-declaration-plugin';\n\n# DisableDeclarationPlugin\n\n:::note{title=\"\"}\n\n在物料库的设计中，**“节点本身”作为一种变量声明，是可选的**。\n\n物料库中 [`VariableSelector`](../components/variable-selector), [`PromptEditorWithVariables`](../components/prompt-editor-with-variables), [`SQLEditorWithVariables`](../components/sql-editor-with-variables) 等组件，都默认支持选择 **“节点变量”**。\n\n:::\n\nDisableDeclarationPlugin 可以**禁用变量声明的可选性（只能选下钻）**，从而使“节点变量”不可选。\n\n## 案例演示\n\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"use-editor-props.tsx\"\nimport { createDisableDeclarationPlugin } from '@flowgram.ai/form-materials';\n\n// ...\n{\n  plugins: () => [createDisableDeclarationPlugin()],\n}\n// ...\n```\n\n## API\n\n### createDisableDeclarationPlugin\n\n用于创建禁用变量声明的插件。该插件会拦截变量引擎的事件，将所有变量声明标记为禁用状态。\n\n```ts\nexport const createDisableDeclarationPlugin = definePluginCreator<void>({...});\n```\n\n**参数**：无\n\n**返回值**：一个插件实例，可直接添加到 Editor 的插件列表中。\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/plugins/disable-declaration-plugin/create-disable-declaration-plugin.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials plugins/disable-declaration-plugin\n```\n\n### 目录结构讲解\n\n```plaintext\npackages/materials/form-materials/src/plugins/disable-declaration-plugin/\n└── create-disable-declaration-plugin.ts  # 插件的主要实现文件\n```\n\n### 核心实现说明\n\nDisableDeclarationPlugin 的核心实现非常简洁，主要包含以下步骤：\n\n1. **初始化插件**：通过 `definePluginCreator` 创建插件，并在 `onInit` 钩子中获取变量引擎实例\n2. **事件监听**：监听变量引擎的 `NewAST` 和 `UpdateAST` 事件\n3. **处理逻辑**：当检测到变量声明类型 (`VariableDeclaration`) 的 AST 节点时，将其 `meta.disabled` 属性设置为 `true`\n\n这种实现方式确保了所有新创建或更新的变量声明都会被自动禁用，从而在变量选择器等组件中不可选。\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`ASTMatch`](https://flowgram.ai/auto-docs/editor/modules/ASTMatch): 用于匹配和判断 AST 节点类型的工具类\n- [`definePluginCreator`](https://flowgram.ai/auto-docs/core/functions/definePluginCreator): 定义插件创建器的函数\n- [`GlobalEventActionType`](https://flowgram.ai/auto-docs/editor/interfaces/GlobalEventActionType): 全局事件动作的类型定义\n- [`VariableEngine`](https://flowgram.ai/auto-docs/editor/classes/VariableEngine): 变量引擎，负责管理和处理变量相关的操作\n"
  },
  {
    "path": "apps/docs/src/zh/materials/common/flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\n\n# FlowValue\n\nFlowValue 是指在 Flowgram 官方物料库 中用于表示数据的一种特殊类型。它可以是常量、引用、表达式或模板，为流程中的数据传递和处理提供了灵活的方式。\n\n## FlowValue 类型\n\nFlowValue 支持以下四种类型：\n\n### 1. 常量 (Constant)\n固定值，在运行时不会改变。可以包含任意类型的数据，如字符串、数字、布尔值或对象。\n\n```typescript\n{\n  type: 'constant',\n  content: 'Hello World', // 可以是任意类型\n  schema?: { type: \"string\" },    // 可选的 JSON Schema 定义\n  extra?: { index: 1 }  // 额外信息，如排序等\n}\n```\n\n### 2. 引用 (Reference)\n对流程中其他变量或数据的引用，通过路径数组来指定引用的位置。\n\n```typescript\n{\n  type: 'ref',\n  content: ['variable', 'name'], // 引用路径数组\n  extra?: { index: 1 }  // 额外信息，如排序等\n}\n```\n\n### 3. 模板 (Template)\n包含变量占位符的字符串模板，使用 `{{variable}}` 语法来嵌入变量。\n\n```typescript\n{\n  type: 'template',\n  content: 'Hello {{user.name}}!', // 模板字符串\n  extra?: { index: 1 }  // 额外信息，如排序等\n}\n```\n\n### 4. WIP: 表达式 (Expression)\nJavaScript 表达式，在运行时会进行求值计算。\n\n```typescript\n{\n  type: 'expression',\n  content: 'a + b', // JavaScript、Python 等表达式字符串\n  extra?: { index: 1 }  // 额外信息，如排序等\n}\n```\n\n::: warning\n表达式类型当前为 WIP 状态，目前暂不支持类型推导能力，未来可能会有 breaking change。\n:::\n\n## FlowValueUtils 工具类\n\nFlowValueUtils 提供了丰富的工具函数来处理 FlowValue：\n\n### 类型判断函数\n\n- `isConstant(value)` - 判断是否为常量类型\n- `isRef(value)` - 判断是否为引用类型\n- `isExpression(value)` - 判断是否为表达式类型\n- `isTemplate(value)` - 判断是否为模板类型\n- `isConstantOrRef(value)` - 判断是否为常量或引用类型\n- `isFlowValue(value)` - 判断是否为有效的 FlowValue 类型\n\n### 遍历和提取函数\n\n- `traverse(value, options)` - 遍历对象中的所有 FlowValue，支持按类型筛选\n- `getTemplateKeyPaths(templateValue)` - 提取模板中所有的变量路径\n\n### Schema 推断函数\n\n- `inferConstantJsonSchema(constantValue)` - 根据常量值推断 JSON Schema\n- `inferJsonSchema(values, scope)` - 根据 FlowValue 推断对应的 JSON Schema\n\n## 使用示例\n\n### 使用工具函数\n\n```typescript\n// 类型判断\nif (FlowValueUtils.isConstant(value)) {\n  console.log('This is a constant value:', value.content);\n}\n\n// 遍历 FlowValues\nfor (const { value, path } of FlowValueUtils.traverse(data, {\n  includeTypes: ['ref', 'template']\n})) {\n  console.log(`Found ${value.type} at path: ${path}`);\n}\n\n// 提取模板变量\nconst templatePaths = FlowValueUtils.getTemplateKeyPaths(templateValue);\nconsole.log('Template variables:', templatePaths); // [['user', 'name'], ['count']]\n```\n\n## 类型定义\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/shared/flow-value/types.ts\"\n/>\n\nFlowValue 的核心类型定义包括：\n\n- `IFlowValue` - FlowValue 的联合类型\n- `IFlowConstantValue` - 常量类型接口\n- `IFlowRefValue` - 引用类型接口\n- `IFlowExpressionValue` - 表达式类型接口\n- `IFlowTemplateValue` - 模板类型接口\n- `IFlowConstantRefValue` - 常量或引用类型的联合类型\n- `IInputsValues` - 输入值的映射类型\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/shared/flow-value\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials shared/flow-value\n```\n\n### 目录结构讲解\n\n```\nflow-value/\n├── index.ts           # 主入口文件，导出所有类型和工具函数\n├── types.ts           # 类型定义文件，包含所有 FlowValue 接口定义\n├── schema.ts          # Zod 模式定义，用于运行时类型验证\n├── utils.ts           # FlowValueUtils 工具类的完整实现\n└── README.md          # 模块说明文档\n```\n\n### 使用到的第三方 API\n\n- [Zod](https://v3.zod.dev/): 用于类型验证和数据解析，判断 FlowValue 的 Schema 是否符合预期。\n"
  },
  {
    "path": "apps/docs/src/zh/materials/common/inject-material.mdx",
    "content": "import { SourceCode } from '@theme';\n\n# 物料组件依赖注入\n\n:::tip{title=\"目前物料库中支持依赖注入的组件物料\"}\n\n- [InjectDynamicValueInput](../components/dynamic-value-input)\n- [InjectTypeSelector](../components/type-selector)\n- [InjectVariableSelector](../components/variable-selector)\n\n:::\n\n## 背景：为什么物料库需要依赖注入 ?\n\n### ❌ 紧耦合：传统依赖问题\n\n```mermaid\ngraph TD\n    A[物料 A] --> B[物料 B]\n    B --> D[物料 D]\n    C[物料 C] --> D\n\n    style D fill:#ff4757\n    style A fill:#ffa502\n    style B fill:#ffa502\n    style C fill:#ffa502\n\n    note[\"💥 问题：D变更导致A、B、C全部需要修改\"]\n```\n\n**问题：** 连锁反应、高维护成本\n\n### ✅ 解耦：依赖注入方案\n\n```mermaid\ngraph TD\n    A[物料 A] --> RenderKey[物料 D RenderKey]\n    B[物料 B] --> RenderKey\n    C[物料 C] --> RenderKey\n\n    RenderKey -.-> BaseD[默认物料 D]\n    CustomD[自定义物料 D] -.-> RenderKey\n\n    style RenderKey fill:#3498db\n    style BaseD fill:#2ed573\n    style CustomD fill:#26d0ce\n    style A fill:#a55eea\n    style B fill:#a55eea\n    style C fill:#a55eea\n\n    note2[\"✅ A、B、C依赖抽象接口，与D实现解耦\"]\n```\n\n**优势：** 热插拔、并行开发、版本兼容\n\n## 使用方式\n\n### 创建可注入的组件物料\n\n```tsx\nimport { createInjectMaterial } from '@flowgram.ai/form-materials';\nimport { VariableSelector } from './VariableSelector';\n\n// 使用 createInjectMaterial 高阶组件包装组件\nconst InjectVariableSelector = createInjectMaterial(VariableSelector);\n\n// 现在你可以像使用普通组件一样使用它\nfunction MyComponent() {\n  return <InjectVariableSelector value={value} onChange={handleChange} />;\n}\n```\n\n### 注册自定义组件\n\n一个组件物料并创建为可注入的物料组件，当被其他物料使用时候，可以在 `use-editor-props.tsx` 中注入该物料的自定义渲染器：\n\n```tsx\nimport { useEditorProps } from '@flowgram.ai/editor';\nimport { YourCustomVariableSelector } from './YourCustomVariableSelector';\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nfunction useCustomEditorProps() {\n  const editorProps = useEditorProps({\n    materials: {\n      components: {\n        // 默认使用组件的 Function Name 作为 renderKey\n        'VariableSelector': YourCustomVariableSelector,\n        'TypeSelector': YourCustomTypeSelector,\n      }\n    }\n  });\n\n  return editorProps;\n}\n```\n\n### 使用自定义 renderKey\n\n如果你的组件需要特定的 renderKey：\n\n**方法 1：** 通过 createInjectMaterial 的第二个参数指定 renderKey\n\n```tsx\nconst InjectCustomComponent = createInjectMaterial(MyComponent, {\n  renderKey: 'my-custom-key'\n});\n// 注册时\n{\n  materials: {\n    components: {\n      'my-custom-key': MyCustomRenderer\n    }\n  }\n}\n```\n\n**方法 2：** 或者直接设置组件的 renderKey 属性\n\n```tsx\nMyComponent.renderKey = 'my-custom-key';\nconst InjectCustomComponent = createInjectMaterial(MyComponent);\n// 注册时\n{\n  materials: {\n    components: {\n      [MyComponent.renderKey]: MyCustomRenderer\n    }\n  }\n}\n\n```\n\n\n:::note{title=\"渲染键优先级\"}\n\n组件渲染键的确定遵循以下优先级顺序：\n\n1. `params.renderKey` (createInjectMaterial 的第二个参数)\n2. `Component.renderKey` (组件自身的 renderKey 属性)\n3. `Component.name` (组件的显示名称)\n4. 空字符串 (最终回退)\n\n:::\n\n## API 参考\n\n```typescript\ninterface CreateInjectMaterialOptions {\n  renderKey?: string;\n}\n\nfunction createInjectMaterial<Props>(\n  Component: React.FC<Props> & { renderKey?: string },\n  params?: CreateInjectMaterialOptions\n): React.FC<Props>\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/shared/inject-material/index.tsx\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials shared/inject-material\n```\n\n### 核心时序图\n\n完整的组件注册和渲染时序图：\n\n```mermaid\nsequenceDiagram\n    participant App as 应用程序\n    participant Editor as use-editor-props\n    participant Registry as FlowRendererRegistry\n    participant Inject as InjectMaterial\n    participant Default as 默认组件\n    participant Custom as 自定义组件\n\n    Note over App,Custom: 组件注册阶段\n    App->>Editor: 调用 use-editor-props()\n    Editor->>Editor: 配置 materials.components\n    Editor->>Registry: 向 FlowRendererRegistry 注册组件\n    Registry->>Registry: 存储映射关系\n    Registry-->>App: 注册完成\n\n    Note over App,Custom: 组件渲染阶段\n    App->>Inject: 渲染 InjectMaterial 组件\n    Inject->>Registry: 查询渲染器 (getRendererComponent)\n\n    alt 存在自定义渲染器\n        Registry-->>Inject: 返回自定义 React 组件\n        Inject->>Custom: 使用自定义组件渲染\n        Custom-->>App: 渲染自定义 UI\n    else 无自定义渲染器\n        Registry-->>Inject: 返回 null 或类型不匹配\n        Inject->>Default: 使用默认组件渲染\n        Default-->>App: 渲染默认 UI\n    end\n```\n"
  },
  {
    "path": "apps/docs/src/zh/materials/common/json-schema-preset.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/common/json-schema-preset';\n\n# 类型管理\n\n物料库类型管理分为两部分实现：\n\n1. **物料层**（物料库预设类型定义）：\n   - 扩展类型引擎，使其可以定义类型的默认渲染器、Condition 规则配置等\n   - 提供了默认类型在物料库中的预设定义（常量输入器渲染、Condition 规则配置等）\n   - 提供 Editor Plugin 方便扩展自定义类型\n2. **引擎层**（核心类型引擎，由 `@flowgram.ai/json-schema` 提供）\n   - 提供了 Json 类型的基本定义，包括 icon，名称展示等\n   - 提供 BaseTypeManager，可以扩展 JsonSchema 官方定义之外的类型定义\n   - 提供 `JsonSchemaUtils` 实现 JsonSchema 和 AST 的互相转换\n\n## 案例演示\n\n### 增加 Color 类型\n\n<BasicStory />\n\n\n```tsx pure title=\"use-editor-props.tsx\"\nimport { createTypePresetPlugin } from \"@flowgram.ai/form-materials\";\n\n// ...\n{\n  plugins: () => [\n    createTypePresetPlugin({\n      types: [\n        types: [\n          {\n            type: 'color',\n            icon: <IconColorPalette />,\n            label: 'Color',\n            ConstantRenderer: ({ value, onChange }) => (\n              <div className=\"json-schema-color-picker-container \">\n                <ColorPicker\n                  alpha={true}\n                  usePopover={true}\n                  value={value ? ColorPicker.colorStringToValue(value) : undefined}\n                  onChange={(_value) => onChange?.(_value.hex)}\n                />\n              </div>\n            ),\n            conditionRule: {\n              eq: { type: 'color' },\n            },\n          },\n        ],\n      ],\n    }),\n  ],\n}\n// ...\n\n```\n\n\n### 获取类型定义\n\n\n```tsx\nconst typeManager = useTypeManager();\n\n// 根据 schema 获取类型定义\nconst type = typeManager.getTypeBySchema({ type: \"color\" });\nconst type2 = typeManager.getTypeBySchema({ type: \"array\", items: { type: \"color\" } });\n\n// 根据类型名获取类型定义\nconst type3 = typeManager.getTypeByName(\"color\");\n```\n\n\n## API\n\n### createTypePresetPlugin\n\n创建一个 Editor Plugin，用于扩展物料库预设类型定义，或者关闭某些物料库中预设的类型。\n\n```typescript\nfunction createTypePresetPlugin(options: TypePresetPluginOptions): Plugin;\n\ninterface TypePresetPluginOptions {\n  // 要添加的自定义类型定义数组\n  types?: TypePresetRegistry[];\n  // 要移除的类型名称数组\n  unregisterTypes?: string[];\n}\n\ninterface TypePresetRegistry {\n  // 类型名称\n  type: string;\n  // 类型图标\n  icon?: React.ReactNode;\n  // 类型标签\n  label?: string;\n  // 常量渲染器组件\n  ConstantRenderer: React.FC<ConstantRendererProps>;\n  // 条件规则配置\n  conditionRule?: IConditionRule | IConditionRuleFactory;\n  // 其他从基础类型继承的属性\n}\n\ninterface ConstantRendererProps<Value = any> {\n  value?: Value;\n  onChange?: (value: Value) => void;\n  readonly?: boolean;\n  [key: string]: any;\n}\n```\n\n## 源码导读\n\n物料层源代码：<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/plugins/json-schema-preset\"\n/>\n\n使用 CLI 命令可以复制物料层源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials plugins/json-schema-preset\n```\n\n引擎层源代码：<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema/src\"\n/>\n\n引擎层由于复杂度较高，因此目前需要通过单独的 `@flowgram.ai/json-schema` 包使用，不支持 CLI 命令下载源代码。\n\n### 物料层核心逻辑\n\n物料层新增的定义被以下物料使用：\n- [ConstantInput](../components/constant-input): 获取类型对应的常量输入\n  - 源代码见：<SourceCode\n    href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/constant-input/index.tsx\"\n  />\n- [ConditionContext](../components/condition-context)：获取类型对应的 Condition 规则\n  - 源代码见：<SourceCode\n    href=\"https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/components/condition-context/hooks/use-condition.tsx\"\n  />\n\n### 引擎层核心逻辑\n\n#### JsonSchemaTypeManager 类结构\n\n```mermaid\nclassDiagram\n  direction LR\n  class BaseTypeManager {\n      -getTypeNameFromSchema(typeSchema): string\n      +getTypeByName(typeName): Registry\n      +getTypeBySchema(type): Registry\n      +getDefaultTypeRegistry(): Registry\n      +getAllTypeRegistries(): Registry[]\n      +register(registry): void\n      +unregister(typeName): void\n  }\n\n  class JsonSchemaTypeManager {\n      +constructor()\n      +getTypeRegistriesWithParentType(parentType): Registry[]\n      +getTypeSchemaDeepChildField(type): Schema\n      +getComplexText(type): string\n      +getDisplayIcon(type): ReactNode\n      +getTypeSchemaProperties(type): Record\n      +getPropertiesParent(type): Schema\n      +getJsonPaths(type): string[]\n      +canAddField(type): boolean\n      +getDefaultValue(type): unknown\n  }\n\n  BaseTypeManager <|-- JsonSchemaTypeManager\n\n  class JsonSchemaTypeRegistry {\n      +type: string\n      +label: string\n      +icon: ReactNode\n      +container: boolean\n      +getJsonPaths(typeSchema): string[]\n      +getDisplayIcon(typeSchema): ReactNode\n      +getPropertiesParent(typeSchema): Schema\n      +canAddField(typeSchema): boolean\n      +getDefaultValue(): unknown\n      +getValueText(value): string\n      +getTypeSchemaProperties(typeSchema): Record\n      +getStringValueByTypeSchema(typeSchema): string\n      +getTypeSchemaByStringValue(optionValue): Schema\n      +getDefaultSchema(): Schema\n      +customComplexText(typeSchema): string\n  }\n  <<Interface>> JsonSchemaTypeRegistry\n\n  JsonSchemaTypeManager --> JsonSchemaTypeRegistry: register\n```\n\n#### JsonSchemaTypeManager 功能概览\n\n**核心功能**：\n\n1. **类型注册与管理**\n   - `register(registry)`: 注册新的类型定义\n   - `unregister(typeName)`: 移除已注册的类型\n   - `getAllTypeRegistries()`: 获取所有已注册的类型\n   - `getTypeByName(typeName)`: 通过类型名称获取类型定义\n   - `getTypeBySchema(schema)`: 通过 schema 获取对应的类型定义\n\n2. **类型信息获取**\n   - `getTypeNameFromSchema(schema)`: 从 schema 中提取类型名称\n   - `getTypeRegistriesWithParentType(parentType)`: 获取指定父类型下的所有类型\n   - `getTypeSchemaDeepChildField(type)`: 获取类型的最深层子字段\n   - `getComplexText(type)`: 获取类型的复杂文本表示（如 Array\\<String\\>）\n   - `getDisplayIcon(type)`: 获取类型的显示图标\n\n3. **类型属性操作**\n   - `getTypeSchemaProperties(type)`: 获取类型的属性定义\n   - `getPropertiesParent(type)`: 获取属性的父节点\n   - `getJsonPaths(type)`: 获取类型在 flow schema 中的 json 路径\n   - `canAddField(type)`: 判断是否可以向类型添加字段\n   - `getDefaultValue(type)`: 获取类型的默认值\n\n**初始化过程**：\n\n在构造函数中，JsonSchemaTypeManager 会自动注册一系列默认类型定义：\n- defaultTypeDefinitionRegistry: 默认类型定义\n- stringRegistryCreator: 字符串类型\n- integerRegistryCreator: 整数类型\n- numberRegistryCreator: 数字类型\n- booleanRegistryCreator: 布尔类型\n- objectRegistryCreator: 对象类型\n- arrayRegistryCreator: 数组类型\n- unknownRegistryCreator: 未知类型\n- mapRegistryCreator: 映射类型\n- dateTimeRegistryCreator: 日期时间类型\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/_meta.json",
    "content": "[\n  \"type-selector\",\n  \"json-schema-editor\",\n  \"json-schema-creator\",\n  \"variable-selector\",\n  \"dynamic-value-input\",\n  \"condition-row\",\n  \"db-condition-row\",\n  \"condition-context\",\n  \"inputs-values\",\n  \"inputs-values-tree\",\n  \"prompt-editor\",\n  \"prompt-editor-with-variables\",\n  \"prompt-editor-with-inputs\",\n  \"code-editor\",\n  \"json-editor-with-variables\",\n  \"sql-editor-with-variables\",\n  \"coze-editor-extensions\",\n  \"constant-input\",\n  \"display-schema-tag\",\n  \"display-schema-tree\",\n  \"display-flow-value\",\n  \"display-inputs-values\",\n  \"display-outputs\",\n  \"assign-row\",\n  \"assign-rows\",\n  \"batch-outputs\",\n  \"batch-variable-selector\",\n  \"blur-input\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/assign-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { AssignModeStory, DeclareModeStory } from 'components/form-materials/components/assign-row';\n\n# AssignRow\n\nAssignRow 是一个赋值行组件，支持两种操作模式：**赋值模式 (assign)** 和 **声明模式 (declare)**。\n\n- 在赋值模式下：左侧是变量选择器，右侧是动态值输入；\n- 在声明模式下：左侧是文本输入框，右侧是动态值输入。\n\n## 案例演示\n\n### 赋值模式\n\nAssignRow **默认为赋值模式**，在赋值模式下，左侧为变量选择器，右侧为动态值输入：\n\n<AssignModeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRow } from '@flowgram.ai/form-materials';\nimport { AssignValueType } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<AssignValueType | undefined> name=\"assign_row\">\n        {({ field }) => (\n          <AssignRow value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 声明模式\n\n声明模式下，左侧为变量名输入，右侧为动态值输入：\n\n<DeclareModeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRow } from '@flowgram.ai/form-materials';\nimport { AssignValueType } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<AssignValueType | undefined> name=\"assign_row\">\n        {({ field }) => (\n          <AssignRow\n            value={{\n              operator: 'declare',\n              left: 'newVariable',\n              right: {\n                type: 'constant',\n                content: 'Hello World',\n                schema: { type: 'string' },\n              },\n            }}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n\n\n## API 参考\n\n### AssignRow Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `AssignValueType` | - | 赋值行的值，包含操作符、左侧值和右侧值 |\n| `onChange` | `(value?: AssignValueType) => void` | - | 值变化时的回调函数 |\n| `onDelete` | `() => void` | - | 删除按钮点击时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n\n### AssignValueType\n\n```typescript\ntype AssignValueType =\n  | {\n      operator: 'assign';\n      left?: IFlowRefValue;      // 变量引用\n      right?: IFlowValue;        // 动态值\n    }\n  | {\n      operator: 'declare';\n      left?: string;             // 变量名\n      right?: IFlowValue;        // 动态值\n    };\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/assign-row\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/assign-row\n```\n\n### 目录结构讲解\n\n```\nassign-row/\n├── index.tsx     # AssignRow 组件主实现\n└── types.ts      # 类型定义文件\n```\n\n### 核心实现说明\n\nAssignRow 组件的核心逻辑是根据 `operator` 字段来渲染不同的左侧输入控件：\n\n1. **赋值模式 (`operator: 'assign'`)**：左侧渲染 `InjectVariableSelector`，用于选择已有变量\n2. **声明模式 (`operator: 'declare'`)**：左侧渲染 `BlurInput`，用于输入新变量名\n3. **右侧统一**：无论哪种模式，右侧都渲染 `InjectDynamicValueInput`，支持常量和变量输入\n\n#### 组件结构\n\n```mermaid\ngraph TD\n    A[AssignRow 组件] --> B{判断 operator 类型}\n    B -->|assign| C[渲染变量选择器]\n    B -->|declare| D[渲染文本输入框]\n\n    C --> E[右侧动态值输入]\n    D --> E\n\n    E --> F[可选的删除按钮]\n\n    C --> G[支持 onDelete 回调]\n    D --> G\n    F --> G\n```\n\n### 依赖梳理\n\n#### 其他物料\n\n[**VariableSelector**](./variable-selector)\n- `InjectVariableSelector`: 依赖注入的变量选择器\n\n[**DynamicValueInput**](./dynamic-value-input)\n- `InjectDynamicValueInput`: 依赖注入的动态值输入组件\n\n[**BlurInput**](./blur-input)\n- `BlurInput`: 失焦输入组件\n\n#### 第三方库\n\n[**Semi Design**](https://semi.design/zh-CN/)\n- `IconButton`: 图标按钮组件\n- `IconMinus`: 减号图标\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/assign-rows.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/assign-rows';\n\n# AssignRows\n\nAssignRows 是一个赋值行列表组件，基于 `FieldArray` 实现，支持动态添加和删除赋值行。\n\n组件提供了两个操作按钮：**赋值** 和 **声明**，可以分别添加赋值模式和声明模式的赋值行。每个赋值行都可以独立配置和删除。\n\n:::tip\n\n`AssignRows` 通常和 [`infer-assign-plugin`](../form-plugins/infer-assign-plugin) 表单插件一起使用，用于将定义的声明转换为节点的输出变量，并实现类型的自动联动。\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { AssignRows } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <AssignRows name=\"assign_rows\" />\n    </>\n  ),\n}\n```\n\n\n\n## API 参考\n\n### AssignRows Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `name` | `string` | - | 表单字段名称，用于 FieldArray |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/assign-rows\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/assign-rows\n```\n\n### 目录结构讲解\n\n```\nassign-rows/\n└── index.tsx     # AssignRows 组件主实现\n```\n\n### 核心实现说明\n\nAssignRows 组件的核心功能是基于 `FieldArray` 实现的动态列表管理：\n\n1. **动态添加**：提供两个按钮分别添加赋值模式和声明模式的行\n2. **动态删除**：每行都支持独立的删除操作\n3. **状态管理**：使用 `FieldArray` 管理整个列表的状态\n4. **组件复用**：每行都复用 `AssignRow` 组件\n\n#### 组件工作流程\n\n```mermaid\ngraph TD\n    A[AssignRows 组件] --> B[FieldArray 包装]\n    B --> C[渲染现有行列表]\n    C --> D[每行使用 AssignRow]\n    D --> E[支持 onChange 和 onDelete]\n\n    B --> F[添加按钮区域]\n    F --> G[赋值按钮]\n    F --> H[声明按钮]\n\n    G --> I[添加赋值行]\n    H --> J[添加声明行]\n\n    I --> K[调用 field.append]\n    J --> K\n\n    E --> L[调用 field.remove]\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `FieldArray`: 表单数组字段组件，用于管理动态列表\n- `FieldArrayRenderProps`: FieldArray 渲染属性类型\n\n#### 其他物料\n\n[**AssignRow**](./assign-row)\n- `AssignRow`: 赋值行组件，处理单行逻辑\n- `AssignValueType`: 赋值行值类型定义\n\n#### 第三方库\n\n[**Semi Design**](https://semi.design/zh-CN/)\n- `Button`: 按钮组件\n- `IconPlus`: 加号图标\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/batch-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/batch-outputs';\n\n# BatchOutputs\n\n`BatchOutputs` 是一个用于配置循环输出的键值对编辑器组件。在循环节点场景中，它允许用户定义每次迭代需要收集的输出值，这些值最终会被聚合成数组。\n\n**核心特性：**\n\n- ➕ **动态添加/删除**：用户可以自由添加或删除输出键值对\n- ✏️ **键名编辑**：为每个输出定义一个唯一的键名\n- 🔗 **变量引用**：通过变量选择器引用循环体内可用的变量\n- 👁️ **只读模式**：支持只读展示，适用于查看场景\n\n:::warning\n\n`BatchOutputs` 必须搭配 [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) 使用才能正常工作。这是因为：\n1. 组件负责 UI 交互，收集用户配置的输出键值对\n2. 插件负责将配置转换为变量声明，并调整作用域链\n\n:::\n\n:::info{title=\"完整方案概览\"}\n\n实现一个完整的循环节点需要以下三个物料配合使用：\n\n| 物料 | 类型 | 职责 |\n|------|------|------|\n| [BatchVariableSelector](./batch-variable-selector) | 组件 | 选择循环的数组数据源 |\n| [provideBatchInputEffect](../effects/provide-batch-input) | 副作用 | 生成 `item` 和 `index` 局部变量 |\n| **BatchOutputs** + [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) | 组件 + 插件 | 配置循环输出并生成数组类型变量 |\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"关于 FormHeader、FormContent、FormItem\"}\n\n上述代码中的 `FormHeader`、`FormContent`、`FormItem` 是用户自定义的布局组件，用于统一表单样式。你可以根据项目需求自行实现或替换为其他 UI 组件。\n\n:::\n\n### 只读模式\n\n通过设置 `readonly` 属性可以禁用编辑功能，适用于查看或预览场景：\n\n```tsx pure\n<BatchOutputs\n  readonly\n  value={{\n    names: { type: 'ref', content: ['item', 'name'] },\n    ages: { type: 'ref', content: ['item', 'age'] },\n  }}\n/>\n```\n\n## API 参考\n\n### BatchOutputs Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `Record<string, IFlowRefValue \\| undefined>` | - | 输出键值对对象，键为输出名称，值为变量引用 |\n| `onChange` | `(value?: Record<string, IFlowRefValue \\| undefined>) => void` | - | 值变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n\n### 值类型说明\n\n```typescript\ntype ValueType = Record<string, IFlowRefValue | undefined>;\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n```\n\n#### 值结构示例\n\n```typescript\n{\n  names: { type: 'ref', content: ['loop_1_locals', 'item', 'name'] },\n  ages: { type: 'ref', content: ['loop_1_locals', 'item', 'age'] },\n  scores: { type: 'ref', content: ['loop_1_locals', 'item', 'score'] },\n}\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/batch-outputs\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/batch-outputs\n```\n\n### 目录结构讲解\n\n```\nbatch-outputs/\n├── index.tsx          # 主组件实现\n├── types.ts           # 类型定义\n└── styles.css         # 样式文件\n```\n\n### 核心实现说明\n\n#### 组件结构\n\nBatchOutputs 组件基于 `useObjectList` hook 实现动态列表管理，每一行包含：\n- **Input**：用于编辑输出键名\n- **InjectVariableSelector**：用于选择变量引用\n- **Delete Button**：删除当前行\n\n#### 数据流向\n\n```mermaid\ngraph TD\n    A[BatchOutputs 组件] --> B[useObjectList Hook]\n    B --> C[list 状态]\n    B --> D[add 方法]\n    B --> E[updateKey 方法]\n    B --> F[updateValue 方法]\n    B --> G[remove 方法]\n\n    C --> H[渲染列表]\n    H --> I[Input 键名编辑]\n    H --> J[VariableSelector 变量选择]\n    H --> K[Delete 删除按钮]\n\n    D --> L[添加按钮]\n\n    I --> E\n    J --> F\n    K --> G\n    \n    E --> M[onChange 回调]\n    F --> M\n    G --> M\n    M --> N[更新外部 value]\n```\n\n#### useObjectList Hook\n\n`useObjectList` 是一个通用的动态对象列表管理 hook，核心功能：\n\n1. **列表状态管理**：维护带有唯一 ID 的列表项\n2. **双向同步**：在 `value` 属性变化时同步更新列表\n3. **增删改操作**：提供 `add`、`remove`、`updateKey`、`updateValue` 方法\n\n```typescript\ninterface UseObjectListOptions<T> {\n  value?: Record<string, T | undefined>;\n  onChange?: (value?: Record<string, T | undefined>) => void;\n}\n\ninterface UseObjectListReturn<T> {\n  list: Array<{ id: string; key: string; value: T | undefined }>;\n  add: () => void;\n  remove: (id: string) => void;\n  updateKey: (id: string, newKey: string) => void;\n  updateValue: (id: string, newValue: T) => void;\n}\n\nconst { list, add, updateKey, updateValue, remove } = useObjectList({\n  value,\n  onChange,\n});\n```\n\n#### 完整数据流时序图\n\n```mermaid\nsequenceDiagram\n    participant User as 用户\n    participant UI as BatchOutputs 组件\n    participant Hook as useObjectList\n    participant Form as 表单系统\n    participant Plugin as batchOutputsPlugin\n\n    User->>UI: 点击添加按钮\n    UI->>Hook: 调用 add()\n    Hook->>Hook: 生成新的列表项（带唯一 ID）\n    Hook->>Form: 触发 onChange\n    Form->>Plugin: 表单值变化\n    Plugin->>Plugin: 生成变量声明\n\n    User->>UI: 编辑键名\n    UI->>Hook: 调用 updateKey(id, newKey)\n    Hook->>Form: 触发 onChange\n    Form->>Plugin: 表单值变化\n    Plugin->>Plugin: 更新变量声明\n\n    User->>UI: 选择变量\n    UI->>Hook: 调用 updateValue(id, refValue)\n    Hook->>Form: 触发 onChange\n    Form->>Plugin: 表单值变化\n    Plugin->>Plugin: 更新变量声明\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/modules/I18n): 国际化工具，用于按钮文案\n\n#### 依赖的其他物料\n\n[**useObjectList**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/hooks/use-object-list)\n- 动态对象列表管理 hook，处理列表的增删改操作\n\n[**InjectVariableSelector**](./variable-selector)\n- 注入式变量选择器，用于选择变量引用\n\n#### 第三方依赖\n\n- `@douyinfe/semi-ui`: UI 组件库，使用 Button、Input 组件\n- `@douyinfe/semi-icons`: 图标库，使用 IconDelete、IconPlus 图标\n\n## 常见问题\n\n### 为什么需要同时使用 BatchOutputs 组件和 batchOutputsPlugin？\n\n这是关注点分离的设计：\n\n| 角色 | 职责 |\n|------|------|\n| `BatchOutputs` 组件 | 提供 UI 交互，让用户配置输出键名和变量引用 |\n| `batchOutputsPlugin` | 处理数据逻辑，将配置转换为变量声明并调整作用域链 |\n\n单独使用组件只能收集数据，无法生成有效的输出变量；单独使用插件则没有 UI 来配置数据。\n\n### BatchOutputs 与 InputsValues 的区别？\n\n| 特性 | BatchOutputs | InputsValues |\n|------|--------------|--------------|\n| 用途 | 循环输出配置 | 节点输入配置 |\n| 值类型 | `Record<string, IFlowRefValue>` | `IInputsValues` |\n| 变量引用 | 只支持变量引用 | 支持常量和变量引用 |\n| 适用场景 | Loop 节点的输出聚合 | 通用节点的输入参数 |\n\n### 如何自定义变量选择器的过滤条件？\n\n目前 `BatchOutputs` 内部使用 `InjectVariableSelector`，不支持自定义过滤条件。如果需要自定义，可以参考源码实现自己的组件：\n\n```tsx\nimport { useObjectList } from '@flowgram.ai/form-materials';\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nfunction CustomBatchOutputs(props) {\n  const { list, add, updateKey, updateValue, remove } = useObjectList(props);\n  \n  return (\n    <div>\n      {list.map((item) => (\n        <div key={item.id}>\n          <Input value={item.key} onChange={(v) => updateKey(item.id, v)} />\n          <VariableSelector\n            value={item.value?.content}\n            onChange={(v) => updateValue(item.id, { type: 'ref', content: v })}\n            includeSchema={{ type: 'string' }}\n          />\n          <Button onClick={() => remove(item.id)}>删除</Button>\n        </div>\n      ))}\n      <Button onClick={() => add()}>添加</Button>\n    </div>\n  );\n}\n```\n\n### 如何获取生成的输出变量类型？\n\n配合 `batchOutputsPlugin` 使用时，如果配置了 `inferTargetKey`，输出变量的 JSON Schema 会在表单提交时自动写入指定字段：\n\n```typescript\nplugins: [\n  createBatchOutputsFormPlugin({ \n    outputKey: 'loopOutputs', \n    inferTargetKey: 'outputs'\n  })\n]\n```\n\n提交后的表单数据结构示例：\n\n```typescript\n{\n  loopOutputs: {\n    names: { type: 'ref', content: ['item', 'name'] },\n    ages: { type: 'ref', content: ['item', 'age'] },\n  },\n  outputs: {\n    type: 'object',\n    properties: {\n      names: { type: 'array', items: { type: 'string' } },\n      ages: { type: 'array', items: { type: 'number' } },\n    }\n  }\n}\n```\n\n### 如何处理键名重复的情况？\n\n目前组件不会自动检测键名重复。建议在表单层面添加校验逻辑：\n\n```typescript\nconst formMeta: FormMeta = {\n  validate: {\n    loopOutputs: (value) => {\n      if (!value) return;\n      const keys = Object.keys(value);\n      const uniqueKeys = new Set(keys);\n      if (keys.length !== uniqueKeys.size) {\n        return '输出键名不能重复';\n      }\n    },\n  },\n};\n```\n\n## 相关物料\n\n- [BatchVariableSelector](./batch-variable-selector): 数组变量选择器，用于选择循环输入\n- [provideBatchInputEffect](../effects/provide-batch-input): 循环输入变量解析副作用\n- [batchOutputsPlugin](../form-plugins/batch-outputs-plugin): 循环输出插件，处理作用域链和类型推导\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/batch-variable-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/batch-variable-selector';\n\n# BatchVariableSelector\n\nBatchVariableSelector 是一个用于选择数组类型变量的组件，它是 [VariableSelector](./variable-selector) 的封装版本。\n\n该组件自动过滤变量树，只显示数组类型的变量，并自动设定[私有作用域](../../guide/variable/concept#节点私有作用域)，常用于批处理场景（如 Loop 节点的循环数据源选择）。\n\n**核心特性：**\n\n- 🔍 **自动过滤**：只显示数组类型（`type: 'array'`）的变量。\n- 🔐 **私有作用域**：通过 [`PrivateScopeProvider`](../../guide/variable/concept#节点私有作用域) 提供独立的变量作用域。\n- 🎯 **专用场景**：专为批处理、循环等需要数组数据源的场景设计。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { BatchVariableSelector, VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      {/* BatchVariableSelector 只显示数组类型变量 */}\n      <Field<string[] | undefined> name=\"batch_variable\">\n        {({ field }) => (\n          <BatchVariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n\n      {/* VariableSelector 显示所有类型变量 */}\n      <Field<string[] | undefined> name=\"normal_variable\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### BatchVariableSelector Props\n\nBatchVariableSelector 继承了 [VariableSelector](./variable-selector) 的所有属性，但 `includeSchema` 属性已被固定为数组类型过滤，无法自定义。\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `string[]` | - | 选中的变量路径数组 |\n| `onChange` | `(value?: string[]) => void` | - | 变量选择变化时的回调函数 |\n| `config` | `VariableSelectorConfig` | `{}` | 配置对象（同 VariableSelector） |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n| `triggerRender` | `(props: TriggerRenderProps) => React.ReactNode` | - | 自定义触发器渲染 |\n\n:::warning\n`includeSchema` 和 `excludeSchema` 属性在 BatchVariableSelector 中不可用，因为组件内部已固定使用 `{ type: 'array', extra: { weak: true } }` 作为过滤条件。\n:::\n\n### VariableSelectorConfig\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `placeholder` | `string` | `'选择变量'` | 占位符文本 |\n| `notFoundContent` | `string` | `'未定义'` | 变量未找到时的显示内容 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/batch-variable-selector\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/batch-variable-selector\n```\n\n### 目录结构讲解\n\n```\nbatch-variable-selector/\n└── index.tsx           # 主组件实现，包含 BatchVariableSelector 核心逻辑\n```\n\n### 核心实现说明\n\n#### 私有作用域机制\n\nBatchVariableSelector 通过 `PrivateScopeProvider` 为子组件提供独立的变量作用域：\n\n```tsx\n<PrivateScopeProvider>\n  <VariableSelector {...props} includeSchema={batchVariableSchema} />\n</PrivateScopeProvider>\n```\n\n`PrivateScopeProvider` 会创建一个[节点私有作用域](../../guide/variable/concept#节点私有作用域)，这在批处理场景中非常重要：\n\n- **循环变量隔离**：在 Loop 节点中，每次迭代的循环变量（如 `item`、`index`）都存储在私有作用域中，避免污染外部作用域\n- **避免命名冲突**：批处理节点内部定义的临时变量不会与外部变量产生命名冲突\n- **支持嵌套结构**：复杂的批处理逻辑可以在私有作用域中定义多层变量结构\n- **数据安全**：私有作用域中的变量只能被当前节点及其子节点访问，确保数据安全性\n\n:::info\n\n更多关于作用域的详细信息，请参考[变量概念文档](../../guide/variable/concept#画布中的变量)。\n\n:::\n\n#### 数组类型过滤\n\n组件内部固定使用以下 schema 进行过滤：\n\n```typescript\nconst batchVariableSchema: IJsonSchema = {\n  type: 'array',\n  extra: { weak: true },\n};\n```\n\n- `type: 'array'`：只显示数组类型的变量\n- `extra: { weak: true }`：启用弱类型匹配，允许匹配可能兼容的类型\n\n### 整体流程\n\n```mermaid\nsequenceDiagram\n    participant User\n    participant BatchVariableSelector\n    participant PrivateScopeProvider\n    participant VariableSelector\n    participant useVariableTree\n    participant VariableTree\n\n    User->>BatchVariableSelector: 选择数组变量\n    BatchVariableSelector->>PrivateScopeProvider: 设定私有作用域上下文\n    PrivateScopeProvider->>VariableSelector: 传递包含数组过滤的 schema\n    VariableSelector->>useVariableTree: 获取可用变量列表\n    useVariableTree->>VariableTree: 查询变量树数据\n    VariableTree-->>useVariableTree: 返回所有变量\n    useVariableTree-->>VariableSelector: 过滤出数组类型变量\n    VariableSelector-->>BatchVariableSelector: 渲染数组变量选择器\n    BatchVariableSelector-->>User: 显示可选的数组变量\n```\n\n### 使用到的 FlowGram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/plugins/node-variable-plugin)\n- [`PrivateScopeProvider`](https://flowgram.ai/auto-docs/node-variable-plugin/functions/PrivateScopeProvider): 提供私有变量作用域的 Context Provider\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义，用于变量类型过滤\n\n### 依赖的其他物料\n\n[**VariableSelector**](./variable-selector) 变量选择器基础组件\n- `VariableSelector`: 核心变量选择组件，BatchVariableSelector 是其封装版本\n- `VariableSelectorProps`: 属性类型定义\n\n## 常见问题\n\n### 为什么需要 PrivateScopeProvider？\n\n`PrivateScopeProvider` 提供了变量作用域隔离机制，在以下场景中非常重要：\n\n1. **循环节点**：在 Loop 节点中，每次迭代都需要一个独立的作用域来存储循环变量（如 `item`, `index`）。参考[变量概念 - 节点私有作用域](../../guide/variable/concept#节点私有作用域)\n2. **嵌套结构**：当节点内部有嵌套的变量声明时，避免与外部变量产生命名冲突\n3. **组件复用**：相同的组件在不同上下文中使用时，确保变量不会互相干扰\n4. **数据安全**：私有作用域中的变量只能被当前节点及其子节点访问，确保数据安全性\n\n更多关于作用域链和变量访问权限的信息，请参考[变量概念 - 作用域链](../../guide/variable/concept#作用域链)。\n\n### BatchVariableSelector 与 VariableSelector 的区别？\n\n| 特性 | BatchVariableSelector | VariableSelector |\n|------|----------------------|------------------|\n| 变量类型过滤 | 固定为数组类型 | 可自定义 |\n| 作用域 | 自带私有作用域 | 使用当前作用域 |\n| 使用场景 | 批处理、循环等 | 通用变量选择 |\n| Schema 配置 | 不可配置 | 完全可配置 |\n\n### 如何获取选中变量的实际值？\n\nBatchVariableSelector 返回的是变量路径（`string[]`），如需获取实际值，需要在表单的 effect 中配合 [`provideBatchInputEffect`](../effects/provide-batch-input) 使用：\n\n```typescript\nexport const formMeta = {\n  render: YourFormRender,\n  effect: {\n    yourFieldName: provideBatchInputEffect,\n  },\n};\n```\n\n`provideBatchInputEffect` 会自动解析变量引用并注入到表单数据中。\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/blur-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, PlaceholderStory, DisabledStory } from 'components/form-materials/components/blur-input';\n\n# BlurInput\n\nBlurInput 是一个特殊的输入框组件，它只在输入框失焦时才会触发值的更新，适用于需要避免频繁触发更新操作的场景。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input\" defaultValue=\"Initial text\">\n        {({ field }) => (\n          <>\n            <BlurInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"Please enter text\"\n            />\n            <p className=\"mt-2\">Current value: {field.value}</p>\n            <p className=\"text-sm text-gray-500\">\n              Note: Value updates after clicking outside the input\n            </p>\n          </>\n        )}\n      </Field>\n    </>\n  )\n}\n```\n\n### 带占位符\n\n<PlaceholderStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input_placeholder\" defaultValue=\"\">\n        {({ field }) => (\n          <>\n            <BlurInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n              placeholder=\"This is an input field with placeholder\"\n            />\n            <p className=\"mt-2\">Current value: {field.value || 'Empty'}</p>\n          </>\n        )}\n      </Field>\n    </>\n  )\n}\n```\n\n### 禁用状态\n\n<DisabledStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"blur_input_disabled\" defaultValue=\"Disabled state text\">\n        {({ field }) => (\n          <BlurInput value={field.value} onChange={(value) => field.onChange(value)} disabled />\n        )}\n      </Field>\n    </>\n  )\n}\n```\n## API 参考\n\n### 属性\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| value | `string` | - | 输入框的值 |\n| onChange | `(value: string, event?: React.FocusEvent) => void` | - | 失焦时触发的回调函数 |\n| placeholder | `string` | - | 输入框占位符文本 |\n| disabled | `boolean` | `false` | 是否禁用输入框 |\n| ref | `React.RefObject<HTMLInputElement>` | - | 输入框的引用 |\n| 其他属性 | 继承自 [Semi UI Input 组件](https://semi.design/zh-CN/input/input) | - | 支持 Semi UI Input 组件的所有其他属性 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/blur-input\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/blur-input\n```\n\n### 目录结构讲解\n\n```\ncomponents/blur-input/\n└── index.tsx  # 组件实现文件\n```\n\n### 核心实现说明\n\nBlurInput 组件的核心实现非常简洁，主要包含以下几个部分：\n\n1. **状态管理**：使用 `useState` 维护内部值状态，与传入的 `value` 属性解耦\n2. **值同步**：使用 `useEffect` 在外部 `value` 变化时更新内部状态\n3. **延迟更新**：在用户输入时只更新内部状态，仅在 `onBlur` 事件触发时才调用外部传入的 `onChange` 回调\n\n这种实现方式确保了值的更新只在用户完成输入并点击外部区域时发生，有效减少了不必要的更新操作，特别适合需要进行表单验证或远程数据请求的场景。\n\n### 依赖梳理\n\n#### 第三方库\n\n[**Semi UI Input 组件**](https://semi.design/zh-CN/input/input) 提供基础的输入框功能支持，BlurInput 组件继承了其所有属性并添加了失焦更新的特性。\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/code-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/code-editor';\n\n# CodeEditor\n\nCodeEditor 是一个功能强大的代码编辑器组件，基于 CodeMirror 6 构建，支持多种编程语言的语法高亮和智能提示。它提供了 TypeScript、Python、SQL、Shell、JSON 等语言的专用编辑器版本。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { CodeEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string | undefined> name=\"code_editor\">\n        {({ field }) => (\n          <CodeEditor\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            languageId=\"typescript\"\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 不同语言的专用编辑器\n\n:::warning 注意事项\n单独引入对应语言的专用编辑器，可以减少打包所需要的依赖\n:::\n\n```tsx\n// TypeScript 编辑器\nimport { TypeScriptCodeEditor } from '@flowgram.ai/form-materials';\n\n// Python 编辑器\nimport { PythonCodeEditor } from '@flowgram.ai/form-materials';\n\n// SQL 编辑器\nimport { SQLCodeEditor } from '@flowgram.ai/form-materials';\n\n// Shell 编辑器\nimport { ShellCodeEditor } from '@flowgram.ai/form-materials';\n\n// JSON 编辑器\nimport { JsonCodeEditor } from '@flowgram.ai/form-materials';\n```\n\n\n## API 参考\n\n### CodeEditor Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `string` | - | 编辑器内容 |\n| `onChange` | `(value: string) => void` | - | 内容变化时的回调函数 |\n| `languageId` | `'python' \\| 'typescript' \\| 'shell' \\| 'json' \\| 'sql'` | - | 代码语言类型 |\n| `theme` | `'dark' \\| 'light'` | `'light'` | 编辑器主题 |\n| `placeholder` | `string` | - | 占位符文本 |\n| `activeLinePlaceholder` | `string` | - | 当前行的占位提示 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `mini` | `boolean` | `false` | 是否为迷你模式 |\n| `options` | `Options` | - | CodeMirror 配置选项 |\n\n### 语言支持\n\nCodeEditor 支持以下语言：\n\n- **typescript**: TypeScript/JavaScript\n- **python**: Python\n- **sql**: SQL\n- **shell**: Shell 脚本\n- **json**: JSON\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/code-editor\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/code-editor\n```\n\n### 目录结构讲解\n\n```\ncode-editor/\n├── index.tsx           # 统一导出文件\n├── editor.tsx          # 基础编辑器组件 BaseCodeEditor\n├── editor-all.tsx      # 全功能编辑器（已废弃）\n├── editor-ts.tsx       # TypeScript 编辑器\n├── editor-python.tsx   # Python 编辑器\n├── editor-sql.tsx      # SQL 编辑器\n├── editor-shell.tsx    # Shell 编辑器\n├── editor-json.tsx     # JSON 编辑器\n├── factory.tsx         # 编辑器工厂函数\n├── theme/              # 主题配置\n│   ├── dark.ts         # 暗色主题\n│   ├── light.ts        # 亮色主题\n│   └── index.ts        # 主题导出\n├── utils.ts            # 工具函数\n└── styles.css          # 样式文件\n```\n\n### 核心实现说明\n\n#### BaseCodeEditor\n\n基础编辑器组件，对 coze-editor 的简单封装，提供了基础的语法高亮、主题切换、迷你模式、只读模式等功能。\n\n:::warning 注意事项\nBaseCodeEditor 不包含任何语言逻辑的加载，语言逻辑是在专用编辑器中实现的。\n:::\n\n\n#### 专用编辑器\n每种语言都有对应的专用编辑器，通过动态导入实现按需加载：\n\n```typescript\nexport const loadTypescriptLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-typescript').then((module) => {\n    // TypeScript 语言加载逻辑\n  });\n\nexport const TypeScriptCodeEditor = CodeEditorFactory<true>(\n  loadTypescriptLanguage,\n  {\n    displayName: 'TypeScriptCodeEditor',\n    fixLanguageId: 'typescript',\n  }\n);\n```\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/coze-editor\n\n@flowgram.ai/coze-editor 是一个基于 CodeMirror 6 构建的代码编辑器组件。源代码见：[coze-dev/rush-dev](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor)\n\n- `createRenderer`: 创建编辑器\n- `preset-code`: 代码编辑器预设配置\n- `EditorProvider`: 编辑器上下文提供者\n- `ActiveLinePlaceholder`: 当前行占位符组件\n\n#### @codemirror/view\n- `EditorView`: CodeMirror 编辑器视图\n\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[CodeEditor 组件] --> B[选择 languageId \\n 或者专用编辑器]\n    B --React.lazy--> C[根据 languageId \\n 加载语言所需要的依赖]\n    C --> D[BaseCodeEditor]\n    D --> E[CozeEditor 编辑器]\n    E --> F[语法高亮]\n    E --> G[主题应用]\n    E --> H[事件处理]\n```\n\n### 性能优化\n\n- **按需加载**: 每种语言按需加载\n- **专用编辑器**: 推荐使用 XXXCodeEditor 而非通用 CodeEditor，以优化打包速度\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/condition-context.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/condition-context';\n\n# ConditionContext\n\nConditionContext 是一个条件配置的上下文管理系统，用于统一管理条件规则和操作符配置，为条件组件提供一致的配置环境。\n\n:::tip\n\nConditionContext 的条件配置上下文可以影响到以下物料：\n\n- [**ConditionRow**](./condition-row)\n- [**DBConditionRow**](./db-condition-row)\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport React from 'react';\nimport { Field } from '@flowgram.ai/free-layout-editor';\nimport {\n  ConditionProvider,\n  ConditionRow,\n  DBConditionRow,\n  type ConditionOpConfigs,\n  type IConditionRule\n} from '@flowgram.ai/form-materials';\n\nconst OPS: ConditionOpConfigs = {\n  cop: {\n    abbreviation: 'C',\n    label: 'Custom Operator',\n  },\n};\n\nconst RULES: Record<string, IConditionRule> = {\n  string: {\n    cop: { type: 'string' },\n  },\n};\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <ConditionProvider ops={OPS} rules={RULES}>\n        <Field<any | undefined> name=\"condition_row\">\n          {({ field }) => (\n            <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n          )}\n        </Field>\n        <Field<any | undefined> name=\"db_condition_row\">\n          {({ field }) => (\n            <DBConditionRow\n              options={[\n                {\n                  label: 'UserName',\n                  value: 'username',\n                  schema: { type: 'string' },\n                },\n              ]}\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n          )}\n        </Field>\n      </ConditionProvider>\n    </>\n  ),\n};\n```\n\n## API 参考\n\n### ConditionProvider\n\n条件配置的上下文提供者组件，用于统一管理条件规则和操作符配置。\n\n| 参数名 | 类型 | 必选 | 说明 |\n|--------|------|------|------|\n| `rules` | `Record<string, IConditionRule>` | 否 | 条件规则配置 |\n| `ops` | `ConditionOpConfigs` | 否 | 操作符配置 |\n| `children` | `React.ReactNode` | 是 | 子组件 |\n\n### useCondition\n\n获取条件配置的 Hook，根据数据类型和操作符获取相应的配置信息。\n\n具体使用可以参考 [ConditionRow 的源代码](https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/form-materials/src/components/condition-row/index.tsx) 中 `useCondition` 的使用。\n\n\n### ConditionPresetOp\n\n预设的操作符枚举，提供常用的比较操作符。\n\n| 枚举值 | 说明 | 缩写 |\n|--------|------|------|\n| `EQ` | 等于 | `=` |\n| `NEQ` | 不等于 | `≠` |\n| `GT` | 大于 | `>` |\n| `GTE` | 大于等于 | `>=` |\n| `LT` | 小于 | `<` |\n| `LTE` | 小于等于 | `<=` |\n| `IN` | 在集合中 | `∈` |\n| `NIN` | 不在集合中 | `∉` |\n| `CONTAINS` | 包含 | `⊇` |\n| `NOT_CONTAINS` | 不包含 | `⊉` |\n| `IS_EMPTY` | 为空 | `=` |\n| `IS_NOT_EMPTY` | 不为空 | `≠` |\n| `IS_TRUE` | 为真 | `=` |\n| `IS_FALSE` | 为假 | `=` |\n\n### 类型定义\n\n```typescript\n// 操作符配置\ninterface ConditionOpConfig {\n  label: string; // 操作符标签\n  abbreviation: string; // 操作符缩写\n  rightDisplay?: string; // 右侧显示文本（当右侧不是值时）\n}\n\n// 操作符配置集合\ntype ConditionOpConfigs = Record<string, ConditionOpConfig>;\n\n// 条件规则\ntype IConditionRule = Record<string, string | IJsonSchema | null>;\n\n// 条件规则工厂函数\ntype IConditionRuleFactory = (\n  schema?: IJsonSchema\n) => Record<string, string | IJsonSchema | null>;\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/condition-context\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/condition-context\n```\n\n### 目录结构讲解\n\n```\ncondition-context/\n├── context.tsx        # Context 相关实现\n├── hooks/             # 钩子函数目录\n│   └── use-condition.tsx  # useCondition 钩子实现\n├── index.tsx          # 统一导出文件\n├── op.ts              # 预设操作符定义\n└── types.ts           # 类型定义\n```\n\n### 核心实现说明\n\n#### ConditionContext 工作流程\n\n以下是 ConditionProvider 和 useCondition Hook 的工作流程时序图：\n\n```mermaid\nsequenceDiagram\n    participant App as 应用组件\n    participant Provider as ConditionProvider\n    participant Context as ConditionContext\n    participant Child as ConditionRow或DBConditionRow\n    participant Hook as useCondition Hook\n    participant TypeManager as useTypeManager\n\n    %% 初始化阶段\n    App->>Provider: 传入 rules 和 ops 配置\n    Provider->>Context: 创建 Context 并设置默认值\n    Provider->>Provider: 接收 rules 和 ops 参数\n    Provider->>Context: 更新 Context 中的配置\n    Provider-->>App: 渲染子组件\n\n    %% 使用阶段\n    Child->>Hook: 调用 useCondition\n    Hook->>Context: 通过 useConditionContext() 获取配置\n    Hook->>TypeManager: 获取类型管理器\n    Hook->>Hook: 合并用户规则和上下文规则\n    Hook->>Hook: 合并用户操作符和上下文操作符\n    Hook->>TypeManager: 根据 leftSchema 获取类型配置\n    Hook->>Hook: 计算当前类型的条件规则\n    Hook->>Hook: 生成可用的操作符选项列表\n    Hook->>Hook: 计算目标值的数据类型 Schema\n    Hook-->>Child: 返回 {rule, opConfig, opOptionList, targetSchema}\n    Child-->>App: 根据返回配置渲染条件组件\n\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): 国际化工具类\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/types/IJsonSchema): JSON Schema 类型定义\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/condition-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/condition-row';\n\n# ConditionRow\n\nConditionRow 是一个条件表达式组件，用于构建变量比较逻辑。它支持选择变量、选择比较操作符、输入比较值，能够根据变量类型自动适配可用的操作符和值类型。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/condition-row.png\" alt=\"Condition Row 组件\" style={{ width: '50%' }} />\n  *第一个条件为 query 变量包含 Hello Flow，第二个条件为 enable 变量为 true*\n</div>\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { ConditionRow } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any | undefined> name=\"condition_row\">\n        {({ field }) => (\n          <ConditionRow value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 带初始值的条件行\n\n```tsx\n<ConditionRow\n  value={{\n    left: { type: 'ref', content: 'user.age' },\n    operator: 'gt',\n    right: { type: 'constant', content: 18, schema: { type: 'number' } }\n  }}\n  onChange={(value) => console.log('条件变化:', value)}\n/>\n```\n\n\n## API 参考\n\n### ConditionRow Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `ConditionRowValueType` | - | 条件表达式值 |\n| `onChange` | `(value?: ConditionRowValueType) => void` | - | 条件变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `ruleConfig` | `{ ops?: ConditionOpConfigs; rules?: Record<string, IConditionRule> }` | - | 操作符和规则配置 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n\n### ConditionRowValueType\n\n```typescript\ninterface ConditionRowValueType {\n  left?: IFlowRefValue;           // 左侧变量引用\n  operator?: string;            // 操作符\n  right?: IFlowConstantRefValue; // 右侧常量值\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content: string; // 变量路径，如 \"user.name\"\n}\n\ninterface IFlowConstantRefValue {\n  type: 'constant';\n  content: any;           // 常量值\n  schema: IJsonSchema;  // 值的类型定义\n}\n```\n\n### 支持的比较操作符\n\n根据左侧变量的类型，ConditionRow 会自动提供相应的比较操作符：\n\n- **字符串类型**: equals, not_equals, contains, not_contains, starts_with, ends_with\n- **数字类型**: equals, not_equals, gt, gte, lt, lte\n- **布尔类型**: equals, not_equals\n- **数组类型**: contains, not_contains, empty, not_empty\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/condition-row\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/condition-row\n```\n\n### 目录结构讲解\n\n```\ncondition-row/\n├── index.tsx           # 主组件实现，包含 ConditionRow 核心逻辑\n├── types.ts            # 类型定义\n└── styles.css          # 样式文件\n```\n\n### 核心实现说明\n\n#### 变量类型推断\n组件会根据左侧选择的变量自动推断其 JSON Schema 类型：\n\n```typescript\nconst leftSchema = useMemo(() => {\n  if (!variable) return undefined;\n  return JsonSchemaUtils.astToSchema(variable.type, { drilldown: false });\n}, [variable?.type?.hash]);\n```\n\n#### 操作符动态适配\n通过 `useCondition` Hook 根据左侧变量类型获取可用的操作符：\n\n```typescript\nconst { rule, opConfig, opOptionList, targetSchema } = useCondition({\n  leftSchema,\n  operator,\n});\n```\n\n#### 右侧值类型自动匹配\n右侧输入框的类型会根据操作符和左侧变量类型自动匹配：\n\n```typescript\ntargetSchema ? (\n  <InjectDynamicValueInput\n    schema={targetSchema}\n    // ... 其他属性\n  />\n) : (\n  // 占位输入框\n)\n```\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/json-schema\n- `JsonSchemaUtils.astToSchema()`: 将 AST 类型转换为 JSON Schema\n- `IJsonSchema`: JSON Schema 类型定义\n\n#### @flowgram.ai/variable-core\n- `useScopeAvailable()`: 获取当前作用域的可用变量\n\n#### @flowgram.ai/i18n\n- `I18n`: 国际化支持\n\n#### 内部组件\n- [`InjectVariableSelector`](./variable-selector): 变量选择器\n- [`InjectDynamicValueInput`](./dynamic-value-input): 动态值输入组件\n- `useCondition`: 条件逻辑 Hook\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[ConditionRow 组件] --> B[选择左侧变量]\n    B --> C[推断变量类型]\n    C --> D[获取可用操作符]\n    D --> E[选择比较操作符]\n    E --> F[确定右侧值类型]\n    F --> G[输入比较值]\n    G --> H[onChange 回调]\n\n    J[变量选择器] --> B\n    K[动态值输入] --> G\n    L[条件上下文] --> D\n```\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/constant-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, FallbackRendererStory, CustomStrategyStory } from 'components/form-materials/components/constant-inputs';\n\n# ConstantInput\n\nConstantInput 是一个常量输入组件，它根据提供的 JSON Schema 自动选择合适的输入类型渲染器。\n\n组件支持自定义渲染策略和兜底渲染器，能够处理各种数据类型的常量输入。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"constant_string\" defaultValue=\"Hello World\">\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n:::tip{title=\"注册新类型的常量输入器\"}\n\n参考 [类型管理](../common/json-schema-preset)，在类型注册的时候为类型配置常量输入器\n\n:::\n\n### 兜底渲染器\n\n当组件无法找到合适的渲染器时，会使用兜底渲染器：\n\n<FallbackRendererStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"constant_fallback\" defaultValue={{ custom: 'data' }}>\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'custom-unsupported-type' }}\n            fallbackRenderer={({ value, onChange, readonly }) => (\n              <div style={{ padding: '8px', background: '#f0f0f0', border: '1px dashed #ccc' }}>\n                <p>Fallback renderer for unsupported type</p>\n              </div>\n            )}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 自定义策略\n\n使用自定义渲染策略来覆盖默认行为：\n\n<CustomStrategyStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string> name=\"constant_custom\" defaultValue=\"自定义值\">\n        {({ field }) => (\n          <ConstantInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'object' }}\n            strategies={[\n              {\n                hit: (schema) => schema.type === 'object',\n                Renderer: ({ value, onChange, readonly }) => (\n                  <p>Object is not supported now</p>\n                ),\n              },\n            ]}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### ConstantInput Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `any` | - | 输入值 |\n| `onChange` | `(value: any) => void` | - | 值变化时的回调函数 |\n| `schema` | `IJsonSchema` | - | 用于确定渲染器的 JSON Schema |\n| `strategies` | `Strategy[]` | - | 自定义渲染策略数组 |\n| `fallbackRenderer` | `React.FC<ConstantRendererProps>` | - | 当没有合适渲染器时使用的兜底渲染器 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n\n### Strategy 接口\n\n```typescript\ninterface Strategy<Value = any> {\n  hit: (schema: IJsonSchema) => boolean;\n  Renderer: React.FC<ConstantRendererProps<Value>>;\n}\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/constant-input\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/constant-input\n```\n\n### 目录结构讲解\n\n```\nconstant-input/\n├── index.tsx    # 主组件实现，包含 ConstantInput 核心逻辑\n└── types.ts     # 类型定义，包括 Strategy 接口和 PropsType\n```\n\n### 核心实现说明\n\n#### 渲染器选择逻辑\n\n组件通过以下步骤选择合适的渲染器：\n\n1. **策略匹配**：首先检查 `strategies` 数组中是否有匹配当前 schema 的策略\n2. **类型管理器**：如果没有匹配的策略，则使用类型管理器 (`useTypeManager`) 获取对应 schema 的渲染器\n3. **兜底渲染**：如果以上都失败，使用提供的 `fallbackRenderer` 或默认的禁用输入框\n\n\n#### 类型系统集成\n\nConstantInput 与 [**物料类型管理**](../common/json-schema-preset) 深度集成：\n\n- 通过 `useTypeManager` Hook 获取类型管理器\n- 使用 `typeManager.getTypeBySchema(schema)` 获取对应类型的渲染器\n- 支持所有 JSON Schema 标准类型（string, number, boolean, object, array 等）\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): 类型管理器 Hook\n\n#### 第三方库\n\n[**Semi UI**](https://semi.design/zh-CN)\n- 基础输入组件库，提供默认的输入控件\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/coze-editor-extensions.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory as PromptWithVariablesStory } from 'components/form-materials/components/prompt-editor-with-variables';\nimport { BasicStory as PromptWithInputsStory } from 'components/form-materials/components/prompt-editor-with-inputs';\n\n# CozeEditorExtensions\n\nCozeEditorExtensions 是一组基于 [coze-editor](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor) 的功能扩展，提供变量、Inputs 选择器与变量标签回显能力。\n\n- `EditorVariableTree`：监听 `@`/`{` 等触发字符，弹出可用变量树并将选中项写入编辑器。\n- `EditorVariableTagInject`：对 `{{variable.path}}` 文本进行标记渲染，展示变量图标、标题与回显提示。\n- `EditorInputsTree`：基于节点 `inputsValues` 构造层级树，支持在提示词中插入 `{{inputs.xxx}}` 引用。\n\n## 案例演示\n\n### 变量树 + 标签回显\n\n<PromptWithVariablesStory />\n\n```tsx pure title=\"prompt-editor-with-extensions.tsx\"\nimport {\n  PromptEditor,\n  EditorVariableTree,\n  EditorVariableTagInject,\n  EditorInputsTree,\n} from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <PromptEditor value={value} onChange={onChange}>\n      <EditorVariableTree triggerCharacters={TRIGGER_CHARACTERS} />\n      <EditorVariableTagInject />\n    </PromptEditor>\n  );\n}\n```\n\n:::tip{title=\"触发方式\"}\n`EditorVariableTree` 默认监听 `{`、`{{` 和 `@`，你可以通过 `triggerCharacters` 自定义触发字符。\n:::\n\n### 节点 Inputs 引用\n\n<PromptWithInputsStory />\n\n\n```tsx pure title=\"prompt-editor-with-extensions.tsx\"\nimport {\n  PromptEditor,\n  EditorVariableTree,\n  EditorVariableTagInject,\n  EditorInputsTree,\n} from '@flowgram.ai/form-materials';\n\n\nexport function PromptEditorWithExtensions({ value, onChange, inputsValues }) {\n  return (\n    <PromptEditor value={value} onChange={onChange}>\n      <EditorInputsTree inputsValues={inputsValues} />\n    </PromptEditor>\n  );\n}\n```\n\n\n### 渲染键与二次扩展\n\nCozeEditorExtensions 通过 [`createInjectMaterial`](../common/inject-material) 声明了固定的 `renderKey`：\n\n- `EditorVariableTree.renderKey = 'EditorVariableTree'`\n- `EditorVariableTagInject.renderKey = 'EditorVariableTagInject'`\n- `EditorInputsTree.renderKey = 'EditorInputsTree'`\n\n你可以在 `use-editor-props` 中注册同名渲染器，以替换默认行为。例如自定义变量选择面板：\n\n```tsx pure title=\"override-variable-tree.tsx\"\nimport { useEditorProps } from '@flowgram.ai/editor';\nimport { EditorVariableTree } from '@flowgram.ai/form-materials';\nimport { CustomVariableTree } from './custom-variable-tree';\n\nexport function useCustomEditorProps() {\n  return useEditorProps({\n    materials: {\n      components: {\n        [EditorVariableTree.renderKey!]: CustomVariableTree,\n      },\n    },\n  });\n}\n```\n\n\n## API 参考\n\n### EditorVariableTree Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `triggerCharacters` | `string[]` | `['{', '{}', '@']` | 触发变量树弹出的字符集 |\n\n### EditorVariableTagInject Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| _无_ | - | - | 组件无额外属性，渲染即生效 |\n\n### EditorInputsTree Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `inputsValues` | `IInputsValues` | - | 节点 Inputs 的键值数据，支持 `FlowValue` 引用 |\n| `triggerCharacters` | `string[]` | `['{', '{}', '@']` | 触发 Inputs 选择器的字符集 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/coze-editor-extensions\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/coze-editor-extensions\n```\n\n### 目录结构讲解\n\n```\ncoze-editor-extensions/\n├── index.tsx                 # 导出可注入物料\n├── extensions/\n│   ├── inputs-tree.tsx       # Inputs Tree 实现\n│   ├── variable-tag.tsx      # 变量标签渲染\n│   └── variable-tree.tsx     # 变量树选择器\n└── styles.css                # 标签样式与弹窗样式\n```\n\n### 核心实现说明\n\n**EditorVariableTree**\n- 弹窗定位与滚动同步：`Mention` + `PositionMirror` 组合确保 Popover 始终贴合光标位置，滚动时会通过 `rePosKey` 触发重新定位\n- 变量树数据来源：`useVariableTree` 读取 `Scope.available` 中注册的变量，自动应用 schema 过滤、禁用状态和图标\n\n**EditorVariableTagInject**\n- 标签渲染与订阅：使用 CodeMirror `MatchDecorator` 将 `{{xxx}}` 文本替换为 `Tag` 小部件，并订阅变量标题/Icon 更新，实现实时回显\n\n**EditorInputsTree**\n- Inputs 树构造：支持 `FlowValue` 与普通对象，递归生成层级节点，并在插入前统一格式化为 `{{path}}`\n\n### 依赖梳理\n\n#### 其他物料\n\n[**InjectMaterial**](../common/inject-material)\n- `createInjectMaterial`: 创建可注入的物料组件\n\n#### 第三方库\n\n[**coze-editor**](https://github.com/coze-dev/rush-arch/tree/main/packages/text-editor)\n- 基础编辑器组件\n\n[**CodeMirror MatchDecorator**](https://codemirror.net/docs/ref/#search.MatchDecorator)\n- 用于变量标签的文本匹配和替换\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/db-condition-row.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/db-condition-row';\n\n# DBConditionRow\n\nDBConditionRow 是一个数据库条件行组件，用于构建数据库查询条件。它提供了字段选择、操作符选择和值输入功能，可以根据字段类型自动显示合适的操作符和输入控件。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DBConditionRow } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any | undefined> name=\"db_condition_row\">\n        {({ field }) => (\n          <DBConditionRow\n            options={[\n              {\n                label: 'TransactionID',\n                value: 'transaction_id',\n                schema: { type: 'integer' },\n              },\n              {\n                label: 'Amount',\n                value: 'amount',\n                schema: { type: 'number' },\n              },\n              {\n                label: 'Description',\n                value: 'description',\n                schema: { type: 'string' },\n              },\n              {\n                label: 'Archived',\n                value: 'archived',\n                schema: { type: 'boolean' },\n              },\n              {\n                label: 'CreateTime',\n                value: 'create_time',\n                schema: { type: 'date-time' },\n              },\n            ]}\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### DBConditionRow Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `DBConditionRowValueType` | - | 条件行的值，包含 left（字段名）、operator（操作符）和 right（值） |\n| `onChange` | `(value?: DBConditionRowValueType) => void` | - | 值变化时的回调函数 |\n| `options` | `DBConditionOptionType[]` | - | 可选字段列表，每个字段包含 label、value 和 schema |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n| `ruleConfig` | `{ ops?: ConditionOpConfigs; rules?: Record<string, IConditionRule> }` | - | **已废弃**，使用 ConditionContext 替代 |\n\n### DBConditionRowValueType 类型\n\n```typescript\ninterface DBConditionRowValueType {\n  left?: string; // 字段名\n  operator?: string; // 操作符\n  right?: IFlowConstantRefValue; // 值，支持常量或变量引用\n}\n```\n\n### DBConditionOptionType 类型\n\n```typescript\ninterface DBConditionOptionType {\n  label: string | JSX.Element; // 字段显示名称\n  value: string; // 字段值\n  schema: IJsonSchema; // 字段类型定义\n}\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/db-condition-row\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/db-condition-row\n```\n\n### 目录结构讲解\n\n```\ndb-condition-row/\n├── index.tsx    # 主组件实现\n├── types.ts     # 类型定义\n└── styles.css   # 样式文件\n```\n\n### 核心实现说明\n\n#### 组件实现流程\n\n```mermaid\nsequenceDiagram\n    participant 用户\n    participant 左值\n    participant useCondition\n    participant 操作符\n    participant 右值\n\n    用户->>左值: 选择字段\n    左值->>左值: 更新 left 值\n    左值->>useCondition: 触发条件计算\n    useCondition->>useCondition: 获取字段类型信息\n    useCondition-->>操作符: 返回可用操作符列表\n    用户->>操作符: 选择操作符\n    操作符->>操作符: 更新 operator 值\n    操作符->>useCondition: 重新计算目标类型\n    useCondition-->>右值: 返回 targetSchema\n\n    alt 存在 targetSchema\n        右值->>右值: 渲染 InjectDynamicValueInput\n        用户->>右值: 输入或选择值\n        右值->>右值: 更新 right 值\n    else 不存在 targetSchema\n        右值->>右值: 渲染禁用的输入框\n        右值->>右值: 显示默认提示文本\n    end\n\n    右值-->>用户: 返回完整条件对象\n```\n\n1. **左值：字段选择器**：使用 Semi UI 的 Select 组件，根据 options 显示可用字段，并展示字段类型图标。\n\n2. **操作符：操作符选择器**：通过 useCondition Hook 获取与字段类型匹配的操作符列表，用户选择后更新 operator。\n\n3. **右值：值输入组件**：根据选择的字段类型和操作符，动态显示合适的输入控件：\n   - 当存在 targetSchema 时，使用 InjectDynamicValueInput 组件\n   - 否则显示禁用的输入框，展示提示信息\n\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): 国际化工具类\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n\n#### 其他物料\n\n[**ConditionContext**](./condition-context)\n- `useCondition`: 获取条件配置的 Hook\n- `ConditionOpConfigs`: 操作符配置类型\n- `IConditionRule`: 条件规则类型\n\n[**DynamicValueInput**](./dynamic-value-input)\n- `InjectDynamicValueInput`: 可注入的动态值输入组件\n\n#### 第三方库\n\n[**Semi UI**](https://semi.design/zh-CN/)\n- [`Select`](https://semi.design/zh-CN/input/select): 选择器组件\n- [`Button`](https://semi.design/zh-CN/basic/button): 按钮组件\n- [`Input`](https://semi.design/zh-CN/input/input): 输入框组件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/display-flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-flow-value';\n\n# DisplayFlowValue\n\nDisplayFlowValue 是一个用于可视化展示 [Flow Value](../common/flow-value) 数据类型的组件，它能够根据 Flow Value 的类型推断出相应的 JSON Schema，并通过友好的界面展示给用户。\n\n## 案例演示\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => <DisplayFlowValue value={field.value} title=\"Display Flow Value\" />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### Props\n\n| 属性名 | 类型 | 必填 | 默认值 | 描述 |\n| --- | --- | --- | --- | --- |\n| value | `IFlowValue` | 否 | - | 要显示的 Flow Value 数据 |\n| title | `string \\| JSX.Element` | 否 | - | 显示在标签上的标题文本 |\n| showIconInTree | `boolean` | 否 | - | 是否在 Schema 树中显示图标 |\n| typeManager | `JsonSchemaTypeManager` | 否 | - | 用于获取类型显示信息的管理器 |\n\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-flow-value\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-flow-value\n```\n\n### 核心实现说明\n\nDisplayFlowValue 组件的主要功能是将 Flow Value 转换为 JSON Schema 并可视化展示。核心实现包括：\n\n1. **类型推断**：根据不同类型的 Flow Value 推断出对应的 JSON Schema\n   - 对于引用类型，从作用域中获取变量的类型信息\n   - 对于模板类型，默认推断为字符串类型\n   - 对于常量类型，根据常量值推断其类型\n\n2. **可视化展示**：通过 DisplaySchemaTag 组件展示推断出的 Schema\n   - 使用半 UI 的 Popover 和 Tag 组件\n   - 点击标签可以查看详细的 Schema 树形结构\n\n3. **错误处理**：当引用类型指向的变量不存在时，会显示警告颜色\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- `JsonSchemaTypeManager`: 用于获取类型显示信息\n- `JsonSchemaUtils`: 提供 JSON Schema 相关的工具函数\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `useScopeAvailable`: 获取当前作用域中可用的变量\n\n#### 其他物料\n\n[**DisplaySchemaTag**](./display-schema-tag) 用于展示 JSON Schema 的标签组件\n\n[**FlowValueUtils**](../common/flow-value)\n- [FlowValueUtils.inferJsonSchema](../common/flow-value#schema-推断函数)\n\n#### 第三方库\n\n[**Semi UI**](https://semi.design/zh-CN/)\n- `Popover`: 弹出框组件\n- `Tag`: 标签组件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/display-inputs-values.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-inputs-values';\n\n# DisplayInputsValues\n\nDisplayInputsValues 是一个用于可视化展示树状结构输入值的组件。\n\n它能够**递归**地遍历输入值对象，对于解析其中 [Flow Value](../common/flow-value) 的类型，并推导出每一个字段的 Json Schema 结构。\n\n:::tip\n\nDisplayInputsValues 支持展示 [InputsValues](./inputs-values) 和 [InputsValuesTree](./inputs-values-tree) 两个组件配置的值。\n\n:::\n\n## 案例演示\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValuesTree, DisplayInputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => <DisplayInputsValues value={field.value} />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### Props\n\n| 属性名 | 类型 | 必填 | 默认值 | 描述 |\n| --- | --- | --- | --- | --- |\n| value | `IInputsValues` | 否 | - | 要展示的输入值数据 |\n| showIconInTree | `boolean` | 否 | - | 是否在 Schema 树中显示图标 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-inputs-values\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-inputs-values\n```\n\n### 核心实现说明\n\nDisplayInputsValues 组件的主要功能是递归地展示树状结构的输入值。核心实现包括：\n\n1. **数据遍历**：遍历输入的 value 对象，处理每一个键值对\n2. **类型区分**：\n   - 对于 Flow Value 类型的值，使用 DisplayFlowValue 组件进行展示\n   - 对于普通对象类型的值，使用 DisplayInputsValueAllInTag 组件展示其 JSON Schema 结构\n3. **递归展示**：支持展示多层嵌套的数据结构\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `useScopeAvailable`: 获取当前作用域中可用的变量\n\n[**@flowgram.ai/form-materials**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials)\n- `FlowValueUtils`: Flow Value 相关工具函数\n- `DisplayFlowValue`: 展示 Flow Value 的组件\n- `DisplaySchemaTag`: 展示 JSON Schema 的标签组件\n\n#### 第三方库\n\n[**lodash-es**](https://lodash.com/)\n- `isPlainObject`: 判断是否为普通对象\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/display-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { DisplayFromScopeStory, DisplayFromFieldStory } from 'components/form-materials/components/display-outputs';\n\n# DisplayOutputs\n\nDisplayOutputs 是一个用于可视化展示节点输出变量的组件，支持从作用域自动获取输出变量或通过字段值手动指定输出模式。\n\n## 案例演示\n\n### 从作用域获取输出变量\n\n当设置 `displayFromScope` 为 `true` 时，组件会自动从当前节点作用域获取输出变量并展示：\n\n<DisplayFromScopeStory />\n\n```tsx pure title=\"form-meta.tsx\" {27}\nimport { DisplayOutputs, provideJsonSchemaOutputs } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined> name=\"outputs\">\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <p>Display Output Schema:</p>\n      <DisplayOutputs displayFromScope />\n    </>\n  ),\n  effect: {\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n### 通过字段值指定输出模式\n\n当不设置 `displayFromScope` 时，组件通过 `value` 属性接收输出的 JSON Schema：\n\n<DisplayFromFieldStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplayOutputs } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined> name=\"outputs\">\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <p>Display Output Schema:</p>\n      <Field<IJsonSchema | undefined> name=\"outputs\">\n        {({ field }) => <DisplayOutputs value={field.value} />}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### DisplayOutputs Props\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| value | `IJsonSchema` | - | 要展示的 JSON Schema 对象，当 `displayFromScope` 为 `false` 时使用 |\n| displayFromScope | `boolean` | `false` | 是否从当前作用域自动获取输出变量 |\n| showIconInTree | `boolean` | `false` | 是否在树形结构中显示图标 |\n| typeManager | `JsonSchemaTypeManager` | - | 自定义类型管理器 |\n| style | `React.CSSProperties` | - | 自定义样式 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-outputs\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials display-outputs\n```\n\n### 目录结构讲解\n\n```plaintext\ndisplay-outputs/\n├── index.tsx    # 组件主入口，实现 DisplayOutputs 组件\n└── styles.css   # 组件样式文件\n```\n\n### 核心实现说明\n\nDisplayOutputs 组件的核心逻辑：\n\n1. **作用域模式** (`displayFromScope=true`): 使用 `useCurrentScope` Hook 获取当前作用域的输出变量，通过 `scope?.output.variables` 获取变量列表并转换为 JSON Schema 属性\n\n2. **字段模式** (`displayFromScope=false`): 直接使用 `value` 属性传入的 JSON Schema 对象\n\n3. **变量监听**: 在作用域模式下，组件会监听输出变量的变化并自动刷新显示\n\n4. **渲染实现**: 将 JSON Schema 的每个属性渲染为 `DisplaySchemaTag` 组件，支持类型图标和警告状态显示\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`useCurrentScope`](https://flowgram.ai/auto-docs/editor/functions/useCurrentScope): 获取当前作用域的 Hook\n- [`useRefresh`](https://flowgram.ai/api/hooks/use-refresh): 强制组件刷新的 Hook\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/common/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n- [`JsonSchemaUtils`](https://flowgram.ai/auto-docs/json-schema/modules/JsonSchemaUtils): JSON Schema 工具函数\n- [`JsonSchemaTypeManager`](https://flowgram.ai/auto-docs/json-schema/classes/JsonSchemaTypeManager): 类型管理器\n\n#### 其他物料\n\n[**DisplaySchemaTag**](./display-schema-tag)\n- `DisplaySchemaTag`: 用于展示单个 JSON Schema 属性的标签组件\n\n#### 第三方库\n\n[**React**](https://react.dev/)\n- `useLayoutEffect`: 用于监听作用域变量变化\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/display-schema-tag.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-schema-tag';\n\n# DisplaySchemaTag\n\nDisplaySchemaTag 是一个用于展示 JSON Schema 的标签组件，当用户点击标签时，会弹出详细的 schema 树形结构，方便查看复杂的数据结构。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplaySchemaTag } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <DisplaySchemaTag\n        title=\"Transaction\"\n        value={{\n          type: 'object',\n          properties: {\n            transaction_id: { type: 'integer' },\n            amount: { type: 'number' },\n            description: { type: 'string' },\n            archived: { type: 'boolean' },\n            owner: {\n              type: 'object',\n              properties: {\n                id: { type: 'integer' },\n                username: { type: 'string' },\n                friends: {\n                  type: 'array',\n                  items: {\n                    type: 'object',\n                    properties: {\n                      id: { type: 'integer' },\n                      username: { type: 'string' },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        }}\n      />\n    </>\n  ),\n}\n```\n\n## API 参考\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| title | `JSX.Element \\| string` | - | 标签显示的标题文本 |\n| value | `IJsonSchema` | `{}` | 要展示的 JSON Schema 对象 |\n| showIconInTree | `boolean` | - | 是否在弹出的树形结构中显示图标 |\n| warning | `boolean` | `false` | 是否显示警告状态（黄色标签） |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-schema-tag\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-schema-tag\n```\n\n### 目录结构讲解\n\n```plaintext\ncomponents/display-schema-tag/\n├── index.tsx  # 组件主要实现文件\n└── styles.css # 组件样式文件\n```\n\n### 核心实现说明\n\nDisplaySchemaTag 组件基于 Semi UI 的 Popover 和 Tag 组件实现，内部集成了 DisplaySchemaTree 来在弹出框中展示详细的 schema 结构。\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): 类型管理器 Hook\n\n\n#### 其他物料\n\n[**DisplaySchemaTree**](./display-schema-tree) 用于在弹出框中展示详细的 schema 树形结构\n\n#### 第三方库\n\n[**Semi UI**](https://semi.design/zh-CN)\n- `Popover`: 弹出框组件\n- `Tag`: 标签组件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/display-schema-tree.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/display-schema-tree';\n\n# DisplaySchemaTree\n\nDisplaySchemaTree 是一个用于可视化展示 JSON Schema 结构的树形组件，可以递归地展示对象、数组等复杂数据结构的层级关系。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DisplaySchemaTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <DisplaySchemaTree\n        value={{\n          type: 'object',\n          properties: {\n            transaction_id: { type: 'integer' },\n            amount: { type: 'number' },\n            description: { type: 'string' },\n            archived: { type: 'boolean' },\n            owner: {\n              type: 'object',\n              properties: {\n                id: { type: 'integer' },\n                username: { type: 'string' },\n                friends: {\n                  type: 'array',\n                  items: {\n                    type: 'object',\n                    properties: {\n                      id: { type: 'integer' },\n                      username: { type: 'string' },\n                    },\n                  },\n                },\n              },\n            },\n          },\n        }}\n      />\n    </>\n  ),\n}\n```\n\n## API 参考\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| value | `IJsonSchema` | `{}` | 要展示的 JSON Schema 对象 |\n| drilldown | `boolean` | `true` | 是否展开嵌套的属性结构 |\n| showIcon | `boolean` | `true` | 是否显示类型图标 |\n| typeManager | `JsonSchemaTypeManager` | - | 类型管理器，用于获取类型相关信息 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/display-schema-tree\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/display-schema-tree\n```\n\n### 目录结构讲解\n\n```plaintext\ncomponents/display-schema-tree/\n├── index.tsx  # 组件主要实现文件\n└── styles.css # 组件样式文件\n```\n\n### 核心实现说明\n\nDisplaySchemaTree 组件通过递归渲染的方式展示 JSON Schema 的层级结构。\n\n它使用 `useTypeManager` 获取类型相关信息，包括图标、显示文本等。组件内部递归渲染 Schema 的子属性，支持多级嵌套的数据结构展示。\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): 类型管理器 Hook\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/dynamic-value-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithSchemaStory } from 'components/form-materials/components/dynamic-value-input';\n\n# DynamicValueInput\n\nDynamicValueInput 是一个动态值输入组件，支持常量和变量两种输入模式。它可以根据提供的 schema 自动选择合适的输入类型，并提供变量选择功能。组件能够智能地在常量输入和变量选择之间切换。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/dynamic-value-input.png\" alt=\"DynamicValueInput 组件\" style={{ width: '50%' }} />\n</div>\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DynamicValueInput } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 带 Schema 约束\n\n<WithSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { DynamicValueInput } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field }) => (\n          <DynamicValueInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### DynamicValueInput Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `IFlowConstantRefValue` | - | 输入值，支持常量或变量引用 |\n| `onChange` | `(value?: IFlowConstantRefValue) => void` | - | 值变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n| `schema` | `IJsonSchema` | - | 约束输入类型的 JSON Schema |\n| `constantProps` | `ConstantInputProps` | - | 传递给常量输入组件的额外属性 |\n\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/dynamic-value-input\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/dynamic-value-input\n```\n\n### 目录结构讲解\n\n```\ndynamic-value-input/\n├── index.tsx           # 主组件实现，包含 DynamicValueInput 核心逻辑\n├── hooks.ts            # 自定义 Hooks，处理变量引用和 schema 选择\n└── styles.css          # 样式文件\n```\n\n### 核心实现说明\n\n#### 变量引用处理\n通过 `useRefVariable` Hook 获取变量引用信息：\n\n```typescript\nconst refVariable = useRefVariable(value);\n```\n\n#### Schema 选择管理\n通过 `useSelectSchema` Hook 管理类型选择：\n\n```typescript\nconst [selectSchema, setSelectSchema] = useSelectSchema(\n  schemaFromProps,\n  constantProps,\n  value\n);\n```\n\n#### 模式切换逻辑\n组件通过判断 `value.type` 来决定渲染常量输入还是变量选择器：\n\n```typescript\nif (value?.type === 'ref') {\n  // 渲染变量选择器\n  return <InjectVariableSelector />;\n} else {\n  // 渲染常量输入\n  return <ConstantInput />;\n}\n```\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[DynamicValueInput 组件] --> B{判断输入模式}\n    B -->|常量模式| C[渲染常量输入]\n    B -->|变量模式| D[渲染变量选择器]\n\n    C --> E[根据 schema 选择输入类型]\n    D --> F[显示可用变量列表]\n\n    E --> G[用户输入值]\n    F --> H[用户选择变量]\n\n    G --> I[生成常量值]\n    H --> J[生成变量引用]\n\n    I --> K[onChange 回调]\n    J --> K\n\n```\n\n### 使用到的 flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`JsonSchemaUtils`](https://flowgram.ai/auto-docs/json-schema/modules/JsonSchemaUtils): JSON Schema 工具类\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n- [`useTypeManager`](https://flowgram.ai/auto-docs/json-schema/functions/useTypeManager): 类型管理器 Hook\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/variable-engine/core)\n- [`useScopeAvailable`](https://flowgram.ai/auto-docs/variable-core/functions/useScopeAvailable): 获取当前作用域的可用变量\n\n\n### 依赖的其他物料\n\n[**TypeSelector**](./type-selector) 类型选择器\n\n[**ConstantInput**](./constant-input) 常量输入组件\n\n[**VariableSelector**](./variable-selector) 变量选择器\n- `InjectVariableSelector`: 依赖注入变量选择器\n\n\n[**FlowValue**](../common/flow-value)\n- `IFlowConstantRefValue`: 常量或变量引用值类型\n\n[**InjectMaterial**](../common/inject-material) 可注入物料组件\n- `createInjectMaterial`: 创建可注入的物料组件\n\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/inputs-values-tree.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/inputs-values-tree';\n\n# InputsValuesTree\n\nInputsValuesTree 是一个用于展示和编辑**树状结构输入值**的组件，每个叶子节点为一个键值对，值支持常量和变量两种输入模式，通过 DynamicValueInput 组件实现灵活的输入方式。组件采用树形层级展示，支持节点的展开和折叠，适用于构建复杂的嵌套数据结构。\n\n:::tip{title=\"和 InputsValues 的区别\"}\n\n- **结构差异**：InputsValues 仅支持一级键值对列表，而 InputsValuesTree 支持树形嵌套结构\n- **展示方式**：InputsValues 使用简单的行列表展示，InputsValuesTree 使用树形结构展示，带有缩进和展开/折叠功能\n- **适用场景**：InputsValues 适用于简单的键值对配置，InputsValuesTree 适用于复杂的多层级数据结构配置\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValuesTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputs_values\"\n        defaultValue={{\n          a: {\n            b: {\n              type: 'ref',\n              content: ['start_0', 'str'],\n            },\n            c: {\n              type: 'constant',\n              content: 'hello',\n            },\n          },\n          d: {\n            type: 'constant',\n            content: '{ \"a\": \"b\"}',\n            schema: { type: 'object' },\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| value | `IInputsValues` | - | 树状结构的输入值对象 |\n| onChange | `(value?: IInputsValues) => void` | - | 值变化时的回调函数 |\n| readonly | `boolean` | `false` | 是否只读模式 |\n| hasError | `boolean` | `false` | 是否显示错误状态 |\n| schema | `IJsonSchema` | - | JSON Schema 定义，用于验证和类型提示 |\n| style | `React.CSSProperties` | - | 自定义样式 |\n| constantProps | `{ strategies?: ConstantInputStrategy[]; [key: string]: any }` | - | 常量输入组件的配置属性 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/inputs-values-tree\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/inputs-values-tree\n```\n\n### 目录结构讲解\n\n```plaintext\ncomponents/inputs-values-tree/\n├── index.tsx          # 组件入口文件\n├── row.tsx            # 树行组件，处理单个节点的展示和编辑\n├── types.ts           # 类型定义文件\n├── icon.tsx           # 图标组件\n├── styles.css         # 组件样式文件\n└── hooks/\n    └── use-child-list.tsx # 处理子节点列表的自定义 Hook\n```\n\n### 核心实现说明\n\nInputsValuesTree 组件主要用于展示和编辑树状结构的输入值，支持两种类型的值：常量值（constant）和引用值（ref）。\n\n#### 工作流程时序图\n\n```mermaid\nsequenceDiagram\n    participant User as 用户\n    participant Tree as InputsValuesTree\n    participant ObjectList as useObjectList\n    participant Row as InputValueRow\n    participant ChildList as useChildList\n    participant DynamicInput as InjectDynamicValueInput\n\n    User->>Tree: 提供初始树状数据\n    Tree->>ObjectList: 初始化顶层数据\n    ObjectList-->>Tree: 返回列表操作方法\n    Tree->>Row: 渲染每个顶层节点\n    Row->>ChildList: 检查是否有子节点\n    ChildList-->>Row: 返回子节点列表和操作方法\n\n    alt 用户展开节点\n        User->>Row: 点击展开按钮\n        Row->>Row: 切换collapse状态\n        Row->>Row: 渲染子节点列表\n    end\n\n    alt 用户添加节点\n        User->>Row: 点击添加按钮\n        Row->>ObjectList: 调用add方法\n        ObjectList-->>Tree: 更新数据\n        Tree-->>User: 触发onChange回调\n    end\n\n    alt 用户修改节点值\n        User->>DynamicInput: 编辑值\n        DynamicInput->>Row: 调用onUpdateValue\n        Row->>ObjectList: 更新节点值\n        ObjectList-->>Tree: 更新数据\n        Tree-->>User: 触发onChange回调\n    end\n```\n\n核心功能特点：\n\n1. **树状结构展示**：通过递归的方式展示嵌套的树状数据结构\n2. **值类型支持**：支持常量值和引用值两种类型，常量值可以是字符串、数字、布尔值等，引用值指向工作流中的其他节点\n3. **增删改操作**：支持添加、删除、修改节点的键名和值\n4. **可配置性**：通过 constantProps 属性可以自定义常量输入组件的行为\n\n组件内部使用 useObjectList 钩子管理对象列表，使用 InputValueRow 组件渲染每一行数据。当用户点击添加按钮时，会默认添加一个空的字符串类型常量值。\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`I18n`](https://flowgram.ai/auto-docs/editor/variables/I18n): 国际化工具类\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n\n#### 第三方库\n\n[**Semi UI**](https://semi.design/zh-CN)\n- `Button`: 按钮组件\n- `IconPlus`: 加号图标组件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/inputs-values.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithSchemaStory } from 'components/form-materials/components/inputs-values';\n\n# InputsValues\n\nInputsValues 是一个键值对输入列表组件，用于收集和管理一组输入参数。每个键值对都支持常量和变量两种输入模式，通过 DynamicValueInput 组件实现灵活的输入方式。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 带 Schema 约束\n\n<WithSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { InputsValues } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined> name=\"inputs_values\">\n        {({ field }) => (\n          <InputsValues\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            schema={{\n              type: 'string',\n            }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 自定义常量输入策略\n通过 `constantProps` 可以自定义每个值的输入行为：\n\n```typescript\nconst customStrategies = [\n  {\n    type: 'string',\n    render: (props) => <CustomStringInput {...props} />\n  },\n  {\n    type: 'number',\n    render: (props) => <CustomNumberInput {...props} />\n  }\n];\n\n<InputsValues\n  constantProps={{\n    strategies: customStrategies\n  }}\n/>\n```\n\n\n## API 参考\n\n### InputsValues Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `Record<string, IFlowValue \\| undefined>` | - | 键值对数据 |\n| `onChange` | `(value?: Record<string, IFlowValue \\| undefined>) => void` | - | 数据变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n| `schema` | `IJsonSchema` | - | 约束所有值类型的 JSON Schema |\n| `constantProps` | `ConstantInputProps` | - | 传递给 DynamicValueInput 的额外属性 |\n\n### 数据结构\n\n```typescript\ninterface PropsType {\n  value?: Record<string, IFlowValue | undefined>;\n  onChange: (value?: Record<string, IFlowValue | undefined>) => void;\n  // ... 其他属性\n}\n\ntype IFlowValue =\n  | IFlowConstantValue  // 常量值\n  | IFlowRefValue;     // 变量引用\n\ninterface IFlowConstantValue {\n  type: 'constant';\n  content: any;           // 常量值\n  schema: IJsonSchema;  // 值的类型定义\n}\n\ninterface IFlowRefValue {\n  type: 'ref';\n  content: string; // 变量路径，如 \"user.name\"\n}\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/inputs-values\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/inputs-values\n```\n\n### 目录结构讲解\n\n```\ninputs-values/\n├── index.tsx           # 主组件实现，包含 InputsValues 核心逻辑\n├── types.ts            # 类型定义\n└── styles.css          # 样式文件\n```\n\n### 核心实现说明\n\n#### 键值对管理\n使用 `useObjectList` Hook 管理键值对列表：\n\n```typescript\nconst { list, updateKey, updateValue, remove, add } = useObjectList<IFlowValue | undefined>({\n  value,\n  onChange,\n  sortIndexKey: 'extra.index',\n});\n```\n\n#### 动态值输入集成\n每个值都使用 `InjectDynamicValueInput` 组件实现输入：\n\n```typescript\n<InjectDynamicValueInput\n  value={item.value as IFlowConstantRefValue}\n  onChange={(v) => updateValue(item.id, v)}\n  schema={schema}\n  constantProps={constantProps}\n/>\n```\n\n#### 键名输入\n使用 `BlurInput` 组件实现键名的输入和验证：\n\n```typescript\n<BlurInput\n  value={item.key}\n  onChange={(v) => updateKey(item.id, v)}\n  placeholder={I18n.t('Input Key')}\n/>\n```\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/i18n\n- `I18n`: 国际化支持\n\n#### 内部组件\n- `InjectDynamicValueInput`: 动态值输入组件\n- `BlurInput`: 失焦输入组件\n- `useObjectList`: 对象列表管理 Hook\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[InputsValues 组件] --> B[渲染键值对列表]\n    B --> C[每个键值对]\n    C --> D[键名输入框]\n    C --> E[值输入组件]\n    C --> F[删除按钮]\n\n    D --> G[用户输入键名]\n    E --> H[用户输入值]\n    F --> I[用户删除键值对]\n\n    G --> J[updateKey 更新]\n    H --> K[updateValue 更新]\n    I --> L[remove 删除]\n\n    J --> M[触发 onChange]\n    K --> M\n    L --> M\n\n\n    O[添加按钮] --> P[add 新增]\n    P --> M\n```\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/json-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-editor-with-variables';\n\n# JsonEditorWithVariables\n\nJsonEditorWithVariables 是一个增强版的 JSON 编辑器，支持在 JSON 中插入变量引用。它基于 JsonCodeEditor 构建，集成了变量选择器和变量标签注入功能，使用户能够在 JSON 字符串中使用 `{{variable}}` 语法引用变量。\n\n## 案例演示\n\n### 基本使用\n\n:::tip{title=\"变量插入\"}\n\n在编辑器中输入 `@` 字符可以触发变量选择器。\n\n输入 `@` 后会显示可用的变量列表，选择变量后会自动插入为 `{{variable.name}}` 格式。\n\n\n**注意**：`JsonEditorWithVariables` 默认不支持 `{` 触发，因为 `{` 是 JSON 的语法。\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"json_editor_with_variables\" defaultValue={`{ \"a\": {{start_0.str}} }`}>\n        {({ field }) => (\n          <JsonEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n\n## API 参考\n\n### JsonEditorWithVariables Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `string` | - | JSON 字符串内容 |\n| `onChange` | `(value: string) => void` | - | 内容变化时的回调函数 |\n| `theme` | `'dark' \\| 'light'` | `'light'` | 编辑器主题 |\n| `placeholder` | `string` | - | 占位符文本 |\n| `activeLinePlaceholder` | `string` | `'按 @ 选择变量'` | 当前行的占位提示 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `options` | `Options` | - | CodeMirror 配置选项 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-editor-with-variables\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-editor-with-variables\n```\n\n### 目录结构讲解\n\n```\njson-editor-with-variables/\n├── index.tsx           # 懒加载导出文件\n└── editor.tsx          # 主组件实现\n```\n\n### 核心实现说明\n\n#### 变量能力集成\n\nJsonEditorWithVariables 扩展了基础 JsonCodeEditor，并基于 coze-editor-extensions 增加了变量引用和回显功能。\n\n### 依赖的其他物料\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorVariableTree`: 变量树选择触发\n- `EditorVariableTagInject`: 变量标签展示\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/json-schema-creator.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-schema-creator';\n\n# JsonSchemaCreator\n\nJsonSchemaCreator 是一个用于从 JSON 字符串自动生成 JSON Schema 的组件。它提供了一个按钮来触发弹窗，用户可以在弹窗中粘贴 JSON 数据，组件会自动分析数据结构并生成对应的 JSON Schema。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaCreator } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined> name=\"json_schema\">\n        {({ field }) => (\n          <div>\n            <JsonSchemaCreator\n              onSchemaCreate={(schema) => field.onChange(schema)}\n            />\n            <div style={{ marginTop: 16 }}>\n              <JsonSchemaEditor\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n              />\n            </div>\n          </div>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### JsonSchemaCreator Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `onSchemaCreate` | `(schema: IJsonSchema) => void` | - | 生成 schema 后的回调函数 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-schema-creator\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-creator\n```\n\n### 目录结构讲解\n\n```\njson-schema-creator/\n├── index.tsx              # 主组件导出，包含 JsonSchemaCreator 和 JsonSchemaCreatorProps\n├── json-schema-creator.tsx # 主组件实现，包含按钮和状态管理\n├── json-input-modal.tsx   # JSON 输入弹窗组件\n└── utils/\n    └── json-to-schema.ts  # JSON 转 Schema 的核心工具函数\n```\n\n### 核心实现说明\n\n#### JSON 解析和 Schema 生成流程\n\n以下是 JSON Schema 创建的完整交互时序图：\n\n```mermaid\nsequenceDiagram\n    participant User as 用户\n    participant Creator as JsonSchemaCreator\n    participant Modal as JsonInputModal\n    participant Parser as jsonToSchema\n    participant Generator as generateSchema\n\n    %% 初始交互\n    User->>Creator: 点击\"从 JSON 创建\"按钮\n    Creator->>Creator: setVisible(true)\n    Creator->>Modal: 显示弹窗\n\n    %% 用户输入和确认\n    User->>Modal: 输入 JSON 字符串\n    User->>Modal: 点击确认\n\n    %% JSON 解析和 Schema 生成\n    Modal->>Parser: 调用 jsonToSchema(jsonString)\n    Parser->>Parser: JSON.parse(jsonString)\n\n    %% 递归生成 Schema\n    Parser->>Generator: 调用 generateSchema(data)\n\n    %% 根据数据类型处理\n    alt 数据为 null\n        Generator->>Generator: 返回 {type: 'string'}\n    else 数据为数组\n        Generator->>Generator: schema = {type: 'array'}\n        loop 对每个数组元素\n            Generator->>Generator: 递归调用 generateSchema(item)\n        end\n    else 数据为对象\n        Generator->>Generator: schema = {type: 'object', properties: {}, required: []}\n        loop 对每个属性\n            Generator->>Generator: 递归调用 generateSchema(value)\n            Generator->>Generator: 添加到 properties 和 required\n        end\n    else 原始类型\n        Generator->>Generator: 返回 {type: typeof value}\n    end\n\n    %% 返回结果\n    Generator-->>Parser: 返回生成的 schema\n    Parser-->>Modal: 返回 IJsonSchema\n\n    %% 回调和关闭\n    Modal->>Creator: 调用 onSchemaCreate(schema)\n    Creator->>Creator: setVisible(false)\n    Creator->>Modal: 关闭弹窗\n    Creator-->>User: Schema 创建完成\n```\n\n### 使用到的 FlowGram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 类型定义\n\n### 依赖的其他物料\n\n[**JsonCodeEditor**](./code-editor) 代码编辑器组件\n- 用于在弹窗中编辑 JSON 数据\n\n### 使用的第三方库\n\n[**@douyinfe/semi-ui**](https://semi.design/)\n- `Button`: 触发弹窗的按钮组件\n- `Modal`: 弹窗容器\n- `Typography`: 文本组件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/json-schema-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/json-schema-editor';\n\n# JsonSchemaEditor\n\nJsonSchemaEditor 是一个可视化的 JSON Schema 编辑器，支持创建和编辑复杂的 JSON Schema 结构。它提供了树形结构的界面，可以直观地定义对象、数组、字符串、数字等各种类型的属性，支持嵌套结构和必填字段标记。\n\nJsonSchema 协议可以通过以下文档学习：\n\n- [Json Schema 官网](https://json-schema.org/learn)\n- [Json Schema 规范（中文版）](https://json-schema.apifox.cn/)\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema> name=\"json_schema\" defaultValue={{ type: 'object' }}>\n        {({ field }) => (\n          <JsonSchemaEditor\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n### JsonSchemaEditor Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `IJsonSchema` | `{ type: 'object' }` | JSON Schema 对象 |\n| `onChange` | `(value: IJsonSchema) => void` | - | Schema 变化时的回调函数 |\n| `config` | `ConfigType` | `{}` | 编辑器配置选项 |\n| `className` | `string` | - | 自定义样式类名 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n\n### ConfigType\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `placeholder` | `string` | `'输入变量名'` | 属性名占位符 |\n| `descTitle` | `string` | `'描述'` | 描述字段标题 |\n| `descPlaceholder` | `string` | `'帮助LLM理解该属性'` | 描述字段占位符 |\n| `defaultValueTitle` | `string` | `'默认值'` | 默认值字段标题 |\n| `defaultValuePlaceholder` | `string` | `'默认值'` | 默认值占位符 |\n| `addButtonText` | `string` | `'添加'` | 添加按钮文本 |\n\n### 支持的类型\n\n| 类型 | 描述 | 示例 |\n|------|------|------|\n| `string` | 字符串类型 | `\"hello\"` |\n| `number` | 数字类型 | `42` |\n| `boolean` | 布尔类型 | `true` |\n| `object` | 对象类型 | `{}` |\n| `array` | 数组类型 | `[]` |\n| `null` | 空值类型 | `null` |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/json-schema-editor\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n```\n\n### 目录结构讲解\n\n```\njson-schema-editor/\n├── index.tsx           # 主组件实现\n├── types.ts            # 类型定义\n├── hooks.tsx           # 状态管理钩子\n├── styles.css          # 样式文件\n├── default-value.tsx   # 默认值编辑器\n└── icon.tsx            # 图标组件\n```\n\n### 核心实现说明\n\n#### 树形结构管理\n组件使用递归的 `PropertyEdit` 组件来渲染嵌套的 Schema 结构：\n\n```typescript\nfunction PropertyEdit(props: {\n  value?: PropertyValueType;\n  config?: ConfigType;\n  onChange?: (value: PropertyValueType) => void;\n  onRemove?: () => void;\n  readonly?: boolean;\n  $level?: number;\n  $isLast?: boolean;\n})\n```\n\n#### 属性编辑状态管理\n使用 `usePropertiesEdit` 钩子管理 Schema 的增删改查：\n\n```typescript\nconst {\n  propertyList,\n  onAddProperty,\n  onRemoveProperty,\n  onEditProperty\n} = usePropertiesEdit(value, onChangeProps);\n```\n\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/json-schema\n- `IJsonSchema`: JSON Schema 类型定义\n\n#### @flowgram.ai/i18n\n- `I18n`: 国际化支持\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[JsonSchemaEditor] --> B[渲染根属性]\n    B --> C[PropertyEdit 组件]\n    C --> D[属性名输入]\n    C --> E[类型选择]\n    C --> F[必填标记]\n    C --> G[操作按钮]\n\n    G --> H[展开/收起详情]\n    G --> I[添加子属性]\n    G --> J[删除属性]\n\n    H --> K[描述输入]\n    H --> L[默认值设置]\n\n    I --> M[递归渲染子属性]\n    M --> C\n\n```\n\n### 高级功能\n\n#### 嵌套结构支持\n支持无限层级的嵌套对象和数组：\n\n```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"level1\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"level2\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"level3\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n#### 数组元素定义\n支持定义数组元素的 Schema：\n\n```json\n{\n  \"type\": \"array\",\n  \"items\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"id\": { \"type\": \"number\" },\n      \"name\": { \"type\": \"string\" }\n    }\n  }\n}\n```\n\n#### 必填字段管理\n支持标记字段为必填，并自动更新 `required` 数组：\n\n```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": { \"type\": \"string\" },\n    \"email\": { \"type\": \"string\" }\n  },\n  \"required\": [\"email\"]\n}\n```\n\n### 使用场景\n\n- **API 文档生成**: 为 REST API 创建请求/响应 Schema\n- **表单验证**: 定义表单字段的验证规则\n- **数据建模**: 创建数据结构模型\n- **配置管理**: 定义配置文件的结构\n- **代码生成**: 为代码生成器提供 Schema 输入\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/prompt-editor-with-inputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithoutCanvas } from 'components/form-materials/components/prompt-editor-with-inputs';\n\n# PromptEditorWithInputs\n\nPromptEditorWithInputs 是一个增强版的提示编辑器，集成了**节点输入提示**功能。\n\n它基于 [PromptEditor](./prompt-editor) 构建，通过 `@`, `{` 字符弹出节点输入选择器，使用户能够在提示模板中方便地引用节点的输入。\n\n## 案例演示\n\n### 基本使用\n\n:::tip{title=\"Inputs 插入\"}\n\n在编辑器中输入 `@`, `{` 字符可以触发 Inputs 选择器。\n\n输入 `@`, `{` 后会显示可用的变量列表，选择变量后会自动插入为 `{{inputs.path}}` 格式。\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithInputs } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IInputsValues | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: { type: 'constant', content: '123' },\n          b: { type: 'ref', content: ['start_0', 'obj'] },\n        }}\n      >\n        {({ field }) => (\n          <InputsValuesTree value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<IInputsValues | undefined> name=\"inputsValues\">\n        {({ field: inputsField }) => (\n          <Field<IFlowTemplateValue | undefined>\n            name=\"prompt_editor_with_inputs\"\n            defaultValue={{\n              type: 'template',\n              content: '# Query \\n {{b.obj2.num}}',\n            }}\n          >\n            {({ field }) => (\n              <PromptEditorWithInputs\n                value={field.value}\n                onChange={(value) => field.onChange(value)}\n                inputsValues={inputsField.value || {}}\n              />\n            )}\n          </Field>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 脱离画布使用\n\n:::warning\n\n脱离画布使用时，由于没法访问变量，inputsValues 内不支持 type: 'ref' 类型的值定义\n\n:::\n\n<WithoutCanvas />\n\n```tsx pure title=\"with-canvas.tsx\"\nexport const WithoutCanvas = () => {\n  const [value, setValue] = useState<IFlowTemplateValue | undefined>({\n    type: 'template',\n    content: '# Role \\n You are a helpful assistant. \\n\\n # Query \\n {{b.obj2.num}} \\n\\n',\n  });\n\n  return (\n    <div>\n      <PromptEditorWithInputs\n        value={value}\n        onChange={(value) => setValue(value)}\n        inputsValues={{\n          a: { type: 'constant', content: '123' },\n          b: {\n            c: {\n              d: { type: 'constant', content: 456 },\n            },\n            e: { type: 'constant', content: 789 },\n          },\n        }}\n      />\n    </div>\n  );\n};\n```\n\n\n## API 参考\n\n### PromptEditorWithInputs Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `{ type: 'template', content: string }` | - | 提示模板内容 |\n| `inputsValues` | `IInputsValues` | `{}` | 输入变量键值对 |\n| `onChange` | `(value: { type: 'template', content: string }) => void` | - | 内容变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `placeholder` | `string` | - | 占位符文本 |\n| `activeLinePlaceholder` | `string` | - | 当前行的占位提示 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `disableMarkdownHighlight` | `boolean` | `false` | 是否禁用Markdown高亮 |\n| `options` | `Options` | - | CodeMirror 配置选项 |\n\n\n\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor-with-inputs\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor-with-inputs\n```\n\n### 目录结构讲解\n\n```\nprompt-editor-with-inputs/\n└──  index.tsx          # 主组件实现\n```\n\n### 核心实现说明\n\n#### 输入变量集成\n\nPromptEditorWithInputs 扩展了基础 [PromptEditor](./prompt-editor)，并基于 [coze-editor-extensions](./coze-editor-extensions) 增加了节点 inputs 引用功能。\n\n\n### 依赖的其他物料\n\n[**PromptEditor**](./prompt-editor)\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorInputsTree`: 输入树选择触发\n\n[**FlowValue**](../common/flow-value)\n- `IInputsValues`: 输入变量类型定义\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/prompt-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, StringOnlyStory } from 'components/form-materials/components/prompt-editor-with-variables';\n\n# PromptEditorWithVariables\n\nPromptEditorWithVariables 是一个增强版的提示编辑器，集成了变量引用功能。\n\n它基于 [PromptEditor](./prompt-editor) 构建，通过 `@`, `{` 弹出变量树选择器，并将输入变量回显为标签，使用户能够在提示模板中方便地引用和管理变量。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/prompt-editor-with-variables.png\" alt=\"组件\" style={{ width: '50%' }} />\n  *LLM_3 和 LLM_4 的提示词中引用了循环的批处理变量*\n</div>\n\n## 案例演示\n\n### 基本使用\n\n:::tip{title=\"变量插入\"}\n\n在编辑器中输入 `@`, `{` 字符可以触发变量选择器。\n\n输入 `@`, `{` 后会显示可用的变量列表，选择变量后会自动插入为 `{{variable.path}}` 格式。\n\n:::\n\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any> name=\"prompt_template\" defaultValue={{\n              type: 'template',\n              content: `# Role\nYou are a helpful assistant\n\n# Query\n{{start_0.str}}`,\n            }}>\n        {({ field }) => (\n          <PromptEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 只能选择 String 类型的变量\n\n<StringOnlyStory />\n\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditorWithVariables, VariableSelectorProvider } from '@flowgram.ai/form-materials';\n\nconst STRING_ONLY_SCHEMA = { type: 'string' };\n\nconst formMeta = {\n  render: () => (\n    <VariableSelectorProvider includeSchema={STRING_ONLY_SCHEMA}>\n      <Field<any> name=\"prompt_template\">\n        {({ field }) => (\n          <PromptEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </VariableSelectorProvider>\n  ),\n}\n```\n\n## API 参考\n\n### PromptEditorWithVariables Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `{ type: 'template', content: string }` | - | 提示模板内容 |\n| `onChange` | `(value: { type: 'template', content: string }) => void` | - | 内容变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `placeholder` | `string` | - | 占位符文本 |\n| `activeLinePlaceholder` | `string` | - | 当前行的占位提示 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `disableMarkdownHighlight` | `boolean` | `false` | 是否禁用Markdown高亮 |\n| `options` | `Options` | - | CodeMirror 配置选项 |\n\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor-with-variables\n```\n\n### 目录结构讲解\n\n```\nprompt-editor-with-variables/\n└── index.tsx           # 主组件实现\n```\n\n### 核心实现说明\n\n#### 变量能力集成\n\nPromptEditorWithVariables 扩展了基础 [PromptEditor](./prompt-editor)，并基于 [CozeEditorExtensions](./coze-editor-extensions) 增加了变量引用和回显功能。\n\n### 依赖的其他物料\n\n[**PromptEditor**](./prompt-editor)\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorVariableTree`: 变量树选择触发\n- `EditorVariableTagInject`: 变量 Tag 回显\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/prompt-editor.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/prompt-editor';\n\n# PromptEditor\n\nPromptEditor 是一个专业的提示词编辑器组件，基于 Coze Editor 构建，支持 Markdown 语法高亮、Jinja 模板语法高亮等功能，适用于创建和编辑 AI 提示词模板。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { PromptEditor } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <Field<any | undefined>\n        name=\"prompt_editor\"\n        defaultValue={{\n          type: 'template',\n          content: '# Role\\n You are a helpful assistant',\n        }}\n      >\n        {({ field }) => (\n          <PromptEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 参考\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| value | `{ type: 'template'; content: string }` | - | 编辑器的值对象 |\n| onChange | `(value: { type: 'template'; content: string }) => void` | - | 值变化时的回调函数 |\n| readonly | `boolean` | `false` | 是否只读模式 |\n| placeholder | `string` | - | 占位符文本 |\n| activeLinePlaceholder | `React.ReactNode` | - | 活动行占位符 |\n| style | `React.CSSProperties` | - | 自定义样式 |\n| hasError | `boolean` | `false` | 是否显示错误状态 |\n| disableMarkdownHighlight | `boolean` | `false` | 是否禁用 Markdown 高亮 |\n| options | `Partial<InferValues<Preset[number]>>` | - | 编辑器额外选项 |\n| children | `React.ReactNode` | - | 子组件 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/prompt-editor\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/prompt-editor\n```\n\n### 目录结构讲解\n\n```plaintext\ncomponents/prompt-editor/\n├── index.tsx          # 组件入口文件\n├── editor.tsx         # 编辑器核心实现\n├── types.tsx          # 类型定义文件\n├── styles.css         # 组件样式文件\n└── extensions/        # 编辑器扩展功能\n    ├── jinja.tsx      # Jinja 模板语法高亮支持\n    ├── markdown.tsx   # Markdown 语法高亮支持\n    └── language-support.tsx # 语言支持基础配置\n```\n\n### 核心实现说明\n\nPromptEditor 组件基于 @flowgram.ai/coze-editor 构建，提供了专业的提示词编辑功能。主要特点包括：\n\n1. **Markdown 语法高亮**：支持标题、斜体、粗体、列表等 Markdown 元素的语法高亮\n2. **Jinja 模板语法高亮**：支持 Jinja 模板引擎的语法高亮，方便创建动态提示词\n3. **响应式更新**：监听外部值变化并同步到编辑器\n4. **可定制性**：支持自定义样式、占位符等多种配置选项\n\n组件内部使用懒加载优化性能，并通过 EditorProvider 和 Renderer 组件提供编辑器核心功能。\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/coze-editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/coze-editor)\n- `Renderer`: 编辑器渲染组件\n- `EditorProvider`: 编辑器上下文提供者\n- `ActiveLinePlaceholder`: 活动行占位符组件\n- `preset-prompt`: 提示词编辑器预设插件\n\n#### 第三方库\n\n[**CodeMirror**](https://codemirror.net/)\n- 提供底层编辑器功能支持\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/sql-editor-with-variables.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/sql-editor-with-variables';\n\n# SQLEditorWithVariables\n\nSQLEditorWithVariables 是一个增强版的 SQL 编辑器，支持在 SQL 中插入变量引用。它基于 SQLCodeEditor 构建，集成了变量选择器和变量标签注入功能，使用户能够在 SQL 字符串中使用 `{{variable}}` 语法引用变量。\n\n## 案例演示\n\n### 基本使用\n\n:::tip{title=\"变量插入\"}\n\n在编辑器中输入 `@`, `{` 字符可以触发变量选择器。\n\n输入 `@`, `{` 后会显示可用的变量列表，选择变量后会自动插入为 `{{variable.name}}` 格式。\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { SQLEditorWithVariables } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string | undefined>\n        name=\"sql_editor_with_variables\"\n        defaultValue=\"SELECT * FROM users WHERE user_id = {{start_0.str}}\"\n      >\n        {({ field }) => (\n          <SQLEditorWithVariables\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 带变量的 SQL 示例\n\n```sql\nSELECT\n  id, name, email\nFROM users\nWHERE id = {{start_0.user_id}}\n  AND email LIKE '%{{start_0.domain}}'\nLIMIT 10;\n```\n\n## API 参考\n\n### SQLEditorWithVariables Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `string` | - | SQL 字符串内容 |\n| `onChange` | `(value: string) => void` | - | 内容变化时的回调函数 |\n| `theme` | `'dark' \\| 'light'` | `'light'` | 编辑器主题 |\n| `placeholder` | `string` | - | 占位符文本 |\n| `activeLinePlaceholder` | `string` | `'按 @ 选择变量'` | 当前行的占位提示 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `options` | `Options` | - | CodeMirror 配置选项 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/sql-editor-with-variables\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/sql-editor-with-variables\n```\n\n### 目录结构讲解\n\n```\nsql-editor-with-variables/\n├── index.tsx           # 懒加载导出文件\n└── editor.tsx          # 主组件实现\n```\n\n### 核心实现说明\n\n#### 变量能力集成\n\nSQLEditorWithVariables 扩展了基础 SQLCodeEditor，并基于 coze-editor-extensions 增加了变量引用和回显功能。\n\n### 依赖的其他物料\n\n[**CozeEditorExtensions**](./coze-editor-extensions)\n- `EditorVariableTree`: 变量树选择触发\n- `EditorVariableTagInject`: 变量标签展示\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/type-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/components/type-selector';\n\n# TypeSelector\n\nTypeSelector 是一个类型选择器组件，用于在表单中选择 JSON Schema 类型。它支持基本类型和复合类型（如数组类型）的选择。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/type-selector.png\" alt=\"TypeSelector 组件\" style={{ width: '50%' }} />\n</div>\n\n:::tip\nIf you want to add new variable types, please read [Type Management](../common/json-schema-preset)\n:::\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { TypeSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Partial<IJsonSchema> | undefined> name=\"type_selector\" defaultValue={{ type: 'string' }}>\n        {({ field }) => (\n          <TypeSelector value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 新增新类型\n\n详见：\n\n\n## API 参考\n\n### TypeSelector Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `Partial<IJsonSchema>` | - | 选中的类型值，符合 JSON Schema 格式 |\n| `onChange` | `(value?: Partial<IJsonSchema>) => void` | - | 类型选择变化时的回调函数 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `disabled` | `boolean` | `false` | 是否禁用（已废弃，请使用 readonly） |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n\n### 类型格式说明\n\nTypeSelector 支持以下 JSON Schema 类型格式：\n\n- **基本类型**: `{ type: 'string' }`, `{ type: 'number' }`, `{ type: 'boolean' }` 等\n- **数组类型**: `{ type: 'array', items: { type: 'string' } }`\n- **嵌套数组**: `{ type: 'array', items: { type: 'array', items: { type: 'string' } } }`\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/type-selector\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/type-selector\n```\n\n### 目录结构讲解\n\n```\ntype-selector/\n└── index.tsx           # 主组件实现，包含 TypeSelector 核心逻辑\n```\n\n### 核心实现说明\n\n#### getTypeSelectValue\n将 JSON Schema 对象转换为 Cascader 组件所需的数组格式：\n\n```typescript\n// 输入: { type: 'array', items: { type: 'string' } }\n// 输出: ['array', 'string']\n```\n\n#### parseTypeSelectValue\n将 Cascader 组件的数组值转换回 JSON Schema 对象：\n\n```typescript\n// 输入: ['array', 'string']\n// 输出: { type: 'array', items: { type: 'string' } }\n```\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/json-schema\n- `useTypeManager()`: 获取类型管理器，用于处理 JSON Schema 类型的显示和验证\n- `IJsonSchema`: JSON Schema 类型定义\n- `JsonSchemaTypeManager`: 类型管理器类，提供类型注册、图标显示等功能\n\n### 整体流程\n\n```mermaid\ngraph LR\n    A[TypeSelector 组件] --> B[useTypeManager Hook]\n    B --> C[获取类型注册表]\n    C --> D[生成 Cascader 选项]\n    D --> E[渲染级联选择器]\n    E --> F[用户选择类型]\n    F --> G[onChange 回调]\n    G --> H[更新 JSON Schema 值]\n```\n"
  },
  {
    "path": "apps/docs/src/zh/materials/components/variable-selector.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, FilterSchemaStory, CustomFilterStory, CustomVariableTreeStory } from 'components/form-materials/components/variable-selector';\n\n# VariableSelector\n\nVariableSelector 是一个用于选择当前作用域变量的组件，它可以根据变量的类型进行过滤。\n\n\n:::warning\n\nVariableSelector 的变量树中，**每一个叶子节点和非叶子节点，都是一个变量**。\n\n在官方物料库的设计中，每个节点都会输出 **一个 ObjectType 变量声明**，这个变量声明中：\n\n- 变量的**元信息包含节点的标题和节点的 Icon**，被 VariableSelector 解析展示\n- 变量的下钻字段会被 VariableSelector **递归展示在节点变量内**\n\n:::\n\n## 案例演示\n\n### 直接使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n\n### 过滤变量类型\n\n<FilterSchemaStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            includeSchema={{ type: 'string' }}\n          />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 自定义过滤逻辑\n\n<CustomFilterStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { VariableSelector } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n      <FormHeader />\n      <Field<string[] | undefined> name=\"variable_selector\">\n        {({ field }) => (\n          <VariableSelector\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n          />\n        )}\n      </Field>\n    </VariableSelectorProvider>\n  ),\n}\n```\n\n### 通过 useVariableTree 获取变量树\n\n<CustomVariableTreeStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { useVariableTree } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => {\n    const treeData = useVariableTree({});\n\n    return (\n      <VariableSelectorProvider skipVariable={(variable) => variable?.key === 'str'}>\n        <FormHeader />\n        <Tree treeData={treeData} defaultExpandAll />\n      </VariableSelectorProvider>\n    );\n  },\n}\n```\n\n:::tip\n\n`useVariableTree` 也被用于 [`PromptEditorWithVariables`](./prompt-editor-with-variables)、[`SQLEditorWithVariables`](./sql-editor-with-variables)、[`JsonEditorWithVariables`](./json-editor-with-variables) 等组件中，用于获取当前作用域的变量树。\n\n:::\n\n\n## API 参考\n\n### VariableSelector Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `value` | `string[]` | - | 选中的变量路径数组 |\n| `onChange` | `(value?: string[]) => void` | - | 变量选择变化时的回调函数 |\n| `config` | `VariableSelectorConfig` | `{}` | 配置对象 |\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 包含的变量类型过滤条件 |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 排除的变量类型过滤条件 |\n| `readonly` | `boolean` | `false` | 是否为只读模式 |\n| `hasError` | `boolean` | `false` | 是否显示错误状态 |\n| `style` | `React.CSSProperties` | - | 自定义样式 |\n| `triggerRender` | `(props: TriggerRenderProps) => React.ReactNode` | - | 自定义触发器渲染 |\n\n### VariableSelectorConfig\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `placeholder` | `string` | `'选择变量'` | 占位符文本 |\n| `notFoundContent` | `string` | `'未定义'` | 变量未找到时的显示内容 |\n\n### VariableSelectorProvider Props\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `skipVariable` | `(variable?: BaseVariableField) => boolean` | - | 自定义变量过滤函数 |\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 包含的变量类型过滤条件 |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 排除的变量类型过滤条件 |\n| `children` | `React.ReactNode` | - | 子组件 |\n\n### useVariableTree\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `includeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 包含的变量类型过滤条件 |\n| `excludeSchema` | `IJsonSchema \\| IJsonSchema[]` | - | 排除的变量类型过滤条件 |\n| `skipVariable` | `(variable?: BaseVariableField) => boolean` | - | 自定义变量过滤函数 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/components/variable-selector\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/variable-selector\n```\n\n### 目录结构讲解\n\n```\nvariable-selector/\n├── index.tsx           # 主组件实现，包含 VariableSelector 核心逻辑\n├── context.tsx         # 提供 `VariableSelectorContext` 上下文，支持全局配置变量过滤逻辑\n├── use-variable-tree.tsx # 自定义 Hook，处理变量树数据的转换和过滤\n└── styles.css          # 样式文件\n```\n\n\n### 整体流程\n\n```mermaid\ngraph TD\n    A[VariableSelector 组件] ---> useVariableTree\n\n    K[VariableSelectorProvider] --> L[全局过滤配置]\n    subgraph useVariableTree\n      C[useAvailableVariables] --> D[获取所有可用变量]\n      D --> F[应用过滤条件]\n      F --> G[生成树形结构]\n      M[includeSchema/excludeSchema] --> F\n    end\n\n    G --> H[渲染 TreeSelect]\n    L --> F\n\n\n```\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/variable-core\n- `useAvailableVariables()`: 获取当前作用域内所有可用的变量\n- `BaseVariableField`: 基础变量字段类型，包含变量键、类型、元数据等\n\n#### @flowgram.ai/json-schema\n- `useTypeManager()`: 获取类型管理器，用于处理变量类型的显示和验证\n- `IJsonSchema`: JSON Schema 类型定义，用于变量类型验证\n- `JsonSchemaUtils`: JSON Schema 工具类，提供类型匹配和转换功能\n\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/_meta.json",
    "content": "[\n  \"auto-rename-ref\",\n  \"listen-ref-value-change\",\n  \"listen-ref-schema-change\",\n  \"provide-batch-input\",\n  \"provide-json-schema-outputs\",\n  \"sync-variable-title\",\n  \"validate-when-variable-sync\"\n]\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/auto-rename-ref.mdx",
    "content": "\nimport { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/auto-rename-ref';\n\n# autoRenameRef\n\nautoRenameRef 是一个自动重命名引用效果，当表单字段的键名发生变化时，自动更新所有引用该字段的引用值和模板值。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/auto-rename-ref.gif\" alt=\"autoRenameRef 效果\" style={{ width: '80%' }} />\n  *query 变量名变化时，自动重命名下游 inputs 中的引用*\n</div>\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effects: {\n    \"inputsValues\": autoRenameRefEffect,\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"inputsValues\">\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n### 在复杂表单中使用\n\n```tsx pure title=\"form-meta.tsx\"\nimport { autoRenameRefEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effects: {\n    // 为多个字段应用自动重命名效果\n    \"inputsValues\": autoRenameRefEffect,\n    \"outputsValues\": autoRenameRefEffect,\n    \"config.data\": autoRenameRefEffect,\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<any> name=\"inputsValues\"> {/* inputsValues 实现 */} </Field>\n      <Field<any> name=\"outputsValues\"> {/* outputsValues 实现 */} </Field>\n      <Field<any> name=\"config.data\"> {/* config.data 实现 */} </Field>\n    </>\n  ),\n}\n```\n\n## API 介绍\n\n### 支持的值类型\n\n当指定 key 下对应的数据满足以下条件时，autoRenameEffect 的自动重命名才会被触发：\n\n- **引用值 (ref)**：`{ type: 'ref', content: ['field', 'path'] }`\n- **模板值 (template)**：`{ type: 'template', content: 'Hello {{user.name}}' }`\n- **包含引用值和模板值的复杂结构**：\n  ```json\n  {\n    \"a\": {\n      \"type\": \"ref\",\n      \"content\": [\"start_0\", \"str\"]\n    },\n    \"b\": {\n      \"c\": {\n        \"type\": \"template\",\n        \"content\": \"Hello {{a}}\"\n      }\n    }\n  }\n  ```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/auto-rename-ref\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/auto-rename-ref\n```\n\n### 核心流程\n\n1. **监听重命名事件**：通过 `VariableFieldKeyRenameService` 监听字段键名变更\n2. **遍历引用值**：查找所有引用值和模板值\n3. **匹配键路径**：检查引用路径是否与变更前的路径匹配\n4. **更新引用**：将匹配的引用路径更新为新的键路径\n\n```mermaid\ngraph TD\n    A[监听重命名事件] --> B[renameService.onRename触发]\n    B --> C[遍历所有值 traverseRef]\n\n    C --> D{值类型判断}\n\n    D -->|ref类型| E[检查路径匹配\n    isKeyPathMatch]\n\n    D -->|template类型| F[检查模板路径匹配\n    遍历+isKeyPathMatch]\n\n    E -->|匹配| G[更新引用路径\n    value.content = newPath]\n\n    E -->|不匹配| I\n\n    F -->|匹配| H[替换模板内容\n    value.content.replace]\n\n    F -->|不匹配| I\n\n    G --> I[更新表单值\n    form.setValueIn]\n\n    H --> I\n\n    I --> J{还有值需要处理?}\n    J -->|是| C\n    J -->|否| K[完成]\n\n    style A fill:#e1f5fe\n    style K fill:#e8f5e8\n    style B fill:#fff3e0\n    style G fill:#ffebee\n    style H fill:#ffebee\n```\n\n### 使用到的核心 API\n\n#### @flowgram.ai/variable-core\n- `VariableFieldKeyRenameService`: 字段键名重命名监听\n\n\n#### @flowgram.ai/node\n- `DataEvent.onValueInit`: 值初始化事件\n- `Effect`: 效果类型定义\n\n#### FlowValueUtils\n- `FlowValueUtils.getTemplateKeyPaths()`: 提取模板中的所有键路径\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/listen-ref-schema-change.mdx",
    "content": "\n\n# listenRefSchemaChange\n\n监听引用变量类型的 json schema 变化，并在变化时触发回调函数。\n\n## 案例演示\n\nimport { BasicStory } from 'components/form-materials/effects/listen-ref-schema-change';\n\n### 基本使用\n\n<BasicStory />\n\n\n```tsx pure title=\"form-meta.tsx\"\nimport { listenRefSchemaChange } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effect: {\n    'inputsValues.*': listenRefSchemaChange(({ name, schema, form, formValues }) => {\n      form.setValueIn(\n        `log`,\n        `${form.getValueIn(`log`) || ''}* ${name}: ${JSON.stringify(schema)} \\n`\n      );\n    }),\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: {\n            type: 'ref',\n            content: ['start_0', 'str'],\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<any> name=\"log\" defaultValue={'When schema updated, log changes:\\n'}>\n        {({ field }) => (\n          <pre style={{ padding: 4, background: '#f5f5f5', fontSize: 12 }}>{field.value}</pre>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 介绍\n\n### listenRefSchemaChange\n\n```typescript\nlistenRefSchemaChange(\n  cb: (props: EffectFuncProps<IFlowRefValue> & { schema?: IJsonSchema }) => void\n): EffectOptions[]\n```\n\n#### 参数\n\n| 参数 | 类型 | 描述 |\n|------|------|------|\n| `cb` | `(props: EffectFuncProps<IFlowRefValue> & { schema?: IJsonSchema }) => void` | 回调函数，当引用的 schema 发生变化时触发 |\n\n#### 回调函数参数\n\n| 参数 | 类型 | 描述 |\n|------|------|------|\n| `name` | `string` | 字段名称 |\n| `value` | `IFlowRefValue` | 引用值对象 |\n| `form` | `IForm` | 表单实例 |\n| `schema` | `IJsonSchema` | 转换后的 JSON Schema |\n\n\n\n### 支持值类型\n\n- `IFlowRefValue`：引用类型值，包含 `type: 'ref'` 和 `content: string[]`\n\n\n## 源码导读\n\nimport { SourceCode } from '@theme';\n\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/listen-ref-schema-change\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/listen-ref-schema-change\n```\n\n### 目录结构\n\n```\nlisten-ref-schema-change/\n└── index.ts          # 主入口文件\n```\n\n### 工作原理\n\n1. **监听值变化**：监听 `DataEvent.onValueInitOrChange` 事件\n2. **类型检查**：检查值是否为 `ref` 类型\n3. **路径跟踪**：使用 `trackByKeyPath` 跟踪引用路径\n4. **Schema 转换**：将 AST 类型转换为 JSON Schema\n5. **回调触发**：当引用的类型发生变化时触发回调\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/variable-core\n\n- `getNodeScope(context.node).available.trackByKeyPath`: 跟踪指定路径的值变化，当路径对应的值发生变化时触发回调。\n\n#### @flowgram.ai/json-schema\n\n- `JsonSchemaUtils.astToSchema`: 将 AST 类型节点转换为 JSON Schema 对象。\n\n\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/listen-ref-value-change.mdx",
    "content": "# listenRefValueChange\n\n监听引用**变量定义**的变化，并在变化时触发回调函数。\n\n## 案例演示\n\nimport { BasicStory } from 'components/form-materials/effects/listen-ref-value-change';\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { listenRefValueChange } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  effect: {\n    'inputsValues.*': listenRefValueChange(({ name, variable, form }) => {\n      form.setValueIn(\n        `log`,\n        `${form.getValueIn(`log`) || ''}* ${name}: ${JSON.stringify(\n          variable?.toJSON() || {}\n        )} \\n`\n      );\n    }),\n  },\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<Record<string, any> | undefined>\n        name=\"inputsValues\"\n        defaultValue={{\n          a: {\n            type: 'ref',\n            content: ['start_0', 'str'],\n          },\n        }}\n      >\n        {({ field }) => (\n          <InputsValues value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n      <br />\n      <Field<any> name=\"log\" defaultValue={'When variable value updated, log changes:\\n'}>\n        {({ field }) => (\n          <pre style={{ width: 500, padding: 4, background: '#f5f5f5', fontSize: 12 }}>\n            {field.value}\n          </pre>\n        )}\n      </Field>\n    </>\n  ),\n}\n```\n\n## API 介绍\n\n### listenRefValueChange\n\n```typescript\nlistenRefValueChange(\n  cb: (props: EffectFuncProps<IFlowRefValue> & { variable?: BaseVariableField }) => void\n): EffectOptions[]\n```\n\n#### 参数\n\n| 参数 | 类型 | 描述 |\n|------|------|------|\n| `cb` | `(props: EffectFuncProps<IFlowRefValue> & { variable?: BaseVariableField }) => void` | 回调函数，当引用的值发生变化时触发 |\n\n#### 回调函数参数\n\n| 参数 | 类型 | 描述 |\n|------|------|------|\n| `name` | `string` | 字段名称 |\n| `value` | `IFlowRefValue` | 引用值对象 |\n| `form` | `IForm` | 表单实例 |\n| `variable` | `BaseVariableField` | 引用指向的变量实例 |\n\n### 支持值类型\n\n- `IFlowRefValue`：引用类型值，包含 `type: 'ref'` 和 `content: string[]`\n\n## 源码导读\n\nimport { SourceCode } from '@theme';\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/listen-ref-value-change\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/listen-ref-value-change\n```\n\n### 目录结构\n\n```\nlisten-ref-value-change/\n└── index.ts          # 主入口文件\n```\n\n### 工作原理\n\n1. **监听值变化**：监听 `DataEvent.onValueInitOrChange` 事件\n2. **类型检查**：检查值是否为 `ref` 类型\n3. **路径跟踪**：使用 `trackByKeyPath` 跟踪引用路径，获取被引用的变量\n4. **回调触发**：当引用的变量值发生变化时触发回调\n\n### 使用到的 flowgram API\n\n#### @flowgram.ai/variable-core\n\n- `getNodeScope(context.node).available.trackByKeyPath`: 跟踪指定路径的值变化，当路径对应的值发生变化时触发回调。\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/provide-batch-input.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/provide-batch-input';\n\n# provideBatchInputEffect\n\n`provideBatchInputEffect` 是一个表单副作用，专门用于循环节点场景。它能够将循环输入的数组变量解析为两个局部变量：\n\n- **item**：当前迭代的数组元素，类型会根据输入数组的元素类型自动推导\n- **index**：当前迭代的索引，类型为 number\n\n**核心特性：**\n\n- 🔄 **自动类型推导**：从数组类型自动推导出 `item` 的元素类型\n- 🔒 **私有作用域**：生成的变量存储在节点私有作用域，仅当前节点及子节点可访问\n- 🎯 **循环专用**：专为批处理/循环场景设计\n\n这使得循环体内的子节点可以引用 `item` 和 `index` 变量，实现对数组元素的逐个处理。\n\n:::info{title=\"完整方案概览\"}\n\n实现一个完整的循环节点需要以下三个物料配合使用：\n\n| 物料 | 类型 | 职责 |\n|------|------|------|\n| [BatchVariableSelector](../components/batch-variable-selector) | 组件 | 选择循环的数组数据源 |\n| **provideBatchInputEffect** | 副作用 | 生成 `item` 和 `index` 局部变量 |\n| [BatchOutputs](../components/batch-outputs) + [batchOutputsPlugin](../form-plugins/batch-outputs-plugin) | 组件 + 插件 | 配置循环输出并生成数组类型变量 |\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n:::tip\n\n选择一个数组类型的变量后，`provideBatchInputEffect` 会自动生成 `item` 和 `index` 局部变量，可以在下方的变量选择器中看到这些变量。\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"关于 FormHeader、FormContent、FormItem\"}\n\n上述代码中的 `FormHeader`、`FormContent`、`FormItem` 是用户自定义的布局组件，用于统一表单样式。你可以根据项目需求自行实现或替换为其他 UI 组件。\n\n:::\n\n## API 参考\n\n### provideBatchInputEffect\n\n提供一个表单副作用，将循环输入的数组变量解析为 `item` 和 `index` 局部变量。\n\n```typescript\nimport { provideBatchInputEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta: FormMeta = {\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n};\n```\n\n#### 参数\n\n该副作用内部使用 `createEffectFromVariableProvider` 创建，配置如下：\n\n| 属性 | 值 | 说明 |\n|------|------|------|\n| `private` | `true` | 生成的变量存储在节点私有作用域 |\n\n:::tip{title=\"关于 private 参数\"}\n\n设置 `private: true` 后，变量会存储在 `node.privateScope` 而非 `node.scope`。这意味着：\n- 变量仅在当前节点及其子节点中可见\n- 不会被父节点的下游节点访问\n- 适用于循环场景中的临时迭代变量\n\n详见：[节点私有作用域](../../guide/variable/concept#节点私有作用域)\n\n:::\n\n#### 返回值\n\n- `EffectOptions[]`: 表单副作用选项数组，用于 `formMeta.effect` 配置\n\n#### 生成的变量结构\n\n副作用会在当前节点的**私有作用域**下创建一个变量 `${nodeId}_locals`，结构如下：\n\n| 字段 | 类型 | 描述 |\n|------|------|------|\n| `item` | 根据数组元素类型推导 | 当前迭代的数组元素 |\n| `index` | `number` | 当前迭代的索引 |\n\n#### 生成的 AST 结构示例\n\n假设循环输入变量路径为 `['start_0', 'list']`，生成的 AST 结构如下：\n\n```typescript\n{\n  kind: 'VariableDeclaration',\n  key: 'loop_1_locals',\n  meta: {\n    title: '循环节点',\n    icon: 'loop-icon'\n  },\n  type: {\n    kind: 'ObjectType',\n    properties: [\n      {\n        kind: 'Property',\n        key: 'item',\n        initializer: {\n          kind: 'EnumerateExpression',\n          enumerateFor: {\n            kind: 'KeyPathExpression',\n            keyPath: ['start_0', 'list']\n          }\n        }\n      },\n      {\n        kind: 'Property',\n        key: 'index',\n        type: { kind: 'NumberType' }\n      }\n    ]\n  }\n}\n```\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-batch-input/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/provide-batch-input\n```\n\n### 目录结构讲解\n\n```\nprovide-batch-input/\n└── index.ts           # 主实现文件，导出 provideBatchInputEffect 表单副作用\n```\n\n### 核心实现说明\n\n#### 变量生成逻辑\n\n`provideBatchInputEffect` 使用 [`createEffectFromVariableProvider`](../../guide/variable/variable-output) 工厂函数创建变量提供器。核心特点：\n\n1. **私有变量**：设置 `private: true`，生成的变量仅在当前节点作用域内可见\n2. **元素类型推导**：使用 `ASTFactory.createEnumerateExpression` 从数组类型推导出元素类型\n3. **索引变量**：固定为 `number` 类型\n\n#### 类型推导原理\n\n`EnumerateExpression` 是变量引擎提供的表达式类型，用于从数组类型推导元素类型：\n\n```mermaid\ngraph LR\n    A[\"Array&lt;string&gt;\"] --> B[EnumerateExpression]\n    B --> C[\"string\"]\n\n    D[\"Array&lt;object&gt;\"] --> E[EnumerateExpression]\n    E --> F[\"object\"]\n```\n\n当上游变量类型变化时，`item` 的类型会**自动联动更新**。\n\n#### 变量生成流程时序图\n\n```mermaid\nsequenceDiagram\n    participant Form as 表单系统\n    participant Parser as parse函数\n    participant AST as ASTFactory\n    participant Node as 当前节点\n\n    Form->>Parser: 提供 IFlowRefValue 值\n    Parser->>Node: 获取节点信息\n    Node-->>Parser: 返回节点ID\n    Parser->>AST: 创建变量声明\n    Note over Parser: 键名: ${nodeId}_locals\n    Parser->>AST: 创建 Object 类型\n    Parser->>AST: 创建 item 属性\n    Note over AST: 使用 createEnumerateExpression\n    Note over AST: 从数组 keyPath 推导元素类型\n    Parser->>AST: 创建 index 属性\n    Note over AST: 固定为 number 类型\n    AST-->>Parser: 返回创建的变量声明\n    Parser-->>Form: 返回变量声明数组\n```\n\n#### 关键代码解析\n\n```typescript\nexport const provideBatchInputEffect: EffectOptions[] = createEffectFromVariableProvider({\n  private: true,\n  parse: (value: IFlowRefValue, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'item',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'index',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/types/EffectOptions): 表单副作用选项类型\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): 节点注册类型定义\n- [`createEffectFromVariableProvider`](../../guide/variable/variable-output): 从变量提供器创建表单副作用的工厂函数\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- [`ASTFactory`](https://flowgram.ai/auto-docs/editor/modules/ASTFactory): AST 创建工厂，用于生成变量声明\n- `ASTFactory.createEnumerateExpression`: 创建枚举表达式，用于从数组类型推导元素类型\n- `ASTFactory.createKeyPathExpression`: 创建键路径表达式，用于引用变量路径\n\n#### 依赖的其他物料\n\n[**BatchVariableSelector**](../components/batch-variable-selector)\n- 用于选择数组类型的变量，配合 `provideBatchInputEffect` 使用\n\n## 常见问题\n\n### 为什么 item 的类型能自动推导？\n\n`provideBatchInputEffect` 使用 `ASTFactory.createEnumerateExpression` 创建 `item` 变量。`EnumerateExpression` 是一种特殊的表达式，它会：\n\n1. 接收一个数组类型的变量引用（通过 `KeyPathExpression`）\n2. 自动推导出数组的元素类型作为自己的返回类型\n3. 当上游数组类型变化时，自动触发类型联动更新\n\n详见：[变量概念 - 表达式](../../guide/variable/concept#表达式)\n\n### 生成的变量为什么在变量选择器中看不到？\n\n检查以下几点：\n\n1. **作用域问题**：`provideBatchInputEffect` 生成的是私有变量（存储在 `node.privateScope`），只有当前节点及其子节点可以访问\n2. **配置位置**：确保 `provideBatchInputEffect` 配置在正确的字段路径上\n3. **组件配置**：如果使用 `BatchVariableSelector`，它会自动提供 `PrivateScopeProvider`；如果使用普通 `VariableSelector`，需要手动包裹 `PrivateScopeProvider`\n\n### 如何自定义 item 和 index 的变量名？\n\n目前 `provideBatchInputEffect` 不支持自定义变量名。如果需要自定义，可以参考源码实现，使用 `createEffectFromVariableProvider` 创建自己的副作用：\n\n```typescript\nimport { createEffectFromVariableProvider, ASTFactory } from '@flowgram.ai/editor';\n\nexport const customBatchInputEffect = createEffectFromVariableProvider({\n  private: true,\n  parse: (value, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'currentItem',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'currentIndex',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n```\n\n### 与 batchOutputsPlugin 的关系是什么？\n\n| 物料 | 职责 | 生成的变量 |\n|------|------|------|\n| `provideBatchInputEffect` | 处理循环**输入** | `item`、`index`（私有变量） |\n| `batchOutputsPlugin` | 处理循环**输出** | 用户配置的输出键（公共变量，数组类型） |\n\n两者配合使用，形成完整的循环节点变量逻辑：\n\n```mermaid\ngraph LR\n    A[数组输入] --> B[provideBatchInputEffect]\n    B --> C[item, index]\n    C --> D[子节点处理]\n    D --> E[BatchOutputs 配置]\n    E --> F[batchOutputsPlugin]\n    F --> G[数组输出]\n```\n\n## 相关物料\n\n- [BatchVariableSelector](../components/batch-variable-selector): 数组变量选择器，用于选择循环输入\n- [BatchOutputs](../components/batch-outputs): 循环输出配置组件\n- [batchOutputsPlugin](../form-plugins/batch-outputs-plugin): 循环输出插件，处理作用域链和类型推导\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/provide-json-schema-outputs.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/provide-json-schema-output';\n\n# provideJsonSchemaOutputs\n\nprovideJsonSchemaOutputs 是一个表单副作用，用于将 JSON Schema 定义转换为 FlowGram 变量引擎中的输出变量。\n\n它能够自动将表单中定义的 JSON Schema 结构解析为变量声明，使工作流中的其他节点可以引用这些输出。\n\n## 案例演示\n\n### 基本使用\n\n:::tip\n\n`provideJsonSchemaOutputs` 通常结合 [`syncVariableTitle`](./sync-variable-title) 使用，保证变量能**实时同步节点的标题**。\n\n:::\n\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { JsonSchemaEditor, provideJsonSchemaOutputs, syncVariableTitle } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/free-layout-editor';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <Field<IJsonSchema | undefined>\n        name=\"outputs\"\n        defaultValue={{\n          type: 'object',\n          properties: {\n            name: { type: 'string' },\n            age: { type: 'number' },\n          },\n        }}\n      >\n        {({ field }) => (\n          <JsonSchemaEditor value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n  effect: {\n    // 同步标题到变量\n    title: syncVariableTitle,\n    // 将 JSON Schema 转换为输出变量\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n\n## API 参考\n\n### provideJsonSchemaOutputs\n\n提供一个表单副作用，将 JSON Schema 转换为工作流输出变量。\n\n#### 参数\n- 无直接参数，作为表单副作用直接使用在 formMeta.effect 中\n\n#### 返回值\n- `EffectOptions[]`: 表单副作用选项数组，用于 formMeta.effect 配置\n\n#### 工作原理\n\n该表单副作用会：\n1. 获取表单中定义的 JSON Schema 值\n2. 将 Schema 转换为 FlowGram 的 AST 类型\n3. 创建变量声明，键名为当前节点 ID\n4. 设置变量的元数据（标题、图标等）\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/provide-json-schema-outputs/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/provide-json-schema-outputs\n```\n\n### 目录结构讲解\n\n```\nprovide-json-schema-outputs/\n└── index.ts           # 主实现文件，导出 provideJsonSchemaOutputs 表单副作用\n```\n\n### 核心实现说明\n\n#### 变量生成逻辑\n\nprovideJsonSchemaOutputs 使用 [`createEffectFromVariableProvider`](/guide/variable/variable-output) 工厂函数创建变量提供器。副作用内使用 `JsonSchemaUtils.schemaToAST` 函数将表单中填写的 JSON Schema 转换为 AST。\n\n:::tip\n\n`JsonSchemaUtils.schemaToAST` 递归解析 json schema 生成 AST，源码见 [utils.ts](https://github.com/bytedance/flowgram.ai/blob/main/packages/variable-engine/json-schema/src/json-schema/utils.ts)\n\n:::\n\n变量生成流程时序图：\n\n```mermaid\nsequenceDiagram\n    participant Form as 表单系统\n    participant Parser as parse函数\n    participant AST as ASTFactory\n    participant Node as 当前节点\n    participant SchemaUtils as JsonSchemaUtils\n\n    Form->>Parser: 提供JSON Schema值\n    Parser->>Node: 获取节点信息\n    Node-->>Parser: 返回节点ID和注册表\n    Parser->>SchemaUtils: 调用schemaToAST转换类型\n    SchemaUtils-->>Parser: 返回转换后的AST类型\n    Parser->>AST: 创建变量声明\n    Parser->>Parser: 设置变量键名为节点ID\n    Parser->>Node: 获取表单标题\n    alt 表单有标题\n        Node-->>Parser: 返回表单标题\n    else 表单无标题\n        Node-->>Parser: 使用节点ID作为标题\n    end\n    Parser->>Node: 获取节点图标\n    Node-->>Parser: 返回节点图标\n    AST-->>Parser: 返回创建的变量声明\n    Parser-->>Form: 返回变量声明数组\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable/json-schema)\n- [`JsonSchemaUtils`](https://flowgram.ai/auto-docs/json-schema/modules/JsonSchemaUtils): JSON Schema 工具类，用于将 Schema 转换为 AST\n- [`IJsonSchema`](https://flowgram.ai/auto-docs/json-schema/interfaces/IJsonSchema): JSON Schema 接口定义\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/types/EffectOptions): 表单副作用选项类型\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): 节点注册类型定义\n- [`createEffectFromVariableProvider`](/guide/variable/variable-output): 从变量提供器创建表单副作用的工厂函数\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`ASTFactory`](https://flowgram.ai/auto-docs/editor/modules/ASTFactory): AST 创建工厂，用于生成变量声明\n\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/sync-variable-title.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/sync-variable-title';\n\n# syncVariableTitle\n\nsyncVariableTitle 是一个表单副作用（Effect），用于同步节点的标题到该节点输出变量的元数据中。\n\n当节点标题发生变化时，会自动更新所有输出变量的 title 和 icon 元数据。\n\n| 引入前 | 引入后 |\n| --- | --- |\n| <img loading=\"lazy\" src=\"/materials/sync-variable-title-without-effect.gif\" alt=\"syncVariableTitle 没引入\" style={{ width: 500 }} /> *变量中的节点标题变化时，无法自动同步到变量中* | <img loading=\"lazy\" src=\"/materials/sync-variable-title-with-effect.gif\" alt=\"syncVariableTitle 引入\" style={{ width: 500 }} /> *变量中的节点标题会自动同步* |\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { syncVariableTitle } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  render: () => (\n    <>\n      <FormHeader />\n      <p>Please Edit Title below to sync to variables:</p>\n      <Field<string | undefined> name=\"title\">\n        {({ field }) => (\n          <Input value={field.value} onChange={(value) => field.onChange(value)} />\n        )}\n      </Field>\n    </>\n  ),\n  effect: {\n    // Sync the title to variables\n    title: syncVariableTitle,\n    outputs: provideJsonSchemaOutputs,\n  },\n}\n```\n\n## API 参考\n\n### syncVariableTitle\n\n提供一个表单副作用，用于同步节点的标题到该节点输出变量的元数据中。\n\n#### 参数\n- 无直接参数，作为表单副作用直接使用在 formMeta.effect 中\n\n#### 返回值\n- `EffectOptions[]`: 表单副作用选项数组，用于 formMeta.effect 配置\n\n#### 工作原理\n\n该表单副作用会：\n1. 监听节点标题字段的 `onValueChange` 事件\n2. 当标题发生变化时，获取节点的所有输出变量\n3. 更新每个输出变量的元数据，包括标题和图标\n4. 如果标题为空，则使用节点 ID 作为标题\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/sync-variable-title\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/sync-variable-title\n```\n\n### 目录结构讲解\n\n```\nsync-variable-title/\n└── index.ts           # 主实现文件，导出 syncVariableTitle 表单副作用\n```\n\n### 同步逻辑\n\n`syncVariableTitle` 通过注册 `DataEvent.onValueChange` 事件监听器，实现节点标题到变量元数据的实时同步。\n\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`DataEvent`](https://flowgram.ai/auto-docs/editor/enums/DataEvent): 数据事件枚举，用于监听值变化事件\n- [`Effect`](https://flowgram.ai/auto-docs/editor/types/Effect): 副作用函数类型定义\n- [`EffectOptions`](https://flowgram.ai/auto-docs/editor/interfaces/EffectOptions): 副作用配置选项接口\n- [`FlowNodeRegistry`](https://flowgram.ai/auto-docs/document/interfaces/FlowNodeRegistry-1): 节点注册类型定义\n- [`FlowNodeVariableData`](https://flowgram.ai/auto-docs/editor/classes/FlowNodeVariableData): 节点变量数据类，提供节点变量管理功能\n"
  },
  {
    "path": "apps/docs/src/zh/materials/effects/validate-when-variable-sync.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/effects/validate-when-variable-sync';\n\n# validateWhenVariableSync\n\n当字段可访问的变量发生了变化时，则重新触发指定**错误字段**的校验。\n\n:::note{title=\"为什么是错误字段？\"}\n\n错误字段的**错误信息可能源于变量引用的有效性**。\n\n如果字段的**可访问变量发生了变化，使得字段的变量引用从无效变为有效**，就需要需要重新校验当前字段，清空之前的错误信息。\n\n:::\n\n| 引入前 | 引入后 |\n| --- | --- |\n| <img loading=\"lazy\" src=\"/materials/validate-when-variable-sync-without-effect.gif\" alt=\"syncVariableTitle 没引入\" style={{ width: 500 }} /> *变量从无效变有效，错误信息还在* | <img loading=\"lazy\" src=\"/materials/validate-when-variable-sync-with-effect.gif\" alt=\"syncVariableTitle 引入\" style={{ width: 500 }} /> *变量从无效变有效，错误信息自动清空* |\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nconst formMeta = {\n  effect: {\n    value: validateWhenVariableSync(),\n  },\n  validate: {\n    value: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        errorMessages: {\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n  },\n};\n```\n\n## API 参考\n\n`validateWhenVariableSync(options?: { scope?: 'private' | 'public' })`\n\n| 参数 | 类型 | 说明 | 默认值 | 是否必选 |\n| --- | --- | --- | --- | --- |\n| `options.scope` | `'private' \\| 'public'` | 变量作用域类型，指定监听私有还是公共变量的变化 | - | 否 |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/effects/validate-when-variable-sync/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials effects/validate-when-variable-sync\n```\n\n### 目录结构讲解\n\n```\npackages/materials/form-materials/src/effects/validate-when-variable-sync/\n└── index.ts           # 核心实现和导出\n```\n\n### 核心实现说明\n\n该效果通过监听字段可访问的变量变化，当变量发生变化时自动触发错误字段的校验，确保当变量引用从无效变为有效时能及时清除错误信息。\n\n主要流程：\n1. 监听表单初始化事件 `DataEvent.onValueInit`\n2. 根据配置获取对应的节点作用域（私有或公共）\n3. 监听作用域中可用变量的变化事件 `available.onListOrAnyVarChange`\n4. 当变量变化时，筛选出包含当前字段名称的错误字段\n5. 对筛选出的错误字段重新进行校验\n6. 返回清理函数以释放事件监听器\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- `scope.available.onListOrAnyVarChange`: 监听作用域中可用变量的变化事件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/form-plugins/_meta.json",
    "content": "[\n  \"batch-outputs-plugin\",\n  \"infer-inputs-plugin\",\n  \"infer-assign-plugin\"\n]"
  },
  {
    "path": "apps/docs/src/zh/materials/form-plugins/batch-outputs-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory, WithInferSchemaStory } from 'components/form-materials/form-plugins/batch-outputs-plugin';\n\n# batchOutputsPlugin\n\n`batchOutputsPlugin` 是一个用于循环节点的表单插件，它实现了两个核心功能：\n\n1. **输出变量生成**：将循环体内收集的变量引用转换为数组类型的输出变量\n2. **作用域链转换**：调整变量作用域链，使循环节点的输出能正确依赖子节点的输出\n\n**核心特性：**\n\n- 🔄 **数组包装**：自动将循环体内引用的变量包装为数组类型输出\n- 🔗 **作用域链调整**：让循环节点的输出变量能正确依赖子节点的输出\n- 📊 **Schema 推导**：可选配置，自动推导输出变量的 JSON Schema\n\n:::tip{title=\"适用场景\"}\n\n- **循环节点**：需要在每次迭代中收集数据并聚合成数组\n- **批处理节点**：需要将多个子任务的结果汇总输出\n- **任何包含子节点的容器节点**：需要从子节点收集输出变量\n\n:::\n\n:::warning\n\n`BatchOutputs` 组件必须搭配 `batchOutputsPlugin` 使用才能正常工作。这是因为：\n1. 组件负责 UI 交互，收集用户配置的输出键值对\n2. 插件负责将配置转换为变量声明，并调整作用域链\n\n:::\n\n:::info{title=\"完整方案概览\"}\n\n实现一个完整的循环节点需要以下三个物料配合使用：\n\n| 物料 | 类型 | 职责 |\n|------|------|------|\n| [BatchVariableSelector](../components/batch-variable-selector) | 组件 | 选择循环的数组数据源 |\n| [provideBatchInputEffect](../effects/provide-batch-input) | 副作用 | 生成 `item` 和 `index` 局部变量 |\n| [BatchOutputs](../components/batch-outputs) + **batchOutputsPlugin** | 组件 + 插件 | 配置循环输出并生成数组类型变量 |\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n:::tip\n\n点开 demo 右上角的 Debug 面板，查看生成的输出变量和传到后端的 JSON 数据\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { FormRenderProps, FlowNodeJSON, Field, FormMeta } from '@flowgram.ai/free-layout-editor';\nimport {\n  BatchOutputs,\n  BatchVariableSelector,\n  createBatchOutputsFormPlugin,\n  IFlowRefValue,\n  provideBatchInputEffect,\n} from '@flowgram.ai/form-materials';\n\ninterface LoopNodeJSON extends FlowNodeJSON {\n  data: {\n    loopFor: IFlowRefValue;\n  };\n}\n\nexport const LoopFormRender = ({ form }: FormRenderProps<LoopNodeJSON>) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<IFlowRefValue> name=\"loopFor\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopFor\" type=\"array\" required>\n              <BatchVariableSelector\n                style={{ width: '100%' }}\n                value={field.value?.content}\n                onChange={(val) => field.onChange({ type: 'ref', content: val })}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n        <Field<Record<string, IFlowRefValue | undefined> | undefined> name=\"loopOutputs\">\n          {({ field, fieldState }) => (\n            <FormItem name=\"loopOutputs\" type=\"object\" vertical>\n              <BatchOutputs\n                style={{ width: '100%' }}\n                value={field.value}\n                onChange={(val) => field.onChange(val)}\n                hasError={Object.keys(fieldState?.errors || {}).length > 0}\n              />\n            </FormItem>\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [createBatchOutputsFormPlugin({ outputKey: 'loopOutputs', inferTargetKey: 'outputs' })],\n};\n```\n\n:::info{title=\"关于 FormHeader、FormContent、FormItem\"}\n\n上述代码中的 `FormHeader`、`FormContent`、`FormItem` 是用户自定义的布局组件，用于统一表单样式。你可以根据项目需求自行实现或替换为其他 UI 组件。\n\n:::\n\n### 配合 Schema 推导\n\n<WithInferSchemaStory />\n\n当配置了 `inferTargetKey` 参数时，插件会在表单提交时自动推导输出变量的 JSON Schema，并存储到指定字段。\n\n## API 参考\n\n### createBatchOutputsFormPlugin\n\n创建批处理输出插件的工厂函数。\n\n```typescript\nfunction createBatchOutputsFormPlugin(options: {\n  outputKey: string;\n  inferTargetKey?: string;\n}): FormPlugin;\n```\n\n| 属性名 | 类型 | 默认值 | 描述 |\n|--------|------|--------|------|\n| `outputKey` | `string` | - | 表单中存储循环输出配置的字段路径 |\n| `inferTargetKey` | `string` | - | 可选，推导的 JSON Schema 存储位置的字段路径 |\n\n### provideBatchOutputsEffect\n\n输出变量生成副作用，将 `Record<string, IFlowRefValue>` 格式的配置转换为数组类型的变量声明。\n\n```typescript\nimport { provideBatchOutputsEffect } from '@flowgram.ai/form-materials';\n\nconst formMeta: FormMeta = {\n  effect: {\n    loopOutputs: provideBatchOutputsEffect,\n  },\n};\n```\n\n:::tip\n\n通常情况下，使用 `createBatchOutputsFormPlugin` 即可，它内部已经包含了 `provideBatchOutputsEffect`。只有在需要单独使用变量生成功能而不需要作用域链转换时，才需要直接使用此副作用。\n\n:::\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/batch-outputs-plugin/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/batch-outputs-plugin\n```\n\n### 目录结构讲解\n\n```\nbatch-outputs-plugin/\n└── index.ts           # 插件完整实现，包含变量生成和作用域链转换逻辑\n```\n\n### 核心实现说明\n\n#### 插件结构\n\n`createBatchOutputsFormPlugin` 使用 `defineFormPluginCreator` 创建，包含两个核心生命周期：\n\n1. **onSetupFormMeta**：配置表单元数据，注册副作用和提交格式化\n2. **onInit**：初始化时注册作用域链转换器\n\n#### 输出变量生成\n\n`provideBatchOutputsEffect` 将 `Record<string, IFlowRefValue>` 转换为变量声明：\n\n```mermaid\ngraph LR\n    A[loopOutputs 配置] --> B[provideBatchOutputsEffect]\n    B --> C[变量声明]\n\n    subgraph 转换过程\n        D[\"{ names: { type: 'ref', content: ['item', 'name'] } }\"]\n        E[\"names: WrapArray(item.name)\"]\n        D --> E\n    end\n\n    C --> F[输出: names 类型为 Array]\n```\n\n关键点：使用 `ASTFactory.createWrapArrayExpression` 将单个值类型包装为数组类型。\n\n#### 作用域链转换\n\n这是该插件最核心的功能，解决循环节点的变量作用域问题。\n\n:::info{title=\"为什么需要作用域链转换？\"}\n\n在默认的作用域链逻辑中（参考[作用域链概念](../../guide/variable/concept#作用域链)）：\n- 子节点的输出变量**不能**被父节点的下游节点访问\n- 循环节点的输出变量**不能**依赖子节点的输出\n\n但在循环场景中，我们需要：\n- 循环节点的输出（如聚合的数组）能依赖子节点在每次迭代中产生的值\n- 子节点的变量能\"覆盖\"到父循环节点的作用域\n\n:::\n\n下图展示了作用域链转换的工作原理：\n\n```mermaid\ngraph TB\n    subgraph 默认作用域链\n        direction TB\n        A1[上游节点.scope] --> B1[循环节点.scope]\n        B1 --> C1[下游节点.scope]\n        B1 --> D1[子节点.scope]\n        D1 -.❌ 不可访问.-> C1\n    end\n\n    subgraph 转换后的作用域链\n        direction TB\n        A2[上游节点.scope] --> B2[循环节点.scope]\n        B2 --> C2[下游节点.scope]\n        B2 --> D2[子节点.scope]\n        D2 -.✅ 可覆盖.-> B2\n        B2 -.依赖.-> D2\n    end\n```\n\n转换器的两个核心方法：\n\n| 方法 | 作用 | 说明 |\n|------|------|------|\n| `transformCovers` | 扩展覆盖作用域 | 让子节点的变量可以\"覆盖\"到父循环节点 |\n| `transformDeps` | 调整依赖作用域 | 让循环节点的公共作用域依赖其子节点的作用域 |\n\n```mermaid\nsequenceDiagram\n    participant Loop as 循环节点\n    participant Child as 子节点\n    participant Transform as ScopeChainTransformService\n\n    Note over Loop,Child: 问题：循环节点输出依赖子节点输出\n\n    Loop->>Transform: 注册转换器\n\n    Note over Transform: transformCovers（覆盖关系）\n    Child->>Transform: 子节点查询覆盖作用域\n    Transform-->>Child: 返回 [父循环节点作用域]\n    Note over Child: 子节点变量可覆盖父循环节点\n\n    Note over Transform: transformDeps（依赖关系）\n    Loop->>Transform: 循环节点查询依赖作用域\n    Transform-->>Loop: 返回 [私有作用域, ...子节点作用域]\n    Note over Loop: 循环节点输出依赖子节点输出\n```\n\n#### 关键代码解析\n\n**1. 变量生成逻辑**\n\n```typescript\nexport const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: Record<string, IFlowRefValue>, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      type: ASTFactory.createObject({\n        properties: Object.entries(value).map(([_key, value]) =>\n          ASTFactory.createProperty({\n            key: _key,\n            initializer: ASTFactory.createWrapArrayExpression({\n              wrapFor: ASTFactory.createKeyPathExpression({\n                keyPath: value?.content || [],\n              }),\n            }),\n          })\n        ),\n      }),\n    }),\n  ],\n});\n```\n\n**2. 作用域链转换器**\n\n```typescript\nchainTransformService.registerTransformer(transformerId, {\n  transformCovers: (covers, ctx) => {\n    const node = ctx.scope.meta?.node;\n    if (node?.parent?.flowNodeType === batchNodeType) {\n      return [...covers, getNodeScope(node.parent)];\n    }\n    return covers;\n  },\n  transformDeps(scopes, ctx) {\n    const scopeMeta = ctx.scope.meta;\n    if (scopeMeta?.type === FlowNodeScopeType.private) {\n      return scopes;\n    }\n    const node = scopeMeta?.node;\n    if (node?.flowNodeType === batchNodeType) {\n      const childBlocks = node.blocks;\n      return [\n        getNodePrivateScope(node),\n        ...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),\n      ];\n    }\n    return scopes;\n  },\n});\n```\n\n**3. Schema 推导（可选）**\n\n```typescript\nif (inferTargetKey) {\n  addFormatOnSubmit((formData, ctx) => {\n    const outputVariable = getNodeScope(ctx.node).output.variables?.[0];\n    if (outputVariable?.type) {\n      set(formData, inferTargetKey, JsonSchemaUtils.astToSchema(outputVariable?.type));\n    }\n    return formData;\n  });\n}\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: 定义表单插件的工厂函数\n- `EffectOptions`: 表单副作用选项类型\n- `createEffectFromVariableProvider`: 从变量提供器创建表单副作用\n- `FlowNodeRegistry`: 节点注册类型定义\n- `FlowNodeScopeType`: 节点作用域类型枚举\n- `getNodeScope`: 获取节点的公共作用域\n- `getNodePrivateScope`: 获取节点的私有作用域\n- `ScopeChainTransformService`: 作用域链转换服务\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n- `ASTFactory`: AST 创建工厂\n- `ASTFactory.createWrapArrayExpression`: 创建数组包装表达式\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `JsonSchemaUtils.astToSchema`: 将 AST 转换为 JSON Schema\n\n#### 依赖的其他物料\n\n[**BatchOutputs**](../components/batch-outputs)\n- 循环输出配置组件，用于收集输出键值对\n\n[**BatchVariableSelector**](../components/batch-variable-selector)\n- 数组变量选择器，用于选择循环输入\n\n[**provideBatchInputEffect**](../effects/provide-batch-input)\n- 循环输入副作用，生成 item 和 index 局部变量\n\n## 常见问题\n\n### 为什么需要同时使用 BatchOutputs 组件和 batchOutputsPlugin？\n\n这是关注点分离的设计：\n\n| 角色 | 职责 |\n|------|------|\n| `BatchOutputs` 组件 | 提供 UI 交互，让用户配置输出键名和变量引用 |\n| `batchOutputsPlugin` | 处理数据逻辑，将配置转换为变量声明并调整作用域链 |\n\n单独使用组件只能收集数据，无法生成有效的输出变量；单独使用插件则没有 UI 来配置数据。\n\n### 什么时候需要配置 inferTargetKey？\n\n当你需要将输出变量的类型信息持久化到表单数据中时（例如后端需要知道输出的 JSON Schema），配置 `inferTargetKey` 可以在表单提交时自动推导并存储 Schema。\n\n### 如何与 provideBatchInputEffect 配合使用？\n\n完整的循环节点通常这样配置：\n\n```typescript\nexport const formMeta: FormMeta = {\n  render: LoopFormRender,\n  effect: {\n    loopFor: provideBatchInputEffect,\n  },\n  plugins: [\n    createBatchOutputsFormPlugin({\n      outputKey: 'loopOutputs',\n      inferTargetKey: 'outputs'\n    })\n  ],\n};\n```\n\n- `provideBatchInputEffect` 负责从循环输入数组生成 `item` 和 `index` 局部变量\n- `batchOutputsPlugin` 负责将循环体内收集的变量聚合为数组输出\n\n### 作用域链转换会影响其他节点吗？\n\n不会。转换器通过 `batchNodeType` 精确匹配当前节点类型，只对该类型的节点生效。转换器 ID 也包含节点类型，避免重复注册。\n\n## 相关物料\n\n- [provideBatchInputEffect](../effects/provide-batch-input): 循环输入变量解析\n- [BatchOutputs](../components/batch-outputs): 循环输出配置组件\n- [BatchVariableSelector](../components/batch-variable-selector): 数组变量选择器\n- [inferInputsPlugin](./infer-inputs-plugin): 输入参数 Schema 推导插件\n"
  },
  {
    "path": "apps/docs/src/zh/materials/form-plugins/infer-assign-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/form-plugins/infer-assign-plugin';\n\n# inferAssignPlugin\n\n`inferAssignPlugin` 是用于变量赋值节点，进行输出变量自动推导的表单插件，通常和 [`AssignRows`](../components/assign-rows) 物料配合使用。\n\n插件针对 `AssignRows` 的 `declare` (声明新变量) 实现了以下能力：\n- 自动生成节点输出变量，变量名即 `declare` 操作符中的左值，变量类型会自动根据 `right` 进行联动推导。\n- 当提交数据到后端时，自动根据输出变量的变量类型，联动生成对应的 JSON Schema。\n\n## 案例演示\n\n### 基本使用\n\n:::tip\n\n点开 demo 右上角的 Debug 面板，查看传到后端的 JSON 数据\n\n:::\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { createInferAssignPlugin, AssignRows, DisplayOutputs } from '@flowgram.ai/form-materials';\n\nexport const VariableFormRender = ({ form }) => {\n  return (\n    <>\n      <FormHeader />\n      <AssignRows\n        name=\"assign\"\n        defaultValue={[\n          // 从常量声明变量\n          {\n            operator: 'declare',\n            left: 'userName',\n            right: {\n              type: 'constant',\n              content: 'John Doe',\n              schema: { type: 'string' },\n            },\n          },\n          // 从变量声明变量\n          {\n            operator: 'declare',\n            left: 'userInfo',\n            right: {\n              type: 'ref',\n              content: ['start_0', 'obj'],\n            },\n          },\n          // 赋值现有变量\n          {\n            operator: 'assign',\n            left: {\n              type: 'ref',\n              content: ['start_0', 'str'],\n            },\n            right: {\n              type: 'constant',\n              content: 'Hello Flowgram',\n              schema: { type: 'string' },\n            },\n          },\n        ]}\n      />\n      <DisplayOutputs displayFromScope />\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: VariableFormRender,\n  plugins: [\n    createInferAssignPlugin({\n      assignKey: 'assign',\n      outputKey: 'outputs'\n    })\n  ],\n};\n```\n\n## API 参考\n\n```typescript\nfunction createInferAssignPlugin(options: {\n  assignKey: string;\n  outputKey: string;\n}): FormPlugin;\n```\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| assignKey | `string` | - | 表单中存储赋值操作数组的字段路径，值类型为 `AssignValueType[]` |\n| outputKey | `string` | - | 输出 JSON Schema 的存储位置字段路径 |\n\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/infer-assign-plugin\"\n/>\n\n使用 CLI 命令可以复制源代码到本地:\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/infer-assign-plugin\n```\n\n### 目录结构讲解\n\n```plaintext\ninfer-assign-plugin/\n└── index.ts                  # 插件主入口，创建插件并导出\n```\n\n### 核心实现说明\n\n\n```mermaid\nsequenceDiagram\n    participant Form as 表单\n    participant Plugin as inferAssignPlugin\n    participant Provider as VariableProvider\n    participant Scope as 变量作用域\n    participant Runtime as 后端运行时\n\n    Form->>Plugin: onSetupFormMeta()\n    Plugin->>Form: 注册变量提供副作用\n\n    loop 遍历每个赋值操作\n        alt operator = 'declare'\n          Plugin->>Plugin: 生成变量声明 (left -> VariableDeclaration)\n        end\n    end\n\n    Plugin->>Provider: 提供变量到作用域\n    Provider->>Scope: 注册输出变量\n    Plugin-->>Runtime: 推导输出变量 JSON Schema\n\n```\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: 定义表单插件的工厂函数\n- `FormPlugin`: 表单插件类型定义\n- `FormPluginSetupMetaCtx`: 插件设置上下文，提供 `mergeEffect`、`addFormatOnSubmit` 方法\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `IJsonSchema`: JSON Schema 类型定义\n\n#### 依赖的其他物料\n\n[**FlowValue**](../common/flow-value)\n- `FlowValueUtils.inferJsonSchema()`: 推断 IFlowValue 的 JSON Schema\n- `FlowValueUtils.isConstant()`, `FlowValueUtils.isRef()`: 类型判断工具\n- `IFlowValue`: Flow 值的联合类型\n- `IFlowRefValue`: 变量引用类型\n- `IFlowConstantValue`: 常量类型\n"
  },
  {
    "path": "apps/docs/src/zh/materials/form-plugins/infer-inputs-plugin.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/form-plugins/infer-inputs-plugin';\n\n# inferInputsPlugin\n\n`inferInputsPlugin`是一个用于**自动推断输入参数 JSON Schema 的表单插件**，它能**在数据传送到后端运行时前**，根据 IFlowValue 类型 (常量或变量引用) 自动生成对应的 JSON Schema 结构，为后端的运行时类型验证和后端接口提供类型信息。\n\n<br />\n<div>\n  <img loading=\"lazy\" src=\"/materials/infer-inputs-plugin.png\" alt=\"Schema 推断\" style={{ width: '75%' }} />\n  *传到后端运行时时，inputs 字段的 JSON Schema 根据 inputsValues 中的值定义推导*\n</div>\n\n:::tip{title=\"适用场景\"}\n\n- **HTTP 节点**：推断请求头 (headers)、查询参数 (params) 的 Schema\n- **代码节点**：推断代码输入参数的类型结构\n- **函数调用节点**：推断函数参数的 Schema\n- **任何接受动态输入的节点**：需要为后端提供输入类型信息\n\n:::\n\n## 案例演示\n\n### 基本使用\n\n\n### 基本使用\n\n:::tip\n\n点开 demo 右上角的 Debug 面板，查看传到后端的 JSON 数据\n\n:::\n\n<BasicStory />\n\n推断 HTTP 请求的 headers 和 body 的 Schema:\n\n```tsx pure title=\"form-meta.tsx\"\nimport { createInferInputsPlugin, InputsValue, InputsValuesTree } from '@flowgram.ai/form-materials';\nimport { Field } from '@flowgram.ai/editor';\n\nexport const HttpFormRender = ({ form }) => {\n  return (\n    <>\n      <FormHeader />\n      <FormContent>\n        <Field<Record<string, IFlowValue>> name=\"headersValues\">\n          {({ field }) => (\n            <InputsValues\n              value={field.value}\n              onChange={(val) => field.onChange(val)}\n            />\n          )}\n        </Field>\n        <Field<Record<string, IFlowValue>> name=\"bodyValues\">\n          {({ field }) => (\n            <InputsValuesTree\n              value={field.value}\n              onChange={(val) => field.onChange(val)}\n            />\n          )}\n        </Field>\n      </FormContent>\n    </>\n  );\n};\n\nexport const formMeta: FormMeta = {\n  render: HttpFormRender,\n  plugins: [\n    // 推断 headers 的 Schema\n    createInferInputsPlugin({\n      sourceKey: 'headersValues',\n      targetKey: 'headersSchema'\n    }),\n    // 推断 body 的 Schema\n    createInferInputsPlugin({\n      sourceKey: 'bodyValues',\n      targetKey: 'bodySchema'\n    })\n  ],\n};\n```\n\n## API 参考\n\n```typescript\nfunction createInferInputsPlugin(options: {\n  sourceKey: string;\n  targetKey: string;\n  scope?: 'private' | 'public';\n  ignoreConstantSchema?: boolean;\n}): FormPlugin;\n```\n\n| 属性名 | 类型 | 默认值 | 说明 |\n| :--- | :--- | :--- | :--- |\n| sourceKey | `string` | - | 表单中存储输入值的字段路径，值类型为包含 IFlowValue 的对象或数组 |\n| targetKey | `string` | - | 推断的 JSON Schema 存储位置的字段路径 |\n| scope | `'private' \\| 'public'` | `public` | 指定用于变量解析的作用域类型 |\n| ignoreConstantSchema | `boolean` | `false` | 是否在提交时剥离常量值的 Schema(仅保留变量引用的 Schema) |\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/form-plugins/infer-inputs-plugin/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials form-plugins/infer-inputs-plugin\n```\n\n### 目录结构讲解\n\n```plaintext\ninfer-inputs-plugin/\n└── index.tsx  # 插件完整实现，包含 Schema 推断和双向转换逻辑\n```\n\n### 核心实现说明\n\n`inferInputsPlugin` 的核心功能是从 IFlowValue 对象推断 JSON Schema。对于常量值，直接使用其 `schema` 字段；对于变量引用，从作用域中查询变量类型；对于表达式和模板，推断为相应的基础类型。插件还支持 `ignoreConstantSchema` 优化，在提交时剥离常量 Schema，在初始化时自动恢复。\n\n#### 工作流程时序图\n\n```mermaid\nsequenceDiagram\n    participant Form as 表单\n    participant Plugin as inferInputsPlugin\n    participant Scope as 变量作用域\n    participant Backend as 后端\n\n    Note over Form: 表单提交\n    Form->>Plugin: onSubmit 触发\n    Plugin->>Plugin: 读取 sourceKey 数据\n\n    loop 遍历每个 IFlowValue\n        alt 类型为 constant\n            Plugin->>Plugin: 使用 value.schema\n            opt ignoreConstantSchema = true\n                Plugin->>Plugin: 剥离 schema\n            end\n        else 类型为 ref\n            Plugin->>Scope: 查询变量类型\n            Scope->>Plugin: 返回变量 Schema\n        else 类型为 expression/template\n            Plugin->>Plugin: 推断为 any/string 类型\n        end\n    end\n\n    Plugin->>Plugin: 合并所有 Schema\n    Plugin->>Form: 设置到 targetKey\n    Form->>Backend: 提交包含 Schema 的数据\n\n    Note over Form: 表单初始化\n    Backend->>Form: 返回数据(可能缺少常量 Schema)\n    Form->>Plugin: onInit 触发\n    opt ignoreConstantSchema = true\n        Plugin->>Plugin: 从 sourceKey 恢复常量 Schema\n        Plugin->>Form: 更新 targetKey\n    end\n```\n\n核心功能特点：\n\n1. **自动 Schema 推断**：扫描表单数据中的 IFlowValue 对象，自动推断其 JSON Schema\n2. **变量类型解析**：对于变量引用，从作用域中解析变量的实际类型\n3. **常量 Schema 优化**：可选地在提交时剥离常量的 Schema，减少后端数据负载\n4. **双向转换**：在表单初始化时恢复 Schema，在表单提交时生成 Schema\n\n### 依赖梳理\n\n#### flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- `defineFormPluginCreator`: 定义表单插件的工厂函数\n- `FormPlugin`: 表单插件类型定义\n- `FormPluginSetupMetaCtx`: 插件设置上下文，提供 `addFormatOnInit`、`addFormatOnSubmit` 方法\n\n[**@flowgram.ai/variable-core**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/variable-core)\n\n[**@flowgram.ai/json-schema**](https://github.com/bytedance/flowgram.ai/tree/main/packages/variable-engine/json-schema)\n- `IJsonSchema`: JSON Schema 类型定义\n\n#### 依赖的其他物料\n\n[**FlowValue**](../common/flow-value)\n- `FlowValueUtils.inferJsonSchema()`: 推断 IFlowValue 的 JSON Schema\n- `FlowValueUtils.traverse()`: 遍历嵌套的 FlowValue 结构\n- `FlowValueUtils.isConstant()`, `FlowValueUtils.isRef()`: 类型判断工具\n- `IFlowValue`: Flow 值的联合类型\n- `IFlowConstantValue`: 常量类型，包含 `schema` 字段\n- `IFlowRefValue`: 变量引用类型，包含变量路径\n"
  },
  {
    "path": "apps/docs/src/zh/materials/introduction.mdx",
    "content": "import { PackageManagerTabs } from '@theme';\n\n# 快速上手\n\n## 如何使用？\n\n### 通过包引用使用\n\n官方表单物料可以直接通过包引用使用：\n\n<PackageManagerTabs command=\"install @flowgram.ai/form-materials\" />\n\n```tsx\nimport { JsonSchemaEditor } from '@flowgram.ai/form-materials'\n```\n\n\n### 通过 CLI 添加物料源代码使用\n\n\n如果业务对组件有定制诉求（如：更改文案、样式、业务逻辑），推荐 **通过 CLI 将物料源代码添加到项目中进行定制**：\n\n```bash\nnpx @flowgram.ai/cli@latest materials\n```\n\n运行后 CLI 会提示用户选择要添加到项目中的物料:\n\n```console\n? Select one material to add: (Use arrow keys)\n❯ components/json-schema-editor\n  components/type-selector\n  components/variable-selector\n```\n\n使用者也可以直接在 CLI 中添加指定物料的源代码:\n\n```bash\nnpx @flowgram.ai/cli@latest materials components/json-schema-editor\n```\n\nCLI 运行成功后，相关物料会自动添加到当前项目下的 `src/form-materials` 目录下\n\n:::warning 注意事项\n\n1. 官方物料目前底层基于 [Semi Design](https://semi.design/) 实现，业务如果有底层组件库的诉求，可以通过 CLI 复制源码进行替换\n2. 一些物料会依赖一些第三方 npm 库，这些库会在 CLI 运行时自动安装\n3. 一些物料会依赖另外一些官方物料，这些被依赖的物料源代码在 CLI 运行时会一起被添加到项目中去\n\n:::\n\n## 附：根据节点类型查找物料\n\n### 实现节点输入输出\n\n**出入参配置**\n- 节点入参：[InputsValues](./components/inputs-values), [InputsValuesTree](./components/inputs-values-tree)\n- 节点出参：[JsonSchemaEditor](./components/json-schema-editor)\n- 入参校验：[validateFlowValue](./validate/validate-flow-value)\n- 出参变量生成：[provideJsonSchemaOutputs](./effects/provide-json-schema-outputs)\n\n**出入参展示**\n- 入参展示：[DisplayInputsValues](./components/display-inputs-values)\n- 出参展示：[DisplayOutputs](./components/display-outputs)\n\n### 实现代码节点\n\n- 代码编辑器：[CodeEditor](./components/code-editor)\n\n### 实现大模型节点\n\n- 提示词编辑器：[PromptEditor](./components/prompt-editor)\n- 可选变量的提示词编辑器：[PromptEditorWithVariables](./components/prompt-editor-with-variables)\n\n### 实现条件分支节点\n\n- 单行条件分支配置：[ConditionRow](./components/condition-row)\n- 监听变量实现分支联动：[listenRefSchemaChange](./effects/listen-ref-schema-change), [listenRefValueChange](./effects/listen-ref-value-change)\n\n### 实现数据库节点\n\n- 单行数据库查询条件配置：[DBConditionRow](./components/db-condition-row)\n- SQL 编辑器：[SQLCodeEditor](./components/code-editor)\n- 可选变量的 SQL 编辑器：[SQLEditorWithVariables](./components/sql-editor-with-variables)\n\n\n#### 实现循环节点\n\n**循环输入**\n- 循环输入数组变量选择器：[BatchVariableSelector](./components/batch-variable-selector)\n- Item、Index 推导：[provideBatchInput](./effects/provide-batch-input)\n\n**循环输出**\n- 循环输出数组变量选择器：[BatchOutputs](./components/batch-outputs)\n- 输出变量作用域链调整 + 类型推导：[batchOutputsPlugin](./form-plugins/batch-outputs-plugin)\n\n\n### 实现 HTTP 节点\n\n- JSON 编辑器：[JsonCodeEditor](./components/code-editor)\n- 可选变量的 JSON 编辑器：[JsonEditorWithVariables](./components/json-editor-with-variables)\n\n### 实现变量赋值/声明节点\n\n- 单行变量赋值、声明：[AssignRow](./components/assign-row)\n- 变量赋值、声明配置列表：[AssignRows](./components/assign-rows)\n- 变量声明类型自动推导：[inferAssignPlugin](./form-plugins/infer-assign-plugin)\n"
  },
  {
    "path": "apps/docs/src/zh/materials/validate/_meta.json",
    "content": "[\n  \"validate-flow-value\"\n]"
  },
  {
    "path": "apps/docs/src/zh/materials/validate/validate-flow-value.mdx",
    "content": "import { SourceCode } from '@theme';\nimport { BasicStory } from 'components/form-materials/validate/validate-flow-value';\n\n# validateFlowValue\n\nvalidateFlowValue 是一个用于验证 [`FlowValue`](../common/flow-value) **必填性和变量引用有效性** 的验证函数。\n\n## 案例演示\n\n### 基本使用\n\n<BasicStory />\n\n```tsx pure title=\"form-meta.tsx\"\nimport { validateFlowValue } from '@flowgram.ai/form-materials';\n\nconst formMeta = {\n  validate: {\n    dynamic_value_input: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        errorMessages: {\n          required: 'Value is required',\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n    required_dynamic_value_input: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        required: true,\n        errorMessages: {\n          required: 'Value is required',\n          unknownVariable: 'Unknown Variable',\n        },\n      }),\n    prompt_editor: ({ value, context }) =>\n      validateFlowValue(value, {\n        node: context.node,\n        required: true,\n        errorMessages: {\n          required: 'Prompt is required',\n          unknownVariable: 'Unknown Variable In Template',\n        },\n      }),\n  },\n  render: ({ form }) => (\n    <>\n      <FormHeader />\n      <b>Validate variable valid</b>\n      <Field<any> name=\"dynamic_value_input\">\n        {({ field, fieldState }) => (\n          <>\n            <DynamicValueInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <b>Validate required value</b>\n      <Field<any> name=\"required_dynamic_value_input\">\n        {({ field, fieldState }) => (\n          <>\n            <DynamicValueInput\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <b>Validate required and variables valid in prompt</b>\n      <Field<any> name=\"prompt_editor\">\n        {({ field, fieldState }) => (\n          <>\n            <PromptEditorWithVariables\n              value={field.value}\n              onChange={(value) => field.onChange(value)}\n            />\n            <span style={{ color: 'red' }}>\n              {fieldState.errors?.map((e) => e.message).join('\\n')}\n            </span>\n          </>\n        )}\n      </Field>\n      <br />\n      <Button onClick={() => form.validate()}>Trigger Validate</Button>\n    </>\n  ),\n};\n```\n\n## API 参考\n\n### validateFlowValue 函数\n\n```typescript\nexport function validateFlowValue(value: IFlowValue | undefined, ctx: Context): {\n  level: FeedbackLevel.Error;\n  message: string;\n} | undefined;\n```\n\n#### 参数\n\n| 参数名 | 类型 | 描述 |\n|--------|------|------|\n| `value` | `IFlowValue \\| undefined` | 要验证的 FlowValue 值 |\n| `ctx` | `Context` | 验证上下文 |\n\n#### Context 接口\n\n```typescript\ninterface Context {\n  node: FlowNodeEntity;\n  required?: boolean; // 是否必填\n  errorMessages?: {\n    required?: string; // 必填错误消息\n    unknownVariable?: string; // 未知变量错误消息\n  };\n}\n```\n\n#### 返回值\n\n- 如果验证通过，返回 `undefined`\n- 如果验证失败，返回包含错误级别和错误消息的对象\n\n### 支持的验证类型\n\n1. **必填验证**：当 `required` 设置为 `true` 时，验证值是否存在且内容不为空\n2. **引用变量验证**：对于 `ref` 类型的值，验证引用的变量是否存在\n3. **模板变量验证**：对于 `template` 类型的值，验证模板中引用的所有变量是否存在\n\n## 源码导读\n\n<SourceCode\n  href=\"https://github.com/bytedance/flowgram.ai/tree/main/packages/materials/form-materials/src/validate/validate-flow-value/index.ts\"\n/>\n\n使用 CLI 命令可以复制源代码到本地：\n\n```bash\nnpx @flowgram.ai/cli@latest materials validate/validate-flow-value\n```\n\n### 目录结构讲解\n\n```\nvalidate-flow-value/\n└── index.tsx           # 主函数实现，包含 validateFlowValue 核心逻辑\n```\n\n### 核心实现说明\n\n#### 必填验证逻辑\n\n```typescript\nif (required && (isNil(value) || isNil(value?.content) || value?.content === '')) {\n  return {\n    level: FeedbackLevel.Error,\n    message: requiredMessage,\n  };\n}\n```\n\n#### 引用变量验证逻辑\n\n```typescript\nif (value?.type === 'ref') {\n  const variable = node.scope.available.getByKeyPath(value?.content || []);\n  if (!variable) {\n    return {\n      level: FeedbackLevel.Error,\n      message: unknownVariableMessage,\n    };\n  }\n}\n```\n\n#### 模板内变量验证逻辑\n\n```typescript\nif (value?.type === 'template') {\n  const allRefs = FlowValueUtils.getTemplateKeyPaths(value);\n\n  for (const ref of allRefs) {\n    const variable = node.scope.available.getByKeyPath(ref);\n    if (!variable) {\n      return {\n        level: FeedbackLevel.Error,\n        message: unknownVariableMessage,\n      };\n    }\n  }\n}\n```\n\n### 使用到的 flowgram API\n\n[**@flowgram.ai/editor**](https://github.com/bytedance/flowgram.ai/tree/main/packages/client/editor)\n- [`FeedbackLevel`](https://flowgram.ai/auto-docs/editor/enums/FeedbackLevel): 反馈级别枚举\n\n### 依赖的其他物料\n\n[**FlowValue**](../common/flow-value)\n- `IFlowValue`: FlowValue 类型定义\n- `FlowValueUtils`: FlowValue 工具类\n  - `getTemplateKeyPaths`: 从模板中提取所有变量引用路径的方法\n\n### 第三方库\n\n[**lodash-es**](https://lodash.com/)\n- `isNil`: 检查值是否为 null 或 undefined\n"
  },
  {
    "path": "apps/docs/theme/components/background/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.background2-canvas {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  pointer-events: none;\n  z-index: -1;\n}\n\n/* Ensure smooth rendering */\n.background2-canvas {\n  image-rendering: -webkit-optimize-contrast;\n  image-rendering: -moz-crisp-edges;\n  image-rendering: crisp-edges;\n  image-rendering: pixelated;\n}\n\n/* Dark mode specific adjustments */\n[data-theme='dark'] .background2-canvas {\n  opacity: 0.9;\n}\n\n/* Light mode specific adjustments */\n[data-theme='light'] .background2-canvas {\n  opacity: 0.8;\n}\n\n/* Animation performance optimization */\n.background2-canvas {\n  will-change: transform;\n  transform: translateZ(0);\n  backface-visibility: hidden;\n}"
  },
  {
    "path": "apps/docs/theme/components/background/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useRef, useState, useCallback, useMemo } from 'react';\n\nimport { useDark } from '@rspress/core/runtime';\nimport './index.css';\n\n// Performance configuration based on device capabilities\ninterface PerformanceConfig {\n  enabled: boolean;\n  meteorCount: number;\n  maxFlameTrails: number;\n  trailLength: number;\n  animationQuality: 'high' | 'medium' | 'low';\n  frameSkip: number;\n}\n\n// Performance detection utilities\nconst detectPerformance = (): PerformanceConfig => {\n  // Check for reduced motion preference\n  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n    return {\n      enabled: false,\n      meteorCount: 0,\n      maxFlameTrails: 0,\n      trailLength: 0,\n      animationQuality: 'low',\n      frameSkip: 0,\n    };\n  }\n\n  // Basic device capability detection\n  const canvas = document.createElement('canvas');\n  const ctx = canvas.getContext('2d');\n  if (!ctx) {\n    return {\n      enabled: false,\n      meteorCount: 0,\n      maxFlameTrails: 0,\n      trailLength: 0,\n      animationQuality: 'low',\n      frameSkip: 0,\n    };\n  }\n\n  // Check hardware concurrency (CPU cores)\n  const cores = navigator.hardwareConcurrency || 2;\n\n  // Check memory (if available)\n  const memory = (navigator as any).deviceMemory || 4;\n\n  // Check if mobile device\n  const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\n    navigator.userAgent\n  );\n\n  // Performance scoring\n  let score = 0;\n  score += cores >= 4 ? 2 : cores >= 2 ? 1 : 0;\n  score += memory >= 8 ? 2 : memory >= 4 ? 1 : 0;\n  score += isMobile ? -1 : 1;\n\n  // Configure based on performance score\n  if (score >= 4) {\n    // High performance\n    return {\n      enabled: true,\n      meteorCount: 12,\n      maxFlameTrails: 15,\n      trailLength: 20,\n      animationQuality: 'high',\n      frameSkip: 1,\n    };\n  } else if (score >= 2) {\n    // Medium performance\n    return {\n      enabled: true,\n      meteorCount: 8,\n      maxFlameTrails: 8,\n      trailLength: 12,\n      animationQuality: 'medium',\n      frameSkip: 2,\n    };\n  } else {\n    // Low performance - disable\n    return {\n      enabled: false,\n      meteorCount: 0,\n      maxFlameTrails: 0,\n      trailLength: 0,\n      animationQuality: 'low',\n      frameSkip: 0,\n    };\n  }\n};\n\n// Trail particle interface for flame effect\ninterface TrailParticle {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  size: number;\n  tick: number;\n  life: number;\n  alpha: number;\n  color: string;\n}\n\n// Meteor particle interface definition\ninterface MeteorParticle {\n  x: number;\n  y: number;\n  vx: number;\n  vy: number;\n  radius: number;\n  color: string;\n  alpha: number;\n  trail: Array<{ x: number; y: number; alpha: number }>;\n  trailLength: number;\n  speed: number;\n  angle: number;\n  // New flame trail system\n  flameTrails: TrailParticle[];\n  maxFlameTrails: number;\n}\n\n// Configuration options for flame effects\nconst flameOptions = {\n  trailSizeBaseMultiplier: 0.6,\n  trailSizeAddedMultiplier: 0.3,\n  trailSizeSpeedMultiplier: 0.15,\n  trailAddedBaseRadiant: -0.8,\n  trailAddedAddedRadiant: 3,\n  trailBaseLifeSpan: 25,\n  trailAddedLifeSpan: 20,\n  trailGenerationChance: 0.3,\n};\n\n// Trail class for managing individual flame particles\nclass Trail {\n  private particle: TrailParticle;\n\n  private parentMeteor: MeteorParticle;\n\n  constructor(parent: MeteorParticle) {\n    this.parentMeteor = parent;\n    this.particle = this.createTrailParticle();\n  }\n\n  // Create a new trail particle based on parent meteor\n  private createTrailParticle = (): TrailParticle => {\n    const baseSize =\n      this.parentMeteor.radius *\n      (flameOptions.trailSizeBaseMultiplier +\n        flameOptions.trailSizeAddedMultiplier * Math.random());\n\n    const radiantOffset =\n      flameOptions.trailAddedBaseRadiant + flameOptions.trailAddedAddedRadiant * Math.random();\n    const trailAngle = this.parentMeteor.angle + radiantOffset;\n    const speed = baseSize * flameOptions.trailSizeSpeedMultiplier;\n\n    return {\n      x: this.parentMeteor.x + (Math.random() - 0.5) * this.parentMeteor.radius,\n      y: this.parentMeteor.y + (Math.random() - 0.5) * this.parentMeteor.radius,\n      vx: speed * Math.cos(trailAngle),\n      vy: speed * Math.sin(trailAngle),\n      size: baseSize,\n      tick: 0,\n      life: Math.floor(\n        flameOptions.trailBaseLifeSpan + flameOptions.trailAddedLifeSpan * Math.random()\n      ),\n      alpha: 0.8 + Math.random() * 0.2,\n      color: this.parentMeteor.color,\n    };\n  };\n\n  // Update trail particle position and lifecycle\n  public step = (): boolean => {\n    this.particle.tick++;\n\n    // Check if trail particle should be removed\n    if (this.particle.tick > this.particle.life) {\n      return false; // Signal for removal\n    }\n\n    // Update position\n    this.particle.x += this.particle.vx;\n    this.particle.y += this.particle.vy;\n\n    // Apply slight deceleration for more realistic flame behavior\n    this.particle.vx *= 0.98;\n    this.particle.vy *= 0.98;\n\n    return true; // Continue existing\n  };\n\n  // Render the trail particle\n  public draw = (ctx: CanvasRenderingContext2D): void => {\n    const lifeRatio = 1 - this.particle.tick / this.particle.life;\n    const currentSize = this.particle.size * lifeRatio;\n    const currentAlpha = this.particle.alpha * lifeRatio;\n\n    if (currentSize <= 0 || currentAlpha <= 0) return;\n\n    const alphaHex = Math.floor(currentAlpha * 255)\n      .toString(16)\n      .padStart(2, '0');\n\n    // Draw flame particle with gradient\n    const gradient = ctx.createRadialGradient(\n      this.particle.x,\n      this.particle.y,\n      0,\n      this.particle.x,\n      this.particle.y,\n      currentSize * 2\n    );\n\n    gradient.addColorStop(0, this.particle.color + 'FF');\n    gradient.addColorStop(0.4, this.particle.color + alphaHex);\n    gradient.addColorStop(0.8, this.particle.color + '33');\n    gradient.addColorStop(1, this.particle.color + '00');\n\n    ctx.beginPath();\n    ctx.arc(this.particle.x, this.particle.y, currentSize, 0, Math.PI * 2);\n    ctx.fillStyle = gradient;\n    ctx.fill();\n\n    // Add glow effect\n    ctx.shadowColor = this.particle.color;\n    ctx.shadowBlur = currentSize * 2;\n    ctx.fill();\n    ctx.shadowBlur = 0;\n  };\n}\n\n// Background2 component - Circular meteor with enhanced flame trailing effect\nexport const Background: React.FC = () => {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const animationRef = useRef<number>(0);\n  const meteorsRef = useRef<MeteorParticle[]>([]);\n  const mouseRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n  const frameCountRef = useRef<number>(0);\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n  const isDark = useDark();\n\n  // Performance configuration - memoized to avoid recalculation\n  const performanceConfig = useMemo(() => detectPerformance(), []);\n\n  // Early return if animation is disabled\n  if (!performanceConfig.enabled) {\n    return null;\n  }\n\n  // Color configuration - adjusted based on theme mode\n  const lightColors = ['#4062A7', '#5482BE', '#5ABAC2', '#86C8C5'];\n  const darkColors = ['#6B8CFF', '#8DA9FF', '#7FDBDA', '#A8EDEA'];\n  const colors = isDark ? darkColors : lightColors;\n\n  // Initialize meteor particles with flame trail system - optimized based on performance\n  const initMeteors = useCallback((): void => {\n    meteorsRef.current = [];\n\n    for (let i = 0; i < performanceConfig.meteorCount; i++) {\n      const angle = Math.PI * 0.25; // Fixed 45 degrees (down-right)\n      const radius = Math.random() * 1.2 + 1.8; // Slightly larger meteors for better flame effect\n      const speed = (radius - 1.8) * 3.0 + 2.0; // Adjusted speed range\n      const trailLength = Math.floor(\n        Math.random() * (performanceConfig.trailLength * 0.5) + performanceConfig.trailLength * 0.5\n      );\n\n      meteorsRef.current.push({\n        x: Math.random() * dimensions.width,\n        y: Math.random() * dimensions.height,\n        vx: Math.cos(angle) * speed,\n        vy: Math.sin(angle) * speed,\n        radius,\n        color: colors[Math.floor(Math.random() * colors.length)],\n        alpha: Math.random() * 0.3 + 0.7,\n        trail: [],\n        trailLength,\n        speed,\n        angle,\n        // Initialize flame trail system with performance-based limits\n        flameTrails: [],\n        maxFlameTrails: Math.floor(\n          radius * performanceConfig.maxFlameTrails * 0.5 + performanceConfig.maxFlameTrails * 0.3\n        ),\n      });\n    }\n  }, [performanceConfig, dimensions.width, dimensions.height, colors]);\n\n  // Update meteor trail (original trail system) - optimized\n  const updateTrail = useCallback(\n    (meteor: MeteorParticle): void => {\n      meteor.trail.unshift({\n        x: meteor.x,\n        y: meteor.y,\n        alpha: meteor.alpha,\n      });\n\n      if (meteor.trail.length > meteor.trailLength) {\n        meteor.trail.pop();\n      }\n\n      // Only update alpha for visible trail points in high quality mode\n      if (performanceConfig.animationQuality === 'high') {\n        meteor.trail.forEach((point, index) => {\n          point.alpha = meteor.alpha * (1 - index / meteor.trailLength);\n        });\n      }\n    },\n    [performanceConfig.animationQuality]\n  );\n\n  // Update flame trails system - optimized\n  const updateFlameTrails = useCallback(\n    (meteor: MeteorParticle): void => {\n      // Reduce flame trail generation based on performance config\n      const generationChance =\n        performanceConfig.animationQuality === 'high'\n          ? flameOptions.trailGenerationChance\n          : performanceConfig.animationQuality === 'medium'\n          ? flameOptions.trailGenerationChance * 0.7\n          : flameOptions.trailGenerationChance * 0.4;\n\n      // Generate new flame trail particles\n      if (meteor.flameTrails.length < meteor.maxFlameTrails && Math.random() < generationChance) {\n        const trail = new Trail(meteor);\n        meteor.flameTrails.push(trail as any); // Type assertion for compatibility\n      }\n\n      // Update existing flame trails and remove expired ones\n      meteor.flameTrails = meteor.flameTrails.filter((trail: any) => trail.step && trail.step());\n    },\n    [performanceConfig.animationQuality]\n  );\n\n  // Draw meteor with enhanced flame trailing effect - optimized\n  const drawMeteor = useCallback(\n    (ctx: CanvasRenderingContext2D, meteor: MeteorParticle): void => {\n      // Draw flame trails first (behind the meteor) - skip in low quality mode\n      if (performanceConfig.animationQuality !== 'low') {\n        meteor.flameTrails.forEach((trail: any) => {\n          if (trail.draw) {\n            trail.draw(ctx);\n          }\n        });\n      }\n\n      // Draw original trail system with reduced complexity for lower quality\n      const trailStep =\n        performanceConfig.animationQuality === 'high'\n          ? 1\n          : performanceConfig.animationQuality === 'medium'\n          ? 2\n          : 3;\n\n      for (let i = 0; i < meteor.trail.length; i += trailStep) {\n        const point = meteor.trail[i];\n        const trailRadius = meteor.radius * (1 - i / meteor.trail.length) * 0.8;\n        const alpha =\n          performanceConfig.animationQuality === 'high'\n            ? point.alpha\n            : meteor.alpha * (1 - i / meteor.trail.length);\n\n        const alphaHex = Math.floor(alpha * 255)\n          .toString(16)\n          .padStart(2, '0');\n\n        ctx.beginPath();\n        ctx.arc(point.x, point.y, trailRadius, 0, Math.PI * 2);\n        ctx.fillStyle = meteor.color + alphaHex;\n        ctx.fill();\n\n        // Reduce shadow effects for better performance\n        if (performanceConfig.animationQuality === 'high') {\n          ctx.shadowColor = meteor.color;\n          ctx.shadowBlur = trailRadius * 2 + meteor.radius;\n          ctx.fill();\n          ctx.shadowBlur = 0;\n        }\n      }\n\n      // Draw main meteor body\n      ctx.beginPath();\n      ctx.arc(meteor.x, meteor.y, meteor.radius, 0, Math.PI * 2);\n\n      // Simplified gradient for lower quality modes\n      if (performanceConfig.animationQuality === 'high') {\n        const gradient = ctx.createRadialGradient(\n          meteor.x,\n          meteor.y,\n          0,\n          meteor.x,\n          meteor.y,\n          meteor.radius * 2.5\n        );\n        gradient.addColorStop(0, meteor.color + 'FF');\n        gradient.addColorStop(0.5, meteor.color + 'DD');\n        gradient.addColorStop(0.8, meteor.color + '77');\n        gradient.addColorStop(1, meteor.color + '00');\n        ctx.fillStyle = gradient;\n      } else {\n        ctx.fillStyle = meteor.color + 'DD';\n      }\n\n      ctx.fill();\n\n      // Add bright core\n      ctx.beginPath();\n      ctx.arc(meteor.x, meteor.y, meteor.radius * 0.7, 0, Math.PI * 2);\n      ctx.fillStyle = meteor.color + 'FF';\n      ctx.fill();\n\n      // Enhanced outer glow - only in high quality mode\n      if (performanceConfig.animationQuality === 'high') {\n        ctx.shadowColor = meteor.color;\n        ctx.shadowBlur = meteor.radius * 5 + 3;\n        ctx.fill();\n        ctx.shadowBlur = 0;\n      }\n    },\n    [performanceConfig.animationQuality]\n  );\n\n  // Update meteor position and behavior\n  const updateMeteor = (meteor: MeteorParticle): void => {\n    // Mouse interaction - meteors are slightly attracted to mouse\n    const dx = mouseRef.current.x - meteor.x;\n    const dy = mouseRef.current.y - meteor.y;\n    const distance = Math.sqrt(dx * dx + dy * dy);\n\n    if (distance < 120) {\n      const force = ((120 - distance) / 120) * 0.008; // Slightly reduced force for stability\n      const angle = Math.atan2(dy, dx);\n      meteor.vx += Math.cos(angle) * force;\n      meteor.vy += Math.sin(angle) * force;\n    }\n\n    // Update both trail systems before moving\n    updateTrail(meteor);\n    updateFlameTrails(meteor);\n\n    // Update position\n    meteor.x += meteor.vx;\n    meteor.y += meteor.vy;\n\n    // Boundary wrapping with smooth transition\n    if (meteor.x > dimensions.width + meteor.radius * 3) {\n      meteor.x = -meteor.radius * 3;\n      meteor.trail = []; // Clear trail when wrapping\n      meteor.flameTrails = []; // Clear flame trails when wrapping\n    }\n    if (meteor.x < -meteor.radius * 3) {\n      meteor.x = dimensions.width + meteor.radius * 3;\n      meteor.trail = [];\n      meteor.flameTrails = [];\n    }\n    if (meteor.y > dimensions.height + meteor.radius * 3) {\n      meteor.y = -meteor.radius * 3;\n      meteor.trail = [];\n      meteor.flameTrails = [];\n    }\n    if (meteor.y < -meteor.radius * 3) {\n      meteor.y = dimensions.height + meteor.radius * 3;\n      meteor.trail = [];\n      meteor.flameTrails = [];\n    }\n\n    // Maintain consistent direction - gently guide back to base direction\n    const baseAngle = Math.PI * 0.25; // 45 degrees\n\n    // Gradually adjust direction\n    meteor.vx += Math.cos(baseAngle) * 0.003;\n    meteor.vy += Math.sin(baseAngle) * 0.003;\n\n    // Apply slight damping to prevent excessive speed\n    meteor.vx *= 0.999;\n    meteor.vy *= 0.999;\n\n    // Maintain minimum speed to keep meteors moving\n    const currentSpeed = Math.sqrt(meteor.vx * meteor.vx + meteor.vy * meteor.vy);\n    if (currentSpeed < 0.5) {\n      meteor.vx = Math.cos(baseAngle) * 0.8;\n      meteor.vy = Math.sin(baseAngle) * 0.8;\n    }\n\n    // Update meteor angle for flame trail generation\n    meteor.angle = Math.atan2(meteor.vy, meteor.vx);\n  };\n\n  // Animation loop - optimized with frame skipping\n  const animate = useCallback((): void => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    // Frame skipping for performance optimization\n    frameCountRef.current++;\n    if (frameCountRef.current % performanceConfig.frameSkip !== 0) {\n      animationRef.current = requestAnimationFrame(animate);\n      return;\n    }\n\n    // Clear canvas completely to avoid permanent trails\n    ctx.clearRect(0, 0, dimensions.width, dimensions.height);\n\n    // Add subtle background with very low opacity for better flame visibility\n    // Skip background overlay in low quality mode\n    if (performanceConfig.animationQuality !== 'low') {\n      const bgColor = isDark ? 'rgba(13, 17, 23, 0.015)' : 'rgba(237, 243, 248, 0.015)';\n      ctx.fillStyle = bgColor;\n      ctx.fillRect(0, 0, dimensions.width, dimensions.height);\n    }\n\n    // Update and draw meteors\n    meteorsRef.current.forEach((meteor) => {\n      updateMeteor(meteor);\n      drawMeteor(ctx, meteor);\n    });\n\n    animationRef.current = requestAnimationFrame(animate);\n  }, [performanceConfig, dimensions, isDark, updateMeteor, drawMeteor]);\n\n  // Handle window resize - optimized\n  useEffect(() => {\n    const handleResize = (): void => {\n      setDimensions({\n        width: window.innerWidth,\n        height: window.innerHeight,\n      });\n    };\n\n    handleResize();\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  // Handle mouse movement - optimized with throttling\n  useEffect(() => {\n    let throttleTimer: NodeJS.Timeout | null = null;\n\n    const handleMouseMove = (e: MouseEvent): void => {\n      if (throttleTimer) return;\n\n      throttleTimer = setTimeout(\n        () => {\n          mouseRef.current = { x: e.clientX, y: e.clientY };\n          throttleTimer = null;\n        },\n        performanceConfig.animationQuality === 'high'\n          ? 16\n          : performanceConfig.animationQuality === 'medium'\n          ? 32\n          : 64\n      );\n    };\n\n    window.addEventListener('mousemove', handleMouseMove);\n    return () => {\n      window.removeEventListener('mousemove', handleMouseMove);\n      if (throttleTimer) {\n        clearTimeout(throttleTimer);\n      }\n    };\n  }, [performanceConfig.animationQuality]);\n\n  // Initialize and start animation - optimized\n  useEffect(() => {\n    if (dimensions.width === 0 || dimensions.height === 0) return;\n\n    initMeteors();\n    animate();\n\n    return () => {\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current);\n      }\n    };\n  }, [dimensions, isDark, initMeteors, animate]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      width={dimensions.width}\n      height={dimensions.height}\n      className=\"background2-canvas\"\n      style={{\n        position: 'absolute',\n        width: '100%',\n        height: '100%',\n        pointerEvents: 'none',\n        zIndex: -1,\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/index.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.flowgram-logo-container {\n  position: absolute;\n  top: 50px;\n  width: calc(30vw - 64px);\n  height: 550px;\n  opacity: 0;\n\n  // Mobile responsive: move to top on small screens to prevent text overlap\n  @media (max-width: 768px) {\n    top: 0;\n    right: 0;\n    width: 100%;\n    height: 300px;\n    margin-bottom: 20px;\n    pointer-events: none;\n  }\n\n  // Tablet responsive: adjust size and position\n  @media (min-width: 769px) and (max-width: 1024px) {\n    width: calc(45vw - 32px);\n    right: 16px;\n  }\n\n  // Ensure minimum width to prevent squashing\n  @media (min-width: 1025px) and (max-width: 1300px) {\n    width: calc(45vw - 32px);\n    right: 0;\n  }\n\n  // Ensure minimum width to prevent squashing\n  @media (min-width: 1301) {\n    width: calc(30vw - 64px);\n    right: 15vw;\n  }\n}\n\n.flowgram-logo-mask {\n  background-image: conic-gradient(from 180deg at 50% 50%, #4161a6, #5681bd, #4a9da3, #479590, #4161a6);\n  filter: blur(120px);\n  opacity: 0.4;\n  z-index: 0;\n  border-radius: 100%;\n  width: 80%;\n  height: 80%;\n  animation: flowgram-logo-spin 2s linear infinite;\n}\n\n@keyframes flowgram-logo-spin {\n  from {\n    transform: translateX(20%) translateY(20%) rotate(0deg) scale(0.8);\n  }\n\n  50% {\n    transform: translateX(20%) translateY(20%) rotate(180deg) scale(1);\n  }\n\n  to {\n    transform: translateX(20%) translateY(20%) rotate(360deg) scale(0.8);\n  }\n}\n\n.gedit-playground {\n  background: transparent !important;\n}\n\n.gedit-playground-scroll-right-block {\n  display: none;\n}\n\n.gedit-playground-scroll-bottom-block {\n  display: none;\n}\n\n\n.flowgram-logo-node {\n  width: 60px;\n  min-height: 150px;\n  height: auto;\n  border-radius: 16px;\n  box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  position: relative;\n  padding: 12px;\n  cursor: move;\n  transition: box-shadow 0.3s ease-in-out, transform 0.2s ease-in-out;\n\n  &:hover {\n    box-shadow:\n      0 2px 6px 0 rgba(0, 0, 0, 0.04),\n      0 4px 12px 0 rgba(0, 0, 0, 0.02),\n      0 0 16px 2px rgba(var(--glow-color, 59, 130, 246), 0.6),\n      0 0 32px 6px rgba(var(--glow-color, 59, 130, 246), 0.4),\n      0 0 48px 12px rgba(var(--glow-color, 59, 130, 246), 0.25),\n      0 0 64px 16px rgba(var(--glow-color, 59, 130, 246), 0.15);\n    transform: scale(1.05);\n  }\n}\n"
  },
  {
    "path": "apps/docs/theme/components/logo/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/free-layout-editor/index.css';\n\nimport { FreeLayoutEditorProvider, EditorRenderer } from '@flowgram.ai/free-layout-editor';\n\nimport './index.less';\n\nimport { useEditorProps } from './use-editor-props';\nimport { FlowGramLogoMask } from './musk';\n\nexport const FlowGramLogo = () => {\n  const editorProps = useEditorProps();\n  return (\n    <div className=\"flowgram-logo-container\">\n      <FlowGramLogoMask />\n      <FreeLayoutEditorProvider {...editorProps}>\n        <EditorRenderer />\n      </FreeLayoutEditorProvider>\n    </div>\n  );\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/initial-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-editor';\n\nexport const initialData: WorkflowJSON = {\n  nodes: [\n    {\n      id: '1',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n    },\n    {\n      id: '2',\n      type: 'custom',\n      meta: {\n        position: { x: 110, y: 0 },\n      },\n    },\n    {\n      id: '3',\n      type: 'custom',\n      meta: {\n        position: { x: 220, y: 0 },\n      },\n    },\n    {\n      id: '4',\n      type: 'end',\n      meta: {\n        position: { x: 330, y: 0 },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: '1',\n      targetNodeID: '2',\n    },\n    {\n      sourceNodeID: '2',\n      targetNodeID: '3',\n    },\n    {\n      sourceNodeID: '3',\n      targetNodeID: '4',\n    },\n  ],\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/musk.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useDark } from '@rspress/core/runtime';\n\nexport const FlowGramLogoMask = () => {\n  const isDark = useDark();\n  return (\n    <div\n      className=\"flowgram-logo-mask\"\n      style={{\n        backgroundImage: isDark\n          ? 'conic-gradient(from 180deg at 50% 50%, #0095ff 0deg, 180deg, #42d392 1turn)'\n          : 'conic-gradient(from 180deg at 50% 50%, #3473fb 0deg, 180deg, #46cbc2 1turn)',\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/node-color.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const NodeColorMap: Record<string, string> = {\n  '1': '#4161A6',\n  '2': '#5681BD',\n  '3': '#5DBBC1',\n  '4': '#8BC9C5',\n};\n\nexport const NodeBorderColorMap: Record<string, string> = {\n  '1': '#355397',\n  '2': '#426ca7',\n  '3': '#46a6ac',\n  '4': '#7cbab6',\n};\n\nexport const NodeGlowColorMap: Record<string, string> = {\n  '1': '141, 178, 254',\n  '2': '145, 190, 254',\n  '3': '155, 248, 254',\n  '4': '191, 255, 250',\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/node-registries.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeRegistry } from '@flowgram.ai/free-layout-editor';\n\nexport const nodeRegistries: WorkflowNodeRegistry[] = [\n  {\n    type: 'start',\n    meta: {\n      isStart: true,\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'output' }],\n    },\n  },\n  {\n    type: 'end',\n    meta: {\n      deleteDisable: true,\n      copyDisable: true,\n      defaultPorts: [{ type: 'input' }],\n    },\n  },\n  {\n    type: 'custom',\n    meta: {},\n    defaultPorts: [{ type: 'output' }, { type: 'input' }],\n  },\n];\n"
  },
  {
    "path": "apps/docs/theme/components/logo/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport '@flowgram.ai/free-layout-editor/index.css';\n\nimport {\n  useNodeRender,\n  WorkflowNodeProps,\n  WorkflowNodeRenderer,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { PortRender } from './port';\nimport { NodeBorderColorMap, NodeColorMap, NodeGlowColorMap } from './node-color';\n\nexport const NodeRender = (props: WorkflowNodeProps) => {\n  const { selected, node, ports } = useNodeRender();\n  const nodeColor = NodeColorMap[node.id] ?? '#fff';\n  const borderColor = NodeBorderColorMap[node.id] ?? '#fff';\n  const glowColor = NodeGlowColorMap[node.id] ?? '59, 130, 246';\n\n  return (\n    <WorkflowNodeRenderer\n      className=\"flowgram-logo-node\"\n      style={\n        {\n          background: nodeColor,\n          border: selected ? `2px solid ${borderColor}` : `2px solid ${nodeColor}`,\n          '--glow-color': glowColor,\n        } as React.CSSProperties & { '--glow-color': string }\n      }\n      portStyle={{\n        display: 'none',\n      }}\n      node={props.node}\n    >\n      {ports.map((p) => (\n        <PortRender key={p.id} entity={p} />\n      ))}\n    </WorkflowNodeRenderer>\n  );\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/port.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useState } from 'react';\n\nimport {\n  useService,\n  WorkflowHoverService,\n  WorkflowLinesManager,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { NodeColorMap } from './node-color';\n\nexport interface WorkflowPortRenderProps {\n  entity: WorkflowPortEntity;\n  className?: string;\n  style?: React.CSSProperties;\n  onClick?: (e: React.MouseEvent<HTMLDivElement>, port: WorkflowPortEntity) => void;\n  /** 激活状态颜色 (linked/hovered) */\n  primaryColor?: string;\n  /** 默认状态颜色 */\n  secondaryColor?: string;\n  /** 错误状态颜色 */\n  errorColor?: string;\n  /** 背景颜色 */\n  backgroundColor?: string;\n}\n\nexport const PortRender: React.FC<WorkflowPortRenderProps> =\n  // eslint-disable-next-line react/display-name\n  React.memo<WorkflowPortRenderProps>((props: WorkflowPortRenderProps) => {\n    const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);\n    const { entity } = props;\n    const { relativePosition } = entity;\n    const [targetElement, setTargetElement] = useState(entity.targetElement);\n    const [posX, updatePosX] = useState(relativePosition.x);\n    const [posY, updatePosY] = useState(relativePosition.y);\n    const [hovered, setHovered] = useState(false);\n\n    useEffect(() => {\n      // useEffect 时序问题可能导致 port.hasError 非最新，需重新触发一次 validate\n      entity.validate();\n      const dispose = entity.onEntityChange(() => {\n        // 如果有挂载的节点，不需要更新位置信息\n        if (entity.targetElement) {\n          if (entity.targetElement !== targetElement) {\n            setTargetElement(entity.targetElement);\n          }\n          return;\n        }\n        const newPos = entity.relativePosition;\n        // 加上 round 避免点位抖动\n        updatePosX(Math.round(newPos.x));\n        updatePosY(Math.round(newPos.y));\n      });\n      const dispose2 = hoverService.onHoveredChange((id) => {\n        setHovered(hoverService.isHovered(entity.id));\n      });\n      return () => {\n        dispose.dispose();\n        dispose2.dispose();\n      };\n    }, [hoverService, entity, targetElement]);\n\n    // 构建 CSS 自定义属性用于颜色覆盖\n    const colorStyles: Record<string, string> = {};\n    if (props.primaryColor) {\n      colorStyles['--g-workflow-port-color-primary'] = props.primaryColor;\n    }\n    if (props.secondaryColor) {\n      colorStyles['--g-workflow-port-color-secondary'] = props.secondaryColor;\n    }\n    if (props.errorColor) {\n      colorStyles['--g-workflow-port-color-error'] = props.errorColor;\n    }\n    if (props.backgroundColor) {\n      colorStyles['--g-workflow-port-color-background'] = props.backgroundColor;\n    }\n\n    const content = (\n      <div\n        style={{\n          width: '24px',\n          height: '24px',\n          borderRadius: '50%',\n          marginTop: '-12px',\n          marginLeft: '-12px',\n          position: 'absolute',\n          background: '#fff',\n          border: 'none',\n          display: 'flex',\n          alignItems: 'center',\n          justifyContent: 'center',\n          cursor: 'pointer',\n          left: posX,\n          top: posY,\n        }}\n        data-port-entity-id={entity.id}\n        data-port-entity-type={entity.portType}\n        data-testid=\"sdk.workflow.canvas.node.port\"\n      >\n        <div\n          style={{\n            width: hovered ? '20px' : '16px',\n            height: hovered ? '20px' : '16px',\n            borderRadius: '50%',\n            background: NodeColorMap[entity.node.id] ?? '#fff',\n            transition: 'width 0.2s ease, height 0.2s ease',\n          }}\n        />\n      </div>\n    );\n    return content;\n  });\n"
  },
  {
    "path": "apps/docs/theme/components/logo/position-groups.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { PositionSchema } from '@flowgram.ai/free-layout-editor';\n\nexport interface PositionGroup {\n  [key: string]: PositionSchema;\n}\n\nconst originPosition: PositionGroup = {\n  '1': {\n    x: 0,\n    y: 0,\n  },\n  '2': {\n    x: 110,\n    y: 0,\n  },\n  '3': {\n    x: 220,\n    y: 0,\n  },\n  '4': {\n    x: 330,\n    y: 0,\n  },\n};\n\nexport const positionGroups: PositionGroup[] = [\n  // 水平线形态\n  {\n    '1': {\n      x: 60,\n      y: 0,\n    },\n    '2': {\n      x: 120,\n      y: 0,\n    },\n    '3': {\n      x: 180,\n      y: 0,\n    },\n    '4': {\n      x: 240,\n      y: 0,\n    },\n  },\n  originPosition,\n  // 锯齿形态\n  {\n    '1': {\n      x: 0,\n      y: -40,\n    },\n    '2': {\n      x: 110,\n      y: 40,\n    },\n    '3': {\n      x: 220,\n      y: -40,\n    },\n    '4': {\n      x: 330,\n      y: 40,\n    },\n  },\n  originPosition,\n  // 弧形形态\n  {\n    '1': {\n      x: 40,\n      y: -30,\n    },\n    '2': {\n      x: 120,\n      y: -50,\n    },\n    '3': {\n      x: 200,\n      y: -50,\n    },\n    '4': {\n      x: 280,\n      y: -30,\n    },\n  },\n  originPosition,\n  // 散布形态\n  {\n    '1': {\n      x: 30,\n      y: 60,\n    },\n    '2': {\n      x: 180,\n      y: -40,\n    },\n    '3': {\n      x: 80,\n      y: -60,\n    },\n    '4': {\n      x: 280,\n      y: 40,\n    },\n  },\n  originPosition,\n  // 对角上升形态\n  {\n    '1': {\n      x: 0,\n      y: 75,\n    },\n    '2': {\n      x: 110,\n      y: 25,\n    },\n    '3': {\n      x: 220,\n      y: -25,\n    },\n    '4': {\n      x: 330,\n      y: -75,\n    },\n  },\n  originPosition,\n  // 波浪形态\n  {\n    '1': {\n      x: 0,\n      y: 0,\n    },\n    '2': {\n      x: 110,\n      y: 80,\n    },\n    '3': {\n      x: 220,\n      y: 40,\n    },\n    '4': {\n      x: 330,\n      y: -20,\n    },\n  },\n  originPosition,\n  // 垂直堆叠形态\n  {\n    '1': {\n      x: 165,\n      y: -60,\n    },\n    '2': {\n      x: 165,\n      y: -20,\n    },\n    '3': {\n      x: 165,\n      y: 20,\n    },\n    '4': {\n      x: 165,\n      y: 60,\n    },\n  },\n  originPosition,\n  // 钻石形态\n  {\n    '1': {\n      x: 165,\n      y: -40,\n    },\n    '2': {\n      x: 110,\n      y: 0,\n    },\n    '3': {\n      x: 165,\n      y: 40,\n    },\n    '4': {\n      x: 220,\n      y: 0,\n    },\n  },\n  originPosition,\n];\n"
  },
  {
    "path": "apps/docs/theme/components/logo/update-position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { delay, FreeLayoutPluginContext, startTween } from '@flowgram.ai/free-layout-editor';\n\nimport { PositionGroup, positionGroups } from './position-groups';\n\nconst updateNodePosition = async (params: {\n  ctx: FreeLayoutPluginContext;\n  positionGroup: PositionGroup;\n}) => {\n  const { ctx, positionGroup } = params;\n  return new Promise<void>((resolve) => {\n    startTween({\n      from: { d: 0 },\n      to: { d: 100 },\n      duration: 1000,\n      onUpdate: (v) => {\n        ctx.document.getAllNodes().forEach((node) => {\n          const { transform } = node.transform;\n          const targetPosition = positionGroup[node.id] ?? {\n            x: transform.position.x,\n            y: transform.position.y,\n          };\n          transform.update({\n            position: {\n              x: transform.position.x + ((targetPosition.x - transform.position.x) * v.d) / 100,\n              y: transform.position.y + ((targetPosition.y - transform.position.y) * v.d) / 100,\n            },\n          });\n        });\n      },\n      onComplete: () => {\n        resolve();\n      },\n    });\n  });\n};\n\nexport const updatePosition = async (ctx: FreeLayoutPluginContext) => {\n  // Cycle through position groups every 2 seconds\n  let currentGroupIndex = 0;\n\n  // Use while loop instead of recursion to avoid stack overflow\n  while (true) {\n    // Wait for 2 seconds before next update\n    await delay(2000);\n\n    // Update to current position group\n    await updateNodePosition({\n      ctx,\n      positionGroup: positionGroups[currentGroupIndex],\n    });\n\n    // Move to next position group (cycle back to 0 when reaching the end)\n    currentGroupIndex = (currentGroupIndex + 1) % positionGroups.length;\n  }\n};\n"
  },
  {
    "path": "apps/docs/theme/components/logo/use-editor-props.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport {\n  Field,\n  FreeLayoutProps,\n  PlaygroundConfigEntity,\n  WorkflowLinesManager,\n} from '@flowgram.ai/free-layout-editor';\n\nimport { updatePosition } from './update-position';\nimport { NodeRender } from './node-render';\nimport { nodeRegistries } from './node-registries';\nimport { NodeColorMap } from './node-color';\nimport { initialData } from './initial-data';\n\nexport const useEditorProps = () =>\n  useMemo<FreeLayoutProps>(\n    () => ({\n      background: false,\n      materials: {\n        renderDefaultNode: NodeRender,\n      },\n      isHideArrowLine: (ctx, line) => {\n        if (line.from && line.to) {\n          return true;\n        }\n        return false;\n      },\n      nodeRegistries,\n      initialData,\n      onInit: (ctx) => {\n        const linesManager = ctx.get(WorkflowLinesManager);\n        linesManager.getLineColor = (line) => {\n          const lineColor = NodeColorMap[line.from?.id ?? line.to?.id ?? ''] ?? '#000';\n          return lineColor;\n        };\n      },\n      onAllLayersRendered: async (ctx) => {\n        await ctx.tools.fitView(false);\n        // disable playground operations\n        const playgroundConfig = ctx.get(PlaygroundConfigEntity);\n        playgroundConfig.updateConfig = () => {};\n        // display logo container\n        const containerDOM = window.document.querySelector('.flowgram-logo-container');\n        if (containerDOM instanceof HTMLDivElement) {\n          containerDOM.style.opacity = '1';\n        }\n        // update nodes position\n        updatePosition(ctx);\n      },\n      getNodeDefaultRegistry(type) {\n        return {\n          type,\n          meta: {\n            defaultExpanded: true,\n          },\n          formMeta: {\n            /**\n             * Render form\n             */\n            render: () => (\n              <>\n                <Field<string> name=\"title\">{({ field }) => <div>{field.value}</div>}</Field>\n              </>\n            ),\n          },\n        };\n      },\n    }),\n    []\n  );\n"
  },
  {
    "path": "apps/docs/theme/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LlmsContainer, LlmsCopyButton, LlmsViewOptions } from '@rspress/plugin-llms/runtime';\nimport {\n  HomeLayout as BaseHomeLayout,\n  getCustomMDXComponent as basicGetCustomMDXComponent,\n} from '@rspress/core/theme-original';\nimport { NoSSR, useDark } from '@rspress/core/runtime';\n\nimport { Background } from './components/background';\n\nimport './theme.css';\nimport { FlowGramLogo } from './components/logo';\nimport { useIsMobile } from './use-is-mobile';\n\nfunction getCustomMDXComponent() {\n  const { h1: H1, ...components } = basicGetCustomMDXComponent();\n\n  const MyH1 = ({ ...props }) => (\n    <>\n      <H1 {...props} />\n      <LlmsContainer>\n        <LlmsCopyButton />\n        <LlmsViewOptions />\n      </LlmsContainer>\n    </>\n  );\n  return {\n    ...components,\n    h1: MyH1,\n  };\n}\n\nfunction HomeLayout(props: Parameters<typeof BaseHomeLayout>[0]) {\n  const isDark = useDark();\n  const isMobile = useIsMobile();\n\n  return (\n    <>\n      <>\n        <NoSSR>\n          {isDark && !isMobile && <Background />}\n          <FlowGramLogo />\n        </NoSSR>\n        <BaseHomeLayout {...props} afterHero={null} afterHeroActions={null} />\n      </>\n    </>\n  );\n}\n\nexport { getCustomMDXComponent, HomeLayout };\nexport * from '@rspress/core/theme-original';\n"
  },
  {
    "path": "apps/docs/theme/theme.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n    --rp-home-mask-background-image: transparent;\n}\n\n.home-layout-container {\n    position: relative;\n}\n"
  },
  {
    "path": "apps/docs/theme/use-is-mobile.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nexport const useIsMobile = (): boolean => {\n  const [isMobile, setIsMobile] = useState<boolean>(false);\n\n  useEffect(() => {\n    const checkIsMobile = (): boolean =>\n      /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);\n\n    setIsMobile(checkIsMobile());\n  }, []);\n\n  return isMobile;\n};\n"
  },
  {
    "path": "apps/docs/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"lib\": [\n      \"DOM\",\n      \"ES2020\"\n    ],\n    \"types\": [\n      \"node\"\n    ],\n    \"moduleResolution\": \"bundler\",\n    \"module\": \"ESNext\",\n    \"jsx\": \"react-jsx\",\n    \"noEmit\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"isolatedModules\": true,\n    \"resolveJsonModule\": true,\n    \"useDefineForClassFields\": true,\n    \"allowImportingTsExtensions\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ],\n      \"@components/*\": [\n        \"components/*\"\n      ],\n    }\n  },\n  \"include\": [\n    \"src\",\n    \"rspress.config.ts\",\n    \"src/zh/playground/canvas-engine/components\",\n    \"components\",\n    \"scripts\"\n  ],\n  \"mdx\": {\n    \"checkMdx\": true\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/dep-check/dep-check.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport { RushConfiguration } from '@rushstack/rush-sdk';\nimport depcheck from 'depcheck';\n\n// 获取 Rush Monorepo 的 `rush.json`\nconst rushConfig = RushConfiguration.loadFromDefaultLocation({\n  startingFolder: process.cwd(),\n});\nconst rushConfigPath = path.join(rushConfig.rushJsonFolder, './');\nif (!fs.existsSync(rushConfigPath)) {\n  console.error(\"❌ rush.json not found. Please run this script from the root of your Rush monorepo.\");\n  process.exit(1);\n}\n\n// 解析 Rush 项目列表\nconst packages = rushConfig.projects.map((p) => p.projectFolder);\n\n// depcheck 配置\nconst options = {\n  ignorePatterns: [\"node_modules\", \"dist\", \"build\", \"coverage\"], // 忽略目录\n  ignoreMatches: [\"typescript\", \"@types/*\", \"vitest\", \"inversify\", \"reflect-metadata\", \"@flowgram.ai/ts-config\", \"@flowgram.ai/eslint-config\", \"eslint\", \"@vitest/coverage-v8\", \"@testing-library/react\", \"zod\"], // 忽略类型依赖\n};\n\n// 异步检查未使用的依赖\nconst checkUnusedDependencies = async (packagePath): Promise<number> => {\n  let unUsedNum = 0;\n  const packageJsonPath = path.join(packagePath, \"package.json\");\n  if (!fs.existsSync(packageJsonPath)) return unUsedNum;\n\n  if (packagePath.includes('/apps/') || packagePath.includes('/common/') || packagePath.includes('/config/')) {\n    console.log('✅ skip apps & common & config')\n    return unUsedNum;\n  }\n  console.log(`\\n🔍 Checking unused dependencies in ${packagePath}...`);\n  const result = await depcheck(packagePath, options);\n\n  if (result.dependencies.length || result.devDependencies.length) {\n    console.log(`🚨 Unused dependencies found in ${packagePath}:`);\n    if (result.dependencies.length) {\n      unUsedNum += result.dependencies.length\n      console.log(`  📦 Unused dependencies: ${result.dependencies.join(\", \")}`);\n    }\n    if (result.devDependencies.length) {\n      unUsedNum += result.devDependencies.length\n      console.log(`  📦 Unused devDependencies: ${result.devDependencies.join(\", \")}`);\n    }\n  } else {\n    console.log(`✅ No unused dependencies found in ${packagePath}`);\n  }\n  return unUsedNum;\n};\n\nexport async function runCheckDep(): Promise<void> {\n  // 遍历所有 Rush 项目\n  (async () => {\n    let unUsedNum = 0\n    for (const pkgPath of packages) {\n      const fullPath = pkgPath;\n      if (fs.existsSync(path.join(fullPath, \"package.json\"))) {\n        const newNum = await checkUnusedDependencies(fullPath);\n        unUsedNum += newNum;\n      }\n    }\n    console.log(`\\n✅ Unused dependency check completed! find ${unUsedNum} Error`);\n  })();\n}\n\nrunCheckDep();\n"
  },
  {
    "path": "common/autoinstallers/dep-check/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nrequire('esbuild-register');\nrequire('./dep-check.ts');\n"
  },
  {
    "path": "common/autoinstallers/dep-check/package.json",
    "content": "{\n  \"name\": \"dep-check\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"289056872@qq.com\",\n  \"scripts\": {\n    \"dep-check\": \"node ./index.js\"\n  },\n  \"dependencies\": {\n    \"depcheck\": \"^1.4.7\",\n    \"esbuild-register\": \"^3.6.0\",\n    \"@rushstack/rush-sdk\": \"^5.150.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\"\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/license-header/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst ig = require(\"ignore\")();\n\nconst ignorePaths = [\n  '.next',\n  'doc_build',\n  'apps/docs/components/free-examples',\n  'apps/docs/components/fixed-examples',\n]\n\n\nconst ignoreFile = fs.readFileSync(path.join(__dirname, \"../../../.gitignore\"), {\n  encoding: \"utf-8\",\n});\nig.add(ignoreFile);\n// ignore custom paths\nig.add(ignorePaths);\n\nconst src = path.resolve(__dirname, '../../../');\n\nconst header = ` Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n SPDX-License-Identifier: MIT`;\n\nconst bashHeaders = [`#!/usr/bin/env`, `#!/bin/sh`];\nconst rushPreHeader = `// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.`;\n\n/**\n* Add License Header to all files in the folder\n* @param {string} targetDir - target folder path (absolute path)\n* @param {string} licenseContent - License content to be added (plain text)\n* @param {object} options - configuration items\n* @param {string[]} [options.includeExts] - file extensions to be processed (such as ['.js', '.ts']), all text files are processed by default\n* @param {string[]} [options.excludeDirs] - directories to be excluded (such as ['node_modules', 'dist'])\n* @param {string} [options.commentLinePrefix] - comment prefix (such as '// ' or '/* '), comment format is not added by default\n* @param {boolean} [options.force] - whether to force overwrite (re-add even if there is already a license), default false\n*/\nfunction addLicenseHeader(targetDir, licenseContent, options = {}) {\n  const {\n    includeExts = [\".js\", \".ts\", \".mjs\", \".cjs\", \".html\", \".css\", \".scss\"],\n    commentLinePrefix = \"// \", // default by js\n    commentPrefix,\n    commentSuffix,\n    force = false,\n  } = options;\n\n  const licensedText =\n    (commentPrefix ? commentPrefix + \"\\n\" : \"\") +\n    licenseContent\n      .split(\"\\n\")\n      .map((line) => commentLinePrefix + line)\n      .join(\"\\n\") +\n    (commentSuffix ? \"\\n\" + commentSuffix : \"\") +\n    \"\\n\\n\";\n\n  function traverseDir(currentDir) {\n    const entries = fs.readdirSync(currentDir, { withFileTypes: true });\n\n    for (const entry of entries) {\n      const fullPath = path.join(currentDir, entry.name);\n\n      if (\n        ig.ignores(path.relative(targetDir, fullPath))\n      ) {\n        continue;\n      }\n\n      if (entry.isDirectory()) {\n        traverseDir(fullPath);\n      } else {\n        processFile(fullPath);\n      }\n    }\n  }\n\n  function processFile(filePath) {\n    const ext = path.extname(filePath).toLowerCase();\n    if (includeExts.length > 0 && !includeExts.includes(ext)) {\n      return;\n    }\n\n    try {\n      const originalContent = fs.readFileSync(filePath, \"utf8\");\n\n      // Check if the license already exists (simple match at the beginning)\n      if (!force && (originalContent.startsWith(licensedText.trim()) || bashHeaders.some(_header => originalContent.startsWith(_header.trim())) || originalContent.startsWith(rushPreHeader.trim()))) {\n        return;\n      }\n\n      let newContent = originalContent;\n\n      const shebangMatch = originalContent.match(/^#!.*\\n/);\n      if (shebangMatch) {\n        newContent =\n          shebangMatch[0] +\n          licensedText +\n          originalContent.replace(shebangMatch[0], \"\");\n      } else {\n        newContent = licensedText + originalContent;\n      }\n\n      fs.writeFileSync(filePath, newContent, \"utf8\");\n      console.log(`[Success] License added: ${filePath}, please manually upload the locally modified file`);\n    } catch (err) {\n      console.error(`[ERROR] Failed to process file ${filePath}:`, err.message);\n    }\n  }\n\n  if (fs.existsSync(targetDir) && fs.statSync(targetDir).isDirectory()) {\n    traverseDir(targetDir);\n    console.log(\"License Header add successfully\");\n  } else {\n    console.error(\"Error: The destination path is not a valid folder\");\n  }\n}\n\naddLicenseHeader(\n    src,\n    header,\n    {\n        includeExts: ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.scss', '.less', '.prisma', '.styl', '.css'],\n        commentPrefix: '/**',\n        commentLinePrefix: ' *',\n        commentSuffix: ' */',\n        force: false\n    }\n);\n\naddLicenseHeader(\n    src,\n    header,\n    {\n        includeExts: ['.sh'],\n        commentLinePrefix: '# ',\n        force: false\n    }\n);\n"
  },
  {
    "path": "common/autoinstallers/license-header/package.json",
    "content": "{\n  \"name\": \"license-header\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"289056872@qq.com\",\n  \"scripts\": {\n    \"license-header-fix\": \"node ./index.js\"\n  },\n  \"dependencies\": {\n    \"ignore\": \"^7.0.4\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\"\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/rush-commands/check-circular-dependency.mjs",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport RushSdk from '@rushstack/rush-sdk';\n\nconst rushConfig = RushSdk.RushConfiguration.loadFromDefaultLocation();\n\nfunction getPackageDependencies(packageName, depsMap) {\n  const projectInfo = rushConfig.getProjectByName(packageName);\n  if (!projectInfo || depsMap[packageName]) return;\n\n  // eslint-disable-next-line no-param-reassign\n  depsMap[packageName] = [...projectInfo.dependencyProjects].map(\n    p => p.packageName,\n  );\n\n  for (const dep of depsMap[packageName]) {\n    getPackageDependencies(dep, depsMap);\n  }\n}\n\nfunction main() {\n  const { projects } = rushConfig;\n  const depsMap = {};\n\n  for (const project of projects) {\n    getPackageDependencies(project.packageName, depsMap);\n  }\n\n  // 定义一个函数来进行深度优先搜索\n  function dfs(node, visited, stack, graph, cycles) {\n    visited.add(node); // 将当前节点标记为已访问\n    stack.push(node); // 将当前节点添加到栈中\n\n    // 遍历当前节点的所有邻居\n    for (const neighbor of graph[node]) {\n      // 如果邻居节点未被访问，则进行深度优先搜索\n      if (!visited.has(neighbor)) {\n        dfs(neighbor, visited, stack, graph, cycles);\n      }\n      // 如果邻居节点已经被访问，并且在栈中，则说明存在循环依赖\n      else if (stack.includes(neighbor)) {\n        cycles.push([...stack.slice(stack.indexOf(neighbor)), neighbor]); // 将循环依赖添加到数组中\n      }\n    }\n\n    // 将当前节点从栈中弹出，回溯到上一个节点\n    stack.pop();\n  }\n\n  // 定义一个函数来查找所有循环依赖\n  function findCycles(graph) {\n    const visited = new Set(); // 用于存储已经访问过的节点\n    const stack = []; // 用于存储遍历过程中经过的节点\n    const cycles = []; // 用于存储所有循环依赖\n\n    // 遍历图中的所有节点\n    for (const node in graph) {\n      // 如果当前节点未被访问，则进行深度优先搜索\n      if (!visited.has(node)) {\n        dfs(node, visited, stack, graph, cycles);\n      }\n    }\n\n    // 返回所有循环依赖\n    return cycles;\n  }\n\n  const cycles = findCycles(depsMap);\n\n  if (cycles.length) {\n    for (const chain of cycles) {\n      console.log('Circular dependency detected:');\n      console.log(chain.join('\\n->'));\n      console.log('\\n');\n      if (process.env.CI === 'true') {\n        console.log(\n          `::add-message level=error::**检测到循环依赖:**${chain.join('->')}`,\n        );\n      }\n    }\n    process.exitCode = 1;\n  }\n}\n\nmain();\n"
  },
  {
    "path": "common/autoinstallers/rush-commands/package.json",
    "content": "{\n  \"name\": \"rush-commands\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@rushstack/rush-sdk\": \"^5.150.0\",\n    \"concurrently\": \"^9.2.1\"\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/rush-commitlint/.cz-config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst chalk = require('chalk');\nconst spawn = require('cross-spawn');\nconst defaultConfig = require('cz-customizable');\nconst { getChangedPackages } = require('./utils');\n\nconst typesConfig = [\n  { value: 'feat', name: 'A new feature' },\n  { value: 'fix', name: 'A bug fix' },\n  { value: 'docs', name: 'Documentation only changes' },\n  {\n    value: 'style',\n    name: 'Changes that do not affect the meaning of the code\\n            (white-space, formatting, missing semi-colons, etc)',\n  },\n  {\n    value: 'refactor',\n    name: 'A code change that neither fixes a bug nor adds a feature',\n  },\n  {\n    value: 'perf',\n    name: 'A code change that improves performance',\n  },\n  { value: 'test', name: 'Adding missing tests' },\n  {\n    value: 'chore',\n    name: 'Changes to the build process or auxiliary tools',\n  },\n  {\n    value: 'build',\n    name: 'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',\n  },\n  {\n    value: 'ci',\n    name: 'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',\n  },\n  {\n    value: 'revert',\n    name: 'Reverts a previous commit',\n  },\n];\n\nconst { stdout = '' } = spawn.sync(`git diff --staged --name-only`, {\n  shell: true,\n  encoding: 'utf8',\n  stdio: 'pipe',\n});\nconst changedFiles = stdout.split('\\n').filter(Boolean);\nconst changeSet = getChangedPackages(changedFiles);\n\nif (changeSet.size > 1) {\n  process.stderr.write(\n    `${[\n      chalk.yellow(\n        `Multiple packages detected in current commit, please consider splitting into smaller commits`,\n      ),\n    ].join('\\n')}\\n`,\n  );\n\n  changeSet.clear();\n  changeSet.add('multiple');\n}\n\nconst changedScopes = [...changeSet];\n\nmodule.exports = {\n  ...defaultConfig,\n  types: typesConfig.map(({ value, name }) => {\n    return {\n      name: `${value}:${new Array(10 - value.length)\n        .fill(' ')\n        .join('')}${name}`,\n      value,\n    };\n  }),\n  messages: {\n    ...defaultConfig.messages,\n    type: \"Select the type of change that you're committing\",\n    scope: 'Ensure the scope of this change',\n    subject: 'Write a short, imperative tense description of the change',\n    body: 'Provide a longer description of the change. Use \"|\" to break new line:\\n',\n    breaking: 'List any BREAKING CHANGES (optional):\\n',\n    confirmCommit: 'Are you sure you want to proceed with the commit above?',\n  },\n  scopes: changedScopes.join(','),\n  allowCustomScopes: false,\n  skipQuestions: ['customScope', 'footer', 'body'],\n  allowBreakingChanges: ['feat', 'fix'],\n};\n"
  },
  {
    "path": "common/autoinstallers/rush-commitlint/commitlint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nmodule.exports = {\n  extends: ['@commitlint/config-conventional'],\n  rules: {\n    'header-max-length': [2, 'always', 150],\n    'subject-full-stop': [0, 'never'],\n    'subject-case': [\n      2,\n      'never',\n      [\n        'upper-case', // UPPERCASE\n      ],\n    ],\n  },\n};\n"
  },
  {
    "path": "common/autoinstallers/rush-commitlint/package.json",
    "content": "{\n  \"name\": \"rush-commitlint\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"author\": \"fanwenjie.fe@bytedance.com\",\n  \"config\": {\n    \"commitizen\": {\n      \"path\": \"common/autoinstallers/rush-commitlint/node_modules/cz-customizable\"\n    }\n  },\n  \"dependencies\": {\n    \"@commitlint/cli\": \"^17.2.0\",\n    \"@commitlint/config-conventional\": \"^17.2.0\",\n    \"@rushstack/rush-sdk\": \"^5.150.0\",\n    \"commitizen\": \"^4.2.6\",\n    \"cz-customizable\": \"^7.2.1\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20\"\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/rush-commitlint/utils.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { RushConfiguration } = require('@rushstack/rush-sdk')\n\nconst getRushConfiguration = (function () {\n  let rushConfiguration = null\n  return function () {\n    // eslint-disable-next-line\n    return (rushConfiguration ||= RushConfiguration.loadFromDefaultLocation({\n      startingFolder: process.cwd(),\n    }))\n  }\n})()\n\nfunction getChangedPackages(changedFiles) {\n  const changedPackages = new Set()\n\n  try {\n    const rushConfiguration = getRushConfiguration()\n    const { rushJsonFolder } = rushConfiguration\n    const lookup = rushConfiguration.getProjectLookupForRoot(rushJsonFolder)\n    for (const file of changedFiles) {\n      const project = lookup.findChildPath(file)\n      // 如果没找到注册的包信息，则认为是通用文件更改\n      const packageName = project?.packageName || 'misc'\n      if (!changedPackages.has(packageName)) {\n        changedPackages.add(packageName)\n      }\n    }\n  } catch (e) {\n    console.error(e)\n    throw e\n  }\n\n  return changedPackages\n}\n\nexports.getChangedPackages = getChangedPackages\nexports.getRushConfiguration = getRushConfiguration\n"
  },
  {
    "path": "common/autoinstallers/rush-lint-staged/.lintstagedrc.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst {\n  excludeIgnoredFiles,\n  groupChangedFilesByProject,\n  getRushConfiguration,\n} = require('./utils');\nconst micromatch = require('micromatch');\nconst path = require('path');\nconst fs = require('fs');\n\nmodule.exports = {\n  // 所有类型文件都加 license header\n  '**/*.{js,ts,tsx,jsx,mjs,cjs,scss,less,css,sh}': async files => {\n    if (!files.length) return [];\n    return [\n      'node common/autoinstallers/license-header/index.js',\n    ];\n  },\n  '**/*.{ts,tsx,js,jsx,mjs,svg}': async files => {\n    const match = micromatch.not(files, [\n      '**/common/_templates/!(_*)/**/(.)?*',\n    ]);\n    const changedFileGroup = await groupChangedFilesByProject(match);\n    const eslintCmds = Object.entries(changedFileGroup)\n      .map(([packageName, changedFiles]) => {\n        const rushConfiguration = getRushConfiguration();\n        const { projectFolder, packageName: name } =\n          rushConfiguration.getProjectByName(packageName);\n        const filesToCheck = changedFiles\n          .map(f => path.relative(projectFolder, f))\n          .join(' ');\n        // TSESTREE_SINGLE_RUN doc https://typescript-eslint.io/packages/parser/#allowautomaticsingleruninference\n        // 切换到项目文件夹，并运行 ESLint 命令\n        const cmd = [\n          `cd ${projectFolder}`,\n          `TSESTREE_SINGLE_RUN=true eslint --cache --fix ${filesToCheck} --no-error-on-unmatched-pattern --cache-location .lintcache/cache`,\n        ].join(' && ');\n        return {\n          name,\n          cmd,\n        };\n      });\n\n    if (!eslintCmds.length) return [];\n\n    if (eslintCmds.length > 16) {\n      console.log(\n        `For performance reason, skip ESlint detection due to ${eslintCmds.length} eslint commands.`,\n      );\n      return [];\n    }\n\n    const res = [\n      // 这里不能直接返回 eslintCmds 数组，因为 lint-staged 会依次串行执行每个命令\n      // 而 concurrently 会并行执行多个命令\n      `concurrently --max-process 8  --names ${eslintCmds\n        .map(r => `${r.name}`)\n        .join(',')} --kill-others-on-fail ${eslintCmds\n        .map(r => `\"${r.cmd}\"`)\n        .join(' ')}`,\n    ];\n    return res;\n  },\n  '**/package.json': async files => {\n    const match = micromatch.not(files, [\n      '**/common/_templates/!(_*)/**/(.)?*',\n    ]);\n    const filesToLint = await excludeIgnoredFiles(match);\n    if (!filesToLint) return [];\n    return [\n      // https://eslint.org/docs/latest/flags/#enable-feature-flags-with-the-cli\n      // eslint v9默认从cwd找配置，这里需要使用 unstable_config_lookup_from_file 配置，否则会报错\n      `eslint --cache ${filesToLint} --flag unstable_config_lookup_from_file`,\n      `prettier ${filesToLint} --write`,\n    ];\n  },\n  '**/!(package).json': 'prettier --write',\n};\n"
  },
  {
    "path": "common/autoinstallers/rush-lint-staged/package.json",
    "content": "{\n  \"name\": \"rush-lint-staged\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"author\": \"chenjiawei.inizio@bytedance.com\",\n  \"resolutions\": {\n    \"@eslint/eslintrc\": \"3.3.3\"\n  },\n  \"dependencies\": {\n    \"@microsoft/rush-lib\": \"5.150.0\",\n    \"eslint\": \"^9.0.0\",\n    \"concurrently\": \"^9.2.1\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"lint-staged\": \"^16.2.7\",\n    \"micromatch\": \"^4.0.8\",\n    \"prettier\": \"^2.7.1\",\n    \"pretty-quick\": \"^3.1.3\",\n    \"typescript\": \"^5.8.3\"\n  }\n}\n"
  },
  {
    "path": "common/autoinstallers/rush-lint-staged/utils.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\n\nconst { ESLint } = require('eslint');\nconst { RushConfiguration } = require('@microsoft/rush-lib');\n\nconst getRushConfiguration = (function () {\n  let rushConfiguration = null;\n  return function () {\n    // eslint-disable-next-line\n    return (rushConfiguration ||= RushConfiguration.loadFromDefaultLocation({\n      startingFolder: process.cwd(),\n    }));\n  };\n})();\n\n// 获取变更文件所在的项目路径\nfunction withProjectFolder(changedFiles) {\n  const projectFolders = [];\n\n  try {\n    const rushConfiguration = getRushConfiguration();\n    const { rushJsonFolder } = rushConfiguration;\n    const lookup = rushConfiguration.getProjectLookupForRoot(rushJsonFolder);\n\n    for (const file of changedFiles) {\n      const project = lookup.findChildPath(path.relative(rushJsonFolder, file));\n      // 忽略不在 rush.json 内定义的项目\n      if (project) {\n        const projectFolder = project?.projectFolder ?? rushJsonFolder;\n        const packageName = project?.packageName;\n        projectFolders.push({\n          file,\n          projectFolder,\n          packageName,\n        });\n      }\n    }\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n\n  return projectFolders;\n}\n\nasync function excludeIgnoredFiles(changedFiles) {\n  try {\n    const eslintInstances = new Map();\n\n    const changedFilesWithIgnored = await Promise.all(\n      withProjectFolder(changedFiles).map(async ({ file, projectFolder }) => {\n        let eslint = eslintInstances.get(projectFolder);\n        if (!eslint) {\n          eslint = new ESLint({ cwd: projectFolder });\n          eslintInstances.set(projectFolder, eslint);\n        }\n\n        return {\n          file,\n          isIgnored: await eslint.isPathIgnored(file),\n        };\n      }),\n    );\n\n    return changedFilesWithIgnored\n      .filter(change => !change.isIgnored)\n      .map(change => change.file)\n      .join(' ');\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n}\n\n// 获取发生变更的项目路径\nfunction getChangedProjects(changedFiles) {\n  const changedProjectFolders = new Set();\n  const changedProjects = new Set();\n\n  withProjectFolder(changedFiles).forEach(({ projectFolder, packageName }) => {\n    if (!changedProjectFolders.has(projectFolder)) {\n      changedProjectFolders.add(projectFolder);\n      changedProjects.add({\n        packageName,\n        projectFolder,\n      });\n    }\n  });\n\n  return [...changedProjects];\n}\n\nconst groupChangedFilesByProject = changedFiles => {\n  const changedFilesMap = withProjectFolder(changedFiles);\n  const result = changedFilesMap.reduce((pre, cur) => {\n    pre[cur.packageName] ||= [];\n    pre[cur.packageName].push(cur.file);\n    return pre;\n  }, {});\n  return result;\n};\n\nexports.excludeIgnoredFiles = excludeIgnoredFiles;\nexports.getRushConfiguration = getRushConfiguration;\nexports.getChangedProjects = getChangedProjects;\nexports.groupChangedFilesByProject = groupChangedFilesByProject;\n"
  },
  {
    "path": "common/config/rush/.npmrc",
    "content": "# Rush uses this file to configure the NPM package registry during installation.  It is applicable\n# to PNPM, NPM, and Yarn package managers.  It is used by operations such as \"rush install\",\n# \"rush update\", and the \"install-run.js\" scripts.\n#\n# NOTE: The \"rush publish\" command uses .npmrc-publish instead.\n#\n# Before invoking the package manager, Rush will generate an .npmrc in the folder where installation\n# is performed.  This generated file will omit any config lines that reference environment variables\n# that are undefined in that session; this avoids problems that would otherwise result due to\n# a missing variable being replaced by an empty string.\n#\n# If \"subspacesEnabled\" is true in subspaces.json, the generated file will merge settings from\n# \"common/config/rush/.npmrc\" and \"common/config/subspaces/<name>/.npmrc\", with the latter taking\n# precedence.\n#\n# * * * SECURITY WARNING * * *\n#\n# It is NOT recommended to store authentication tokens in a text file on a lab machine, because\n# other unrelated processes may be able to read that file.  Also, the file may persist indefinitely,\n# for example if the machine loses power.  A safer practice is to pass the token via an\n# environment variable, which can be referenced from .npmrc using ${} expansion.  For example:\n#\n#   //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\n#\nregistry=https://registry.npmjs.org/\nalways-auth=false\n"
  },
  {
    "path": "common/config/rush/.npmrc-publish",
    "content": "# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish\n# is used by the \"rush publish\" command, as publishing often involves different credentials\n# and registries than other operations.\n#\n# Before invoking the package manager, Rush will copy this file to \"common/temp/publish-home/.npmrc\"\n# and then temporarily map that folder as the \"home directory\" for the current user account.\n# This enables the same settings to apply for each project folder that gets published.  The copied file\n# will omit any config lines that reference environment variables that are undefined in that session;\n# this avoids problems that would otherwise result due to a missing variable being replaced by\n# an empty string.\n#\n# * * * SECURITY WARNING * * *\n#\n# It is NOT recommended to store authentication tokens in a text file on a lab machine, because\n# other unrelated processes may be able to read the file.  Also, the file may persist indefinitely,\n# for example if the machine loses power.  A safer practice is to pass the token via an\n# environment variable, which can be referenced from .npmrc using ${} expansion.  For example:\n#\n#   //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\n#\n"
  },
  {
    "path": "common/config/rush/.pnpmfile.cjs",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\n/**\n * When using the PNPM package manager, you can use pnpmfile.js to workaround\n * dependencies that have mistakes in their package.json file.  (This feature is\n * functionally similar to Yarn's \"resolutions\".)\n *\n * For details, see the PNPM documentation:\n * https://pnpm.io/pnpmfile#hooks\n *\n * IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY TO INVALIDATE\n * ANY CACHED DEPENDENCY ANALYSIS.  After any modification to pnpmfile.js, it's recommended to run\n * \"rush update --full\" so that PNPM will recalculate all version selections.\n */\nmodule.exports = {\n  hooks: {\n    readPackage\n  }\n};\n\n/**\n * This hook is invoked during installation before a package's dependencies\n * are selected.\n * The `packageJson` parameter is the deserialized package.json\n * contents for the package that is about to be installed.\n * The `context` parameter provides a log() function.\n * The return value is the updated object.\n */\nfunction readPackage(packageJson, context) {\n\n  // // The karma types have a missing dependency on typings from the log4js package.\n  // if (packageJson.name === '@types/karma') {\n  //  context.log('Fixed up dependencies for @types/karma');\n  //  packageJson.dependencies['log4js'] = '0.6.38';\n  // }\n\n  return packageJson;\n}\n"
  },
  {
    "path": "common/config/rush/artifactory.json",
    "content": "/**\n * This configuration file manages Rush integration with JFrog Artifactory services.\n * More documentation is available on the Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/artifactory.schema.json\",\n\n  \"packageRegistry\": {\n    /**\n     * (Required) Set this to \"true\" to enable Rush to manage tokens for an Artifactory NPM registry.\n     * When enabled, \"rush install\" will automatically detect when the user's ~/.npmrc\n     * authentication token is missing or expired.  And \"rush setup\" will prompt the user to\n     * renew their token.\n     *\n     * The default value is false.\n     */\n    \"enabled\": false,\n\n    /**\n     * (Required) Specify the URL of your NPM registry.  This is the same URL that appears in\n     * your .npmrc file.  It should look something like this example:\n     *\n     *   https://your-company.jfrog.io/your-project/api/npm/npm-private/\n     */\n    \"registryUrl\": \"\",\n\n    /**\n     * A list of custom strings that \"rush setup\" should add to the user's ~/.npmrc file at the time\n     * when the token is updated.  This could be used for example to configure the company registry\n     * to be used whenever NPM is invoked as a standalone command (but it's not needed for Rush\n     * operations like \"rush add\" and \"rush install\", which get their mappings from the monorepo's\n     * common/config/rush/.npmrc file).\n     *\n     * NOTE: The ~/.npmrc settings are global for the user account on a given machine, so be careful\n     * about adding settings that may interfere with other work outside the monorepo.\n     */\n    \"userNpmrcLinesToAdd\": [\n      // \"@example:registry=https://your-company.jfrog.io/your-project/api/npm/npm-private/\"\n    ],\n\n    /**\n     * (Required) Specifies the URL of the Artifactory control panel where the user can generate\n     * an API key.  This URL is printed after the \"visitWebsite\" message.\n     * It should look something like this example:  https://your-company.jfrog.io/\n     * Specify an empty string to suppress this line entirely.\n     */\n    \"artifactoryWebsiteUrl\": \"\",\n\n    /**\n     * Uncomment this line to specify the type of credential to save in the user's ~/.npmrc file.\n     * The default is \"password\", which means the user's API token will be traded in for an\n     * npm password specific to that registry. Optionally you can specify \"authToken\", which\n     * will save the user's API token as credentials instead.\n     */\n    // \"credentialType\": \"password\",\n\n    /**\n     * These settings allow the \"rush setup\" interactive prompts to be customized, for\n     * example with messages specific to your team or configuration.  Specify an empty string\n     * to suppress that message entirely.\n     */\n    \"messageOverrides\": {\n      /**\n       * Overrides the message that normally says:\n       * \"This monorepo consumes packages from an Artifactory private NPM registry.\"\n       */\n      // \"introduction\": \"\",\n\n      /**\n       * Overrides the message that normally says:\n       * \"Please contact the repository maintainers for help with setting up an Artifactory user account.\"\n       */\n      // \"obtainAnAccount\": \"\",\n\n      /**\n       * Overrides the message that normally says:\n       * \"Please open this URL in your web browser:\"\n       *\n       * The \"artifactoryWebsiteUrl\" string is printed after this message.\n       */\n      // \"visitWebsite\": \"\",\n\n      /**\n       * Overrides the message that normally says:\n       * \"Your user name appears in the upper-right corner of the JFrog website.\"\n       */\n      // \"locateUserName\": \"\",\n\n      /**\n       * Overrides the message that normally says:\n       * \"Click 'Edit Profile' on the JFrog website.  Click the 'Generate API Key'\n       * button if you haven't already done so previously.\"\n       */\n      // \"locateApiKey\": \"\"\n\n      /**\n       * Overrides the message that normally prompts:\n       * \"What is your Artifactory user name?\"\n       */\n      // \"userNamePrompt\": \"\"\n\n      /**\n       * Overrides the message that normally prompts:\n       * \"What is your Artifactory API key?\"\n       */\n      // \"apiKeyPrompt\": \"\"\n    }\n  }\n}\n"
  },
  {
    "path": "common/config/rush/build-cache.json",
    "content": "/**\n * This configuration file manages Rush's build cache feature.\n * More documentation is available on the Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/build-cache.schema.json\",\n\n  /**\n   * (Required) EXPERIMENTAL - Set this to true to enable the build cache feature.\n   *\n   * See https://rushjs.io/pages/maintainer/build_cache/ for details about this experimental feature.\n   */\n  \"buildCacheEnabled\": false,\n\n  /**\n   * (Required) Choose where project build outputs will be cached.\n   *\n   * Possible values: \"local-only\", \"azure-blob-storage\", \"amazon-s3\"\n   */\n  \"cacheProvider\": \"local-only\",\n\n  /**\n   * Setting this property overrides the cache entry ID.  If this property is set, it must contain\n   * a [hash] token.\n   *\n   * Other available tokens:\n   *  - [projectName]             Example: \"@my-scope/my-project\"\n   *  - [projectName:normalize]   Example: \"my-scope+my-project\"\n   *  - [phaseName]               Example: \"_phase:test/api\"\n   *  - [phaseName:normalize]     Example: \"_phase:test+api\"\n   *  - [phaseName:trimPrefix]    Example: \"test/api\"\n   *  - [os]                      Example: \"win32\"\n   *  - [arch]                    Example: \"x64\"\n   */\n  // \"cacheEntryNamePattern\": \"[projectName:normalize]-[phaseName:normalize]-[hash]\"\n\n  /**\n   * (Optional) Salt to inject during calculation of the cache key. This can be used to invalidate the cache for all projects when the salt changes.\n   */\n  // \"cacheHashSalt\": \"1\",\n\n  /**\n   * Use this configuration with \"cacheProvider\"=\"azure-blob-storage\"\n   */\n  \"azureBlobStorageConfiguration\": {\n    /**\n     * (Required) The name of the the Azure storage account to use for build cache.\n     */\n    // \"storageAccountName\": \"example\",\n\n    /**\n     * (Required) The name of the container in the Azure storage account to use for build cache.\n     */\n    // \"storageContainerName\": \"my-container\",\n\n    /**\n     * The Azure environment the storage account exists in. Defaults to AzurePublicCloud.\n     *\n     * Possible values: \"AzurePublicCloud\", \"AzureChina\", \"AzureGermany\", \"AzureGovernment\"\n     */\n    // \"azureEnvironment\": \"AzurePublicCloud\",\n\n    /**\n     * An optional prefix for cache item blob names.\n     */\n    // \"blobPrefix\": \"my-prefix\",\n\n    /**\n     * If set to true, allow writing to the cache. Defaults to false.\n     */\n    // \"isCacheWriteAllowed\": true\n  },\n\n  /**\n   * Use this configuration with \"cacheProvider\"=\"amazon-s3\"\n   */\n  \"amazonS3Configuration\": {\n    /**\n     * (Required unless s3Endpoint is specified) The name of the bucket to use for build cache.\n     * Example: \"my-bucket\"\n     */\n    // \"s3Bucket\": \"my-bucket\",\n\n    /**\n     * (Required unless s3Bucket is specified) The Amazon S3 endpoint of the bucket to use for build cache.\n     * This should not include any path; use the s3Prefix to set the path.\n     * Examples: \"my-bucket.s3.us-east-2.amazonaws.com\" or \"http://localhost:9000\"\n     */\n    // \"s3Endpoint\": \"https://my-bucket.s3.us-east-2.amazonaws.com\",\n\n    /**\n     * (Required) The Amazon S3 region of the bucket to use for build cache.\n     * Example: \"us-east-1\"\n     */\n    // \"s3Region\": \"us-east-1\",\n\n    /**\n     * An optional prefix (\"folder\") for cache items. It should not start with \"/\".\n     */\n    // \"s3Prefix\": \"my-prefix\",\n\n    /**\n     * If set to true, allow writing to the cache. Defaults to false.\n     */\n    // \"isCacheWriteAllowed\": true\n  },\n\n  /**\n   * Use this configuration with \"cacheProvider\"=\"http\"\n   */\n  \"httpConfiguration\": {\n    /**\n     * (Required) The URL of the server that stores the caches.\n     * Example: \"https://build-cacches.example.com/\"\n     */\n    // \"url\": \"https://build-cacches.example.com/\",\n\n    /**\n     * (Optional) The HTTP method to use when writing to the cache (defaults to PUT).\n     * Should be one of PUT, POST, or PATCH.\n     * Example: \"PUT\"\n     */\n    // \"uploadMethod\": \"PUT\",\n\n    /**\n     * (Optional) HTTP headers to pass to the cache server.\n     * Example: { \"X-HTTP-Company-Id\": \"109283\" }\n     */\n    // \"headers\": {},\n\n    /**\n     * (Optional) Shell command that prints the authorization token needed to communicate with the\n     * cache server, and exits with exit code 0. This command will be executed from the root of\n     * the monorepo.\n     * Example: { \"exec\": \"node\", \"args\": [\"common/scripts/auth.js\"] }\n     */\n    // \"tokenHandler\": { \"exec\": \"node\", \"args\": [\"common/scripts/auth.js\"] },\n\n    /**\n     * (Optional) Prefix for cache keys.\n     * Example: \"my-company-\"\n     */\n    // \"cacheKeyPrefix\": \"\",\n\n    /**\n     * (Optional) If set to true, allow writing to the cache. Defaults to false.\n     */\n    // \"isCacheWriteAllowed\": true\n  }\n}\n"
  },
  {
    "path": "common/config/rush/cobuild.json",
    "content": "/**\n * This configuration file manages Rush's cobuild feature.\n * More documentation is available on the Rush website: https://rushjs.io\n */\n {\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/cobuild.schema.json\",\n\n  /**\n   * (Required) EXPERIMENTAL - Set this to true to enable the cobuild feature.\n   * RUSH_COBUILD_CONTEXT_ID should always be specified as an environment variable with an non-empty string,\n   * otherwise the cobuild feature will be disabled.\n   */\n  \"cobuildFeatureEnabled\": false,\n\n  /**\n   * (Required) Choose where cobuild lock will be acquired.\n   *\n   * The lock provider is registered by the rush plugins.\n   * For example, @rushstack/rush-redis-cobuild-plugin registers the \"redis\" lock provider.\n   */\n  \"cobuildLockProvider\": \"redis\"\n}\n"
  },
  {
    "path": "common/config/rush/command-line.json",
    "content": "/**\n * This configuration file defines custom commands for the \"rush\" command-line.\n * More documentation is available on the Rush website: https://rushjs.io\n */\n{\n\t\"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json\",\n\t/**\n   * Custom \"commands\" introduce new verbs for the command-line.  To see the help for these\n   * example commands, try \"rush --help\", \"rush my-bulk-command --help\", or\n   * \"rush my-global-command --help\".\n   */\n\t\"commands\": [\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom command.\n\t\t//    * Rush's \"bulk\" commands are invoked separately for each project.  By default, the command will run for\n\t\t//    * every project in the repo, according to the dependency graph (similar to how \"rush build\" works).\n\t\t//    * The set of projects can be restricted e.g. using the \"--to\" or \"--from\" parameters.\n\t\t//    */\n\t\t//   \"commandKind\": \"bulk\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) The name that will be typed as part of the command line.  This is also the name\n\t\t//    * of the \"scripts\" hook in the project's package.json file (if \"shellCommand\" is not specified).\n\t\t//    *\n\t\t//    * The name should be comprised of lower case words separated by hyphens or colons. The name should include an\n\t\t//    * English verb (e.g. \"deploy\"). Use a hyphen to separate words (e.g. \"upload-docs\"). A group of related commands\n\t\t//    * can be prefixed with a colon (e.g. \"docs:generate\", \"docs:deploy\", \"docs:serve\", etc).\n\t\t//    *\n\t\t//    * Note that if the \"rebuild\" command is overridden here, it becomes separated from the \"build\" command\n\t\t//    * and will call the \"rebuild\" script instead of the \"build\" script.\n\t\t//    */\n\t\t//   \"name\": \"my-bulk-command\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A short summary of the custom command to be shown when printing command line\n\t\t//    * help, e.g. \"rush --help\".\n\t\t//    */\n\t\t//   \"summary\": \"Example bulk custom command\",\n\t\t//\n\t\t//   /**\n\t\t//    * A detailed description of the command to be shown when printing command line\n\t\t//    * help (e.g. \"rush --help my-command\").\n\t\t//    * If omitted, the \"summary\" text will be shown instead.\n\t\t//    *\n\t\t//    * Whenever you introduce commands/parameters, taking a little time to write meaningful\n\t\t//    * documentation can make a big difference for the developer experience in your repo.\n\t\t//    */\n\t\t//   \"description\": \"This is an example custom command that runs separately for each project\",\n\t\t//\n\t\t//   /**\n\t\t//    * By default, Rush operations acquire a lock file which prevents multiple commands from executing simultaneously\n\t\t//    * in the same repo folder.  (For example, it would be a mistake to run \"rush install\" and \"rush build\" at the\n\t\t//    * same time.)  If your command makes sense to run concurrently with other operations,\n\t\t//    * set \"safeForSimultaneousRushProcesses\" to true to disable this protection.\n\t\t//    *\n\t\t//    * In particular, this is needed for custom scripts that invoke other Rush commands.\n\t\t//    */\n\t\t//   \"safeForSimultaneousRushProcesses\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * (Optional) If the `shellCommand` field is set for a bulk command, Rush will invoke it for each\n\t\t//    * selected project; otherwise, Rush will invoke the package.json `\"scripts\"` entry matching Rush command name.\n\t\t//    *\n\t\t//    * The string is the path to a script that will be invoked using the OS shell. The working directory will be\n\t\t//    * the folder that contains rush.json.  If custom parameters are associated with this command, their\n\t\t//    * values will be appended to the end of this string.\n\t\t//    */\n\t\t//   // \"shellCommand\": \"node common/scripts/my-bulk-command.js\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) If true, then this command is safe to be run in parallel, i.e. executed\n\t\t//    * simultaneously for multiple projects.  Similar to \"rush build\", regardless of parallelism\n\t\t//    * projects will not start processing until their dependencies have completed processing.\n\t\t//    */\n\t\t//   \"enableParallelism\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * Normally projects will be processed according to their dependency order: a given project will not start\n\t\t//    * processing the command until all of its dependencies have completed.  This restriction doesn't apply for\n\t\t//    * certain operations, for example a \"clean\" task that deletes output files.  In this case\n\t\t//    * you can set \"ignoreDependencyOrder\" to true to increase parallelism.\n\t\t//    */\n\t\t//   \"ignoreDependencyOrder\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * Normally Rush requires that each project's package.json has a \"scripts\" entry matching\n\t\t//    * the custom command name.  To disable this check, set \"ignoreMissingScript\" to true;\n\t\t//    * projects with a missing definition will be skipped.\n\t\t//    */\n\t\t//   \"ignoreMissingScript\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * When invoking shell scripts, Rush uses a heuristic to distinguish errors from warnings:\n\t\t//    * - If the shell script returns a nonzero process exit code, Rush interprets this as \"one or more errors\".\n\t\t//    * Error output is displayed in red, and it prevents Rush from attempting to process any downstream projects.\n\t\t//    * - If the shell script returns a zero process exit code but writes something to its stderr stream,\n\t\t//    * Rush interprets this as \"one or more warnings\". Warning output is printed in yellow, but does NOT prevent\n\t\t//    * Rush from processing downstream projects.\n\t\t//    *\n\t\t//    * Thus, warnings do not interfere with local development, but they will cause a CI job to fail, because\n\t\t//    * the Rush process itself returns a nonzero exit code if there are any warnings or errors. This is by design.\n\t\t//    * In an active monorepo, we've found that if you allow any warnings in your main branch, it inadvertently\n\t\t//    * teaches developers to ignore warnings, which quickly leads to a situation where so many \"expected\" warnings\n\t\t//    * have accumulated that warnings no longer serve any useful purpose.\n\t\t//    *\n\t\t//    * Sometimes a poorly behaved task will write output to stderr even though its operation was successful.\n\t\t//    * In that case, it's strongly recommended to fix the task.  However, as a workaround you can set\n\t\t//    * allowWarningsInSuccessfulBuild=true, which causes Rush to return a nonzero exit code for errors only.\n\t\t//    *\n\t\t//    * Note: The default value is false. In Rush 5.7.x and earlier, the default value was true.\n\t\t//    */\n\t\t//   \"allowWarningsInSuccessfulBuild\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * If true then this command will be incremental like the built-in \"build\" command\n\t\t//    */\n\t\t//   \"incremental\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * (EXPERIMENTAL) Normally Rush terminates after the command finishes. If this option is set to \"true\" Rush\n\t\t//    * will instead enter a loop where it watches the file system for changes to the selected projects. Whenever a\n\t\t//    * change is detected, the command will be invoked again for the changed project and any selected projects that\n\t\t//    * directly or indirectly depend on it.\n\t\t//    *\n\t\t//    * For details, refer to the website article \"Using watch mode\".\n\t\t//    */\n\t\t//   \"watchForChanges\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * (EXPERIMENTAL) Disable cache for this action. This may be useful if this command affects state outside of\n\t\t//    * projects' own folders.\n\t\t//    */\n\t\t//   \"disableBuildCache\": false\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom command.\n\t\t//    * Rush's \"global\" commands are invoked once for the entire repo.\n\t\t//    */\n\t\t//   \"commandKind\": \"global\",\n\t\t//\n\t\t//   \"name\": \"my-global-command\",\n\t\t//   \"summary\": \"Example global custom command\",\n\t\t//   \"description\": \"This is an example custom command that runs once for the entire repo\",\n\t\t//\n\t\t//   \"safeForSimultaneousRushProcesses\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A script that will be invoked using the OS shell. The working directory will be\n\t\t//    * the folder that contains rush.json.  If custom parameters are associated with this command, their\n\t\t//    * values will be appended to the end of this string.\n\t\t//    */\n\t\t//   \"shellCommand\": \"node common/scripts/my-global-command.js\",\n\t\t//\n\t\t//   /**\n\t\t//    * If your \"shellCommand\" script depends on NPM packages, the recommended best practice is\n\t\t//    * to make it into a regular Rush project that builds using your normal toolchain.  In cases where\n\t\t//    * the command needs to work without first having to run \"rush build\", the recommended practice\n\t\t//    * is to publish the project to an NPM registry and use common/scripts/install-run.js to launch it.\n\t\t//    *\n\t\t//    * Autoinstallers offer another possibility: They are folders under \"common/autoinstallers\" with\n\t\t//    * a package.json file and shrinkwrap file. Rush will automatically invoke the package manager to\n\t\t//    * install these dependencies before an associated command is invoked.  Autoinstallers have the\n\t\t//    * advantage that they work even in a branch where \"rush install\" is broken, which makes them a\n\t\t//    * good solution for Git hook scripts.  But they have the disadvantages of not being buildable\n\t\t//    * projects, and of increasing the overall installation footprint for your monorepo.\n\t\t//    *\n\t\t//    * The \"autoinstallerName\" setting must not contain a path and must be a valid NPM package name.\n\t\t//    * For example, the name \"my-task\" would map to \"common/autoinstallers/my-task/package.json\", and\n\t\t//    * the \"common/autoinstallers/my-task/node_modules/.bin\" folder would be added to the shell PATH when\n\t\t//    * invoking the \"shellCommand\".\n\t\t//    */\n\t\t//   // \"autoinstallerName\": \"my-task\"\n\t\t// }\n\t\t// 提交前的 lint 检查\n\t\t{\n\t\t\t\"name\": \"lint-staged\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Use to run some task before commit\",\n\t\t\t\"autoinstallerName\": \"rush-lint-staged\",\n\t\t\t\"shellCommand\": \"lint-staged --config common/autoinstallers/rush-lint-staged/.lintstagedrc.js\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"commitlint\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Used by the pre-commit Git hook. This command invokes commitlint to ensure that the commit messages meet the conventional commit format\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"autoinstallerName\": \"rush-commitlint\",\n\t\t\t\"shellCommand\": \"commitlint\"\n\t\t},\n\t\t// 循环依赖检查\n\t\t{\n\t\t\t\"name\": \"check-circular-dependency\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Auto check circular dependency\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"shellCommand\": \"node common/autoinstallers/rush-commands/check-circular-dependency.mjs\"\n\t\t},\n\t\t/**\n\t\t * 检查依赖关系\n\t\t * check dependencies\n\t   */\n\t\t{\n\t\t\t\"name\": \"dep-check\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Auto check dependency\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"autoinstallerName\": \"dep-check\",\n\t\t\t\"shellCommand\": \"node common/autoinstallers/dep-check/index.js\"\n\t\t},\n\t\t{\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"name\": \"test\",\n\t\t\t\"description\": \"Executes automated tests.\",\n\t\t\t\"allowWarningsInSuccessfulBuild\": true,\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t\"incremental\": true,\n\t\t\t\"summary\": \"⭐️️ Run test command for each package\"\n\t\t},\n\t\t{\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"name\": \"test:cov\",\n\t\t\t\"description\": \"Executes automated tests with coverage collection.\",\n\t\t\t\"allowWarningsInSuccessfulBuild\": true,\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t\"incremental\": true,\n\t\t\t\"summary\": \"⭐️️ Run coverage command for each package\"\n\t\t},\n\t\t// 本地包构建 + watch\n\t\t{\n\t\t\t\"name\": \"build:watch\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Run build:watch command for each package\",\n\t\t\t\"description\": \"For details, see the article \\\"Using watch mode\\\" on the Rush website: https://rushjs.io/\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t// 使用增量构建逻辑 (重要)\n\t\t\t\"incremental\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t// 启用 \"watch mode\"\n\t\t\t\"watchForChanges\": true\n\t\t},\n\t\t// 一键 build 所有包 + 运行 docs 官网 dev\n\t\t{\n\t\t\t\"name\": \"dev:docs\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Run dev in apps/docs\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/docs\\\" \\\"cd apps/docs && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"ts-check\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Run ts check in packages\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": true\n\t\t},\n\t\t{\n\t\t\t\"name\": \"lint\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Run eslint check in packages\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t\"allowWarningsInSuccessfulBuild\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": true\n\t\t},\n\t\t{\n\t\t\t\"name\": \"lint:fix\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Run eslint fix in packages\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": true\n\t\t},\n\t\t{\n\t\t\t\"name\": \"e2e:test\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Run e2e cases in packages\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": false,\n\t\t\t\"allowWarningsInSuccessfulBuild\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": false\n\t\t},\n\t\t{\n\t\t\t\"name\": \"e2e:update-screenshot\",\n\t\t\t\"commandKind\": \"bulk\",\n\t\t\t\"summary\": \"⭐️️ Update screenshots of e2e cases\",\n\t\t\t\"ignoreMissingScript\": true,\n\t\t\t\"enableParallelism\": false,\n\t\t\t\"allowWarningsInSuccessfulBuild\": true,\n\t\t\t\"safeForSimultaneousRushProcesses\": false\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-fixed-layout\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-fixed-layout\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"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\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-fixed-layout-animation\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-fixed-layout-animation\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"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\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-fixed-layout-simple\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-fixed-layout-simple\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --raw --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/demo-fixed-layout-simple\\\" \\\"cd apps/demo-fixed-layout-simple && rushx ts-check && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-free-layout\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-free-layout\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --raw --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/demo-free-layout\\\" \\\"cd apps/demo-free-layout && rushx ts-check && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-free-layout-simple\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-free-layout-simple\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --raw --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/demo-free-layout-simple\\\" \\\"cd apps/demo-free-layout-simple && rushx ts-check && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-nextjs\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-nextjs\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --raw --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/demo-nextjs\\\" \\\"cd apps/demo-nextjs && rushx ts-check && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"dev:demo-nextjs-antd\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ run dev in app/demo-nextjs-antd\",\n\t\t\t\"autoinstallerName\": \"rush-commands\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"shellCommand\": \"concurrently --kill-others --raw --prefix \\\"{name}\\\" --names [watch],[demo] -c white,blue \\\"rush build:watch --to-except @flowgram.ai/demo-nextjs-antd\\\" \\\"cd apps/demo-nextjs-antd && rushx ts-check && rushx dev\\\"\"\n\t\t},\n\t\t{\n\t\t\t\"name\": \"license-header\",\n\t\t\t\"commandKind\": \"global\",\n\t\t\t\"summary\": \"⭐️️ Update license header of all code files.\",\n\t\t\t\"safeForSimultaneousRushProcesses\": true,\n\t\t\t\"autoinstallerName\": \"license-header\",\n\t\t\t\"shellCommand\": \"node common/autoinstallers/license-header/index.js\"\n\t\t}\n\t],\n\t/**\n   * Custom \"parameters\" introduce new parameters for specified Rush command-line commands.\n   * For example, you might define a \"--production\" parameter for the \"rush build\" command.\n   */\n\t\"parameters\": [\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * A \"flag\" is a custom command-line parameter whose presence acts as an on/off switch.\n\t\t//    */\n\t\t//   \"parameterKind\": \"flag\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) The long name of the parameter.  It must be lower-case and use dash delimiters.\n\t\t//    */\n\t\t//   \"longName\": \"--my-flag\",\n\t\t//\n\t\t//   /**\n\t\t//    * An optional alternative short name for the parameter.  It must be a dash followed by a single\n\t\t//    * lower-case or upper-case letter, which is case-sensitive.\n\t\t//    *\n\t\t//    * NOTE: The Rush developers recommend that automation scripts should always use the long name\n\t\t//    * to improve readability.  The short name is only intended as a convenience for humans.\n\t\t//    * The alphabet letters run out quickly, and are difficult to memorize, so *only* use\n\t\t//    * a short name if you expect the parameter to be needed very often in everyday operations.\n\t\t//    */\n\t\t//   \"shortName\": \"-m\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A long description to be shown in the command-line help.\n\t\t//    *\n\t\t//    * Whenever you introduce commands/parameters, taking a little time to write meaningful\n\t\t//    * documentation can make a big difference for the developer experience in your repo.\n\t\t//    */\n\t\t//   \"description\": \"A custom flag parameter that is passed to the scripts that are invoked when building projects\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A list of custom commands and/or built-in Rush commands that this parameter may\n\t\t//    * be used with.  The parameter will be appended to the shell command that Rush invokes.\n\t\t//    */\n\t\t//   \"associatedCommands\": [\"build\", \"rebuild\"]\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * A \"string\" is a custom command-line parameter whose argument is a single text string.\n\t\t//    */\n\t\t//   \"parameterKind\": \"string\",\n\t\t//   \"longName\": \"--my-string\",\n\t\t//   \"description\": \"A custom string parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//\n\t\t//   \"argumentName\": \"SOME_TEXT\",\n\t\t//\n\t\t//   /**\n\t\t//    * If true, this parameter must be included with the command.  The default is false.\n\t\t//    */\n\t\t//   \"required\": false\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * A \"choice\" is a custom command-line parameter whose argument must be chosen from a list of\n\t\t//    * allowable alternatives (similar to an enum).\n\t\t//    */\n\t\t//   \"parameterKind\": \"choice\",\n\t\t//   \"longName\": \"--my-choice\",\n\t\t//   \"description\": \"A custom choice parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//   \"required\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * If a \"defaultValue\" is specified, then if the Rush command line is invoked without\n\t\t//    * this parameter, it will be automatically added with the \"defaultValue\" as the argument.\n\t\t//    * The value must be one of the defined alternatives.\n\t\t//    */\n\t\t//   \"defaultValue\": \"vanilla\",\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A list of alternative argument values that can be chosen for this parameter.\n\t\t//    */\n\t\t//   \"alternatives\": [\n\t\t//     {\n\t\t//       /**\n\t\t//        * A token that is one of the alternatives that can be used with the choice parameter,\n\t\t//        * e.g. \"vanilla\" in \"--flavor vanilla\".\n\t\t//        */\n\t\t//       \"name\": \"vanilla\",\n\t\t//\n\t\t//       /**\n\t\t//        * A detailed description for the alternative that can be shown in the command-line help.\n\t\t//        *\n\t\t//        * Whenever you introduce commands/parameters, taking a little time to write meaningful\n\t\t//        * documentation can make a big difference for the developer experience in your repo.\n\t\t//        */\n\t\t//       \"description\": \"Use the vanilla flavor\"\n\t\t//     },\n\t\t//\n\t\t//     {\n\t\t//       \"name\": \"chocolate\",\n\t\t//       \"description\": \"Use the chocolate flavor\"\n\t\t//     },\n\t\t//\n\t\t//     {\n\t\t//       \"name\": \"strawberry\",\n\t\t//       \"description\": \"Use the strawberry flavor\"\n\t\t//     }\n\t\t//   ]\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * An \"integer\" is a custom command-line parameter whose value is an integer number.\n\t\t//    */\n\t\t//   \"parameterKind\": \"integer\",\n\t\t//   \"longName\": \"--my-integer\",\n\t\t//   \"description\": \"A custom integer parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//   \"argumentName\": \"SOME_NUMBER\",\n\t\t//   \"required\": false\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * An \"integerList\" is a custom command-line parameter whose argument is an integer.\n\t\t//    * The parameter can be specified multiple times to build a list.\n\t\t//    *\n\t\t//    * For example, if the parameter name is \"--my-integer-list\", then the custom command\n\t\t//    * might be invoked as\n\t\t//    * `rush my-global-command --my-integer-list 1 --my-integer-list 2 --my-integer-list 3`\n\t\t//    * and the parsed array would be [1,2,3].\n\t\t//    */\n\t\t//   \"parameterKind\": \"integerList\",\n\t\t//   \"longName\": \"--my-integer-list\",\n\t\t//   \"description\": \"A custom integer list parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//   \"argumentName\": \"SOME_NUMBER\",\n\t\t//   \"required\": false\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * An \"stringList\" is a custom command-line parameter whose argument is a text string.\n\t\t//    * The parameter can be specified multiple times to build a list.\n\t\t//    *\n\t\t//    * For example, if the parameter name is \"--my-string-list\", then the custom command\n\t\t//    * might be invoked as\n\t\t//    * `rush my-global-command --my-string-list A --my-string-list B --my-string-list C`\n\t\t//    * and the parsed array would be [A,B,C].\n\t\t//    */\n\t\t//   \"parameterKind\": \"stringList\",\n\t\t//   \"longName\": \"--my-string-list\",\n\t\t//   \"description\": \"A custom string list parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//   \"argumentName\": \"SOME_TEXT\",\n\t\t//   \"required\": false\n\t\t// },\n\t\t//\n\t\t// {\n\t\t//   /**\n\t\t//    * (Required) Determines the type of custom parameter.\n\t\t//    * A \"choice\" is a custom command-line parameter whose argument must be chosen from a list of\n\t\t//    * allowable alternatives (similar to an enum).\n\t\t//    * The parameter can be specified multiple times to build a list.\n\t\t//    *\n\t\t//    * For example, if the parameter name is \"--my-choice-list\", then the custom command\n\t\t//    * might be invoked as\n\t\t//    * `rush my-global-command --my-string-list vanilla --my-string-list chocolate`\n\t\t//    * and the parsed array would be [vanilla,chocolate].\n\t\t//    */\n\t\t//   \"parameterKind\": \"choiceList\",\n\t\t//   \"longName\": \"--my-choice-list\",\n\t\t//   \"description\": \"A custom choice list parameter for the \\\"my-global-command\\\" custom command\",\n\t\t//\n\t\t//   \"associatedCommands\": [\"my-global-command\"],\n\t\t//   \"required\": false,\n\t\t//\n\t\t//   /**\n\t\t//    * (Required) A list of alternative argument values that can be chosen for this parameter.\n\t\t//    */\n\t\t//   \"alternatives\": [\n\t\t//     {\n\t\t//       /**\n\t\t//        * A token that is one of the alternatives that can be used with the choice parameter,\n\t\t//        * e.g. \"vanilla\" in \"--flavor vanilla\".\n\t\t//        */\n\t\t//       \"name\": \"vanilla\",\n\t\t//\n\t\t//       /**\n\t\t//        * A detailed description for the alternative that can be shown in the command-line help.\n\t\t//        *\n\t\t//        * Whenever you introduce commands/parameters, taking a little time to write meaningful\n\t\t//        * documentation can make a big difference for the developer experience in your repo.\n\t\t//        */\n\t\t//       \"description\": \"Use the vanilla flavor\"\n\t\t//     },\n\t\t//\n\t\t//     {\n\t\t//       \"name\": \"chocolate\",\n\t\t//       \"description\": \"Use the chocolate flavor\"\n\t\t//     },\n\t\t//\n\t\t//     {\n\t\t//       \"name\": \"strawberry\",\n\t\t//       \"description\": \"Use the strawberry flavor\"\n\t\t//     }\n\t\t//   ]\n\t\t// }\n\t\t{\n\t\t\t\"parameterKind\": \"string\",\n\t\t\t\"argumentName\": \"MESSAGE\",\n\t\t\t\"longName\": \"--edit\",\n\t\t\t\"description\": \"\",\n\t\t\t\"associatedCommands\": [\n\t\t\t\t\"commitlint\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"parameterKind\": \"string\",\n\t\t\t\"argumentName\": \"MESSAGE\",\n\t\t\t\"longName\": \"--config\",\n\t\t\t\"description\": \"\",\n\t\t\t\"associatedCommands\": [\n\t\t\t\t\"commitlint\"\n\t\t\t]\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "common/config/rush/common-versions.json",
    "content": "/**\n * This configuration file specifies NPM dependency version selections that affect all projects\n * in a Rush repo.  More documentation is available on the Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json\",\n\n  /**\n   * A table that specifies a \"preferred version\" for a given NPM package.  This feature is typically used\n   * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies.\n   *\n   * The \"preferredVersions\" value can be any SemVer range specifier (e.g. \"~1.2.3\").  Rush injects these values into\n   * the \"dependencies\" field of the top-level common/temp/package.json, which influences how the package manager\n   * will calculate versions.  The specific effect depends on your package manager.  Generally it will have no\n   * effect on an incompatible or already constrained SemVer range.  If you are using PNPM, similar effects can be\n   * achieved using the pnpmfile.js hook.  See the Rush documentation for more details.\n   *\n   * After modifying this field, it's recommended to run \"rush update --full\" so that the package manager\n   * will recalculate all version selections.\n   */\n  \"preferredVersions\": {\n    /**\n     * When someone asks for \"^1.0.0\" make sure they get \"1.2.3\" when working in this repo,\n     * instead of the latest version.\n     */\n    // \"some-library\": \"1.2.3\"\n  },\n\n  /**\n   * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions,\n   * except in cases where different projects specify different version ranges for a given dependency.  For older\n   * package managers, this tended to reduce duplication of indirect dependencies.  However, it can sometimes cause\n   * trouble for indirect dependencies with incompatible peerDependencies ranges.\n   *\n   * The default value is true.  If you're encountering installation errors related to peer dependencies,\n   * it's recommended to set this to false.\n   *\n   * After modifying this field, it's recommended to run \"rush update --full\" so that the package manager\n   * will recalculate all version selections.\n   */\n  // \"implicitlyPreferredVersions\": false,\n\n  /**\n   * If you would like the version specifiers for your dependencies to be consistent, then\n   * uncomment this line. This is effectively similar to running \"rush check\" before any\n   * of the following commands:\n   *\n   *   rush install, rush update, rush link, rush version, rush publish\n   *\n   * In some cases you may want this turned on, but need to allow certain packages to use a different\n   * version. In those cases, you will need to add an entry to the \"allowedAlternativeVersions\"\n   * section of the common-versions.json.\n   *\n   * In the case that subspaces is enabled, this setting will take effect at a subspace level.\n   */\n  // \"ensureConsistentVersions\": true,\n\n  /**\n   * The \"rush check\" command can be used to enforce that every project in the repo must specify\n   * the same SemVer range for a given dependency.  However, sometimes exceptions are needed.\n   * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be\n   * accepted by \"rush check\" for a given dependency.\n   *\n   * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE\n   * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO).\n   * This design avoids unnecessary churn in this file.\n   */\n  \"allowedAlternativeVersions\": {\n    /**\n     * For example, allow some projects to use an older TypeScript compiler\n     * (in addition to whatever \"usual\" version is being used by other projects in the repo):\n     */\n    \"react\": [\"^16.8.6\"],\n    \"react-dom\": [\"^16.8.6\"],\n    \"@types/react\": [\"^16.8.6\"],\n    \"@types/react-dom\": [\"^16.8.6\"],\n    \"typescript\": [\"5.0.4\", \"5.8.3\"]\n  }\n}\n"
  },
  {
    "path": "common/config/rush/custom-tips.json",
    "content": "/**\n * This configuration file allows repo maintainers to configure extra details to be\n * printed alongside certain Rush messages.  More documentation is available on the\n * Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/custom-tips.schema.json\",\n\n  /**\n   * Custom tips allow you to annotate Rush's console messages with advice tailored for\n   * your specific monorepo.\n   */\n  \"customTips\": [\n    // {\n    //   /**\n    //    * (REQUIRED) An identifier indicating a message that may be printed by Rush.\n    //    * If that message is printed, then this custom tip will be shown.\n    //    * The list of available tip identifiers can be found on this page:\n    //    * https://rushjs.io/pages/maintainer/custom_tips/\n    //    */\n    //   \"tipId\": \"TIP_RUSH_INCONSISTENT_VERSIONS\",\n    // \n    //   /**\n    //    * (REQUIRED) The message text to be displayed for this tip.\n    //    */\n    //   \"message\": \"For additional troubleshooting information, refer this wiki article:\\n\\nhttps://intranet.contoso.com/docs/pnpm-mismatch\"\n    // }\n  ]\n}\n"
  },
  {
    "path": "common/config/rush/experiments.json",
    "content": "/**\n * This configuration file allows repo maintainers to enable and disable experimental\n * Rush features.  More documentation is available on the Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json\",\n\n  /**\n   * By default, 'rush install' passes --no-prefer-frozen-lockfile to 'pnpm install'.\n   * Set this option to true to pass '--frozen-lockfile' instead for faster installs.\n   */\n  // \"usePnpmFrozenLockfileForRushInstall\": true,\n\n  /**\n   * By default, 'rush update' passes --no-prefer-frozen-lockfile to 'pnpm install'.\n   * Set this option to true to pass '--prefer-frozen-lockfile' instead to minimize shrinkwrap changes.\n   */\n  // \"usePnpmPreferFrozenLockfileForRushUpdate\": true,\n\n  /**\n   * By default, 'rush update' runs as a single operation.\n   * Set this option to true to instead update the lockfile with `--lockfile-only`, then perform a `--frozen-lockfile` install.\n   * Necessary when using the `afterAllResolved` hook in .pnpmfile.cjs.\n   */\n  // \"usePnpmLockfileOnlyThenFrozenLockfileForRushUpdate\": true,\n\n  /**\n   * If using the 'preventManualShrinkwrapChanges' option, restricts the hash to only include the layout of external dependencies.\n   * Used to allow links between workspace projects or the addition/removal of references to existing dependency versions to not\n   * cause hash changes.\n   */\n  // \"omitImportersFromPreventManualShrinkwrapChanges\": true,\n\n  /**\n   * If true, the chmod field in temporary project tar headers will not be normalized.\n   * This normalization can help ensure consistent tarball integrity across platforms.\n   */\n  // \"noChmodFieldInTarHeaderNormalization\": true,\n\n  /**\n   * If true, build caching will respect the allowWarningsInSuccessfulBuild flag and cache builds with warnings.\n   * This will not replay warnings from the cached build.\n   */\n  // \"buildCacheWithAllowWarningsInSuccessfulBuild\": true,\n\n  /**\n   * If true, build skipping will respect the allowWarningsInSuccessfulBuild flag and skip builds with warnings.\n   * This will not replay warnings from the skipped build.\n   */\n  // \"buildSkipWithAllowWarningsInSuccessfulBuild\": true,\n\n  /**\n   * If true, perform a clean install after when running `rush install` or `rush update` if the\n   * `.npmrc` file has changed since the last install.\n   */\n  // \"cleanInstallAfterNpmrcChanges\": true,\n\n  /**\n   * If true, print the outputs of shell commands defined in event hooks to the console.\n   */\n  // \"printEventHooksOutputToConsole\": true,\n\n  /**\n   * If true, Rush will not allow node_modules in the repo folder or in parent folders.\n   */\n  // \"forbidPhantomResolvableNodeModulesFolders\": true,\n\n  /**\n   * (UNDER DEVELOPMENT) For certain installation problems involving peer dependencies, PNPM cannot\n   * correctly satisfy versioning requirements without installing duplicate copies of a package inside the\n   * node_modules folder. This poses a problem for \"workspace:*\" dependencies, as they are normally\n   * installed by making a symlink to the local project source folder. PNPM's \"injected dependencies\"\n   * feature provides a model for copying the local project folder into node_modules, however copying\n   * must occur AFTER the dependency project is built and BEFORE the consuming project starts to build.\n   * The \"pnpm-sync\" tool manages this operation; see its documentation for details.\n   * Enable this experiment if you want \"rush\" and \"rushx\" commands to resync injected dependencies\n   * by invoking \"pnpm-sync\" during the build.\n   */\n  // \"usePnpmSyncForInjectedDependencies\": true,\n\n  /**\n   * If set to true, Rush will generate a `project-impact-graph.yaml` file in the repository root during `rush update`.\n   */\n  // \"generateProjectImpactGraphDuringRushUpdate\": true,\n\n  /**\n   * If true, when running in watch mode, Rush will check for phase scripts named `_phase:<name>:ipc` and run them instead\n   * of `_phase:<name>` if they exist. The created child process will be provided with an IPC channel and expected to persist\n   * across invocations.\n   */\n  // \"useIPCScriptsInWatchMode\": true,\n\n  /**\n   * (UNDER DEVELOPMENT) The Rush alerts feature provides a way to send announcements to engineers\n   * working in the monorepo, by printing directly in the user's shell window when they invoke Rush commands.\n   * This ensures that important notices will be seen by anyone doing active development, since people often\n   * ignore normal discussion group messages or don't know to subscribe.\n   */\n   // \"rushAlerts\": true,\n\n\n  /**\n   * When using cobuilds, this experiment allows uncacheable operations to benefit from cobuild orchestration without using the build cache.\n   */\n   // \"allowCobuildWithoutCache\": true\n}\n"
  },
  {
    "path": "common/config/rush/pnpm-config.json",
    "content": "/**\n * This configuration file provides settings specific to the PNPM package manager.\n * More documentation is available on the Rush website: https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/pnpm-config.schema.json\",\n\n  /**\n   * If true, then `rush install` and `rush update` will use the PNPM workspaces feature\n   * to perform the install, instead of the old model where Rush generated the symlinks\n   * for each projects's node_modules folder.\n   *\n   * When using workspaces, Rush will generate a `common/temp/pnpm-workspace.yaml` file referencing\n   * all local projects to install. Rush will also generate a `.pnpmfile.cjs` shim which implements\n   * Rush-specific features such as preferred versions.  The user's `common/config/rush/.pnpmfile.cjs`\n   * is invoked by the shim.\n   *\n   * This option is strongly recommended. The default value is false.\n   */\n  \"useWorkspaces\": true,\n\n  /**\n   * This setting determines how PNPM chooses version numbers during `rush update`.\n   * For example, suppose `lib-x@3.0.0` depends on `\"lib-y\": \"^1.2.3\"` whose latest major\n   * releases are `1.8.9` and `2.3.4`.  The resolution mode `lowest-direct` might choose\n   * `lib-y@1.2.3`, wheres `highest` will choose 1.8.9, and `time-based` will pick the\n   * highest compatible version at the time when `lib-x@3.0.0` itself was published (ensuring\n   * that the version could have been tested by the maintainer of \"lib-x\").  For local workspace\n   * projects, `time-based` instead works like `lowest-direct`, avoiding upgrades unless\n   * they are explicitly requested. Although `time-based` is the most robust option, it may be\n   * slightly slower with registries such as npmjs.com that have not implemented an optimization.\n   *\n   * IMPORTANT: Be aware that PNPM 8.0.0 initially defaulted to `lowest-direct` instead of\n   * `highest`, but PNPM reverted this decision in 8.6.12 because it caused confusion for users.\n   * Rush version 5.106.0 and newer avoids this confusion by consistently defaulting to\n   * `highest` when `resolutionMode` is not explicitly set in pnpm-config.json or .npmrc,\n   * regardless of your PNPM version.\n   *\n   * PNPM documentation: https://pnpm.io/npmrc#resolution-mode\n   *\n   * Possible values are: `highest`, `time-based`, and `lowest-direct`.\n   * The default is `highest`.\n   */\n  // \"resolutionMode\": \"time-based\",\n\n  /**\n   * This setting determines whether PNPM will automatically install (non-optional)\n   * missing peer dependencies instead of reporting an error.  Doing so conveniently\n   * avoids the need to specify peer versions in package.json, but in a large monorepo\n   * this often creates worse problems.  The reason is that peer dependency behavior\n   * is inherently complicated, and it is easier to troubleshoot consequences of an explicit\n   * version than an invisible heuristic.  The original NPM RFC discussion pointed out\n   * some other problems with this feature: https://github.com/npm/rfcs/pull/43\n\n   * IMPORTANT: Without Rush, the setting defaults to true for PNPM 8 and newer; however,\n   * as of Rush version 5.109.0 the default is always false unless `autoInstallPeers`\n   * is specified in pnpm-config.json or .npmrc, regardless of your PNPM version.\n\n   * PNPM documentation: https://pnpm.io/npmrc#auto-install-peers\n\n   * The default value is false.\n   */\n  \"autoInstallPeers\": true,\n\n  /**\n   * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when\n   * invoking PNPM.  This causes `rush update` to fail if there are unsatisfied peer dependencies,\n   * which is an invalid state that can cause build failures or incompatible dependency versions.\n   * (For historical reasons, JavaScript package managers generally do not treat this invalid\n   * state as an error.)\n   *\n   * PNPM documentation: https://pnpm.io/npmrc#strict-peer-dependencies\n   *\n   * The default value is false to avoid legacy compatibility issues.\n   * It is strongly recommended to set `strictPeerDependencies=true`.\n   */\n  // \"strictPeerDependencies\": true,\n\n  /**\n   * Environment variables that will be provided to PNPM.\n   */\n  // \"environmentVariables\": {\n  //   \"NODE_OPTIONS\": {\n  //     \"value\": \"--max-old-space-size=4096\",\n  //     \"override\": false\n  //   }\n  // },\n\n  /**\n   * Specifies the location of the PNPM store.  There are two possible values:\n   *\n   * - `local` - use the `pnpm-store` folder in the current configured temp folder:\n   *   `common/temp/pnpm-store` by default.\n   * - `global` - use PNPM's global store, which has the benefit of being shared\n   *    across multiple repo folders, but the disadvantage of less isolation for builds\n   *    (for example, bugs or incompatibilities when two repos use different releases of PNPM)\n   *\n   * In both cases, the store path can be overridden by the environment variable `RUSH_PNPM_STORE_PATH`.\n   *\n   * The default value is `local`.\n   */\n  \"pnpmStore\": \"global\",\n\n  /**\n   * If true, then `rush install` will report an error if manual modifications\n   * were made to the PNPM shrinkwrap file without running `rush update` afterwards.\n   *\n   * This feature protects against accidental inconsistencies that may be introduced\n   * if the PNPM shrinkwrap file (`pnpm-lock.yaml`) is manually edited.  When this\n   * feature is enabled, `rush update` will append a hash to the file as a YAML comment,\n   * and then `rush update` and `rush install` will validate the hash.  Note that this\n   * does not prohibit manual modifications, but merely requires `rush update` be run\n   * afterwards, ensuring that PNPM can report or repair any potential inconsistencies.\n   *\n   * To temporarily disable this validation when invoking `rush install`, use the\n   * `--bypass-policy` command-line parameter.\n   *\n   * The default value is false.\n   */\n  // \"preventManualShrinkwrapChanges\": true,\n\n  /**\n   * When a project uses `workspace:` to depend on another Rush project, PNPM normally installs\n   * it by creating a symlink under `node_modules`.  This generally works well, but in certain\n   * cases such as differing `peerDependencies` versions, symlinking may cause trouble\n   * such as incorrectly satisfied versions.  For such cases, the dependency can be declared\n   * as \"injected\", causing PNPM to copy its built output into `node_modules` like a real\n   * install from a registry.  Details here: https://rushjs.io/pages/advanced/injected_deps/\n   *\n   * When using Rush subspaces, these sorts of versioning problems are much more likely if\n   * `workspace:` refers to a project from a different subspace.  This is because the symlink\n   * would point to a separate `node_modules` tree installed by a different PNPM lockfile.\n   * A comprehensive solution is to enable `alwaysInjectDependenciesFromOtherSubspaces`,\n   * which automatically treats all projects from other subspaces as injected dependencies\n   * without having to manually configure them.\n   *\n   * NOTE: Use carefully -- excessive file copying can slow down the `rush install` and\n   * `pnpm-sync` operations if too many dependencies become injected.\n   *\n   * The default value is false.\n   */\n  // \"alwaysInjectDependenciesFromOtherSubspaces\": false,\n\n  /**\n   * Defines the policies to be checked for the `pnpm-lock.yaml` file.\n   */\n  \"pnpmLockfilePolicies\": {\n    /**\n     * This policy will cause \"rush update\" to report an error if `pnpm-lock.yaml` contains\n     * any SHA1 integrity hashes.\n     *\n     * For each NPM dependency, `pnpm-lock.yaml` normally stores an `integrity` hash.  Although\n     * its main purpose is to detect corrupted or truncated network requests, this hash can also\n     * serve as a security fingerprint to protect against attacks that would substitute a\n     * malicious tarball, for example if a misconfigured .npmrc caused a machine to accidentally\n     * download a matching package name+version from npmjs.com instead of the private NPM registry.\n     * NPM originally used a SHA1 hash; this was insecure because an attacker can too easily craft\n     * a tarball with a matching fingerprint.  For this reason, NPM later deprecated SHA1 and\n     * instead adopted a cryptographically strong SHA512 hash.  Nonetheless, SHA1 hashes can\n     * occasionally reappear during \"rush update\", for example due to missing metadata fallbacks\n     * (https://github.com/orgs/pnpm/discussions/6194) or an incompletely migrated private registry.\n     * The `disallowInsecureSha1` policy prevents this, avoiding potential security/compliance alerts.\n     */\n    // \"disallowInsecureSha1\": {\n    //   /**\n    //    * Enables the \"disallowInsecureSha1\" policy.  The default value is false.\n    //    */\n    //   \"enabled\": true,\n    //\n    //   /**\n    //    * In rare cases, a private NPM registry may continue to serve SHA1 hashes for very old\n    //    * package versions, perhaps due to a caching issue or database migration glitch.  To avoid\n    //    * having to disable the \"disallowInsecureSha1\" policy for the entire monorepo, the problematic\n    //    * package versions can be individually ignored.  The \"exemptPackageVersions\" key is the\n    //    * package name, and the array value lists exact version numbers to be ignored.\n    //    */\n    //   \"exemptPackageVersions\": {\n    //     \"example1\": [\"1.0.0\"],\n    //     \"example2\": [\"2.0.0\", \"2.0.1\"]\n    //   }\n    // }\n  },\n\n  /**\n   * The \"globalOverrides\" setting provides a simple mechanism for overriding version selections\n   * for all dependencies of all projects in the monorepo workspace.  The settings are copied\n   * into the `pnpm.overrides` field of the `common/temp/package.json` file that is generated\n   * by Rush during installation.\n   *\n   * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by\n   * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,\n   * and `globalOverrides` has lowest precedence.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmoverrides\n   */\n  \"globalOverrides\": {\n    // \"example1\": \"^1.0.0\",\n    // \"example2\": \"npm:@company/example2@^1.0.0\"\n  },\n\n  /**\n   * The `globalPeerDependencyRules` setting provides various settings for suppressing validation errors\n   * that are reported during installation with `strictPeerDependencies=true`.  The settings are copied\n   * into the `pnpm.peerDependencyRules` field of the `common/temp/package.json` file that is generated\n   * by Rush during installation.\n   *\n   * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by\n   * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,\n   * and `globalOverrides` has lowest precedence.\n   *\n   * https://pnpm.io/package_json#pnpmpeerdependencyrules\n   */\n  \"globalPeerDependencyRules\": {\n    // \"ignoreMissing\": [\"@eslint/*\"],\n    // \"allowedVersions\": { \"react\": \"17\" },\n    // \"allowAny\": [\"@babel/*\"]\n  },\n\n  /**\n   * The `globalPackageExtension` setting provides a way to patch arbitrary package.json fields\n   * for any PNPM dependency of the monorepo.  The settings are copied into the `pnpm.packageExtensions`\n   * field of the `common/temp/package.json` file that is generated by Rush during installation.\n   * The `globalPackageExtension` setting has similar capabilities as `.pnpmfile.cjs` but without\n   * the downsides of an executable script (nondeterminism, unreliable caching, performance concerns).\n   *\n   * Order of precedence: `.pnpmfile.cjs` has the highest precedence, followed by\n   * `unsupportedPackageJsonSettings`, `globalPeerDependencyRules`, `globalPackageExtensions`,\n   * and `globalOverrides` has lowest precedence.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmpackageextensions\n   */\n  \"globalPackageExtensions\": {\n    // \"fork-ts-checker-webpack-plugin\": {\n    //   \"dependencies\": {\n    //     \"@babel/core\": \"1\"\n    //   },\n    //   \"peerDependencies\": {\n    //     \"eslint\": \">= 6\"\n    //   },\n    //   \"peerDependenciesMeta\": {\n    //     \"eslint\": {\n    //       \"optional\": true\n    //     }\n    //   }\n    // }\n  },\n\n  /**\n   * The `globalNeverBuiltDependencies` setting suppresses the `preinstall`, `install`, and `postinstall`\n   * lifecycle events for the specified NPM dependencies.  This is useful for scripts with poor practices\n   * such as downloading large binaries without retries or attempting to invoke OS tools such as\n   * a C++ compiler.  (PNPM's terminology refers to these lifecycle events as \"building\" a package;\n   * it has nothing to do with build system operations such as `rush build` or `rushx build`.)\n   * The settings are copied into the `pnpm.neverBuiltDependencies` field of the `common/temp/package.json`\n   * file that is generated by Rush during installation.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmneverbuiltdependencies\n   */\n  \"globalNeverBuiltDependencies\": [\n    // \"fsevents\"\n  ],\n\n  /**\n   * The `globalIgnoredOptionalDependencies` setting suppresses the installation of optional NPM\n   * dependencies specified in the list. This is useful when certain optional dependencies are\n   * not needed in your environment, such as platform-specific packages or dependencies that\n   * fail during installation but are not critical to your project.\n   * These settings are copied into the `pnpm.overrides` field of the `common/temp/package.json`\n   * file that is generated by Rush during installation, instructing PNPM to ignore the specified\n   * optional dependencies.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmignoredoptionaldependencies\n   */\n  \"globalIgnoredOptionalDependencies\": [\n    // \"fsevents\"\n  ],\n\n  /**\n   * The `globalAllowedDeprecatedVersions` setting suppresses installation warnings for package\n   * versions that the NPM registry reports as being deprecated.  This is useful if the\n   * deprecated package is an indirect dependency of an external package that has not released a fix.\n   * The settings are copied into the `pnpm.allowedDeprecatedVersions` field of the `common/temp/package.json`\n   * file that is generated by Rush during installation.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmalloweddeprecatedversions\n   *\n   * If you are working to eliminate a deprecated version, it's better to specify `allowedDeprecatedVersions`\n   * in the package.json file for individual Rush projects.\n   */\n  \"globalAllowedDeprecatedVersions\": {\n    // \"request\": \"*\"\n  },\n\n  /**\n   * (THIS FIELD IS MACHINE GENERATED)  The \"globalPatchedDependencies\" field is updated automatically\n   * by the `rush-pnpm patch-commit` command.  It is a dictionary, where the key is an NPM package name\n   * and exact version, and the value is a relative path to the associated patch file.\n   *\n   * PNPM documentation: https://pnpm.io/package_json#pnpmpatcheddependencies\n   */\n  \"globalPatchedDependencies\": {},\n\n  /**\n   * (USE AT YOUR OWN RISK)  This is a free-form property bag that will be copied into\n   * the `common/temp/package.json` file that is generated by Rush during installation.\n   * This provides a way to experiment with new PNPM features.  These settings will override\n   * any other Rush configuration associated with a given JSON field except for `.pnpmfile.cjs`.\n   *\n   * USAGE OF THIS SETTING IS NOT SUPPORTED BY THE RUSH MAINTAINERS AND MAY CAUSE RUSH\n   * TO MALFUNCTION.  If you encounter a missing PNPM setting that you believe should\n   * be supported, please create a GitHub issue or PR.  Note that Rush does not aim to\n   * support every possible PNPM setting, but rather to promote a battle-tested installation\n   * strategy that is known to provide a good experience for large teams with lots of projects.\n   */\n  \"unsupportedPackageJsonSettings\": {\n    // \"dependencies\": {\n    //   \"not-a-good-practice\": \"*\"\n    // },\n    // \"scripts\": {\n    //   \"do-something\": \"echo Also not a good practice\"\n    // },\n    // \"pnpm\": { \"futurePnpmFeature\": true }\n  }\n}\n"
  },
  {
    "path": "common/config/rush/repo-state.json",
    "content": "// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.\n{\n  \"preferredVersionsHash\": \"bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f\"\n}\n"
  },
  {
    "path": "common/config/rush/rush-plugins.json",
    "content": "/**\n * This configuration file manages Rush's plugin feature.\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/rush-plugins.schema.json\",\n  \"plugins\": [\n    /**\n     * Each item configures a plugin to be loaded by Rush.\n     */\n    // {\n    //   /**\n    //    * The name of the NPM package that provides the plugin.\n    //    */\n    //   \"packageName\": \"@scope/my-rush-plugin\",\n    //   /**\n    //    * The name of the plugin.  This can be found in the \"pluginName\"\n    //    * field of the \"rush-plugin-manifest.json\" file in the NPM package folder.\n    //    */\n    //   \"pluginName\": \"my-plugin-name\",\n    //   /**\n    //    * The name of a Rush autoinstaller that will be used for installation, which\n    //    * can be created using \"rush init-autoinstaller\".  Add the plugin's NPM package\n    //    * to the package.json \"dependencies\" of your autoinstaller, then run\n    //    * \"rush update-autoinstaller\".\n    //    */\n    //   \"autoinstallerName\": \"rush-plugins\"\n    // }\n  ]\n}"
  },
  {
    "path": "common/config/rush/subspaces.json",
    "content": "/**\n * This configuration file manages the experimental \"subspaces\" feature for Rush,\n * which allows multiple PNPM lockfiles to be used in a single Rush workspace.\n * For full documentation, please see https://rushjs.io\n */\n{\n  \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/subspaces.schema.json\",\n\n  /**\n   * Set this flag to \"true\" to enable usage of subspaces.\n   */\n  \"subspacesEnabled\": false,\n\n  /**\n   * (DEPRECATED) This is a temporary workaround for migrating from an earlier prototype\n   * of this feature: https://github.com/microsoft/rushstack/pull/3481\n   * It allows subspaces with only one project to store their config files in the project folder.\n   */\n  \"splitWorkspaceCompatibility\": false,\n\n  /**\n   * When a command such as \"rush update\" is invoked without the \"--subspace\" or \"--to\"\n   * parameters, Rush will install all subspaces.  In a huge monorepo with numerous subspaces,\n   * this would be extremely slow.  Set \"preventSelectingAllSubspaces\" to true to avoid this\n   * mistake by always requiring selection parameters for commands such as \"rush update\".\n   */\n  \"preventSelectingAllSubspaces\": false,\n\n  /**\n   * The list of subspace names, which should be lowercase alphanumeric words separated by\n   * hyphens, for example \"my-subspace\".  The corresponding config files will have paths\n   * such as \"common/config/subspaces/my-subspace/package-lock.yaml\".\n   */\n  \"subspaceNames\": []\n}\n"
  },
  {
    "path": "common/config/rush/version-policies.json",
    "content": "[\n  {\n    \"policyName\": \"publishPolicy\",\n    \"definitionName\": \"lockStepVersion\",\n    \"version\": \"0.1.0\",\n    \"nextBump\": \"patch\"\n  },\n  {\n    \"policyName\": \"appPolicy\",\n    \"definitionName\": \"lockStepVersion\",\n    \"version\": \"0.1.0\",\n    \"nextBump\": \"patch\"\n  }\n]\n"
  },
  {
    "path": "common/git-hooks/commit-msg",
    "content": "#!/bin/bash\n#\n# This is an example Git hook for use with Rush.  To enable this hook, rename this file\n# to \"commit-msg\" and then run \"rush install\", which will copy it from common/git-hooks\n# to the .git/hooks folder.\n#\n# TO LEARN MORE ABOUT GIT HOOKS\n#\n# The Git documentation is here: https://git-scm.com/docs/githooks\n# Some helpful resources: https://githooks.com\n#\n# ABOUT THIS EXAMPLE\n#\n# The commit-msg hook is called by \"git commit\" with one argument, the name of the file\n# that has the commit message.  The hook should exit with non-zero status after issuing\n# an appropriate message if it wants to stop the commit.  The hook is allowed to edit\n# the commit message file.\n\n# This example enforces that commit message should contain a minimum amount of\n# description text.\n# if [ `cat $1 | wc -w` -lt 3 ]; then\n#   echo \"\"\n#   echo \"Invalid commit message: The message must contain at least 3 words.\"\n#   exit 1\n# fi\n\n# rebase 过程中分支名格式为 (no branch, rebasing chore/replace-rushtool)\n# 正常提交时分支名格式为 chore/replace-rushtool\nBRANCH_NAME=$(git branch | grep '*' | sed 's/* //')\n# 如果匹配到 rebase 格式的输出，认为是在rebase ，则跳过自动推送\nif [[ \"X${BRANCH_NAME}\" == \"X(no branch\"* ]]; then\n  exit\nelse\n  node common/scripts/install-run-rush.js -q commitlint \\\n    --config common/autoinstallers/rush-commitlint/commitlint.config.js \\\n    --edit \"$1\" || exit 1\nfi\n"
  },
  {
    "path": "common/git-hooks/post-checkout",
    "content": "#!/bin/bash\n# avoid conflicts in pnpm lock\n# https://7tonshark.com/posts/avoid-conflicts-in-pnpm-lock/\ngit config merge.ours.driver true\n"
  },
  {
    "path": "common/git-hooks/pre-commit",
    "content": "#!/bin/bash\n# Called by \"git commit\" with no arguments.  The hook should\n# exit with non-zero status after issuing an appropriate message if\n# it wants to stop the commit.\n\n# Invoke the \"rush prettier\" custom command to reformat files whenever they\n# are committed. The command is defined in common/config/rush/command-line.json\n# and uses the \"rush-lint-staged\" autoinstaller.\n\n# Force to update codeowners file if packasge.json changed.\nif [ \"$PRE_LINT\" != \"1\" ]; then\n  node common/scripts/install-run-rush.js -q lint-staged || exit $?\n  node common/scripts/install-run-rush.js check\nfi\n"
  },
  {
    "path": "common/scripts/install-run-rush-pnpm.js",
    "content": "// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.\n//\n// This script is intended for usage in an automated build environment where the Rush command may not have\n// been preinstalled, or may have an unpredictable version.  This script will automatically install the version of Rush\n// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the\n// rush-pnpm command.\n//\n// An example usage would be:\n//\n//    node common/scripts/install-run-rush-pnpm.js pnpm-command\n//\n// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/\n//\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See the @microsoft/rush package's LICENSE file for details.\n\n/******/ (() => { // webpackBootstrap\n/******/ \t\"use strict\";\nvar __webpack_exports__ = {};\n/*!*****************************************************!*\\\n  !*** ./lib-esnext/scripts/install-run-rush-pnpm.js ***!\n  \\*****************************************************/\n\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\nrequire('./install-run-rush');\n//# sourceMappingURL=install-run-rush-pnpm.js.map\nmodule.exports = __webpack_exports__;\n/******/ })()\n;\n//# sourceMappingURL=install-run-rush-pnpm.js.map"
  },
  {
    "path": "common/scripts/install-run-rush.js",
    "content": "// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.\n//\n// This script is intended for usage in an automated build environment where the Rush command may not have\n// been preinstalled, or may have an unpredictable version.  This script will automatically install the version of Rush\n// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.\n// An example usage would be:\n//\n//    node common/scripts/install-run-rush.js install\n//\n// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/\n//\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See the @microsoft/rush package's LICENSE file for details.\n\n/******/ (() => { // webpackBootstrap\n/******/ \t\"use strict\";\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 179896:\n/*!*********************!*\\\n  !*** external \"fs\" ***!\n  \\*********************/\n/***/ ((module) => {\n\nmodule.exports = require(\"fs\");\n\n/***/ }),\n\n/***/ 16928:\n/*!***********************!*\\\n  !*** external \"path\" ***!\n  \\***********************/\n/***/ ((module) => {\n\nmodule.exports = require(\"path\");\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tvar cachedModule = __webpack_module_cache__[moduleId];\n/******/ \t\tif (cachedModule !== undefined) {\n/******/ \t\t\treturn cachedModule.exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t(() => {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = (module) => {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\t() => (module['default']) :\n/******/ \t\t\t\t() => (module);\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t(() => {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = (exports, definition) => {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t(() => {\n/******/ \t\t__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/make namespace object */\n/******/ \t(() => {\n/******/ \t\t// define __esModule on exports\n/******/ \t\t__webpack_require__.r = (exports) => {\n/******/ \t\t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n/******/ \t\t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n/******/ \t\t\t}\n/******/ \t\t\tObject.defineProperty(exports, '__esModule', { value: true });\n/******/ \t\t};\n/******/ \t})();\n/******/\n/************************************************************************/\nvar __webpack_exports__ = {};\n// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.\n(() => {\n/*!************************************************!*\\\n  !*** ./lib-esnext/scripts/install-run-rush.js ***!\n  \\************************************************/\n__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ 16928);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n/* eslint-disable no-console */\n\n\nconst { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode } = require('./install-run');\nconst PACKAGE_NAME = '@microsoft/rush';\nconst RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION';\nconst INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_RUSH_LOCKFILE_PATH';\nfunction _getRushVersion(logger) {\n    const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION];\n    if (rushPreviewVersion !== undefined) {\n        logger.info(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`);\n        return rushPreviewVersion;\n    }\n    const rushJsonFolder = findRushJsonFolder();\n    const rushJsonPath = path__WEBPACK_IMPORTED_MODULE_0__.join(rushJsonFolder, RUSH_JSON_FILENAME);\n    try {\n        const rushJsonContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(rushJsonPath, 'utf-8');\n        // Use a regular expression to parse out the rushVersion value because rush.json supports comments,\n        // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.\n        const rushJsonMatches = rushJsonContents.match(/\\\"rushVersion\\\"\\s*\\:\\s*\\\"([0-9a-zA-Z.+\\-]+)\\\"/);\n        return rushJsonMatches[1];\n    }\n    catch (e) {\n        throw new Error(`Unable to determine the required version of Rush from ${RUSH_JSON_FILENAME} (${rushJsonFolder}). ` +\n            `The 'rushVersion' field is either not assigned in ${RUSH_JSON_FILENAME} or was specified ` +\n            'using an unexpected syntax.');\n    }\n}\nfunction _getBin(scriptName) {\n    switch (scriptName.toLowerCase()) {\n        case 'install-run-rush-pnpm.js':\n            return 'rush-pnpm';\n        case 'install-run-rushx.js':\n            return 'rushx';\n        default:\n            return 'rush';\n    }\n}\nfunction _run() {\n    const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv;\n    // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the\n    // appropriate binary inside the rush package to run\n    const scriptName = path__WEBPACK_IMPORTED_MODULE_0__.basename(scriptPath);\n    const bin = _getBin(scriptName);\n    if (!nodePath || !scriptPath) {\n        throw new Error('Unexpected exception: could not detect node path or script path');\n    }\n    let commandFound = false;\n    let logger = { info: console.log, error: console.error };\n    for (const arg of packageBinArgs) {\n        if (arg === '-q' || arg === '--quiet') {\n            // The -q/--quiet flag is supported by both `rush` and `rushx`, and will suppress\n            // any normal informational/diagnostic information printed during startup.\n            //\n            // To maintain the same user experience, the install-run* scripts pass along this\n            // flag but also use it to suppress any diagnostic information normally printed\n            // to stdout.\n            logger = {\n                info: () => { },\n                error: console.error\n            };\n        }\n        else if (!arg.startsWith('-') || arg === '-h' || arg === '--help') {\n            // We either found something that looks like a command (i.e. - doesn't start with a \"-\"),\n            // or we found the -h/--help flag, which can be run without a command\n            commandFound = true;\n        }\n    }\n    if (!commandFound) {\n        console.log(`Usage: ${scriptName} <command> [args...]`);\n        if (scriptName === 'install-run-rush-pnpm.js') {\n            console.log(`Example: ${scriptName} pnpm-command`);\n        }\n        else if (scriptName === 'install-run-rush.js') {\n            console.log(`Example: ${scriptName} build --to myproject`);\n        }\n        else {\n            console.log(`Example: ${scriptName} custom-command`);\n        }\n        process.exit(1);\n    }\n    runWithErrorAndStatusCode(logger, () => {\n        const version = _getRushVersion(logger);\n        logger.info(`The ${RUSH_JSON_FILENAME} configuration requests Rush version ${version}`);\n        const lockFilePath = process.env[INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE];\n        if (lockFilePath) {\n            logger.info(`Found ${INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE}=\"${lockFilePath}\", installing with lockfile.`);\n        }\n        return installAndRun(logger, PACKAGE_NAME, version, bin, packageBinArgs, lockFilePath);\n    });\n}\n_run();\n//# sourceMappingURL=install-run-rush.js.map\n})();\n\nmodule.exports = __webpack_exports__;\n/******/ })()\n;\n//# sourceMappingURL=install-run-rush.js.map"
  },
  {
    "path": "common/scripts/install-run-rushx.js",
    "content": "// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.\n//\n// This script is intended for usage in an automated build environment where the Rush command may not have\n// been preinstalled, or may have an unpredictable version.  This script will automatically install the version of Rush\n// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the\n// rushx command.\n//\n// An example usage would be:\n//\n//    node common/scripts/install-run-rushx.js custom-command\n//\n// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/\n//\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See the @microsoft/rush package's LICENSE file for details.\n\n/******/ (() => { // webpackBootstrap\n/******/ \t\"use strict\";\nvar __webpack_exports__ = {};\n/*!*************************************************!*\\\n  !*** ./lib-esnext/scripts/install-run-rushx.js ***!\n  \\*************************************************/\n\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\nrequire('./install-run-rush');\n//# sourceMappingURL=install-run-rushx.js.map\nmodule.exports = __webpack_exports__;\n/******/ })()\n;\n//# sourceMappingURL=install-run-rushx.js.map"
  },
  {
    "path": "common/scripts/install-run.js",
    "content": "// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.\n//\n// This script is intended for usage in an automated build environment where a Node tool may not have\n// been preinstalled, or may have an unpredictable version.  This script will automatically install the specified\n// version of the specified tool (if not already installed), and then pass a command-line to it.\n// An example usage would be:\n//\n//    node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io\n//\n// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/\n//\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See the @microsoft/rush package's LICENSE file for details.\n\n/******/ (() => { // webpackBootstrap\n/******/ \t\"use strict\";\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 832286:\n/*!************************************************!*\\\n  !*** ./lib-esnext/utilities/npmrcUtilities.js ***!\n  \\************************************************/\n/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {\n\n__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   isVariableSetInNpmrcFile: () => (/* binding */ isVariableSetInNpmrcFile),\n/* harmony export */   syncNpmrc: () => (/* binding */ syncNpmrc),\n/* harmony export */   trimNpmrcFileLines: () => (/* binding */ trimNpmrcFileLines)\n/* harmony export */ });\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ 179896);\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ 16928);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n// IMPORTANT - do not use any non-built-in libraries in this file\n\n\n/**\n * This function reads the content for given .npmrc file path, and also trims\n * unusable lines from the .npmrc file.\n *\n * @returns\n * The text of the the .npmrc.\n */\n// create a global _combinedNpmrc for cache purpose\nconst _combinedNpmrcMap = new Map();\nfunction _trimNpmrcFile(options) {\n    const { sourceNpmrcPath, linesToPrepend, linesToAppend, supportEnvVarFallbackSyntax } = options;\n    const combinedNpmrcFromCache = _combinedNpmrcMap.get(sourceNpmrcPath);\n    if (combinedNpmrcFromCache !== undefined) {\n        return combinedNpmrcFromCache;\n    }\n    let npmrcFileLines = [];\n    if (linesToPrepend) {\n        npmrcFileLines.push(...linesToPrepend);\n    }\n    if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {\n        npmrcFileLines.push(...fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(sourceNpmrcPath).toString().split('\\n'));\n    }\n    if (linesToAppend) {\n        npmrcFileLines.push(...linesToAppend);\n    }\n    npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());\n    const resultLines = trimNpmrcFileLines(npmrcFileLines, process.env, supportEnvVarFallbackSyntax);\n    const combinedNpmrc = resultLines.join('\\n');\n    //save the cache\n    _combinedNpmrcMap.set(sourceNpmrcPath, combinedNpmrc);\n    return combinedNpmrc;\n}\n/**\n *\n * @param npmrcFileLines The npmrc file's lines\n * @param env The environment variables object\n * @param supportEnvVarFallbackSyntax Whether to support fallback values in the form of `${VAR_NAME:-fallback}`\n * @returns\n */\nfunction trimNpmrcFileLines(npmrcFileLines, env, supportEnvVarFallbackSyntax) {\n    var _a;\n    const resultLines = [];\n    // This finds environment variable tokens that look like \"${VAR_NAME}\"\n    const expansionRegExp = /\\$\\{([^\\}]+)\\}/g;\n    // Comment lines start with \"#\" or \";\"\n    const commentRegExp = /^\\s*[#;]/;\n    // Trim out lines that reference environment variables that aren't defined\n    for (let line of npmrcFileLines) {\n        let lineShouldBeTrimmed = false;\n        //remove spaces before or after key and value\n        line = line\n            .split('=')\n            .map((lineToTrim) => lineToTrim.trim())\n            .join('=');\n        // Ignore comment lines\n        if (!commentRegExp.test(line)) {\n            const environmentVariables = line.match(expansionRegExp);\n            if (environmentVariables) {\n                for (const token of environmentVariables) {\n                    /**\n                     * Remove the leading \"${\" and the trailing \"}\" from the token\n                     *\n                     * ${nameString}                  -> nameString\n                     * ${nameString-fallbackString}   -> name-fallbackString\n                     * ${nameString:-fallbackString}  -> name:-fallbackString\n                     */\n                    const nameWithFallback = token.substring(2, token.length - 1);\n                    let environmentVariableName;\n                    let fallback;\n                    if (supportEnvVarFallbackSyntax) {\n                        /**\n                         * Get the environment variable name and fallback value.\n                         *\n                         *                                name          fallback\n                         * nameString                 ->  nameString    undefined\n                         * nameString-fallbackString  ->  nameString    fallbackString\n                         * nameString:-fallbackString ->  nameString    fallbackString\n                         */\n                        const matched = nameWithFallback.match(/^([^:-]+)(?:\\:?-(.+))?$/);\n                        // matched: [originStr, variableName, fallback]\n                        environmentVariableName = (_a = matched === null || matched === void 0 ? void 0 : matched[1]) !== null && _a !== void 0 ? _a : nameWithFallback;\n                        fallback = matched === null || matched === void 0 ? void 0 : matched[2];\n                    }\n                    else {\n                        environmentVariableName = nameWithFallback;\n                    }\n                    // Is the environment variable and fallback value defined.\n                    if (!env[environmentVariableName] && !fallback) {\n                        // No, so trim this line\n                        lineShouldBeTrimmed = true;\n                        break;\n                    }\n                }\n            }\n        }\n        if (lineShouldBeTrimmed) {\n            // Example output:\n            // \"; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}\"\n            resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);\n        }\n        else {\n            resultLines.push(line);\n        }\n    }\n    return resultLines;\n}\nfunction _copyAndTrimNpmrcFile(options) {\n    const { logger, sourceNpmrcPath, targetNpmrcPath } = options;\n    logger.info(`Transforming ${sourceNpmrcPath}`); // Verbose\n    logger.info(`  --> \"${targetNpmrcPath}\"`);\n    const combinedNpmrc = _trimNpmrcFile(options);\n    fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(targetNpmrcPath, combinedNpmrc);\n    return combinedNpmrc;\n}\nfunction syncNpmrc(options) {\n    const { sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish, logger = {\n        // eslint-disable-next-line no-console\n        info: console.log,\n        // eslint-disable-next-line no-console\n        error: console.error\n    }, createIfMissing = false } = options;\n    const sourceNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');\n    const targetNpmrcPath = path__WEBPACK_IMPORTED_MODULE_1__.join(targetNpmrcFolder, '.npmrc');\n    try {\n        if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath) || createIfMissing) {\n            // Ensure the target folder exists\n            if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcFolder)) {\n                fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(targetNpmrcFolder, { recursive: true });\n            }\n            return _copyAndTrimNpmrcFile(Object.assign({ sourceNpmrcPath,\n                targetNpmrcPath,\n                logger }, options));\n        }\n        else if (fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(targetNpmrcPath)) {\n            // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target\n            logger.info(`Deleting ${targetNpmrcPath}`); // Verbose\n            fs__WEBPACK_IMPORTED_MODULE_0__.unlinkSync(targetNpmrcPath);\n        }\n    }\n    catch (e) {\n        throw new Error(`Error syncing .npmrc file: ${e}`);\n    }\n}\nfunction isVariableSetInNpmrcFile(sourceNpmrcFolder, variableKey, supportEnvVarFallbackSyntax) {\n    const sourceNpmrcPath = `${sourceNpmrcFolder}/.npmrc`;\n    //if .npmrc file does not exist, return false directly\n    if (!fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(sourceNpmrcPath)) {\n        return false;\n    }\n    const trimmedNpmrcFile = _trimNpmrcFile({ sourceNpmrcPath, supportEnvVarFallbackSyntax });\n    const variableKeyRegExp = new RegExp(`^${variableKey}=`, 'm');\n    return trimmedNpmrcFile.match(variableKeyRegExp) !== null;\n}\n//# sourceMappingURL=npmrcUtilities.js.map\n\n/***/ }),\n\n/***/ 535317:\n/*!********************************!*\\\n  !*** external \"child_process\" ***!\n  \\********************************/\n/***/ ((module) => {\n\nmodule.exports = require(\"child_process\");\n\n/***/ }),\n\n/***/ 179896:\n/*!*********************!*\\\n  !*** external \"fs\" ***!\n  \\*********************/\n/***/ ((module) => {\n\nmodule.exports = require(\"fs\");\n\n/***/ }),\n\n/***/ 370857:\n/*!*********************!*\\\n  !*** external \"os\" ***!\n  \\*********************/\n/***/ ((module) => {\n\nmodule.exports = require(\"os\");\n\n/***/ }),\n\n/***/ 16928:\n/*!***********************!*\\\n  !*** external \"path\" ***!\n  \\***********************/\n/***/ ((module) => {\n\nmodule.exports = require(\"path\");\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tvar cachedModule = __webpack_module_cache__[moduleId];\n/******/ \t\tif (cachedModule !== undefined) {\n/******/ \t\t\treturn cachedModule.exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t(() => {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = (module) => {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\t() => (module['default']) :\n/******/ \t\t\t\t() => (module);\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t(() => {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = (exports, definition) => {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t(() => {\n/******/ \t\t__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))\n/******/ \t})();\n/******/\n/******/ \t/* webpack/runtime/make namespace object */\n/******/ \t(() => {\n/******/ \t\t// define __esModule on exports\n/******/ \t\t__webpack_require__.r = (exports) => {\n/******/ \t\t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n/******/ \t\t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n/******/ \t\t\t}\n/******/ \t\t\tObject.defineProperty(exports, '__esModule', { value: true });\n/******/ \t\t};\n/******/ \t})();\n/******/\n/************************************************************************/\nvar __webpack_exports__ = {};\n// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.\n(() => {\n/*!*******************************************!*\\\n  !*** ./lib-esnext/scripts/install-run.js ***!\n  \\*******************************************/\n__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   RUSH_JSON_FILENAME: () => (/* binding */ RUSH_JSON_FILENAME),\n/* harmony export */   findRushJsonFolder: () => (/* binding */ findRushJsonFolder),\n/* harmony export */   getNpmPath: () => (/* binding */ getNpmPath),\n/* harmony export */   installAndRun: () => (/* binding */ installAndRun),\n/* harmony export */   runWithErrorAndStatusCode: () => (/* binding */ runWithErrorAndStatusCode)\n/* harmony export */ });\n/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! child_process */ 535317);\n/* harmony import */ var child_process__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(child_process__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! fs */ 179896);\n/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! os */ 370857);\n/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! path */ 16928);\n/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../utilities/npmrcUtilities */ 832286);\n// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.\n// See LICENSE in the project root for license information.\n/* eslint-disable no-console */\n\n\n\n\n\nconst RUSH_JSON_FILENAME = 'rush.json';\nconst RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER';\nconst INSTALL_RUN_LOCKFILE_PATH_VARIABLE = 'INSTALL_RUN_LOCKFILE_PATH';\nconst INSTALLED_FLAG_FILENAME = 'installed.flag';\nconst NODE_MODULES_FOLDER_NAME = 'node_modules';\nconst PACKAGE_JSON_FILENAME = 'package.json';\n/**\n * Parse a package specifier (in the form of name\\@version) into name and version parts.\n */\nfunction _parsePackageSpecifier(rawPackageSpecifier) {\n    rawPackageSpecifier = (rawPackageSpecifier || '').trim();\n    const separatorIndex = rawPackageSpecifier.lastIndexOf('@');\n    let name;\n    let version = undefined;\n    if (separatorIndex === 0) {\n        // The specifier starts with a scope and doesn't have a version specified\n        name = rawPackageSpecifier;\n    }\n    else if (separatorIndex === -1) {\n        // The specifier doesn't have a version\n        name = rawPackageSpecifier;\n    }\n    else {\n        name = rawPackageSpecifier.substring(0, separatorIndex);\n        version = rawPackageSpecifier.substring(separatorIndex + 1);\n    }\n    if (!name) {\n        throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);\n    }\n    return { name, version };\n}\nlet _npmPath = undefined;\n/**\n * Get the absolute path to the npm executable\n */\nfunction getNpmPath() {\n    if (!_npmPath) {\n        try {\n            if (_isWindows()) {\n                // We're on Windows\n                const whereOutput = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('where npm', { stdio: [] }).toString();\n                const lines = whereOutput.split(os__WEBPACK_IMPORTED_MODULE_2__.EOL).filter((line) => !!line);\n                // take the last result, we are looking for a .cmd command\n                // see https://github.com/microsoft/rushstack/issues/759\n                _npmPath = lines[lines.length - 1];\n            }\n            else {\n                // We aren't on Windows - assume we're on *NIX or Darwin\n                _npmPath = child_process__WEBPACK_IMPORTED_MODULE_0__.execSync('command -v npm', { stdio: [] }).toString();\n            }\n        }\n        catch (e) {\n            throw new Error(`Unable to determine the path to the NPM tool: ${e}`);\n        }\n        _npmPath = _npmPath.trim();\n        if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(_npmPath)) {\n            throw new Error('The NPM executable does not exist');\n        }\n    }\n    return _npmPath;\n}\nfunction _ensureFolder(folderPath) {\n    if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(folderPath)) {\n        const parentDir = path__WEBPACK_IMPORTED_MODULE_3__.dirname(folderPath);\n        _ensureFolder(parentDir);\n        fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(folderPath);\n    }\n}\n/**\n * Create missing directories under the specified base directory, and return the resolved directory.\n *\n * Does not support \".\" or \"..\" path segments.\n * Assumes the baseFolder exists.\n */\nfunction _ensureAndJoinPath(baseFolder, ...pathSegments) {\n    let joinedPath = baseFolder;\n    try {\n        for (let pathSegment of pathSegments) {\n            pathSegment = pathSegment.replace(/[\\\\\\/]/g, '+');\n            joinedPath = path__WEBPACK_IMPORTED_MODULE_3__.join(joinedPath, pathSegment);\n            if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(joinedPath)) {\n                fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(joinedPath);\n            }\n        }\n    }\n    catch (e) {\n        throw new Error(`Error building local installation folder (${path__WEBPACK_IMPORTED_MODULE_3__.join(baseFolder, ...pathSegments)}): ${e}`);\n    }\n    return joinedPath;\n}\nfunction _getRushTempFolder(rushCommonFolder) {\n    const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME];\n    if (rushTempFolder !== undefined) {\n        _ensureFolder(rushTempFolder);\n        return rushTempFolder;\n    }\n    else {\n        return _ensureAndJoinPath(rushCommonFolder, 'temp');\n    }\n}\n/**\n * Compare version strings according to semantic versioning.\n * Returns a positive integer if \"a\" is a later version than \"b\",\n * a negative integer if \"b\" is later than \"a\",\n * and 0 otherwise.\n */\nfunction _compareVersionStrings(a, b) {\n    const aParts = a.split(/[.-]/);\n    const bParts = b.split(/[.-]/);\n    const numberOfParts = Math.max(aParts.length, bParts.length);\n    for (let i = 0; i < numberOfParts; i++) {\n        if (aParts[i] !== bParts[i]) {\n            return (Number(aParts[i]) || 0) - (Number(bParts[i]) || 0);\n        }\n    }\n    return 0;\n}\n/**\n * Resolve a package specifier to a static version\n */\nfunction _resolvePackageVersion(logger, rushCommonFolder, { name, version }) {\n    if (!version) {\n        version = '*'; // If no version is specified, use the latest version\n    }\n    if (version.match(/^[a-zA-Z0-9\\-\\+\\.]+$/)) {\n        // If the version contains only characters that we recognize to be used in static version specifiers,\n        // pass the version through\n        return version;\n    }\n    else {\n        // version resolves to\n        try {\n            const rushTempFolder = _getRushTempFolder(rushCommonFolder);\n            const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');\n            (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({\n                sourceNpmrcFolder,\n                targetNpmrcFolder: rushTempFolder,\n                logger,\n                supportEnvVarFallbackSyntax: false\n            });\n            const npmPath = getNpmPath();\n            // This returns something that looks like:\n            // ```\n            // [\n            //   \"3.0.0\",\n            //   \"3.0.1\",\n            //   ...\n            //   \"3.0.20\"\n            // ]\n            // ```\n            //\n            // if multiple versions match the selector, or\n            //\n            // ```\n            // \"3.0.0\"\n            // ```\n            //\n            // if only a single version matches.\n            const spawnSyncOptions = {\n                cwd: rushTempFolder,\n                stdio: [],\n                shell: _isWindows()\n            };\n            const platformNpmPath = _getPlatformPath(npmPath);\n            const npmVersionSpawnResult = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier', '--json'], spawnSyncOptions);\n            if (npmVersionSpawnResult.status !== 0) {\n                throw new Error(`\"npm view\" returned error code ${npmVersionSpawnResult.status}`);\n            }\n            const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();\n            const parsedVersionOutput = JSON.parse(npmViewVersionOutput);\n            const versions = Array.isArray(parsedVersionOutput)\n                ? parsedVersionOutput\n                : [parsedVersionOutput];\n            let latestVersion = versions[0];\n            for (let i = 1; i < versions.length; i++) {\n                const latestVersionCandidate = versions[i];\n                if (_compareVersionStrings(latestVersionCandidate, latestVersion) > 0) {\n                    latestVersion = latestVersionCandidate;\n                }\n            }\n            if (!latestVersion) {\n                throw new Error('No versions found for the specified version range.');\n            }\n            return latestVersion;\n        }\n        catch (e) {\n            throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);\n        }\n    }\n}\nlet _rushJsonFolder;\n/**\n * Find the absolute path to the folder containing rush.json\n */\nfunction findRushJsonFolder() {\n    if (!_rushJsonFolder) {\n        let basePath = __dirname;\n        let tempPath = __dirname;\n        do {\n            const testRushJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(basePath, RUSH_JSON_FILENAME);\n            if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(testRushJsonPath)) {\n                _rushJsonFolder = basePath;\n                break;\n            }\n            else {\n                basePath = tempPath;\n            }\n        } while (basePath !== (tempPath = path__WEBPACK_IMPORTED_MODULE_3__.dirname(basePath))); // Exit the loop when we hit the disk root\n        if (!_rushJsonFolder) {\n            throw new Error(`Unable to find ${RUSH_JSON_FILENAME}.`);\n        }\n    }\n    return _rushJsonFolder;\n}\n/**\n * Detects if the package in the specified directory is installed\n */\nfunction _isPackageAlreadyInstalled(packageInstallFolder) {\n    try {\n        const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);\n        if (!fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(flagFilePath)) {\n            return false;\n        }\n        const fileContents = fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(flagFilePath).toString();\n        return fileContents.trim() === process.version;\n    }\n    catch (e) {\n        return false;\n    }\n}\n/**\n * Delete a file. Fail silently if it does not exist.\n */\nfunction _deleteFile(file) {\n    try {\n        fs__WEBPACK_IMPORTED_MODULE_1__.unlinkSync(file);\n    }\n    catch (err) {\n        if (err.code !== 'ENOENT' && err.code !== 'ENOTDIR') {\n            throw err;\n        }\n    }\n}\n/**\n * Removes the following files and directories under the specified folder path:\n *  - installed.flag\n *  -\n *  - node_modules\n */\nfunction _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath) {\n    try {\n        const flagFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);\n        _deleteFile(flagFile);\n        const packageLockFile = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, 'package-lock.json');\n        if (lockFilePath) {\n            fs__WEBPACK_IMPORTED_MODULE_1__.copyFileSync(lockFilePath, packageLockFile);\n        }\n        else {\n            // Not running `npm ci`, so need to cleanup\n            _deleteFile(packageLockFile);\n            const nodeModulesFolder = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);\n            if (fs__WEBPACK_IMPORTED_MODULE_1__.existsSync(nodeModulesFolder)) {\n                const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler');\n                fs__WEBPACK_IMPORTED_MODULE_1__.renameSync(nodeModulesFolder, path__WEBPACK_IMPORTED_MODULE_3__.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`));\n            }\n        }\n    }\n    catch (e) {\n        throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);\n    }\n}\nfunction _createPackageJson(packageInstallFolder, name, version) {\n    try {\n        const packageJsonContents = {\n            name: 'ci-rush',\n            version: '0.0.0',\n            dependencies: {\n                [name]: version\n            },\n            description: \"DON'T WARN\",\n            repository: \"DON'T WARN\",\n            license: 'MIT'\n        };\n        const packageJsonPath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, PACKAGE_JSON_FILENAME);\n        fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));\n    }\n    catch (e) {\n        throw new Error(`Unable to create package.json: ${e}`);\n    }\n}\n/**\n * Run \"npm install\" in the package install folder.\n */\nfunction _installPackage(logger, packageInstallFolder, name, version, command) {\n    try {\n        logger.info(`Installing ${name}...`);\n        const npmPath = getNpmPath();\n        const platformNpmPath = _getPlatformPath(npmPath);\n        const result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformNpmPath, [command], {\n            stdio: 'inherit',\n            cwd: packageInstallFolder,\n            env: process.env,\n            shell: _isWindows()\n        });\n        if (result.status !== 0) {\n            throw new Error(`\"npm ${command}\" encountered an error`);\n        }\n        logger.info(`Successfully installed ${name}@${version}`);\n    }\n    catch (e) {\n        throw new Error(`Unable to install package: ${e}`);\n    }\n}\n/**\n * Get the \".bin\" path for the package.\n */\nfunction _getBinPath(packageInstallFolder, binName) {\n    const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');\n    const resolvedBinName = _isWindows() ? `${binName}.cmd` : binName;\n    return path__WEBPACK_IMPORTED_MODULE_3__.resolve(binFolderPath, resolvedBinName);\n}\n/**\n * Returns a cross-platform path - windows must enclose any path containing spaces within double quotes.\n */\nfunction _getPlatformPath(platformPath) {\n    return _isWindows() && platformPath.includes(' ') ? `\"${platformPath}\"` : platformPath;\n}\nfunction _isWindows() {\n    return os__WEBPACK_IMPORTED_MODULE_2__.platform() === 'win32';\n}\n/**\n * Write a flag file to the package's install directory, signifying that the install was successful.\n */\nfunction _writeFlagFile(packageInstallFolder) {\n    try {\n        const flagFilePath = path__WEBPACK_IMPORTED_MODULE_3__.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);\n        fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(flagFilePath, process.version);\n    }\n    catch (e) {\n        throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);\n    }\n}\nfunction installAndRun(logger, packageName, packageVersion, packageBinName, packageBinArgs, lockFilePath = process.env[INSTALL_RUN_LOCKFILE_PATH_VARIABLE]) {\n    const rushJsonFolder = findRushJsonFolder();\n    const rushCommonFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushJsonFolder, 'common');\n    const rushTempFolder = _getRushTempFolder(rushCommonFolder);\n    const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`);\n    if (!_isPackageAlreadyInstalled(packageInstallFolder)) {\n        // The package isn't already installed\n        _cleanInstallFolder(rushTempFolder, packageInstallFolder, lockFilePath);\n        const sourceNpmrcFolder = path__WEBPACK_IMPORTED_MODULE_3__.join(rushCommonFolder, 'config', 'rush');\n        (0,_utilities_npmrcUtilities__WEBPACK_IMPORTED_MODULE_4__.syncNpmrc)({\n            sourceNpmrcFolder,\n            targetNpmrcFolder: packageInstallFolder,\n            logger,\n            supportEnvVarFallbackSyntax: false\n        });\n        _createPackageJson(packageInstallFolder, packageName, packageVersion);\n        const command = lockFilePath ? 'ci' : 'install';\n        _installPackage(logger, packageInstallFolder, packageName, packageVersion, command);\n        _writeFlagFile(packageInstallFolder);\n    }\n    const statusMessage = `Invoking \"${packageBinName} ${packageBinArgs.join(' ')}\"`;\n    const statusMessageLine = new Array(statusMessage.length + 1).join('-');\n    logger.info('\\n' + statusMessage + '\\n' + statusMessageLine + '\\n');\n    const binPath = _getBinPath(packageInstallFolder, packageBinName);\n    const binFolderPath = path__WEBPACK_IMPORTED_MODULE_3__.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');\n    // Windows environment variables are case-insensitive.  Instead of using SpawnSyncOptions.env, we need to\n    // assign via the process.env proxy to ensure that we append to the right PATH key.\n    const originalEnvPath = process.env.PATH || '';\n    let result;\n    try {\n        // `npm` bin stubs on Windows are `.cmd` files\n        // Node.js will not directly invoke a `.cmd` file unless `shell` is set to `true`\n        const platformBinPath = _getPlatformPath(binPath);\n        process.env.PATH = [binFolderPath, originalEnvPath].join(path__WEBPACK_IMPORTED_MODULE_3__.delimiter);\n        result = child_process__WEBPACK_IMPORTED_MODULE_0__.spawnSync(platformBinPath, packageBinArgs, {\n            stdio: 'inherit',\n            windowsVerbatimArguments: false,\n            shell: _isWindows(),\n            cwd: process.cwd(),\n            env: process.env\n        });\n    }\n    finally {\n        process.env.PATH = originalEnvPath;\n    }\n    if (result.status !== null) {\n        return result.status;\n    }\n    else {\n        throw result.error || new Error('An unknown error occurred.');\n    }\n}\nfunction runWithErrorAndStatusCode(logger, fn) {\n    process.exitCode = 1;\n    try {\n        const exitCode = fn();\n        process.exitCode = exitCode;\n    }\n    catch (e) {\n        logger.error('\\n\\n' + e.toString() + '\\n\\n');\n    }\n}\nfunction _run() {\n    const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;\n    if (!nodePath) {\n        throw new Error('Unexpected exception: could not detect node path');\n    }\n    if (path__WEBPACK_IMPORTED_MODULE_3__.basename(scriptPath).toLowerCase() !== 'install-run.js') {\n        // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control\n        // to the script that (presumably) imported this file\n        return;\n    }\n    if (process.argv.length < 4) {\n        console.log('Usage: install-run.js <package>@<version> <command> [args...]');\n        console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');\n        process.exit(1);\n    }\n    const logger = { info: console.log, error: console.error };\n    runWithErrorAndStatusCode(logger, () => {\n        const rushJsonFolder = findRushJsonFolder();\n        const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common');\n        const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier);\n        const name = packageSpecifier.name;\n        const version = _resolvePackageVersion(logger, rushCommonFolder, packageSpecifier);\n        if (packageSpecifier.version !== version) {\n            console.log(`Resolved to ${name}@${version}`);\n        }\n        return installAndRun(logger, name, version, packageBinName, packageBinArgs);\n    });\n}\n_run();\n//# sourceMappingURL=install-run.js.map\n})();\n\nmodule.exports = __webpack_exports__;\n/******/ })()\n;\n//# sourceMappingURL=install-run.js.map"
  },
  {
    "path": "config/eslint-config/CHANGELOG.json",
    "content": "{\n  \"name\": \"@flowgram.ai/eslint-config\",\n  \"entries\": [\n    {\n      \"version\": \"0.1.0\",\n      \"tag\": \"@flowgram.ai/eslint-config_v0.1.0\",\n      \"date\": \"Mon, 17 Feb 2025 08:21:49 GMT\",\n      \"comments\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "config/eslint-config/CHANGELOG.md",
    "content": "# Change Log - @flowgram.ai/eslint-config\n\nThis log was last generated on Mon, 17 Feb 2025 08:21:49 GMT and should not be manually modified.\n\n## 0.1.0\nMon, 17 Feb 2025 08:21:49 GMT\n\n_Initial release_\n\n"
  },
  {
    "path": "config/eslint-config/eslint.base.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nmodule.exports = {\n  plugins: ['prettier'],\n  ignorePatterns: [\n    '**/*.d.ts',\n    '**/__mocks__',\n    '**/node_modules',\n    '**/build',\n    '**/dist',\n    '**/es',\n    '**/lib',\n    '**/.codebase',\n    '**/.changeset',\n    '**/config',\n    '**/common/scripts',\n    '**/output',\n    'error-log-str.js',\n    '*.bundle.js',\n    '*.min.js',\n    '*.js.map',\n    '**/*.log',\n    '**/tsconfig.tsbuildinfo',\n    '**/vitest.config.ts',\n    'package.json',\n    '*.json',\n  ],\n\n  settings: {\n    'import/resolver': {\n      node: {\n        moduleDirectory: ['node_modules', 'src'],\n        extensions: ['.js', '.jsx', '.ts', '.tsx'],\n      },\n    },\n    react: {\n      version: 'detect',\n    },\n  },\n\n  rules: {\n    'prettier/prettier': [\n      'warn',\n      {\n        semi: true,\n        singleQuote: true,\n        printWidth: 100,\n        usePrettierrc: false,\n      },\n    ],\n    'import/prefer-default-export': 'off',\n    'lines-between-class-members': 'warn',\n    'import/no-unresolved': 'warn',\n    'react/jsx-no-useless-fragment': 'off',\n    'no-unused-vars': 'off',\n    'no-redeclare': 'off',\n    'prefer-destructurin': 'off',\n    'no-underscore-dangle': 'off',\n    'no-empty-function': 'off',\n    'no-multi-assign': 'off',\n    'arrow-body-style': 'warn',\n    'no-useless-constructor': 'off',\n    'no-param-reassign': 'off',\n    'max-classes-per-file': 'off',\n    'grouped-accessor-pairs': 'off',\n    'no-plusplus': 'off',\n    'no-restricted-syntax': 'off',\n    'react/destructuring-assignment': 'off',\n    'import/extensions': 'off',\n    'consistent-return': 'off',\n    'jsx-a11y/no-static-element-interactions': 'off',\n    'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }],\n    'no-use-before-define': 'off',\n    'no-bitwise': 'off',\n    'no-case-declarations': 'off',\n    'react/no-array-index-key': 'off',\n    'react/require-default-props': 'off',\n    'no-dupe-class-members': 'off',\n    'react/self-closing-comp': ['error', { component: true, html: false }],\n    'react/jsx-props-no-spreading': 'off',\n    'no-console': ['error', { allow: ['warn', 'error'] }],\n    'no-shadow': 'off',\n    'class-methods-use-this': 'off',\n    'default-param-last': 'off',\n    'import/no-cycle': 'error',\n    'import/no-extraneous-dependencies': ['error', { devDependencies: true }],\n    'import/no-relative-packages': 'error',\n    'import/order': [\n      'warn',\n      {\n        groups: ['builtin', 'external', ['internal', 'parent', 'sibling', 'index'], 'unknown'],\n        pathGroups: [\n          { pattern: 'react*', group: 'builtin', position: 'before' },\n          { pattern: '@/**', group: 'internal', position: 'before' },\n          {\n            pattern: './*.+(css|sass|less|scss|pcss|styl)',\n            patternOptions: { dot: true, nocomment: true },\n            group: 'unknown',\n            position: 'after',\n          },\n        ],\n        alphabetize: {\n          order: 'desc',\n          caseInsensitive: true,\n        },\n        pathGroupsExcludedImportTypes: ['builtin'],\n        'newlines-between': 'always',\n      },\n    ],\n  },\n\n  overrides: [\n    {\n      files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n      parserOptions: {\n        ecmaFeatures: {\n          jsx: true,\n        },\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "config/eslint-config/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\n\nconst { main } = require('./package.json');\n\nconst { defineFlatConfig } = require(path.resolve(__dirname, main));\n\nmodule.exports = defineFlatConfig({\n  packageRoot: __dirname,\n  preset: 'node',\n  settings: {\n    react: {\n      version: 'detect',\n    },\n  },\n  ignore: [\n    'node_modules',\n    'dist',\n    'package.json',\n    '.rush'\n  ]\n});\n"
  },
  {
    "path": "config/eslint-config/eslint.node.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst baseConfig = require('./eslint.base.config.js');\n\nmodule.exports = {\n  ignorePatterns: baseConfig.ignorePatterns || [],\n\n  globals: {\n    NodeJS: true,\n  },\n\n  settings: {\n    ...(baseConfig.settings || {}),\n  },\n\n  rules: {\n    ...(baseConfig.rules || {}),\n  },\n\n  overrides: [\n    {\n      files: ['**/*.js', '**/*.ts'],\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n      },\n    },\n    ...(baseConfig.overrides || []),\n  ],\n};\n"
  },
  {
    "path": "config/eslint-config/eslint.web.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst baseConfig = require('./eslint.base.config.js');\n\nmodule.exports = {\n  ignorePatterns: baseConfig.ignorePatterns || [],\n\n  globals: {\n    React: true,\n    jsdom: true,\n    JSX: true,\n  },\n\n  settings: {\n    ...(baseConfig.settings || {}),\n    'import/resolver': {\n      node: {\n        extensions: ['.js', '.jsx', '.ts', '.tsx'],\n      },\n    },\n  },\n\n  rules: {\n    ...(baseConfig.rules || {}),\n    'import/no-cycle': 'off',\n  },\n\n  overrides: baseConfig.overrides || [],\n};\n"
  },
  {
    "path": "config/eslint-config/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/eslint-config\",\n  \"version\": \"0.1.8\",\n  \"author\": \"chenjiawei.inizio@bytedance.com\",\n  \"maintainers\": [],\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"build\": \"tsc -b --force\",\n    \"dev\": \"npm run build -- -w\",\n    \"lint\": \"eslint ./src --cache\",\n    \"watch\": \"exit\",\n    \"test\": \"exit\",\n    \"test:cov\": \"exit\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \">=7.11.0\",\n    \"@babel/eslint-parser\": \"~7.19.1\",\n    \"@babel/eslint-plugin\": \"^7.22.10\",\n    \"@babel/preset-env\": \"~7.20.2\",\n    \"@babel/preset-react\": \"~7.13.13\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.0.0\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"@eslint/eslintrc\": \"3.3.3\",\n    \"eslint\": \"^9.0.0\",\n    \"eslint-config-prettier\": \"^8.5.0\",\n    \"eslint-define-config\": \"~1.12.0\",\n    \"eslint-import-resolver-typescript\": \"^3\",\n    \"eslint-plugin-babel\": \"^5.3.1\",\n    \"eslint-plugin-import\": \"^2.25.3\",\n    \"eslint-plugin-jsx-a11y\": \"^6.5.1\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-react\": \"^7.28.0\",\n    \"eslint-plugin-tsdoc\": \"^0.2.16\",\n    \"prettier\": \"^2\",\n    \"prettier-plugin-packagejson\": \"^2.3.0\",\n    \"ts-node\": \"^10.9.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/node\": \"^18\",\n    \"react\": \"^18\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "config/eslint-config/src/defineFlatConfig.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\n\nfunction defineFlatConfig(config) {\n  const { packageRoot, preset, settings = {}, rules = {}, ignore } = config;\n\n  const basePreset = require(path.resolve(__dirname, `../eslint.${preset}.config.js`));\n\n  let prettierPlugin;\n  let reactPlugin;\n  let a11yPlugin;\n  let tsPlugin;\n  let importPlugin;\n  let tsParser;\n\n  try {\n    const requireFromCwd = require('module').createRequire(process.cwd() + '/');\n    prettierPlugin = requireFromCwd('eslint-plugin-prettier');\n  } catch (e1) {\n    try {\n      prettierPlugin = require('eslint-plugin-prettier');\n    } catch (e2) {\n      prettierPlugin = undefined;\n    }\n  }\n\n  try {\n    reactPlugin = require('eslint-plugin-react');\n  } catch (e) {\n    reactPlugin = undefined;\n  }\n  try {\n    a11yPlugin = require('eslint-plugin-jsx-a11y');\n  } catch (e) {\n    a11yPlugin = undefined;\n  }\n  try {\n    tsPlugin = require('@typescript-eslint/eslint-plugin');\n  } catch (e) {\n    tsPlugin = undefined;\n  }\n  try {\n    importPlugin = require('eslint-plugin-import');\n  } catch (e) {\n    importPlugin = undefined;\n  }\n  try {\n    tsParser = require('@typescript-eslint/parser');\n  } catch (e) {\n    tsParser = undefined;\n  }\n\n  const ignorePatterns = basePreset.ignorePatterns || [];\n\n  const flatConfig = [];\n\n  if (ignore && Array.isArray(ignore) && ignore.length > 0) {\n    flatConfig.push({ ignores: ignore });\n  } else if (typeof ignore === 'string' && ignore.length > 0) {\n    flatConfig.push({ ignores: [ignore] });\n  }\n\n  if (ignorePatterns.length > 0) {\n    flatConfig.push({ ignores: ignorePatterns });\n  }\n\n  const plugins = {};\n  if (prettierPlugin) plugins.prettier = prettierPlugin;\n  if (tsPlugin) plugins['@typescript-eslint'] = tsPlugin;\n  if (importPlugin) plugins.import = importPlugin;\n  if (reactPlugin) plugins.react = reactPlugin;\n  if (a11yPlugin) plugins['jsx-a11y'] = a11yPlugin;\n\n  const mergedSettings = {\n    ...(basePreset.settings || {}),\n    ...settings,\n    'import/resolver': {\n      ...(basePreset.settings?.['import/resolver'] || {}),\n      ...(settings['import/resolver'] || {}),\n      typescript: {\n        project: packageRoot,\n      },\n    },\n  };\n\n  const mergedRules = {\n    ...(basePreset.rules || {}),\n    ...rules,\n  };\n\n  if (basePreset.overrides && basePreset.overrides.length > 0) {\n    basePreset.overrides.forEach((override) => {\n      const overrideConfig = {\n        files: override.files || ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n      };\n\n      if (tsParser) {\n        overrideConfig.languageOptions = {\n          parser: tsParser,\n          parserOptions: override.parserOptions || {\n            ecmaFeatures: {\n              jsx: true,\n            },\n            ecmaVersion: 'latest',\n            sourceType: 'module',\n          },\n        };\n      }\n\n      if (Object.keys(plugins).length > 0) {\n        overrideConfig.plugins = plugins;\n      }\n\n      overrideConfig.settings = {\n        ...mergedSettings,\n        ...(override.settings || {}),\n      };\n\n      overrideConfig.rules = {\n        ...mergedRules,\n        ...(override.rules || {}),\n      };\n\n      flatConfig.push(overrideConfig);\n    });\n  } else {\n    const mainConfig = {\n      files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n    };\n\n    if (tsParser) {\n      mainConfig.languageOptions = {\n        parser: tsParser,\n        parserOptions: {\n          ecmaFeatures: {\n            jsx: true,\n          },\n          ecmaVersion: 'latest',\n          sourceType: 'module',\n        },\n      };\n    }\n\n    if (Object.keys(plugins).length > 0) {\n      mainConfig.plugins = plugins;\n    }\n\n    mainConfig.settings = mergedSettings;\n    mainConfig.rules = mergedRules;\n\n    flatConfig.push(mainConfig);\n  }\n\n  return flatConfig;\n}\n\nmodule.exports = { defineFlatConfig };\n"
  },
  {
    "path": "config/eslint-config/src/defineFlatConfig.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\nimport { FlatCompat } from '@eslint/eslintrc';\nimport type { ESLintConfig, Rules } from 'eslint-define-config';\n\ntype ESLintConfigMode = 'web' | 'node' | 'base';\n\nexport interface EnhanceESLintConfig extends ESLintConfig {\n  packageRoot?: string;\n  preset: ESLintConfigMode;\n  ignore?: string | string[];\n}\n\n/**\n * 定义 ESLint v9 Flat Config\n */\nexport const defineFlatConfig = (config: EnhanceESLintConfig): any[] => {\n  const { packageRoot, preset, settings = {}, rules = {}, ignore } = config;\n\n  // 兼容旧式 eslintrc\n  const basePresetPath = path.resolve(__dirname, `../.eslintrc.${preset}.js`);\n  const compat = new FlatCompat({ baseDirectory: path.dirname(basePresetPath) });\n  const basePreset = require(basePresetPath);\n\n  const flatConfig: any[] = [];\n\n  // 合并 ignore\n  const mergedIgnore = [\n    ...(Array.isArray(ignore) ? ignore : typeof ignore === 'string' ? [ignore] : []),\n    ...(basePreset.ignorePatterns || []),\n  ];\n  if (mergedIgnore.length > 0) {\n    flatConfig.push({ ignores: mergedIgnore });\n  }\n\n  // 扩展旧 eslintrc\n  flatConfig.push(...compat.extends(basePresetPath));\n\n  // 合并 settings & rules\n  const mergedSettings: Record<string, any> = {\n    ...(basePreset.settings || {}),\n    ...(settings as Record<string, any>),\n    'import/resolver': {\n      ...(basePreset.settings?.['import/resolver'] || {}),\n      ...((settings as Record<string, any>)['import/resolver'] || {}),\n      typescript: { project: packageRoot },\n    },\n  };\n  const mergedRules = { ...(basePreset.rules || {}), ...(rules as Rules) };\n\n  // 如果有 overrides，保留 override 自己的 rules/settings\n  if (basePreset.overrides && basePreset.overrides.length > 0) {\n    basePreset.overrides.forEach((override: any) => {\n      const overrideConfig: any = {\n        files: override.files || ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n        rules: { ...mergedRules, ...(override.rules || {}) },\n        settings: { ...mergedSettings, ...(override.settings || {}) },\n      };\n\n      if (override.parser || override.parserOptions) {\n        overrideConfig.languageOptions = {\n          parser: require('@typescript-eslint/parser'),\n          parserOptions: override.parserOptions || {\n            ecmaFeatures: { jsx: true },\n            ecmaVersion: 'latest',\n            sourceType: 'module',\n          },\n        };\n      }\n\n      flatConfig.push(overrideConfig);\n    });\n  } else {\n    const mainConfig: any = {\n      files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n      rules: mergedRules,\n      settings: mergedSettings,\n      languageOptions: {\n        parser: require('@typescript-eslint/parser'),\n        parserOptions: { ecmaFeatures: { jsx: true }, ecmaVersion: 'latest', sourceType: 'module' },\n      },\n    };\n    flatConfig.push(mainConfig);\n  }\n\n  return flatConfig;\n};\n"
  },
  {
    "path": "config/eslint-config/src/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { FlatCompat } = require('@eslint/eslintrc');\n\nconst { defineFlatConfig } = require('./defineFlatConfig.js');\n\nmodule.exports = { defineFlatConfig, FlatCompat };\n"
  },
  {
    "path": "config/eslint-config/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.infra.node.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"outDir\": \"dist\",\n    \"sourceMap\": false,\n    \"esModuleInterop\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "config/ts-config/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport default [\n  {\n    ignores: [\n      '**/*.d.ts',\n      '**/node_modules',\n      '**/dist',\n      '**/build',\n    ],\n  },\n  {\n    files: ['**/*.js', '**/*.ts'],\n    languageOptions: {\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n    },\n  },\n];\n"
  },
  {
    "path": "config/ts-config/global.d.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n"
  },
  {
    "path": "config/ts-config/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/ts-config\",\n  \"version\": \"0.1.8\",\n  \"type\": \"module\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"ISC\",\n  \"author\": \"chenjiawei.inizio@bytedance.com\",\n  \"scripts\": {\n    \"build\": \"exit\",\n    \"test\": \"exit\",\n    \"lint\": \"exit\",\n    \"watch\": \"exit\",\n    \"test:cov\": \"exit 0\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.8.3\",\n    \"eslint\": \"^9.0.0\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"reflect-metadata\", \"inversify\", \"vitest/globals\"],\n    \"typeRoots\": [\"node_modules/@types\"],\n    \"jsx\": \"react\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"],\n  },\n  \"include\": [\"packages\", \"apps\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.flow.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"bundler\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"noImplicitReturns\": false,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"jsx\": \"preserve\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\",\n      \"dom.iterable\"\n    ],\n  }\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.flow.path.json",
    "content": "{\n  \"extends\": \"./tsconfig.flow.base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {}\n  }\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.infra.base.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"allowJs\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"alwaysStrict\": true,\n    \"declaration\": true,\n    \"composite\": true,\n    \"incremental\": true,\n    \"strictNullChecks\": false,\n    \"noImplicitAny\": false,\n    \"strictBindCallApply\": false,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"node\",\n    \"module\": \"CommonJS\",\n    \"noFallthroughCasesInSwitch\": true,\n    // 这个普遍反馈会让代码变得啰嗦，暂定遵循原本 bot 的设置，关闭\n    \"noImplicitReturns\": false,\n    \"removeComments\": false,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"target\": \"es2018\"\n  },\n  \"watchOptions\": {\n    \"excludeDirectories\": [\n      \"**/node_modules\",\n      \"**/__tests__\",\n      \"**/__coverage__\",\n      \"**/__mocks__\",\n      \"output\",\n      \"dist\",\n      \"public/externals/vendors\"\n    ]\n  }\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.infra.node.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"extends\": \"./tsconfig.infra.base.json\",\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"target\": \"ES2019\"\n  },\n  \"ts-node\": {\n    \"files\": true\n  }\n}\n"
  },
  {
    "path": "config/ts-config/tsconfig.node.json",
    "content": "{\n  \"$schema\": \"http://json.schemastore.org/tsconfig\",\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"target\": \"ES2019\"\n  },\n  \"ts-node\": {\n    \"files\": true\n  }\n}\n"
  },
  {
    "path": "cspell.json",
    "content": "{\n  \"version\": \"0.2\",\n  \"ignorePaths\": [],\n  \"dictionaryDefinitions\": [],\n  \"dictionaries\": [],\n  \"words\": [\n    \"closebracket\",\n    \"codesandbox\",\n    \"douyinfe\",\n    \"edgesep\",\n    \"flowgram\",\n    \"flowgram.ai\",\n    \"gedit\",\n    \"Hoverable\",\n    \"langchain\",\n    \"marginx\",\n    \"marginy\",\n    \"openbracket\",\n    \"rsbuild\",\n    \"rspack\",\n    \"rspress\",\n    \"Sandpack\",\n    \"testrun\",\n    \"zoomin\",\n    \"zoomout\",\n    \"Bytedance\"\n  ],\n  \"ignoreWords\": [],\n  \"import\": []\n}\n"
  },
  {
    "path": "doc_build.sh",
    "content": "#  Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n#  SPDX-License-Identifier: MIT\n\nrush build\ncd apps/docs\nnpm run docs\ncd ../..\ncp -r apps/docs/src/zh/auto-docs apps/docs/src/en/auto-docs\ncd apps/docs\nnpm run build\ncd ../..\nrm -rf docs && mv apps/docs/doc_build docs\n"
  },
  {
    "path": "e2e/fixed-layout/README.md",
    "content": "# FixedLayout E2E Testing Project\n\n> This project contains end-to-end (E2E) tests for demo-fixed-layout to ensure core workflows are stable and reliable.\n\n---\n\n## 📦 Project Structure\n\ne2e/\n├─ tests/           # Test cases\n│ ├─ layout.spec.js\n│ ├─ node.spec.js\n│ └─ ...\n├─ test-results/    # Store Test Results\n├─ utils/           # Some utils\n\n\n---\n\n## 🚀 How to Run\n\n```bash\n\n# Install dependencies\nrush update\n\n# Run all tests\ncd e2e/fixed-layout & npm run e2e:test\n\n# Update ScreenShots\ncd e2e/fixed-layout & npm run e2e:update-screenshot\n\n```\n"
  },
  {
    "path": "e2e/fixed-layout/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "e2e/fixed-layout/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/e2e-fixed-layout\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"exit\",\n    \"e2e:test\": \"npx playwright test\",\n    \"e2e:update-screenshot\": \"npx playwright test --update-snapshots\"\n  },\n  \"dependencies\": {\n    \"@playwright/test\": \"^1.55.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/node\": \"^18\"\n  }\n}\n"
  },
  {
    "path": "e2e/fixed-layout/playwright.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  timeout: 60 * 1000,\n  retries: 1,\n  use: {\n    baseURL: 'http://localhost:3000',\n    headless: true,\n    actionTimeout: 10 * 1000, // timeout for waitFor/click...\n  },\n  webServer: {\n    command: 'rush dev:demo-fixed-layout',\n    port: 3000,\n    timeout: 120 * 1000,\n    reuseExistingServer: !process.env.GITHUB_ACTIONS,\n  },\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/drag.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from '@playwright/test';\n\nimport { getOffsetByLocator, cssEscape } from '../utils';\nimport PageModel from './models';\n\nconst OFFSET = 10;\n\ntest.describe('test drag', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n    await page.waitForTimeout(1000);\n  });\n\n  test('drag node', async ({ page }) => {\n    // 获取 node\n    const DRAG_NODE_ID = 'agent_0';\n    const DRAG_TO_PORT_ID = 'switch_0';\n    const agentLocator = await page.locator(`#${cssEscape(`$slotIcon$${DRAG_NODE_ID}`)}`);\n\n    const fromOffset = await getOffsetByLocator(agentLocator);\n    const from = {\n      x: fromOffset.left + OFFSET,\n      y: fromOffset.top + OFFSET,\n    };\n\n    const toLocator = await page.locator(`[data-from=\"${DRAG_TO_PORT_ID}\"]`);\n    const toOffset = await getOffsetByLocator(toLocator);\n\n    const to = {\n      x: toOffset.left,\n      y: toOffset.top,\n    };\n\n    await editorPage.drag(from, to);\n    await page.waitForTimeout(100);\n\n    // 通过 data-to 判断是否移动成功\n    const toLocator2 = await page.locator(`[data-from=\"${DRAG_TO_PORT_ID}\"]`);\n    const attribute = await toLocator2?.getAttribute('data-to');\n    expect(attribute).toEqual(DRAG_NODE_ID);\n  });\n\n  test('drag branch', async ({ page }) => {\n    const START_ID = 'case_0';\n    const END_ID = 'case_default_1';\n    const branchLocator = page.locator(`#${cssEscape(`$blockOrderIcon$${START_ID}`)}`);\n    const fromOffset = await getOffsetByLocator(branchLocator);\n    const from = {\n      x: fromOffset.left + OFFSET,\n      y: fromOffset.top + OFFSET,\n    };\n    const toBranchLocator = await page.locator(`#${cssEscape(`$blockOrderIcon$${END_ID}`)}`);\n    const toOffset = await getOffsetByLocator(toBranchLocator);\n    const to = {\n      x: toOffset.left - OFFSET / 2,\n      y: toOffset.top + OFFSET,\n    };\n    await editorPage.drag(from, to);\n    await page.waitForTimeout(100);\n\n    const fromOffset2 = await getOffsetByLocator(branchLocator);\n    expect(fromOffset2.centerX).toBeGreaterThan(fromOffset.centerX);\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/drawer.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('test llm drawer', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n    await page.waitForTimeout(1000);\n  });\n\n  test('sync data', async ({ page }) => {\n    // 确保 llm drawer 更改表单数据，数据同步\n    const LLM_NODE_ID = 'llm_0';\n    const DRAWER_CLASSNAME = 'gedit-flow-panel-wrap';\n\n    const TEST_FILL_VALUE = '123';\n\n    const llmLocator = await page.locator(`#${LLM_NODE_ID}`);\n\n    await llmLocator.click();\n\n    const drawerLocator = await page.locator(`.${DRAWER_CLASSNAME}`);\n    expect(drawerLocator).toBeVisible();\n\n    const input = await drawerLocator.locator('input').first();\n    await input.fill(TEST_FILL_VALUE);\n\n    const inputValue = await llmLocator.locator('input').first().inputValue();\n    expect(inputValue).toEqual(TEST_FILL_VALUE);\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/layout.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test } from '@playwright/test';\n\ntest('page render test', async ({ page }) => {\n  await page.goto('http://localhost:3000');\n  await page.waitForSelector('.gedit-playground-pipeline');\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/models/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Page } from '@playwright/test';\n\nimport type { DragPosition } from '../typings/index';\n\ntype InsertEdgeOptions = {\n  from: string;\n  to: string;\n};\n\nclass FixedLayoutModel {\n  private page: Page;\n\n  constructor(page: Page) {\n    this.page = page;\n  }\n\n  public async getNodeCount() {\n    return await this.page.locator('.gedit-flow-activity-node').count();\n  }\n\n  public async isStartNodeExist() {\n    return await this.page.locator('[data-node-id=\"start_0\"]').count();\n  }\n\n  public async isEndNodeExist() {\n    return await this.page.locator('[data-node-id=\"end_0\"]').count();\n  }\n\n  public async isConditionNodeExist() {\n    return await this.page.locator('[data-node-id=\"$blockIcon$switch_0\"]').count();\n  }\n\n  public async drag(from: DragPosition, to: DragPosition) {\n    await this.page.mouse.move(from.x, from.y);\n    await this.page.mouse.down();\n    await this.page.mouse.move(to.x, to.y);\n    await this.page.mouse.up();\n  }\n\n  public async insert(searchText: string, { from, to }: InsertEdgeOptions) {\n    const preConditionNodes = await this.page.locator('.gedit-flow-activity-node');\n    const preCount = await preConditionNodes.count();\n    const element = await this.page.locator(\n      `[data-testid=\"sdk.flowcanvas.line.adder\"][data-from=\"${from}\"][data-to=\"${to}\"]`\n    );\n\n    await element.waitFor({ state: 'visible' });\n    await element.scrollIntoViewIfNeeded();\n    await element.hover({\n      timeout: 3000,\n    });\n    const adder = this.page.locator('.semi-icon-plus_circle');\n\n    await adder.waitFor({ state: 'visible', timeout: 3000 });\n    await adder.scrollIntoViewIfNeeded();\n    await adder.click();\n    const nodeItem = await this.page.locator('.semi-popover-content').getByText(searchText);\n    await nodeItem.click();\n\n    await this.page.waitForFunction(\n      (expectedCount) =>\n        document.querySelectorAll('.gedit-flow-activity-node').length >= expectedCount,\n      preCount + 1\n    );\n  }\n}\n\nexport default FixedLayoutModel;\n"
  },
  {
    "path": "e2e/fixed-layout/tests/node.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('node operations', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n  });\n\n  test('node preview', async () => {\n    const startCount = await editorPage.isStartNodeExist();\n    const endCount = await editorPage.isEndNodeExist();\n    const conditionCount = await editorPage.isConditionNodeExist();\n    expect(startCount).toEqual(1);\n    expect(endCount).toEqual(1);\n    expect(conditionCount).toEqual(1);\n  });\n\n  test('add node', async () => {\n    const prevCount = await editorPage.getNodeCount();\n    await editorPage.insert('switch', {\n      from: 'llm_0',\n      to: 'switch_0',\n    });\n    const defaultNodeCount = await editorPage.getNodeCount();\n    expect(defaultNodeCount).toEqual(prevCount + 4);\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/testrun.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { expect, test } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('test testrun', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n    await page.waitForTimeout(1000);\n  });\n\n  test('trigger testrun', async ({ page }) => {\n    const runBtn = await page.getByText('Run');\n    await runBtn.click();\n\n    // 等待第一条 flowing line\n    const hasAnimation = await page.$eval('[data-line-id=\"start_0\"]', (el) => {\n      const style = window.getComputedStyle(el);\n      return style.animationName !== 'none';\n    });\n\n    expect(hasAnimation).toBe(true);\n\n    await page.waitForFunction(() => {\n      const start_line = document.querySelector('[data-line-id=\"start_0\"]');\n      const style = window.getComputedStyle(start_line!);\n      return style.animationName === 'none';\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/typings/drag.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface DragPosition {\n  x: number;\n  y: number;\n}\n"
  },
  {
    "path": "e2e/fixed-layout/tests/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { DragPosition } from './drag';\n"
  },
  {
    "path": "e2e/fixed-layout/tests/validate.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('test validate', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n  });\n\n  test('save', async ({ page }) => {\n    const saveBtn = await page.getByText('Save');\n    saveBtn.click();\n\n    const badge = page.locator('span.semi-badge-danger');\n    await expect(badge).toHaveText('2');\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tests/variable.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { expect, test } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('test variable', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n    await page.waitForTimeout(1000);\n  });\n\n  test('test variable type', async ({ page }) => {\n    const llmNode = page.locator('#llm_0');\n    const trigger = llmNode.locator('.semi-icon-setting').first();\n    await trigger.click();\n    const selectionBefore = llmNode.locator('.semi-tree-option-level-2');\n    await expect(selectionBefore).not.toBeVisible();\n\n    const semiTreeWrapper = llmNode.locator('.semi-tree-wrapper');\n\n    const dropdown = semiTreeWrapper.locator('.semi-tree-option-expand-icon').first();\n    await dropdown.click({\n      force: true,\n    });\n\n    const selection = llmNode.locator('.semi-tree-option-level-2');\n    await expect(selection).toBeVisible({\n      timeout: 10000,\n    });\n    const selectionCount = await selection.count();\n    expect(selectionCount).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "e2e/fixed-layout/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\n      \"node\"\n    ],\n    \"typeRoots\": [\n      \"node_modules/@types\"\n    ],\n    \"jsx\": \"react\",\n    \"lib\": [\n      \"es6\",\n      \"dom\",\n      \"es2020\",\n      \"es2019.Array\"\n    ]\n  },\n  \"include\": [\n    \"./tests\",\n    \"playwright.config.ts\",\n    \"./utils\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "e2e/fixed-layout/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Locator } from '@playwright/test';\n\n/**\n * @param {import('@playwright/test').Locator} locator\n */\nexport async function getOffsetByLocator(locator: Locator) {\n  return locator.evaluate((el) => {\n    const rect = el.getBoundingClientRect();\n    const left = rect.left;\n    const top = rect.top;\n    const width = rect.width;\n    const height = rect.height;\n\n    return {\n      left,\n      top,\n      width,\n      height,\n      centerX: left + width / 2,\n      centerY: top + height / 2,\n      right: left + width,\n      bottom: top + height,\n    };\n  });\n}\n\nexport function cssEscape(str: string) {\n  return str.replace(/([ !\"#$%&'()*+,.\\/:;<=>?@[\\]^`{|}~])/g, '\\\\$1');\n}\n"
  },
  {
    "path": "e2e/free-layout/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-restricted-syntax': [\n      'warn',\n      {\n        selector: \"CallExpression[callee.property.name='waitForTimeout']\",\n        message: 'Consider using waitForFunction instead of waitForTimeout.',\n      },\n    ],\n  },\n});\n"
  },
  {
    "path": "e2e/free-layout/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/e2e-free-layout\",\n  \"version\": \"0.1.0\",\n  \"description\": \"\",\n  \"keywords\": [],\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"build\": \"exit\",\n    \"e2e:debug\": \"npx playwright test --debug\",\n    \"e2e:test\": \"npx playwright test\",\n    \"e2e:update-screenshot\": \"npx playwright test --update-snapshots\"\n  },\n  \"dependencies\": {\n    \"@playwright/test\": \"^1.55.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/node\": \"^18\"\n  }\n}\n"
  },
  {
    "path": "e2e/free-layout/playwright.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  timeout: 60 * 1000,\n  retries: 1,\n  use: {\n    baseURL: 'http://localhost:3000',\n    headless: true,\n  },\n  webServer: {\n    command: 'rush dev:demo-free-layout',\n    port: 3000,\n    timeout: 120 * 1000,\n    reuseExistingServer: !process.env.GITHUB_ACTIONS,\n  },\n});\n"
  },
  {
    "path": "e2e/free-layout/tests/layout.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test } from '@playwright/test';\n\n// ensure layout render\ntest.describe('page render screen shot', () => {\n  test('screenshot', async ({ page }) => {\n    await page.goto('http://localhost:3000');\n    await page.waitForSelector('.gedit-playground-pipeline');\n  });\n});\n"
  },
  {
    "path": "e2e/free-layout/tests/models/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Page } from '@playwright/test';\n\nclass FreeLayoutModel {\n  public readonly page: Page;\n\n  constructor(page: Page) {\n    this.page = page;\n  }\n\n  // 获取节点数量\n  async getNodeCount() {\n    return await this.page.evaluate(\n      () => document.querySelectorAll('[data-testid=\"sdk.workflow.canvas.node\"]').length\n    );\n  }\n\n  public async isStartNodeExist() {\n    return await this.page.locator('[data-node-id=\"start_0\"]').count();\n  }\n\n  public async isEndNodeExist() {\n    return await this.page.locator('[data-node-id=\"end_0\"]').count();\n  }\n\n  public async isConditionNodeExist() {\n    return await this.page.locator('[data-node-id=\"condition_0\"]').count();\n  }\n\n  async addConditionNode() {\n    const preConditionNodes = await this.page.locator('.gedit-flow-activity-node');\n    const preCount = await preConditionNodes.count();\n    const button = this.page.locator('[data-testid=\"demo.free-layout.add-node\"]');\n    // open add node panel\n    await button.click();\n    await this.page.waitForSelector('[data-testid=\"demo-free-node-list-condition\"]');\n    // add condition\n    const conditionItem = this.page.locator('[data-testid=\"demo-free-node-list-condition\"]');\n    await conditionItem.click();\n    // determine whether the node was successfully added\n    await this.page.waitForFunction(\n      (expectedCount) =>\n        document.querySelectorAll('.gedit-flow-activity-node').length === expectedCount,\n      preCount + 1\n    );\n  }\n}\n\nexport default FreeLayoutModel;\n"
  },
  {
    "path": "e2e/free-layout/tests/node.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from '@playwright/test';\n\nimport PageModel from './models';\n\ntest.describe('node operations', () => {\n  let editorPage: PageModel;\n\n  test.beforeEach(async ({ page }) => {\n    editorPage = new PageModel(page);\n    await page.goto('http://localhost:3000');\n  });\n\n  test('node preview', async () => {\n    const startCount = await editorPage.isStartNodeExist();\n    const endCount = await editorPage.isEndNodeExist();\n    const conditionCount = await editorPage.isConditionNodeExist();\n    expect(startCount).toEqual(1);\n    expect(endCount).toEqual(1);\n    expect(conditionCount).toEqual(1);\n  });\n\n  test('add node', async () => {\n    const prevCount = await editorPage.getNodeCount();\n    await editorPage.addConditionNode();\n    const defaultNodeCount = await editorPage.getNodeCount();\n    expect(defaultNodeCount).toEqual(prevCount + 1);\n  });\n});\n"
  },
  {
    "path": "e2e/free-layout/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"target\": \"es2020\",\n    \"module\": \"esnext\",\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"skipLibCheck\": true,\n    \"noUnusedLocals\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"types\": [\"node\"],\n    \"typeRoots\": [\"node_modules/@types\"],\n    \"jsx\": \"react\",\n    \"lib\": [\"es6\", \"dom\", \"es2020\", \"es2019.Array\"]\n  },\n  \"include\": [\"./tests\", \"playwright.config.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/__mocks__/create-entity.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container } from 'inversify'\n\nimport {\n  // AbleManager,\n  Entity,\n  EntityManager,\n  PlaygroundContext,\n} from '../src'\n\nfunction createContainer(): Container {\n  const child = new Container({ defaultScope: 'Singleton' })\n  // child.bind(AbleManager).toSelf()\n  child.bind(PlaygroundContext).toConstantValue({})\n  child.bind(EntityManager).toSelf()\n  return child\n}\n\nconst container = createContainer()\nexport const entityManager = container.get<EntityManager>(EntityManager)\n\nclass TestEntity extends Entity {\n  static type = TestEntity.name\n}\n\nexport function createEntity<T extends Entity>(t = TestEntity, opts: any = {}): T {\n  return entityManager.createEntity<T>(t, opts)\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/__mocks__/layers.mock.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { Layer } from '../src/core';\nimport { Entity, EntityData, observeEntityDatas } from '../src/common';\n\nexport class MockEntityDataRegistry extends EntityData {\n  getDefaultData() {\n    return {};\n  }\n\n  static type = 'mock';\n}\n\nexport class TestUtilsLayer extends Layer {\n  node = domUtils.createDivWithClass('test-layer');\n\n  renderWithReactMemo: boolean;\n\n  setRenderWithReactMemo(status: boolean) {\n    this.renderWithReactMemo = status;\n  }\n}\n\nexport class _TestEntity extends Entity {\n  static type = 'test-entity';\n}\n\nexport class TestRenderLayer1 extends Layer {\n  autorun = () => {};\n}\n\nexport class TestRenderLayer2 extends Layer {\n  node = domUtils.createDivWithClass('test-layer');\n\n  renderWithReactMemo: true;\n\n  render = () => <div></div>;\n}\n\nexport class TestRenderLayer3 extends Layer {\n  // mock entityRegistries、entityDataRegistries\n  @observeEntityDatas(_TestEntity, MockEntityDataRegistry) _transforms: any[];\n\n  public resizeTimes = 0;\n  public blurTimes = 0;\n  public focusTimes = 0;\n  public zoomTimes = 0;\n  public scrollTimes = 0;\n  public fireViewportChanged = false;\n  public readonlyOrDisabledChanged = false;\n\n  onResize = () => this.resizeTimes++;\n  onBlur = () => this.blurTimes++;\n  onFocus = () => this.focusTimes++;\n  onZoom = () => this.zoomTimes++;\n  onScroll = () => this.scrollTimes++;\n  onViewportChange = () => { this.fireViewportChanged = true };\n  onReadonlyOrDisabledChange = () => { this.readonlyOrDisabledChanged = true };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/__mocks__/playground-container.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Playground, createPlaygroundContainer } from '../src'\n\nexport function createPlayground(): Playground {\n  return createPlaygroundContainer().get(Playground)\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/__snapshots__/pipeline.spec.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`pipeline render > pipeline-entites 1`] = `\n{\n  \"asFragment\": [Function],\n  \"baseElement\": <body>\n    <div />\n  </body>,\n  \"container\": <div />,\n  \"debug\": [Function],\n  \"findAllByAltText\": [Function],\n  \"findAllByDisplayValue\": [Function],\n  \"findAllByLabelText\": [Function],\n  \"findAllByPlaceholderText\": [Function],\n  \"findAllByRole\": [Function],\n  \"findAllByTestId\": [Function],\n  \"findAllByText\": [Function],\n  \"findAllByTitle\": [Function],\n  \"findByAltText\": [Function],\n  \"findByDisplayValue\": [Function],\n  \"findByLabelText\": [Function],\n  \"findByPlaceholderText\": [Function],\n  \"findByRole\": [Function],\n  \"findByTestId\": [Function],\n  \"findByText\": [Function],\n  \"findByTitle\": [Function],\n  \"getAllByAltText\": [Function],\n  \"getAllByDisplayValue\": [Function],\n  \"getAllByLabelText\": [Function],\n  \"getAllByPlaceholderText\": [Function],\n  \"getAllByRole\": [Function],\n  \"getAllByTestId\": [Function],\n  \"getAllByText\": [Function],\n  \"getAllByTitle\": [Function],\n  \"getByAltText\": [Function],\n  \"getByDisplayValue\": [Function],\n  \"getByLabelText\": [Function],\n  \"getByPlaceholderText\": [Function],\n  \"getByRole\": [Function],\n  \"getByTestId\": [Function],\n  \"getByText\": [Function],\n  \"getByTitle\": [Function],\n  \"queryAllByAltText\": [Function],\n  \"queryAllByDisplayValue\": [Function],\n  \"queryAllByLabelText\": [Function],\n  \"queryAllByPlaceholderText\": [Function],\n  \"queryAllByRole\": [Function],\n  \"queryAllByTestId\": [Function],\n  \"queryAllByText\": [Function],\n  \"queryAllByTitle\": [Function],\n  \"queryByAltText\": [Function],\n  \"queryByDisplayValue\": [Function],\n  \"queryByLabelText\": [Function],\n  \"queryByPlaceholderText\": [Function],\n  \"queryByRole\": [Function],\n  \"queryByTestId\": [Function],\n  \"queryByText\": [Function],\n  \"queryByTitle\": [Function],\n  \"rerender\": [Function],\n  \"unmount\": [Function],\n}\n`;\n\nexports[`pipeline render > pipeline-react-utils 1`] = `<div />`;\n\nexports[`pipeline render > should pipeline rendered 1`] = `\n<DocumentFragment>\n  <div\n    class=\"gedit-playground-container\"\n  >\n    <div\n      class=\"gedit-playground\"\n      data-testid=\"sdk.workflow.canvas\"\n      tabindex=\"0\"\n    >\n      <div\n        class=\"gedit-playground-pipeline\"\n      />\n      <div />\n    </div>\n  </div>\n</DocumentFragment>\n`;\n\nexports[`pipeline render > should pipeline rendered with flowContainer 1`] = `\n<DocumentFragment>\n  <div\n    class=\"gedit-playground-container\"\n  >\n    <div\n      class=\"gedit-playground\"\n      data-testid=\"sdk.workflow.canvas\"\n      tabindex=\"0\"\n    >\n      <div\n        class=\"gedit-playground-pipeline\"\n      />\n    </div>\n  </div>\n</DocumentFragment>\n`;\n\nexports[`pipeline render > should pipeline rendered with playgroundContext 1`] = `\n<DocumentFragment>\n  <div\n    class=\"gedit-playground-container\"\n  >\n    <div\n      class=\"gedit-playground\"\n      data-testid=\"sdk.workflow.canvas\"\n      tabindex=\"0\"\n    >\n      <div\n        class=\"gedit-playground-pipeline\"\n      />\n    </div>\n  </div>\n</DocumentFragment>\n`;\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/__snapshots__/playground.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`playground > should playground-layer config normal 1`] = `\n{\n  \"x\": 200,\n  \"y\": 200,\n}\n`;\n\nexports[`playground > should playground-layer config normal 2`] = `\n{\n  \"x\": 400,\n  \"y\": 400,\n}\n`;\n\nexports[`playground > should playground-layer config normal 3`] = `\n{\n  \"x\": 0,\n  \"y\": 0,\n}\n`;\n\nexports[`playground > should playground-layer config normal 4`] = `\n_Rectangle {\n  \"height\": 0,\n  \"type\": 1,\n  \"width\": 0,\n  \"x\": 100,\n  \"y\": 100,\n}\n`;\n\nexports[`playground > should playground-layer config normal 5`] = `\n_Rectangle {\n  \"height\": 0,\n  \"type\": 1,\n  \"width\": 0,\n  \"x\": 200,\n  \"y\": 200,\n}\n`;\n\nexports[`playground > should render playground-layer 1`] = `\"\"`;\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/core/layer/config/editor-state-config-entity.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EditorStateConfigEntity } from '../../../../src/core/layer/config/editor-state-config-entity';\n\nit('should expose isPressingSpaceBar', () => {\n  const editorStateConfig = new EditorStateConfigEntity({\n    entityManager: {\n      getDataRegistryByType: vi.fn(),\n      registerEntityData: vi.fn(),\n      getDataInjector() {\n        return () => {};\n      },\n    } as any,\n  });\n\n  expect(editorStateConfig.isPressingSpaceBar).toBe(false);\n  editorStateConfig.isPressingSpaceBar = true;\n  expect(editorStateConfig.isPressingSpaceBar).toBe(true);\n});\n\n\nit('should return correct state from shortcuts', () => {\n  const editorStateConfig = new EditorStateConfigEntity({\n    entityManager: {\n      getDataRegistryByType: vi.fn(),\n      registerEntityData: vi.fn(),\n      getDataInjector() {\n        return () => {};\n      },\n    } as any,\n  });\n\n  let currState = editorStateConfig.getStateFromShortcut({ key: ' '} as KeyboardEvent);\n  expect(currState?.id).toBe('STATE_GRAB');\n\n  // currState = editorStateConfig.getStateFromShortcut({ key: 'V'} as KeyboardEvent);\n  // expect(currState?.id).toBe('STATE_SELECT');\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/core/layer/config/payground-config-entity.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PlaygroundConfigEntity } from '../../../../src/core/layer/config/playground-config-entity';\n\nit('should disable grab', () => {\n  const playgroundConfig = new PlaygroundConfigEntity({\n    entityManager: {\n      getDataRegistryByType: vi.fn(),\n      registerEntityData: vi.fn(),\n      getDataInjector() {\n        return () => {};\n      },\n    } as any,\n  });\n  let changeTimes = 0;\n  playgroundConfig.onGrabDisableChange(() => changeTimes++);\n  playgroundConfig.grabDisable = false;\n  expect(changeTimes).toBe(0)\n  playgroundConfig.grabDisable = true;\n  expect(changeTimes).toBe(1)\n  playgroundConfig.grabDisable = true;\n  expect(changeTimes).toBe(1)\n  playgroundConfig.grabDisable = false;\n  expect(changeTimes).toBe(2)\n});\n\n\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/core/layer/playground-layer.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { render } from '@testing-library/react';\n\nimport {\n  EditorState,\n  PlaygroundLayer,\n  PlaygroundReactProvider,\n  PlaygroundReactRenderer,\n} from '../../../src';\nimport { createPlayground } from '../../../__mocks__/playground-container.mock';\n\ndescribe('Layer', () => {\n  beforeAll(() => {\n    const modules: interfaces.ContainerModule[] = [];\n    // 渲染 playground\n    render(\n      <PlaygroundReactProvider containerModules={modules}>\n        <PlaygroundReactRenderer>\n          <div id=\"div\"></div>\n          <textarea id=\"text\"></textarea>\n        </PlaygroundReactRenderer>\n      </PlaygroundReactProvider>,\n    );\n  });\n\n  test('playground-layer', () => {\n    const playground = createPlayground();\n    playground.registerLayer(PlaygroundLayer);\n    const playgroundLayer = playground.getLayer(PlaygroundLayer)!;\n    const registry = playground.pipelineRegistry;\n    playgroundLayer.options.preventGlobalGesture = true;\n    playground.ready();\n    document.body.appendChild(playgroundLayer.pipelineNode.parentElement!);\n\n    // @ts-ignore\n    const editorStateConfig = playgroundLayer.editorStateConfig;\n\n    expect(editorStateConfig.is(EditorState.STATE_GRAB.id)).toBe(false);\n    registry.renderer.node.parentNode!.dispatchEvent(\n      new MouseEvent('mousedown', {\n        button: 1,\n      }),\n    );\n    expect(editorStateConfig.is(EditorState.STATE_GRAB.id)).toBe(true);\n\n    editorStateConfig?.changeState(EditorState.STATE_SELECT.id, new MouseEvent('mousedown') as any);\n    expect(editorStateConfig.is(EditorState.STATE_SELECT.id)).toBe(true);\n\n    // 切换鼠标模式\n    editorStateConfig?.changeState(\n      EditorState.STATE_MOUSE_FRIENDLY_SELECT.id,\n      new MouseEvent('mousedown') as any,\n    );\n    expect(editorStateConfig.is(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id)).toBe(true);\n\n    // 鼠标模式为小手模式\n    expect(playgroundLayer.config.cursor).toBe('grab');\n\n    // 按下 shift 键\n    registry.renderer.node.parentNode!.dispatchEvent(\n      new KeyboardEvent('keydown', {\n        key: 'Shift',\n        code: 'ShiftLeft',\n        shiftKey: true,\n        bubbles: true,\n        cancelable: true,\n      }),\n    );\n\n    // 鼠标变成箭头\n    expect(playgroundLayer.config.cursor).toBe('');\n\n    // 释放 shift 键\n    registry.renderer.node.parentNode!.dispatchEvent(\n      new KeyboardEvent('keyup', {\n        key: 'Shift',\n        code: 'ShiftLeft',\n        shiftKey: true,\n        bubbles: true,\n        cancelable: true,\n      }),\n    );\n\n    // 触发滚动事件\n    registry.renderer.node.dispatchEvent(\n      new MouseEvent('wheel', {\n        deltaY: 100, // 向下滚动 100,\n        bubbles: true,\n        cancelable: true,\n      } as any),\n    );\n\n    // 切换触控板\n    editorStateConfig?.changeState(EditorState.STATE_SELECT.id, new MouseEvent('mousedown') as any);\n    // 聚焦\n    registry.onFocusEmitter.fire();\n    // 按下 space bar\n    registry.renderer.node.parentNode!.dispatchEvent(\n      new KeyboardEvent('keypress', {\n        key: ' ',\n        code: 'Space',\n        bubbles: true,\n        cancelable: true,\n      }),\n    );\n    expect(editorStateConfig.isPressingSpaceBar).toBe(true);\n\n    // 开始拖拽\n    registry.renderer.node.parentNode!.dispatchEvent(\n      new MouseEvent('mousedown', {\n        clientX: 100,\n        clientY: 100,\n      }),\n    );\n\n    // 注销 layer\n    document.body.removeChild(playgroundLayer.pipelineNode.parentElement!);\n    playgroundLayer?.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/entity.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi } from 'vitest';\nimport { Container } from 'inversify';\n\nimport {\n  // Able,\n  // AbleManager,\n  // type AbleRegistry,\n  ConfigEntity,\n  Entity,\n  EntityData,\n  type EntityDataRegistry,\n  EntityManager,\n  PlaygroundContainerFactory,\n  PlaygroundContext,\n} from '../src';\n\nfunction createContainer(): Container {\n  const child = new Container({ defaultScope: 'Singleton' });\n  // child.bind(AbleManager).toSelf()\n  child.bind(PlaygroundContext).toConstantValue({});\n  child\n    .bind(PlaygroundContainerFactory)\n    .toDynamicValue((ctx) => ctx.container)\n    .inSingletonScope();\n  child.bind(EntityManager).toSelf();\n  return child;\n}\n\nconst container = createContainer();\nconst entityManager = container.get<EntityManager>(EntityManager);\n\nfunction createEntity<T extends Entity>(t = TestEntity, opts: any = {}): T {\n  return entityManager.createEntity<T>(t, opts);\n}\n\ninterface TestSchema {\n  v1: string;\n  v2: number[];\n}\n\nclass TestData extends EntityData<TestSchema> {\n  static type = 'TestData';\n\n  getDefaultData(): TestSchema {\n    return { v1: 'test', v2: [1, 2] };\n  }\n}\n\ninterface Test1Schema {\n  v: string;\n}\n\nclass Test1Data extends EntityData<Test1Schema> {\n  static type = Test1Data.name;\n\n  getDefaultData(): Test1Schema {\n    return { v: 'test1' };\n  }\n}\n\nclass SingleValueData extends EntityData<string> {\n  static type = SingleValueData.name;\n\n  getDefaultData(): string {\n    return 'test';\n  }\n}\n\nclass TestEntity extends Entity {\n  static type = TestEntity.name;\n}\n\n// class TestAble extends Able {\n//   static type = TestAble.name\n//\n//   handle(...args: any[]): void {\n//     throw new Error('Method not implemented.')\n//   }\n// }\n//\n// class Test1Able extends Able {\n//   static type = Test1Able.name\n//\n//   handle(...args: any[]): void {\n//     throw new Error('Method not implemented.')\n//   }\n// }\n\ndescribe('EntityData', () => {\n  test('data', () => {\n    expect(new TestData(createEntity()).data).toEqual({ v1: 'test', v2: [1, 2] });\n  });\n\n  test('type', () => {\n    expect(new TestData(createEntity()).type).toEqual('TestData');\n\n    class _TestData extends EntityData<string> {\n      static type = '';\n\n      getDefaultData(): string {\n        return 'test1';\n      }\n    }\n\n    expect(() => new _TestData(createEntity()).type).toThrow('need a type');\n  });\n\n  test('update & fromJSON & toJSON', () => {\n    const singleValueData = new SingleValueData(createEntity());\n    singleValueData.update('new');\n    expect(singleValueData.data).toEqual('new');\n\n    const testData = new TestData(createEntity());\n    testData.update({ v1: 'new' });\n    expect(testData.data.v1).toEqual('new');\n    testData.update('v2', [3, 4]);\n    expect(testData.data.v2).toEqual([3, 4]);\n\n    testData.fromJSON({ v1: 'new1', v2: [5] });\n    expect(testData.toJSON()).toEqual({ v1: 'new1', v2: [5] });\n  });\n\n  test('onWillChange', () => {\n    let oldData: any;\n    let toDispose;\n    const singleValueData = new SingleValueData(createEntity());\n    toDispose = singleValueData.onWillChange((event) => {\n      oldData = event.data;\n    });\n    singleValueData.update('new');\n    expect(oldData).toEqual('test');\n    singleValueData.update('new1');\n    expect(oldData).toEqual('new');\n    toDispose.dispose();\n\n    const testData = new TestData(createEntity());\n    toDispose = testData.onWillChange((event) => {\n      oldData = JSON.parse(JSON.stringify(event.data));\n    });\n    testData.update({ v1: 'new' });\n    expect(oldData.v1).toEqual('test');\n    testData.update('v2', [3, 4]);\n    expect(oldData.v1).toEqual('new');\n    testData.update('v2', [4, 5]);\n    expect(oldData.v2).toEqual([3, 4]);\n  });\n\n  test('changeLocked & version', () => {\n    const testData = new TestData(createEntity());\n    expect(testData.version).toEqual(0);\n    testData.update({ v1: 'new' });\n    expect(testData.version).toEqual(1);\n\n    const testData1 = new TestData(createEntity());\n    expect(testData1.version).toEqual(0);\n    expect(testData1.changeLocked).toEqual(false);\n    testData1.changeLocked = true;\n    testData1.update({ v1: 'new' });\n    expect(testData1.changeLocked).toEqual(true);\n    expect(testData1.version).toEqual(0);\n  });\n\n  test.skip('bindChange', () => {\n    const cb = vi.fn();\n    const testData2 = new TestData(createEntity());\n    testData2.dispose();\n    expect(cb.mock.calls).toHaveLength(1);\n  });\n});\n\ndescribe('Entity', () => {\n  test('isRegistryOf', () => {\n    class A {}\n\n    class B extends A {}\n\n    class C extends B {}\n\n    expect(Entity.isRegistryOf(A, A)).toEqual(true);\n    expect(Entity.isRegistryOf(B, B)).toEqual(true);\n    expect(Entity.isRegistryOf(C, C)).toEqual(true);\n    expect(Entity.isRegistryOf(B, A)).toEqual(true);\n    expect(Entity.isRegistryOf(C, B)).toEqual(true);\n    expect(Entity.isRegistryOf(C, A)).toEqual(true);\n    expect(Entity.isRegistryOf(A, C)).toEqual(false);\n  });\n\n  test('version', () => {\n    const entity = createEntity();\n    const v = entity.version;\n    expect(v).toBeGreaterThan(0);\n    expect(createEntity().version).toEqual(v + 1);\n  });\n\n  test('type', () => {\n    const entity = createEntity();\n    expect(entity.type).toEqual(TestEntity.name);\n    expect(Entity.getType(TestEntity)).toEqual(TestEntity.name);\n\n    class _TestEntity extends Entity {\n      static type = '';\n    }\n\n    expect(() => entityManager.createEntity(_TestEntity).type).toThrow('type');\n  });\n\n  test('data CRUD', () => {\n    const entity = createEntity();\n    expect(entity.hasData(TestData)).toEqual(false);\n    entity.addData(TestData);\n    expect(entity.hasData(TestData)).toEqual(true);\n    expect(entity.getData(TestData)?.data).toEqual({ v1: 'test', v2: [1, 2] });\n    entity.updateData(TestData, { v1: 'test1', v2: [3] });\n    expect(entity.getData(TestData)?.data).toEqual({ v1: 'test1', v2: [3] });\n    entity.removeData(TestData);\n    expect(entity.hasData(TestData)).toEqual(false);\n\n    // duplicated addData\n    entity.addData(TestData);\n    const entityData = entity.addData(TestData, { v1: 'test0', v2: [1, 2] });\n    expect(entityData.data).toEqual({ v1: 'test0', v2: [1, 2] });\n  });\n\n  test('getDefaultAbleRegistries & getDefaultDataRegistries', () => {\n    class _TestEntity extends Entity {\n      static type = TestEntity.name;\n\n      // getDefaultAbleRegistries(): AbleRegistry[] {\n      //   return [TestAble]\n      // }\n\n      getDefaultDataRegistries(): EntityDataRegistry[] {\n        return [TestData];\n      }\n    }\n\n    const entity = createEntity(_TestEntity);\n    expect(entity.hasData(TestData)).toEqual(true);\n    // expect(entity.hasAble(TestAble)).toEqual(true)\n  });\n\n  test('data initilize', () => {\n    const entity = createEntity(TestEntity, {\n      datas: [{ registry: Test1Data, data: { v: 'new-test1' } }],\n    });\n    expect(entity.hasData(Test1Data)).toEqual(true);\n    expect(entity.hasData(TestData)).toEqual(false);\n\n    entity.addInitializeData([TestData]);\n    entity.removeData(TestData);\n    expect(entity.hasData(TestData)).toEqual(true);\n  });\n\n  test('fromJSON & toJSON - NORMAL json', () => {\n    const entity = createEntity();\n    entity.addData(TestData);\n\n    const json = {\n      dataList: [\n        {\n          data: { v1: 'test', v2: [1, 2] },\n          type: TestData.name,\n        },\n      ],\n      // ableList: [],\n      id: 'test',\n      type: TestEntity.name,\n    };\n    entity.fromJSON(json);\n    const json1 = entity.toJSON();\n    expect(json1).toEqual({ ...json, id: json1?.id });\n  });\n\n  test('fromJSON & toJSON - ABNORMAL json', () => {\n    const entity = createEntity();\n    entity.addData(TestData);\n\n    const json = {\n      dataList: [\n        {\n          data: { v1: 'new-test', v2: [1, 2] },\n          type: TestData.name,\n        },\n      ],\n      // ableList: [],\n      type: TestEntity.name,\n    };\n    entity.fromJSON(json as any);\n    const json1 = entity.toJSON();\n    expect(json1).not.toEqual({ ...json, id: json1?.id });\n  });\n\n  test('savedInManager', () => {\n    const entity = createEntity(TestEntity, { savedInManager: undefined });\n    expect(entity.savedInManager).toEqual(true);\n  });\n\n  test('dispose', () => {\n    const entity = createEntity();\n    const cb = vi.fn();\n    entity.onDispose(cb);\n    entity.dispose();\n    expect(entity.disposed).toEqual(true);\n    expect(cb.mock.calls.length).toEqual(1);\n  });\n\n  // test('able CRUD', () => {\n  //   const entity = createEntity(TestEntity, { ables: [Test1Able] })\n  //   expect(entity.hasAble(Test1Able)).toEqual(true)\n  //   expect(entity.hasAbles(TestAble)).toEqual(false)\n  //   entity.addAbles(TestAble)\n  //   expect(entity.hasAble(TestAble)).toEqual(true)\n  //   expect(entity.hasAbles(TestAble, Test1Able)).toEqual(true)\n  //   expect(entity.ables.has(TestAble)).toEqual(true)\n  //   entity.removeAbles(TestAble)\n  //   expect(entity.hasAble(TestAble)).toEqual(false)\n  // })\n});\n\ninterface TestConfigEntityData {\n  v: number;\n}\n\nclass TestConfigEntity extends ConfigEntity<TestConfigEntityData> {\n  static type = TestConfigEntity.name;\n\n  getDefaultConfig(): TestConfigEntityData {\n    return { v: 0 };\n  }\n\n  toDataJSON(): TestConfigEntityData {\n    return {\n      v: this.config.v + 1,\n    };\n  }\n}\n\ndescribe('ConfigEntity', () => {\n  test('basic ConfigEntity', () => {\n    const configEntity = new ConfigEntity({ entityManager });\n    const cb = vi.fn();\n    configEntity.onConfigChanged(cb);\n    configEntity.updateConfig({ a: 1 });\n    expect(cb.mock.calls.length).toEqual(1);\n    expect(configEntity.config).toEqual({ a: 1 });\n    expect(configEntity.toJSON()?.dataList.map((d: any) => (d as any).data)).toEqual([{ a: 1 }]);\n  });\n\n  test('custom ConfigEntity', () => {\n    const configEntity = new TestConfigEntity({ entityManager });\n    configEntity.updateConfig({ v: 1 });\n    expect(configEntity.config).toEqual({ v: 1 });\n    expect(configEntity.toJSON()?.dataList.map((d: any) => (d as any).data)).toEqual([{ v: 1 }]);\n  });\n});\n\ndescribe('EntityManager', () => {\n  let container1!: Container;\n  let entityManager1!: EntityManager;\n\n  beforeEach(() => {\n    container1 = createContainer();\n    entityManager1 = container1.get<EntityManager>(EntityManager);\n  });\n\n  test('Entity CRUD', () => {\n    const entity1 = entityManager1.createEntity(Entity);\n    const entity2 = entityManager1.createEntity(Entity);\n    expect(entity1 !== entity2).toBeTruthy();\n    expect(entityManager1.getEntities(Entity)).toEqual([entity1, entity2]);\n    expect(entityManager1.getEntityById(entity1.id)).toEqual(entity1);\n    expect(entityManager1.removeEntityById(entity1.id)).toEqual(true);\n    expect(entityManager1.removeEntityById(entity1.id)).toEqual(false);\n    expect(entityManager1.getEntityById(entity1.id)).toEqual(undefined);\n    expect(entityManager1.getEntities(Entity)).toEqual([entity2]);\n\n    const entity3 = entityManager1.createEntity(Entity);\n    expect(entityManager1.getEntities(Entity)).toEqual([entity2, entity3]);\n    entityManager1.removeEntities(Entity);\n    expect(entityManager1.getEntities(Entity)).toEqual([]);\n\n    // getEntity with autoCreate\n    expect(entityManager1.getEntityVersion(TestEntity)).toEqual(0);\n    const entity4 = entityManager1.getEntity(TestEntity);\n    expect(entity4).toBeUndefined();\n    expect(entityManager1.hasEntity(TestEntity)).toBeFalsy();\n    const entity5 = entityManager1.getEntity(TestEntity, true);\n    expect(entityManager1.getEntityVersion(TestEntity.name)).toEqual(1);\n    entityManager1.changeEntityLocked = true;\n    entityManager1.fireEntityChanged(TestEntity.name);\n    entityManager1.changeEntityLocked = false;\n    expect(entityManager1.getEntityVersion(TestEntity.name)).toEqual(2);\n    expect(entity5).not.toBeUndefined();\n    expect(entityManager1.hasEntity(TestEntity)).toBeTruthy();\n\n    // saveEntity edge case\n    const entity6 = entityManager1.createEntity(Entity, { id: entity5!.id });\n    expect(entity6 !== entityManager1.getEntityById(entity6.id)).toBeTruthy();\n    expect(entity5 === entityManager1.getEntityById(entity6.id)).toBeTruthy();\n\n    // dispose\n    entityManager1.dispose();\n    // FIXME? expect(entityManager1.hasEntity(TestEntity)).toBeFalsy()\n  });\n\n  test('EntityData CRUD', () => {\n    const entity1 = entityManager1.createEntity(Entity);\n    const entity2 = entityManager1.createEntity(Entity);\n    entity1.addData(TestData, { v1: 'test1', v2: [3] });\n    expect(entity1.getData(TestData)?.data).toEqual({ v1: 'test1', v2: [3] });\n    expect(entity2.getData(TestData)?.data).toEqual(undefined);\n    entityManager1.resetEntities(Entity);\n    expect(entity1.getData(TestData)?.data).toEqual(undefined);\n    expect(entity2.getData(TestData)?.data).toEqual(undefined);\n\n    entity1.addData(TestData, { v1: 'test1', v2: [3] });\n    entityManager1.resetEntity(Entity);\n    expect(entity1.getData(TestData)?.data).toEqual(undefined);\n\n    entity1.addData(TestData, { v1: 'test1', v2: [3] });\n    entity2.addData(TestData, { v1: 'test2', v2: [3] });\n    expect(entityManager1.getEntityDataVersion(SingleValueData)).toEqual(0);\n    entity2.addData(SingleValueData, 'test2');\n    expect(entity1.getData(TestData)?.data).toEqual({ v1: 'test1', v2: [3] });\n    expect(entity2.getData(SingleValueData)?.data).toEqual('test2');\n    expect(entityManager1.getEntityDatas(Entity, TestData).map((d) => d.data)).toEqual([\n      { v1: 'test1', v2: [3] },\n      { v1: 'test2', v2: [3] },\n    ]);\n    expect(entityManager1.getEntityDataVersion(SingleValueData)).toEqual(1);\n    entityManager1.reset();\n    expect(entityManager1.getEntityDataVersion(SingleValueData.name)).toEqual(2);\n    entityManager1.fireEntityDataChanged(entity1.type, SingleValueData.name);\n    expect(entityManager1.getEntityDataVersion(SingleValueData.name)).toEqual(3);\n    expect(entity1.getData(TestData)?.data).toEqual(undefined);\n    expect(entity2.getData(TestData)?.data).toEqual(undefined);\n    expect(entity2.getData(SingleValueData)?.data).toEqual(undefined);\n  });\n\n  // test.skip('Able CRUD', () => {})\n\n  test.skip('ConfigEntity CRUD', () => {\n    const getTestConfigEntity = () => entityManager1.getEntity<TestConfigEntity>(TestConfigEntity);\n    expect(entityManager1.isConfigEntity(ConfigEntity.type)).toEqual(false);\n    expect(entityManager1.isConfigEntity(TestConfigEntity.type)).toEqual(false);\n    const testConfigEntity = entityManager1.createEntity(TestConfigEntity);\n    expect(entityManager1.isConfigEntity(testConfigEntity.type)).toEqual(true);\n    const testConfigEntity1 = entityManager1.createEntity(TestConfigEntity);\n    expect(testConfigEntity === testConfigEntity1).toBeTruthy();\n\n    entityManager1.updateConfigEntity(TestConfigEntity, { v: 2 });\n    expect(getTestConfigEntity()?.config).toEqual({ v: 2 });\n    const state = entityManager1.storeState();\n    // console.log(JSON.stringify(state, null, 2))\n    //\n    // [\n    //   {\n    //     \"type\": \"TestConfigEntity\",\n    //     \"id\": \"k24ThXyF7hbjRBOD7z-4w\",\n    //     \"ableList\": [],\n    //     \"dataList\": [\n    //       {\n    //         \"type\": \"_TestConfigEntityDataMixin\",\n    //         \"data\": {\n    //           \"v\": 3\n    //         }\n    //       }\n    //     ]\n    //   }\n    // ]\n    const datas = (state[0].dataList as EntityData[]).map((d) => d.data);\n    expect(datas).toEqual([{ v: 3 }]);\n\n    entityManager1.registerEntity(TestConfigEntity);\n    expect(entityManager1.getRegistryByType(state[0].type)).toEqual(TestConfigEntity);\n    entityManager1.removeEntities(TestConfigEntity);\n    expect(getTestConfigEntity()?.config).toEqual(undefined);\n    entityManager1.restoreState(state);\n    expect(getTestConfigEntity()?.id.length).toBeGreaterThan(1);\n    expect(getTestConfigEntity()?.config).toEqual({ v: 3 });\n    (state[0].dataList as EntityData[])[0].data.v = 4;\n\n    // restoreState edge cases\n    entityManager1.removeEntities(TestConfigEntity);\n    entityManager1.restoreState(undefined as any);\n    expect(getTestConfigEntity()?.config).toEqual(undefined);\n    entityManager1.restoreState([{}] as any);\n    expect(getTestConfigEntity()?.config).toEqual(undefined);\n    state[0].type = `${TestConfigEntity.name}-1`;\n    entityManager1.restoreState(state);\n    state[0].type = TestConfigEntity.name;\n    expect(getTestConfigEntity()?.config).toEqual(undefined);\n  });\n\n  test('Entity Registry CRUD', () => {\n    expect(entityManager1.getRegistryByType(TestEntity.type)).toEqual(undefined);\n    entityManager1.registerEntity(TestEntity);\n    expect(entityManager1.getRegistryByType(TestEntity.type)).toEqual(TestEntity);\n    entityManager1.registerEntity(TestEntity); // duplicated\n    expect(entityManager1.getRegistryByType(TestEntity.type)).toEqual(TestEntity);\n\n    class _TestEntity extends Entity {\n      static type = '';\n    }\n\n    expect(() => entityManager1.registerEntity(_TestEntity)).toThrow('need a type');\n\n    class _Test1Entity extends Entity {\n      static type = TestEntity.name;\n    }\n\n    expect(() => entityManager1.registerEntity(_Test1Entity)).toThrow('need a new type');\n  });\n\n  test('EntityData Registry CRUD', () => {\n    expect(entityManager1.getDataRegistryByType(TestData.type)).toEqual(undefined);\n    entityManager1.registerEntityData(TestData);\n    expect(entityManager1.getDataRegistryByType(TestData.type)).toEqual(TestData);\n    entityManager1.registerEntityData(TestData); // duplicated\n    expect(entityManager1.getDataRegistryByType(TestData.type)).toEqual(TestData);\n\n    class _TestEntityData extends EntityData {\n      getDefaultData() {\n        throw new Error('Method not implemented.');\n      }\n\n      static type = '';\n    }\n\n    expect(() => entityManager1.registerEntityData(_TestEntityData)).toThrow('need a type');\n\n    // class _Test1EntityData extends EntityData {\n    //   getDefaultData() {\n    //     throw new Error('Method not implemented.')\n    //   }\n\n    //   static type = TestData.name\n    // }\n    // expect(() => entityManager1.registerEntityData(_Test1EntityData)).toThrow('need a new type')\n  });\n  test('Entity getService', () => {\n    const entity1 = entityManager1.createEntity(Entity);\n    expect(entity1.getService(EntityManager)).toEqual(entityManager1);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/layer.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { render } from '@testing-library/react';\n\nimport {\n  EditorState,\n  PlaygroundLayer,\n  PlaygroundReactProvider,\n  PlaygroundReactRenderer,\n} from '../src';\nimport { createPlayground } from '../__mocks__/playground-container.mock';\nimport { TestUtilsLayer } from '../__mocks__/layers.mock';\n\ndescribe('Layer', () => {\n  beforeAll(() => {\n    const modules: interfaces.ContainerModule[] = [];\n    // 渲染 playground\n    render(\n      <PlaygroundReactProvider containerModules={modules}>\n        <PlaygroundReactRenderer>\n          <div></div>\n        </PlaygroundReactRenderer>\n      </PlaygroundReactProvider>,\n    );\n  });\n\n  test('layer', () => {\n    const playground = createPlayground();\n    playground.registerLayer(TestUtilsLayer);\n    const testLayer = playground.getLayer(TestUtilsLayer)!;\n    const cache = testLayer.createDOMCache('cache', 'mock children');\n    // 成功创建 cache\n    expect(cache.get()).not.toBeNull();\n    testLayer.node = null as any;\n    // mock 错误 case\n    expect(() => testLayer.createDOMCache('cache')).toThrowError(\n      new Error('DomCache need a parent dom node.'),\n    );\n\n    // 获取鼠标位置方法\n    const pos = testLayer.getPosFromMouseEvent({ clientX: 0, clientY: 0 });\n    expect(pos).toEqual({\n      x: 0,\n      y: 0,\n    });\n    const pos2 = testLayer.getPosFromMouseEvent({ clientX: 0, clientY: 0 }, false);\n    expect(pos2).toEqual({\n      x: 0,\n      y: 0,\n    });\n  });\n\n  test('playground-layer', () => {\n    const playground = createPlayground();\n    playground.registerLayer(PlaygroundLayer);\n    const playgroundLayer = playground.getLayer(PlaygroundLayer);\n    const registry = playground.pipelineRegistry;\n    if (playgroundLayer) {\n      playgroundLayer.options.preventGlobalGesture = true;\n    }\n    playground.ready();\n    let testEventCount = 0;\n    registry.listenGlobalEvent('test-event', () => {\n      testEventCount++;\n      return undefined;\n    });\n    // 调用方法\n    document.dispatchEvent(new Event('test-event'));\n    expect(testEventCount).toEqual(1);\n\n    document.dispatchEvent(new Event('keypress'));\n    document.dispatchEvent(new Event('keyup'));\n\n    registry.renderer.node.parentNode!.dispatchEvent(new Event('wheel'));\n\n    // 切换到 grab 模式\n    // @ts-ignore\n    const editorStateConfig = playgroundLayer?.editorStateConfig;\n    editorStateConfig?.changeState(EditorState.STATE_GRAB.id, new MouseEvent('mousedown') as any);\n    registry.renderer.node.parentNode!.dispatchEvent(new Event('mousedown'));\n    // 注册条件 state, mode = esc\n    editorStateConfig?.registerState({\n      id: 'TEST_STATE',\n      cancelMode: 'esc',\n      handle: () => null,\n    });\n    document.dispatchEvent(new Event('keydown'));\n    editorStateConfig?.changeState('TEST_STATE', new MouseEvent('mousedown') as any);\n    // // 注册条件，mode = once\n    // editorStateConfig?.changeState(\n    //   EditorState.STATE_ZOOM_CENTER.id,\n    //   new MouseEvent('mousedown') as any,\n    // );\n\n    // @ts-ignore\n    editorStateConfig?.onCancel(EditorState.STATE_GRAB.id, () => {});\n    // editorStateConfig?.onCancel(EditorState.STATE_ZOOM_CENTER.id, () => {});\n    editorStateConfig?.onCancel('TEST_STATE', () => {});\n    // 触发 autorun\n    playgroundLayer?.createGesture();\n\n    // 注销 layer\n    playgroundLayer?.dispose();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/pipeline.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { cleanup, render } from '@testing-library/react';\nimport { Message } from '@phosphor/messaging';\n\nimport { PlaygroundReactProvider, PlaygroundReactRenderer } from '../src/react';\nimport { createPlaygroundContainer } from '../src/playground-container';\nimport { createLayerReactAutorun } from '../src/core/pipeline/pipline-react-utils';\nimport { PipelineEntitiesImpl, PipelineMessage } from '../src/core';\nimport { ConfigEntity } from '../src/common';\nimport { createPlayground } from '../__mocks__/playground-container.mock';\nimport {\n  TestUtilsLayer,\n  TestRenderLayer1,\n  TestRenderLayer2,\n  TestRenderLayer3,\n  _TestEntity,\n  MockEntityDataRegistry,\n} from '../__mocks__/layers.mock';\nimport { entityManager } from '../__mocks__/create-entity.mock';\n\nclass TestConfigEntity extends ConfigEntity<any> {\n  static type = TestConfigEntity.name;\n}\n\ndescribe('pipeline render', () => {\n  beforeAll(() => {\n    // Mock the ResizeObserver\n    const ResizeObserverMock = vi.fn(() => ({\n      observe: vi.fn(),\n      unobserve: vi.fn(),\n      disconnect: vi.fn(),\n    }));\n\n    // Stub the global ResizeObserver\n    vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n  });\n  it('should pipeline rendered', () => {\n    const modules: interfaces.ContainerModule[] = [];\n    const playgroundRender = render(\n      <PlaygroundReactProvider containerModules={modules}>\n        <PlaygroundReactRenderer>\n          <div></div>\n        </PlaygroundReactRenderer>\n      </PlaygroundReactProvider>,\n    );\n\n    expect(playgroundRender.asFragment()).toMatchSnapshot();\n  });\n\n  it('should pipeline rendered with flowContainer', () => {\n    const modules: interfaces.ContainerModule[] = [];\n    const playgroundRender = render(\n      <PlaygroundReactProvider\n        containerModules={modules}\n        playgroundContainer={createPlaygroundContainer()}\n      >\n        <PlaygroundReactRenderer />\n      </PlaygroundReactProvider>,\n    );\n\n    expect(playgroundRender.asFragment()).toMatchSnapshot();\n  });\n\n  it('should pipeline rendered with playgroundContext', () => {\n    const modules: interfaces.ContainerModule[] = [];\n    const playgroundRender = render(\n      <PlaygroundReactProvider containerModules={modules} playgroundContext={{}}>\n        <PlaygroundReactRenderer />\n      </PlaygroundReactProvider>,\n    );\n\n    expect(playgroundRender.asFragment()).toMatchSnapshot();\n  });\n\n  it('should pipeline implement entities', () => {\n    const pipelineEntitiesImpl = new PipelineEntitiesImpl(entityManager);\n    expect(pipelineEntitiesImpl.size).toEqual(0);\n    entityManager.registerEntity(TestConfigEntity);\n    expect(pipelineEntitiesImpl.has(TestConfigEntity)).toEqual(false);\n  });\n\n  it('pipeline-react-utils', () => {\n    const playground = createPlayground();\n    let cbSignal = 1;\n    playground.ready();\n    const testLayer = new TestUtilsLayer();\n    playground.registerLayer(TestUtilsLayer);\n    const mockOriginRender = () => <div></div>;\n    const renderer = playground.pipelineRegistry.renderer;\n    renderer.isReady = false;\n    const renderCb = () => {\n      cbSignal++;\n    };\n    const { portal: Portal1, autorun } = createLayerReactAutorun(\n      testLayer,\n      mockOriginRender,\n      renderCb,\n      renderer,\n    );\n    render(<Portal1 />);\n    autorun();\n    // 1. 没有渲染 dom\n    testLayer.setRenderWithReactMemo(false);\n    expect(cbSignal).toEqual(1);\n    renderer.isReady = true;\n    const { portal: Portal2 } = createLayerReactAutorun(\n      testLayer,\n      mockOriginRender,\n      renderCb,\n      renderer,\n    );\n    render(<Portal2 />);\n    // 2. 渲染完成调用 renderCb\n    expect(cbSignal).toEqual(2);\n    const { portal: Portal3 } = createLayerReactAutorun(\n      testLayer,\n      mockOriginRender,\n      renderCb,\n      renderer,\n    );\n    // 3, 使用 undefined 触发 catchError 分支\n    render(<Portal3 />);\n    cleanup();\n    // 4. 渲染空组件\n    const mockOriginRenderReturnNull = () => {};\n    const { portal: Portal4 } = createLayerReactAutorun(\n      testLayer,\n      mockOriginRenderReturnNull,\n      renderCb,\n      renderer,\n    );\n    const { container } = render(<Portal4 />);\n    expect(container).toMatchSnapshot();\n  });\n\n  it('pipeline-registry', () => {\n    const playground = createPlayground();\n    playground.ready();\n    // mock 方法\n    playground.registerLayer(TestRenderLayer3);\n    const registry = playground.pipelineRegistry;\n    // 已经注册的 layer 一定有\n    const testLayer = registry.getLayer(TestRenderLayer3) as TestRenderLayer3;\n    registry.onResizeEmitter.fire({ width: 0, height: 0, clientX: 0, clientY: 0 });\n    registry.onFocusEmitter.fire();\n    registry.onBlurEmitter.fire();\n    registry.onZoomEmitter.fire(1);\n    registry.onScrollEmitter.fire({ scrollX: 0, scrollY: 0 });\n    expect(testLayer.resizeTimes).toEqual(1);\n    expect(testLayer.focusTimes).toEqual(1);\n    expect(testLayer.blurTimes).toEqual(1);\n    expect(testLayer.zoomTimes).toEqual(1);\n    expect(testLayer.scrollTimes).toEqual(1);\n\n    // 模拟事件调用\n    registry.listenPlaygroundEvent('test', () => undefined);\n    registry.renderer.node.parentNode!.dispatchEvent(new Event('test'));\n    registry.renderer.node.parentNode!.dispatchEvent(new Event('test'));\n\n    registry.processMessage(new Message(PipelineMessage.ZOOM));\n    registry.processMessage(new Message(PipelineMessage.SCROLL));\n    registry.processMessage(new Message('123'));\n    expect(testLayer.zoomTimes).toEqual(2);\n    expect(testLayer.scrollTimes).toEqual(2);\n    testLayer.reloadEntities();\n    expect(testLayer.isFocused).toEqual(false);\n  });\n\n  it('pipeline-entites', () => {\n    const playground = createPlayground();\n    playground.ready();\n    // autorun 和 render 分支\n    playground.registerLayer(TestRenderLayer1);\n    playground.registerLayer(TestRenderLayer2);\n    const layer1 = playground.getLayer(TestRenderLayer1)!;\n    const layer2 = playground.getLayer(TestRenderLayer2)!;\n    layer1.autorun();\n    layer2.render();\n    const renderer = playground.pipelineRegistry.renderer;\n    renderer.toReactComponent();\n    const Comp = renderer.toReactComponent();\n    expect(render(<Comp />)).toMatchSnapshot();\n  });\n\n  it('pipeline-entities', () => {\n    const playground = createPlayground();\n    playground.ready();\n    // autorun 和 render 分支\n    playground.registerLayer(TestRenderLayer1);\n    const layer = playground.getLayer(TestRenderLayer1);\n    entityManager.registerEntityData(MockEntityDataRegistry);\n    // mock 数据一定能获取值\n    const data = entityManager.getDataRegistryByType('mock') as any;\n    const observeManager = layer?.observeManager;\n    observeManager?.getEntities(data);\n    observeManager?.createEntity(data, { savedInManager: false });\n    expect(observeManager?.get(data, 'type')).toEqual(undefined);\n    observeManager?.updateConfig(data, {});\n    expect(observeManager?.getConfig(data)).toEqual(undefined);\n    observeManager?.removeEntities(data);\n    // 创建 entity\n    expect(observeManager?.createEntity(_TestEntity).type).toEqual('test-entity');\n    // 第一次运行 set\n    observeManager?.getEntityDatas(data, data);\n    // 第二次运行有 cache\n    observeManager?.getEntityDatas(data, data);\n    expect(observeManager?.getConfig(data)).toEqual(undefined);\n    // 打印所有的 observeEntities\n    for (let i of observeManager!) {\n      expect(i).toMatchSnapshot();\n    }\n  });\n\n  it('pipeline-entities-selector', () => {\n    const playground = createPlayground();\n    playground.ready();\n    const selector = playground.pipelineRegistry.selector;\n    playground.registerLayer(TestRenderLayer1);\n    const layer1 = playground.getLayer(TestRenderLayer1)!;\n    // mock EntityData\n    entityManager.registerEntityData(MockEntityDataRegistry);\n    const data = entityManager.getDataRegistryByType('mock') as any;\n    selector.subscribleEntityByData(layer1, _TestEntity, data);\n    const { datas } = selector.getLayerEntityDatas(layer1);\n    expect(datas?.length).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/playground-contribution.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { render, cleanup } from '@testing-library/react';\n\nimport {\n  Playground,\n  createPlaygroundContainer,\n  PlaygroundReactProvider,\n  PlaygroundReactRenderer,\n  PlaygroundContribution,\n  Layer,\n  createPlaygroundPlugin,\n} from '../src';\n\nexport class Layer1 extends Layer {\n  renderTimes = 0;\n\n  render = () => {\n    this.renderTimes += 1;\n    return <div></div>;\n  };\n}\n\nexport class Layer2 extends Layer {\n  renderTimes = 0;\n\n  render = () => {\n    this.renderTimes += 1;\n    return <div></div>;\n  };\n}\n\ndescribe('playground-contribution', () => {\n  it('contribution bind', () => {\n    const container = createPlaygroundContainer();\n    const lifecycle: string[] = [];\n    container.bind(PlaygroundContribution).toConstantValue({\n      onInit(playground: Playground) {\n        playground.registerLayer(Layer1);\n        playground.registerLayer(Layer2);\n        playground.onAllLayersRendered(() => {\n          lifecycle.push('onAllLayersRendered2');\n        });\n        lifecycle.push('onInit');\n      },\n      onReady(playground: Playground) {\n        expect(playground.getLayer(Layer1)!.renderTimes).toEqual(0);\n        lifecycle.push('onReady');\n      },\n      onAllLayersRendered(playground: Playground) {\n        expect(playground.getLayer(Layer1)!.renderTimes).toEqual(1);\n        expect(playground.getLayer(Layer2)!.renderTimes).toEqual(1);\n        lifecycle.push('onAllLayersRendered');\n      },\n      onDispose(playground: Playground) {\n        lifecycle.push('onDispose');\n      },\n    } as PlaygroundContribution);\n    render(\n      <PlaygroundReactProvider playgroundContainer={container}>\n        <PlaygroundReactRenderer />\n      </PlaygroundReactProvider>,\n    );\n    expect(lifecycle).toEqual(['onInit', 'onReady', 'onAllLayersRendered', 'onAllLayersRendered2']);\n    cleanup();\n    expect(lifecycle).toEqual([\n      'onInit',\n      'onReady',\n      'onAllLayersRendered',\n      'onAllLayersRendered2',\n      'onDispose',\n    ]);\n  });\n  it('createPlaygroundPlugin', () => {\n    const container = createPlaygroundContainer();\n    const lifecycle: string[] = [];\n    render(\n      <PlaygroundReactProvider\n        playgroundContainer={container}\n        plugins={() => [\n          createPlaygroundPlugin({\n            onInit(ctx) {\n              ctx.playground.registerLayer(Layer1);\n              ctx.playground.registerLayer(Layer2);\n              lifecycle.push('onInit');\n            },\n            onReady(ctx) {\n              expect(ctx.playground.getLayer(Layer1)!.renderTimes).toEqual(0);\n              lifecycle.push('onReady');\n            },\n            onAllLayersRendered(ctx) {\n              expect(ctx.playground.getLayer(Layer1)!.renderTimes).toEqual(1);\n              expect(ctx.playground.getLayer(Layer2)!.renderTimes).toEqual(1);\n              lifecycle.push('onAllLayersRendered');\n            },\n            onDispose() {\n              lifecycle.push('onDispose');\n            },\n          }),\n        ]}\n      >\n        <PlaygroundReactRenderer />\n      </PlaygroundReactProvider>,\n    );\n    expect(lifecycle).toEqual(['onInit', 'onReady', 'onAllLayersRendered']);\n    cleanup();\n    expect(lifecycle).toEqual(['onInit', 'onReady', 'onAllLayersRendered', 'onDispose']);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/playground-mock-tools.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\nimport { ContainerModule } from 'inversify';\n\nimport { EntityManager, PlaygroundLayer, PlaygroundMockTools, Playground, Layer } from '../src';\n\nconst { createContainer, createLayerTestState, createPlayground } = PlaygroundMockTools;\n\ndescribe('playground-mock-tools', () => {\n  it('createContainer', () => {\n    const container = createContainer();\n    expect(container.get(EntityManager)).toBeInstanceOf(EntityManager);\n    const container2 = createContainer([\n      new ContainerModule((bind) => {\n        bind('abc').toConstantValue('abc');\n      }),\n    ]);\n    expect(container2.get('abc')).toEqual('abc');\n  });\n  it('createPlayground', () => {\n    const playground = createPlayground();\n    expect(playground).toBeInstanceOf(Playground);\n  });\n  it('createLayerTestState base', async () => {\n    class MockLayer extends Layer<any> {\n      onReady() {}\n\n      onResize() {}\n\n      onFocus() {}\n\n      onBlur() {}\n\n      onZoom() {}\n\n      onScroll() {}\n\n      onViewportChange() {}\n\n      onReadonlyOrDisabledChange() {}\n\n      autorun() {}\n    }\n\n    const layer = createLayerTestState(MockLayer);\n    expect(layer.onReady.mock.calls.length).toEqual(1);\n    // resize 会触发一次\n    expect(layer.onResize.mock.calls.length).toEqual(0);\n    expect(layer.onViewportChange.mock.calls.length).toEqual(0);\n    expect(layer.autorun.mock.calls.length).toEqual(1);\n    expect(layer.onFocus.mock.calls.length).toEqual(0);\n    expect(layer.onBlur.mock.calls.length).toEqual(0);\n    expect(layer.onZoom.mock.calls.length).toEqual(0);\n    expect(layer.onScroll.mock.calls.length).toEqual(0);\n    expect(layer.onReadonlyOrDisabledChange.mock.calls.length).toEqual(0);\n    layer.playground.focus();\n    expect(layer.onFocus.mock.calls.length).toEqual(1);\n    layer.playground.focus(); // 重复触发 focus\n    expect(layer.onFocus.mock.calls.length).toEqual(1);\n    layer.playground.blur();\n    expect(layer.onBlur.mock.calls.length).toEqual(1);\n    layer.playground.blur(); // 重复触发 blur\n    expect(layer.onBlur.mock.calls.length).toEqual(1);\n    layer.playground.config.updateConfig({\n      zoom: 0.8,\n    });\n    expect(layer.onZoom.mock.calls).toEqual([[0.8]]);\n    expect(layer.onViewportChange.mock.calls.length).toEqual(1);\n    layer.playground.config.updateConfig({\n      scrollX: 100,\n      scrollY: 100,\n    });\n    expect(layer.onScroll.mock.calls).toEqual([\n      [\n        {\n          scrollX: 100,\n          scrollY: 100,\n        },\n      ],\n    ]);\n    expect(layer.onViewportChange.mock.calls.length).toEqual(2);\n    layer.playground.resize({\n      width: 100,\n      height: 100,\n    });\n    expect(layer.onResize.mock.calls).toEqual([[{ width: 100, height: 100 }]]);\n    expect(layer.onViewportChange.mock.calls.length).toEqual(3);\n    layer.playground.config.readonly = true;\n    layer.playground.config.disabled = true;\n    expect(layer.onReadonlyOrDisabledChange.mock.calls).toEqual([\n      [{ readonly: true, disabled: false }],\n      [{ readonly: true, disabled: true }],\n    ]);\n  });\n  it('createLayerTestState playgorundLayer', () => {\n    const layerState = createLayerTestState(PlaygroundLayer);\n    expect(layerState.onReady.mock.calls.length).toEqual(1);\n    expect(layerState.autorun.mock.calls.length).toEqual(1);\n    layerState.playground.config.updateConfig({\n      scrollX: 100,\n    });\n    expect(layerState.onReady.mock.calls.length).toEqual(1);\n    expect(layerState.autorun.mock.calls.length).toEqual(2);\n    layerState.playground.resize({\n      width: 100,\n      height: 100,\n    });\n    expect(layerState.autorun.mock.calls.length).toEqual(3);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/playground-react.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useRef, useEffect } from 'react';\n\nimport { expect } from 'vitest';\nimport { render } from '@testing-library/react';\n\nimport { PluginContext, PlaygroundReactProvider, PlaygroundReactRenderer } from '../src';\n\ndescribe('PlaygroundReact', () => {\n  test('ref', () => {\n    function PlaygroundDemo() {\n      const ref = useRef<PluginContext>();\n      useEffect(() => {\n        expect(ref.current!.playground).toBeDefined();\n      }, []);\n      return (\n        <PlaygroundReactProvider ref={ref}>\n          <PlaygroundReactRenderer>\n            <div></div>\n          </PlaygroundReactRenderer>\n        </PlaygroundReactProvider>\n      );\n    }\n    render(<PlaygroundDemo />);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/playground.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\n\nimport { createPlayground } from '../__mocks__/playground-container.mock';\nimport { createEntity } from '../__mocks__/create-entity.mock';\n\ndescribe('playground', () => {\n  it('should render playground-layer', () => {\n    const playground = createPlayground();\n    expect(playground?.pipelineNode.innerHTML).toMatchSnapshot();\n  });\n\n  it('should playground-layer config normal', () => {\n    const playground = createPlayground();\n    const scrollLimitCallback = vi.fn(() => ({\n      scrollX: 200,\n      scrollY: 200,\n    }));\n    const playgroundConfig = playground.config;\n    playgroundConfig.addScrollLimit(scrollLimitCallback);\n    playgroundConfig.updateConfig({\n      originX: 0,\n      originY: 0,\n      scrollLimitX: 200,\n      scrollLimitY: 200,\n      overflowX: 'hidden',\n      overflowY: 'hidden',\n      zoom: 1,\n    });\n    playgroundConfig.updateConfig({\n      scrollX: -100,\n      scrollY: -100,\n      scrollLimitX: 200,\n      scrollLimitY: 200,\n      overflowX: 'hidden',\n      overflowY: 'hidden',\n      zoom: 2.5,\n    });\n    playgroundConfig.updateCursor('pointer');\n    expect(\n      playgroundConfig.getPosFromMouseEvent({\n        clientX: 200,\n        clientY: 200,\n      })\n    ).toMatchSnapshot();\n    expect(\n      playgroundConfig.getPosFromMouseEvent(\n        {\n          clientX: 200,\n          clientY: 200,\n        },\n        false\n      )\n    ).toMatchSnapshot();\n    expect(\n      playgroundConfig.toFixedPos({\n        x: 200,\n        y: 200,\n      })\n    ).toMatchSnapshot();\n    expect(playgroundConfig.getViewport()).toMatchSnapshot();\n    expect(playgroundConfig.getViewport(false)).toMatchSnapshot();\n    playgroundConfig.readonly = false;\n    playgroundConfig.disabled = false;\n    expect(playgroundConfig.readonly).toEqual(false);\n    expect(playgroundConfig.disabled).toEqual(false);\n    playgroundConfig.zoomEnable = false;\n    expect(playgroundConfig.zoomEnable).toEqual(false);\n    playgroundConfig.loading = true;\n    expect(playgroundConfig.loading).toEqual(true);\n    playgroundConfig.updateZoom(2.5);\n    playgroundConfig.zoomEnable = true;\n    playgroundConfig.updateZoom(0.1);\n    // 放大\n    playgroundConfig.zoomin();\n    // 缩小\n    playgroundConfig.zoomout();\n  });\n  it('should scroll normally', () => {\n    const playground = createPlayground();\n    const playgroundConfig = playground.config;\n    const bounds = new Rectangle(50, 50, 200, 200);\n    expect(playgroundConfig.isViewportVisible(bounds)).toEqual(false);\n    const mockEntity = createEntity();\n    playgroundConfig.scrollToView({\n      entities: [mockEntity],\n    });\n    playgroundConfig.scrollToView({\n      position: { x: 200, y: 200 },\n    });\n    playgroundConfig.scrollToView();\n    playgroundConfig.scroll({ scrollX: 400 });\n    playgroundConfig.scroll({ scrollX: 400 }, false);\n    playgroundConfig.fixLayerPosition(playground?.pipelineNode);\n    playgroundConfig.setPageBounds(bounds);\n    expect(playgroundConfig.getPageBounds()).toEqual(bounds);\n    playgroundConfig.scrollPageBoundsToCenter();\n  });\n  it('should playground all layers rendered', () => {\n    // mock E2E 测试环境调用函数\n    vi.stubGlobal('REPORT_TTI_FOR_E2E', vi.fn());\n\n    const playground = createPlayground();\n    const renderer = playground.pipelineRegistry.renderer;\n\n    // 设置每一个 layer 都渲染完成，mock TTI 上报\n    renderer.layers.forEach((layer) => {\n      renderer.reportLayerRendered(layer);\n    });\n    const allLayersRendered = Array.from(renderer.layerRenderedMap.values()).every((v) => v);\n    expect(allLayersRendered).toEqual(true);\n  });\n  it('scrollDisable', () => {\n    const playground = createPlayground();\n    playground.config.scrollDisable = true;\n    playground.config.updateConfig({\n      scrollX: 10000,\n      scrollY: 10000,\n    });\n    expect(playground.config.scrollData).toEqual({ scrollX: 0, scrollY: 0 });\n    playground.config.scrollDisable = false;\n    playground.config.updateConfig({\n      scrollX: 10000,\n      scrollY: 10000,\n    });\n    expect(playground.config.scrollData).toEqual({ scrollX: 10000, scrollY: 10000 });\n  });\n  it('zoomDisable', () => {\n    const playground = createPlayground();\n    playground.config.zoomDisable = true;\n    playground.config.updateConfig({\n      zoom: 1.3,\n    });\n    expect(playground.config.zoom).toEqual(1);\n    playground.config.zoomDisable = false;\n    playground.config.updateConfig({\n      zoom: 1.3,\n    });\n    expect(playground.config.zoom).toEqual(1.3);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/plugin.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces, ContainerModule } from 'inversify';\n\nimport {\n  createPlaygroundPlugin,\n  definePluginCreator,\n  Playground,\n  createPlaygroundContainer,\n  PluginContext,\n  loadPlugins,\n  createPluginContextDefault,\n} from '../src';\n\ndescribe('playground plugin', () => {\n  let container: interfaces.Container;\n  let playground: Playground;\n  let getPluginContext = () => container.get<PluginContext>(PluginContext);\n  beforeEach(() => {\n    container = createPlaygroundContainer();\n    playground = container.get<Playground>(Playground);\n  });\n  it('createPlaygroundPlugin', () => {\n    const customModel1 = {};\n    let isInit = false;\n    let isReady = false;\n    let isDispose = false;\n    const customPlugin = createPlaygroundPlugin({\n      onBind({ bind }) {\n        bind('customModel1').toConstantValue(customModel1);\n      },\n      onInit(ctx) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        isInit = true;\n      },\n      onReady(ctx) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        isReady = true;\n      },\n      onDispose(ctx) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        isDispose = true;\n      },\n    });\n    loadPlugins([customPlugin], container);\n    // check plugin context\n    expect(getPluginContext().playground).toEqual(playground);\n    expect(getPluginContext().get('customModel1')).toEqual(customModel1);\n    expect(getPluginContext().getAll('customModel1')).toEqual([customModel1]);\n    playground.init();\n    expect(isInit).toEqual(true);\n    expect(isReady).toEqual(false);\n    playground.ready();\n    expect(isReady).toEqual(true);\n    expect(isDispose).toEqual(false);\n    playground.dispose();\n    expect(isDispose).toEqual(true);\n  });\n  it('custom container modules', () => {\n    const customModel2 = {};\n    const customContainerModules = new ContainerModule((bind) =>\n      bind('customModel2').toConstantValue(customModel2)\n    );\n    const customPlugin = createPlaygroundPlugin({\n      containerModules: [customContainerModules],\n    });\n    loadPlugins([customPlugin], container);\n    expect(getPluginContext().get('customModel2')).toEqual(customModel2);\n  });\n  it('definePluginCreator', () => {\n    const someOpts = { isOpts1: true };\n    const someOpts2 = { isOpts2: true };\n\n    let times = 0;\n    const createMinePlugin = definePluginCreator<any>({\n      onInit(ctx, opts) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        expect(opts).toEqual(someOpts);\n        times++;\n      },\n      onReady(ctx, opts) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        expect(opts).toEqual(someOpts);\n        times++;\n      },\n      onDispose(ctx, opts) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        expect(opts).toEqual(someOpts);\n        times++;\n      },\n    });\n    loadPlugins([createMinePlugin(someOpts), createMinePlugin(someOpts2)], container);\n    playground.init();\n    playground.ready();\n    playground.dispose();\n    expect(times).toEqual(3);\n  });\n  it('definePluginCreator with contribution', () => {\n    const contributionKey1 = Symbol('contributionKey');\n    const contributionKey2 = Symbol('contributionKey2');\n    const createMinePlugin = definePluginCreator({\n      contributionKeys: [contributionKey1, contributionKey2],\n    });\n    loadPlugins(\n      [createMinePlugin({ contrib1: true }), createMinePlugin({ contrib2: true })],\n      container\n    );\n    expect(getPluginContext().getAll(contributionKey1)).toEqual([\n      { contrib1: true },\n      { contrib2: true },\n    ]);\n    expect(getPluginContext().getAll(contributionKey2)).toEqual([\n      { contrib1: true },\n      { contrib2: true },\n    ]);\n  });\n  it('load simple plugin', () => {\n    const allOpts: any[] = [];\n    const createMinePlugin = definePluginCreator<any>({\n      onInit(ctx, opts) {\n        allOpts.push(opts);\n      },\n    });\n    loadPlugins([createMinePlugin({ v: 1 }), createMinePlugin({ v: 2 })], container);\n    playground.init();\n    // 只加载第一个\n    expect(allOpts).toEqual([{ v: 1 }]);\n  });\n  it('load simple plugin from different playground', () => {\n    const allOpts: any[] = [];\n    const container1 = createPlaygroundContainer();\n    const playground1 = container1.get<Playground>(Playground);\n    const container2 = createPlaygroundContainer();\n    const playground2 = container2.get<Playground>(Playground);\n    const createMinePlugin = definePluginCreator<any>({\n      onInit(ctx, opts) {\n        allOpts.push(opts);\n      },\n    });\n    loadPlugins([createMinePlugin({ v: 1 }), createMinePlugin({ v: 2 })], container1);\n    playground1.init();\n    loadPlugins([createMinePlugin({ v: 3 }), createMinePlugin({ v: 4 })], container2);\n    playground2.init();\n    // 两个画布使用插件不互相干扰\n    expect(allOpts).toEqual([{ v: 1 }, { v: 3 }]);\n  });\n  it('rebind plugin context', () => {\n    container\n      .rebind(PluginContext)\n      .toDynamicValue((ctx) => ({\n        ...createPluginContextDefault(ctx.container),\n        document: { isDocument: true },\n      }))\n      .inSingletonScope();\n    let isInit = false;\n    const customPlugin = createPlaygroundPlugin<any>({\n      onInit(ctx) {\n        expect(ctx).toEqual(container.get(PluginContext));\n        expect(ctx.document.isDocument).toEqual(true);\n        isInit = true;\n      },\n    });\n    loadPlugins([customPlugin], container);\n    playground.init();\n    expect(isInit).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/react-hooks.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { expect } from 'vitest';\nimport { describe, it } from 'vitest';\nimport { ContainerModule, injectable, interfaces } from 'inversify';\nimport { renderHook } from '@testing-library/react-hooks';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport {\n  useEntities,\n  useConfigEntity,\n  useEntityDataFromContext,\n  useEntityFromContext,\n  useListenEvents,\n  usePlaygroundContext,\n  useRefresh,\n  useService,\n} from '../src/react-hooks';\nimport {\n  PlaygroundEntityContext,\n  PlaygroundReactContainerContext,\n  PlaygroundReactContext,\n  PlaygroundReactProvider,\n  PlaygroundReactRenderer,\n} from '../src/react';\nimport { createPlaygroundContainer } from '../src/playground-container';\nimport { Playground } from '../src/playground';\nimport { ConfigEntity, createConfigDataRegistry } from '../src/common';\nimport { createPlayground } from '../__mocks__/playground-container.mock';\n\nclass MockEntity extends ConfigEntity {\n  public mockType: string = 'mock-entity';\n\n  change() {\n    this.fireChange();\n  }\n}\n\n@injectable()\nclass MockService {\n  invoked = false;\n\n  setInvoked() {\n    this.invoked = true;\n  }\n}\n\nconst mockContainerModule = new ContainerModule((bind) => {\n  bind(MockService).toSelf().inSingletonScope();\n});\n\nconst modules: interfaces.ContainerModule[] = [mockContainerModule];\n\n// 构建环境，保证 hook 内部的 usePlaygroundContainer 能获取到值\nconst wrapper = ({ children }: { children: any }) => (\n  <PlaygroundReactProvider containerModules={modules}>\n    <PlaygroundReactRenderer>\n      <PlaygroundReactContext.Provider value={{ a: 1 }}>{children}</PlaygroundReactContext.Provider>\n    </PlaygroundReactRenderer>\n  </PlaygroundReactProvider>\n);\n\ndescribe('react-hooks', () => {\n  it('use-entities', () => {\n    const playground = createPlayground();\n    playground.entityManager.registerEntity(MockEntity);\n    const entity = new MockEntity({\n      entityManager: playground.entityManager,\n    });\n    renderHook(() => useEntities(MockEntity), { wrapper });\n    playground.entityManager.fireEntityChanged(entity);\n  });\n\n  it('use-entity', () => {\n    const { result } = renderHook(() => useConfigEntity<MockEntity>(MockEntity), { wrapper });\n    expect(result.current?.mockType).toEqual('mock-entity');\n    // 调用 firechange\n    const prevVersion = result.current?.version;\n    result.current?.change();\n    // 触发 onEntityChange，version + 1\n    expect(result.current?.version).toEqual(prevVersion + 1);\n  });\n\n  it('use-entity-from-context', () => {\n    const playground = createPlayground();\n    playground.entityManager.registerEntity(MockEntity);\n    const entity = new MockEntity({\n      entityManager: playground.entityManager,\n    });\n    const playgroundEntityContextWrapper = ({ children }: { children: any }) => (\n      <PlaygroundEntityContext.Provider value={entity}>\n        {wrapper({ children })}\n      </PlaygroundEntityContext.Provider>\n    );\n    const { result } = renderHook(() => useEntityFromContext(), {\n      wrapper,\n    });\n    expect(() => result.current).toThrowError(\n      new Error('[useEntityFromContext] Unknown entity from \"PlaygroundEntityContext\"')\n    );\n    const { result: resultWithoutError } = renderHook(() => useEntityFromContext(true), {\n      wrapper: playgroundEntityContextWrapper,\n    });\n    const prevVersion = resultWithoutError.current.version;\n    (resultWithoutError.current as MockEntity)?.change();\n    expect(resultWithoutError.current.version).toEqual(prevVersion + 1);\n  });\n\n  it('use-entity-data-from-context', () => {\n    const container = createPlaygroundContainer();\n    const playground = container.get(Playground);\n    playground.entityManager.registerEntity(MockEntity);\n    const entity = new MockEntity({\n      entityManager: playground.entityManager,\n    });\n\n    const containerWrapper = ({ children }: { children: any }) => (\n      <PlaygroundReactContainerContext.Provider value={container}>\n        <PlaygroundEntityContext.Provider value={entity}>\n          {/* {wrapper({ children })} */}\n          {children}\n        </PlaygroundEntityContext.Provider>\n      </PlaygroundReactContainerContext.Provider>\n    );\n    const dataRegistry = createConfigDataRegistry(entity);\n    renderHook(() => useEntityDataFromContext(dataRegistry), {\n      wrapper: containerWrapper,\n    });\n  });\n\n  it('use-playground-context', () => {\n    const { result } = renderHook(() => usePlaygroundContext(), {\n      wrapper,\n    });\n    expect(result.current).toEqual({ a: 1 });\n  });\n\n  it('use-listen-events with use-refresh', () => {\n    const eventEmitter = new Emitter<number>();\n    const event = eventEmitter.event;\n\n    renderHook(() => useListenEvents(event));\n    const { result } = renderHook(() => useRefresh());\n    result.current?.(1);\n  });\n\n  it('use-listen-events', async () => {\n    const eventEmitter = new Emitter<any>();\n    const event = eventEmitter.event;\n\n    let count = 0;\n    renderHook(() => {\n      useListenEvents(event);\n      count++;\n    });\n\n    expect(count).toBe(1);\n\n    // fire with empty string\n    eventEmitter.fire('');\n    expect(count).toBe(2);\n    eventEmitter.fire('');\n    expect(count).toBe(3);\n    eventEmitter.fire('');\n    expect(count).toBe(4);\n\n    // fire with 0\n    eventEmitter.fire(0);\n    expect(count).toBe(5);\n    eventEmitter.fire(0);\n    expect(count).toBe(6);\n    eventEmitter.fire(0);\n    expect(count).toBe(7);\n\n    // fire with the same object\n    const object = {};\n    eventEmitter.fire(object);\n    expect(count).toBe(8);\n    eventEmitter.fire(object);\n    expect(count).toBe(9);\n    eventEmitter.fire(object);\n    expect(count).toBe(10);\n\n    // fire with undefined\n    eventEmitter.fire(undefined);\n    expect(count).toBe(11);\n    eventEmitter.fire(undefined);\n    expect(count).toBe(12);\n    eventEmitter.fire(undefined);\n    expect(count).toBe(13);\n  });\n\n  it('use-service', () => {\n    const { result } = renderHook(() => useService<MockService>(MockService), { wrapper });\n    // 触发一次重渲染\n    result.current?.setInvoked();\n    expect(result.current?.invoked).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/schema.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container } from 'inversify';\n\nimport {\n  Entity,\n  EntityManager,\n  PlaygroundContext,\n  OpacityData,\n  OpacitySchemaDecoration,\n  OriginData,\n  OriginSchemaDecoration,\n  PositionData,\n  PositionSchemaDecoration,\n  RotationSchemaDecoration,\n  RotationData,\n  ScaleData,\n  ScaleSchemaDecoration,\n  SizeData,\n  SizeSchema,\n  SizeSchemaDecoration,\n  SkewData,\n  SkewSchemaDecoration,\n} from '../src';\n\nfunction createContainer(): Container {\n  const container = new Container({ defaultScope: 'Singleton' });\n  container.bind(PlaygroundContext).toConstantValue({});\n  container.bind(EntityManager).toSelf();\n  return container;\n}\n\ndescribe('core/common/schema', () => {\n  let container: Container;\n\n  beforeEach(() => {\n    container = createContainer();\n  });\n\n  function createEntity() {\n    return container.get<EntityManager>(EntityManager).createEntity<Entity>(Entity);\n  }\n\n  test('OpacityData', async () => {\n    expect(new OpacityData(createEntity()).getDefaultData()).toEqual(1);\n    expect(OpacitySchemaDecoration).not.toBeUndefined();\n  });\n\n  test('OriginData', async () => {\n    const data = new OriginData(createEntity());\n    expect(data.getDefaultData()).toEqual({ x: 0.5, y: 0.5 });\n\n    data.x = 1;\n    data.y = 1;\n    expect(data.x).toBeCloseTo(1);\n    expect(data.y).toBeCloseTo(1);\n\n    expect(OriginSchemaDecoration).not.toBeUndefined();\n  });\n\n  test('PositionData', async () => {\n    const data = new PositionData(createEntity());\n    expect(data.getDefaultData()).toEqual({ x: 0, y: 0 });\n\n    data.x = 1;\n    data.y = 1;\n    expect(data.x).toBeCloseTo(1);\n    expect(data.y).toBeCloseTo(1);\n\n    expect(PositionSchemaDecoration).not.toBeUndefined();\n  });\n\n  test('RotationData', async () => {\n    const data = new RotationData(createEntity());\n    expect(data.getDefaultData()).toEqual(0);\n    expect(RotationSchemaDecoration).not.toBeUndefined();\n  });\n\n  test('ScaleData', async () => {\n    const data = new ScaleData(createEntity());\n    expect(data.getDefaultData()).toEqual({ x: 1, y: 1 });\n\n    data.x = 2;\n    data.y = 2;\n    expect(data.x).toBeCloseTo(2);\n    expect(data.y).toBeCloseTo(2);\n\n    expect(ScaleSchemaDecoration).not.toBeUndefined();\n  });\n\n  test('SizeData ', async () => {\n    const data = new SizeData(createEntity());\n    expect(data.getDefaultData()).toEqual({ width: 0, height: 0, locked: false });\n\n    data.width = 1;\n    data.height = 1;\n    data.locked = true;\n    expect(data.width).toBeCloseTo(1);\n    expect(data.height).toBeCloseTo(1);\n    expect(data.locked).toBeTruthy();\n\n    expect(SizeSchema).not.toBeUndefined();\n    expect(SizeSchemaDecoration).not.toBeUndefined();\n  });\n\n  test('SkewData', async () => {\n    const data = new SkewData(createEntity());\n    expect(data.getDefaultData()).toEqual({ x: 0, y: 0 });\n\n    data.x = 1;\n    data.y = 1;\n    expect(data.x).toBeCloseTo(1);\n    expect(data.y).toBeCloseTo(1);\n\n    expect(SkewSchemaDecoration).not.toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/selection.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi } from 'vitest';\n\nimport { createPlayground } from '../__mocks__/playground-container.mock';\n\nit('selectionService', () => {\n  const playground = createPlayground();\n  const selection = playground.selectionService;\n\n  const onSelectionCallback = vi.fn();\n  selection.onSelectionChanged(onSelectionCallback);\n  const selectEntities = [playground.config];\n  selection.selection = selectEntities;\n  expect(selection.selection).toEqual(selectEntities);\n  expect(onSelectionCallback.mock.calls[0]).toEqual([selectEntities]);\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/services/clipboard-service.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, beforeEach, it, expect, vi } from 'vitest';\n\nimport { DefaultClipboardService } from '../../src/services';\n\ndescribe('ClipboardService', () => {\n  beforeEach(() => {});\n  it('base', () => {\n    const service = new DefaultClipboardService();\n    const onChange = vi.fn();\n    service.onClipboardChanged(onChange);\n    service.writeText('abc');\n    expect(service.readText()).toEqual('abc');\n    expect(onChange.mock.calls).toEqual([['abc']]);\n    service.writeText('abc');\n    expect(onChange.mock.calls).toEqual([['abc']]);\n    service.writeText('abc2'); // no change\n    expect(onChange.mock.calls).toEqual([['abc'], ['abc2']]);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/services/storage-service.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, beforeEach, it, expect, vi } from 'vitest';\n\nimport { LocalStorageService, StorageService } from '../../src/services';\nimport { createPlaygroundContainer } from '../../src';\n\ndescribe('LocalStorageService', () => {\n  beforeEach(() => {});\n  it('storage-service set get data', () => {\n    const container = createPlaygroundContainer();\n    const service: LocalStorageService = container.get(StorageService);\n\n    service.setData('key1', 'value1');\n    expect(service.getData('key1')).toEqual('value1');\n    expect(service.getData('key2', 'defaultValue')).toEqual('defaultValue');\n    const MOCK_PREFIX = '_test_prefix';\n    service.setPrefix(MOCK_PREFIX);\n    const MOCK_KEY = '123';\n    expect(service.prefix(MOCK_KEY)).toEqual(`${MOCK_PREFIX}${MOCK_KEY}`);\n  });\n\n  it('window undefined', () => {\n    const container = createPlaygroundContainer();\n\n    // 触发一次 postConstruct\n    vi.stubGlobal('window', undefined);\n    const service: LocalStorageService = container.get(StorageService);\n\n    // 赋值为 {} 正常使用\n    service.setData('key1', 'value1');\n    expect(service.getData('key1')).toEqual('value1');\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/transform-schema.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container } from 'inversify';\nimport { type IPoint, PI, Rectangle, type SizeSchema } from '@flowgram.ai/utils';\n\nimport {\n  // AbleManager,\n  Entity,\n  EntityManager,\n  PlaygroundContext,\n  TransformData,\n  TransformSchema,\n  TransformSchemaDecoration,\n} from '../src';\n\nfunction createContainer(): Container {\n  const child = new Container({ defaultScope: 'Singleton' });\n  // child.bind(AbleManager).toSelf()\n  child.bind(PlaygroundContext).toConstantValue({});\n  child.bind(EntityManager).toSelf();\n  return child;\n}\n\nconst container = createContainer();\n\nfunction createEntity(): Entity {\n  return container.get<EntityManager>(EntityManager).createEntity<Entity>(Entity);\n}\n\nfunction createTransform(entity?: Entity): TransformData {\n  return new TransformData(entity ?? createEntity());\n}\n\nfunction getIds(transform: TransformData): { localID: number; worldID: number } {\n  return {\n    localID: transform.localID,\n    worldID: transform.worldID,\n  };\n}\n\nfunction expectRectangle(target: Rectangle, arr: number[]): void {\n  expect(target.x).toBeCloseTo(arr[0]);\n  expect(target.y).toBeCloseTo(arr[1]);\n  expect(target.width).toBeCloseTo(arr[2]);\n  expect(target.height).toBeCloseTo(arr[3]);\n}\n\nfunction expectSize(target: SizeSchema, arr: number[]): void {\n  expect(target.width).toBeCloseTo(arr[0]);\n  expect(target.height).toBeCloseTo(arr[1]);\n}\n\nfunction expectIPoint(target: IPoint, arr: number[]): void {\n  expect(target.x).toBeCloseTo(arr[0]);\n  expect(target.y).toBeCloseTo(arr[1]);\n}\n\ndescribe('Playground.schema.transform', () => {\n  it('should decompose negative scale into rotation', () => {\n    const eps = 1e-3;\n\n    const transform = createTransform();\n    const parent = createTransform();\n    const otherTransform = createTransform();\n\n    transform.position.x = 20;\n    transform.position.y = 10;\n    transform.scale.x = -2;\n    transform.scale.y = -3;\n    transform.rotation = Math.PI / 6;\n    transform.setParent(parent);\n\n    otherTransform.setFromMatrix(transform.worldTransform);\n\n    const { position, scale, skew } = otherTransform;\n\n    expect(position.x).toBeCloseTo(20, eps);\n    expect(position.y).toBeCloseTo(10, eps);\n    expect(scale.x).toBeCloseTo(2, eps);\n    expect(scale.y).toBeCloseTo(3, eps);\n    expect(skew.x).toEqual(0);\n    expect(skew.y).toEqual(0);\n    expect(otherTransform.rotation).toBeCloseTo((-5 * Math.PI) / 6, eps);\n  });\n\n  it('should decompose mirror into skew', () => {\n    const eps = 1e-3;\n\n    const transform = createTransform();\n    const parent = createTransform();\n    const otherTransform = createTransform();\n\n    transform.position.x = 20;\n    transform.position.y = 10;\n    transform.scale.x = 2;\n    transform.scale.y = -3;\n    transform.rotation = Math.PI / 6;\n    transform.setParent(parent);\n\n    otherTransform.setFromMatrix(transform.worldTransform);\n\n    const { position, scale, skew } = otherTransform;\n\n    expect(position.x).toBeCloseTo(20, eps);\n    expect(position.y).toBeCloseTo(10, eps);\n    expect(scale.x).toBeCloseTo(2, eps);\n    expect(scale.y).toBeCloseTo(3, eps);\n    expect(skew.x).toBeCloseTo((5 * Math.PI) / 6, eps);\n    expect(skew.y).toBeCloseTo(Math.PI / 6, eps);\n    expect(otherTransform.rotation).toEqual(0);\n  });\n\n  it('should apply skew before scale, like in adobe animate and spine', () => {\n    // this example looks the same in CSS and in PIXI, made with PIXI-animate by @bigtimebuddy\n\n    const eps = 1e-3;\n\n    const transform = createTransform();\n    const parent = createTransform();\n    const otherTransform = createTransform();\n\n    transform.position.x = 387.8;\n    transform.position.y = 313.95;\n    transform.scale.x = 0.572;\n    transform.scale.y = 4.101;\n    transform.skew.x = -0.873;\n    transform.skew.y = 0.175;\n    transform.setParent(parent);\n\n    const mat = transform.worldTransform;\n\n    expect(mat.a).toBeCloseTo(0.563, eps);\n    expect(mat.b).toBeCloseTo(0.1, eps);\n    expect(mat.c).toBeCloseTo(-3.142, eps);\n    expect(mat.d).toBeCloseTo(2.635, eps);\n    expect(mat.tx).toBeCloseTo(387.8, eps);\n    expect(mat.ty).toBeCloseTo(313.95, eps);\n\n    otherTransform.setFromMatrix(transform.worldTransform);\n\n    const { position } = otherTransform;\n    const { scale } = otherTransform;\n    const { skew } = otherTransform;\n\n    expect(position.x).toBeCloseTo(387.8, eps);\n    expect(position.y).toBeCloseTo(313.95, eps);\n    expect(scale.x).toBeCloseTo(0.572, eps);\n    expect(scale.y).toBeCloseTo(4.101, eps);\n    expect(skew.x).toBeCloseTo(-0.873, eps);\n    expect(skew.y).toBeCloseTo(0.175, eps);\n    expect(otherTransform.rotation).toEqual(0);\n  });\n\n  test('transform basic', () => {\n    const child = createTransform();\n    expect(child.children).toEqual([]);\n    // update\n    child.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n      scale: { x: 2, y: 1 },\n      skew: { x: 0, y: 0 },\n      rotation: 0,\n    });\n    expect(child.bounds).toEqual(new Rectangle(100, 100, 200, 100));\n\n    // setters\n    child.size = { width: 200, height: 200 };\n    child.position = { x: 100, y: 100 };\n    child.origin = { x: 0.5, y: 0.5 };\n    child.scale = { x: 1, y: 1 };\n    child.skew = { x: 0, y: 0 };\n    child.rotation = 0;\n    expectRectangle(child.bounds, [0, 0, 200, 200]);\n    expect(child.data).toEqual({\n      origin: { x: 0.5, y: 0.5 },\n      position: { x: 100, y: 100 },\n      rotation: 0,\n      scale: { x: 1, y: 1 },\n      size: { width: 200, height: 200, locked: false },\n      skew: { x: 0, y: 0 },\n    });\n\n    // rotation\n    child.rotation = PI / 4;\n    expectRectangle(child.bounds, [-41.42, -41.42, 282.84, 282.84]);\n    expectRectangle(child.boundsWithoutRotation, [0, 0, 200, 200]);\n  });\n\n  test('transform with children & scale', () => {\n    const child = createTransform();\n    const parent = createTransform();\n\n    child.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n    });\n    parent.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      scale: { x: 2, y: 1 },\n      origin: { x: 0, y: 0 },\n    });\n\n    // Building process\n    expectRectangle(child.bounds, [100, 100, 100, 100]);\n    expectRectangle(child.localBounds, [100, 100, 100, 100]);\n    expect(parent.children).toEqual([]);\n    child.setParent(parent);\n    expect(parent.children).toEqual([child]);\n    // child\n    expect(child.isParent(parent)).toEqual(true);\n    expect(child.isParentTransform(parent)).toEqual(true);\n    expectRectangle(child.localBounds, [100, 100, 100, 100]);\n    expectRectangle(child.bounds, [300, 200, 200, 100]);\n    expectSize(child.localSize, [100, 100]);\n    expectSize(child.worldSize, [200, 100]);\n    expect(child.contains(300, 200)).toEqual(true);\n    expect(child.contains(299, 199)).toEqual(false);\n    expect(child.contains(200, 200, true)).toEqual(false);\n    expect(child.contains(400, 250, true)).toEqual(true);\n    expect(createTransform().contains(0, 0)).toEqual(false);\n    expect(child.worldRotation).toEqual(0);\n    expect(child.worldDegree).toEqual(0);\n    expect(child.widthToScaleX(100)).toEqual(1);\n    expect(child.widthToScaleX(100, true)).toEqual(0.5);\n    expect(child.heightToScaleY(100)).toEqual(1);\n    expect(child.heightToScaleY(100, true)).toEqual(1);\n    expect(child.sizeToScaleValue({ width: 100, height: 100 }, true)).toEqual({ x: 0.5, y: 1 });\n    expectIPoint(child.localOrigin, [100, 100]);\n    expectIPoint(child.worldOrigin, [300, 200]);\n    // parent\n    expect(parent.isParent(child)).toEqual(false);\n    expect(parent.isParentTransform(child)).toEqual(false);\n    expectRectangle(parent.bounds, [300, 200, 200, 100]);\n    expectRectangle(parent.localBounds, [300, 200, 200, 100]);\n    expectSize(parent.localSize, [100, 100]);\n    expectSize(parent.worldSize, [200, 100]);\n    expect(parent.contains(300, 200)).toEqual(true);\n    expect(parent.contains(299, 199)).toEqual(false);\n  });\n\n  test('transform with deep tree', () => {\n    const child1 = createTransform();\n    const child2 = createTransform();\n    const child3 = createTransform();\n    const child21 = createTransform();\n    const parent = createTransform();\n\n    expect(parent.children).toEqual([]);\n    child1.setParent(parent);\n    child2.setParent(parent);\n    child3.setParent(parent);\n    child21.setParent(child2);\n    // FIXME: Circular JSON stringify ERROR\n    // expect(parent.children).toEqual([child1, child2])\n    expect(child1.children).toEqual([]);\n    expect(child2.children).toEqual([child21]);\n    expect(child1.isParent(parent)).toEqual(true);\n    expect(child2.isParent(parent)).toEqual(true);\n    expect(child21.isParent(child2)).toEqual(true);\n    expect(child21.isParentTransform(child2)).toEqual(true);\n    expect(child21.isParent(parent)).toEqual(true);\n    expect(child21.isParentTransform(parent)).toEqual(true);\n\n    child21.setParent(undefined);\n    expect(child21.isParent(child2)).toEqual(false);\n\n    child21.changeLocked = true;\n    child21.setParent(child3);\n    child21.changeLocked = false;\n    expect(child21.isParent(child3)).toEqual(true);\n\n    child3.dispose();\n    expect(child21.isParent(child3)).toEqual(false);\n  });\n\n  test('transform with children', () => {\n    const child1 = createTransform();\n    const child2 = createTransform();\n    const parent = createTransform();\n\n    child1.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n    });\n    child2.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n    });\n    parent.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n    });\n\n    // Building process\n    expect(child1.bounds.x).toEqual(100);\n    expect(child1.localBounds.x).toEqual(100);\n    child1.setParent(parent);\n    child2.setParent(parent);\n    expectRectangle(child1.bounds, [200, 200, 100, 100]);\n    expectRectangle(child1.localBounds, [100, 100, 100, 100]);\n    expectRectangle(parent.bounds, [200, 200, 100, 100]);\n    expectRectangle(parent.localBounds, [200, 200, 100, 100]);\n    const ids = { child1: getIds(child1), child2: getIds(child2), parent: getIds(parent) };\n\n    // Child changed\n    child1.update({ position: { x: 90, y: 100 } });\n    expect(child1.bounds.x).toEqual(190);\n    expect(child2.bounds.x).toEqual(200);\n    expect(parent.bounds.x).toEqual(190);\n    expect(parent.bounds.width).toEqual(110);\n    expect(child1.localBounds.x).toEqual(90);\n    expect(child2.localBounds.x).toEqual(100);\n    expect(parent.localBounds.x).toEqual(190);\n    expect(getIds(child1).localID).toEqual(ids.child1.localID + 1);\n    expect(getIds(child1).worldID).toEqual(ids.child1.worldID + 1);\n    expect(getIds(child2)).toEqual(ids.child2);\n    expect(getIds(parent)).toEqual(ids.parent);\n\n    // Parent changed\n    const ids2 = { child1: getIds(child1), child2: getIds(child2), parent: getIds(parent) };\n    parent.update({ position: { x: 90, y: 100 } });\n    expect(getIds(child1)).toEqual(ids2.child1);\n    expect(getIds(child2)).toEqual(ids2.child2);\n    expect(getIds(parent).localID).toEqual(ids2.parent.localID + 1);\n    expect(child1.bounds.x).toEqual(180);\n    expect(child2.bounds.x).toEqual(190);\n    expect(parent.bounds.x).toEqual(180);\n    expect(parent.bounds.width).toEqual(110);\n\n    // 子节点 local 不变 world 变了\n    expect(getIds(child1)).toEqual({\n      localID: ids2.child1.localID,\n      worldID: ids2.child1.worldID + 1,\n    });\n    expect(getIds(child2)).toEqual({\n      localID: ids2.child2.localID,\n      worldID: ids2.child2.worldID + 1,\n    });\n    expect(getIds(parent).localID).toEqual(ids2.parent.localID + 1);\n  });\n\n  test('initial values', () => {\n    const target = createTransform();\n    expect(target.origin.x).toEqual(0.5);\n    expect(target.origin.y).toEqual(0.5);\n    expect(target.position.x).toEqual(0);\n    expect(target.position.y).toEqual(0);\n    expect(target.size.width).toEqual(0);\n    expect(target.size.height).toEqual(0);\n    expect(target.scale.x).toEqual(1);\n    expect(target.scale.y).toEqual(1);\n    expect(target.skew.x).toEqual(0);\n    expect(target.skew.y).toEqual(0);\n    expect(target.rotation).toEqual(0);\n  });\n\n  test('intersects', () => {\n    const child = createTransform();\n    child.update({\n      size: { width: 100, height: 100 },\n      position: { x: 100, y: 100 },\n      origin: { x: 0, y: 0 },\n    });\n\n    expect(child.intersects(new Rectangle(0, 0, 101, 101))).toEqual(true);\n    expect(child.intersects(new Rectangle(100, 100, 1, 1))).toEqual(true);\n    expect(child.intersects(new Rectangle(0, 0, 100, 100))).toEqual(false);\n\n    expect(createTransform().intersects(new Rectangle(0, 0, 100, 100))).toEqual(false);\n  });\n\n  test('TransformSchema', () => {\n    expect(TransformSchema).not.toBeUndefined();\n    expect(TransformData).not.toBeUndefined();\n    expect(TransformSchemaDecoration).not.toBeUndefined();\n  });\n\n  // TODO\n  test.skip('isParentOrChildrenTransform', () => {\n    const entity = createEntity();\n    const transform = createTransform(entity);\n    const entity1 = createEntity();\n    const transform1 = createTransform(entity1);\n\n    entity.addData(TransformData);\n    entity.updateData(TransformData, transform);\n    expect(entity.getData<TransformData>(TransformData)).toEqual(false);\n    expect(TransformData.isParentOrChildrenTransform([entity1], entity)).toEqual(false);\n    transform1.setParent(transform);\n    expect(TransformData.isParentOrChildrenTransform([entity1], entity)).toEqual(true);\n\n    expect(TransformData.isParentOrChildrenTransform([], entity)).toEqual(false);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/__tests__/utils.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable, domUtils } from '@flowgram.ai/utils';\n\nimport {\n  PlaygroundConfigEntity,\n  PlaygroundDrag,\n  PlaygroundGesture,\n  scrollIntoViewWithTween,\n} from '../src/core';\nimport { createPlayground } from '../__mocks__/playground-container.mock';\n\ninterface Config {\n  onPinch: (props: {\n    origin: number[];\n    first: boolean;\n    last: boolean;\n    movement: any;\n    offset: number[];\n  }) => void;\n}\n\ninterface PinchConfig {\n  pinch: {\n    scaleBounds: () => void;\n    from: () => number[];\n  };\n}\n\nvi.mock('@use-gesture/vanilla', () => {\n  class MockGesture {\n    config: any;\n\n    constructor(target: HTMLElement, config: Config, pinchConfig: PinchConfig) {\n      this.config = {\n        target,\n        config,\n        pinchConfig,\n      };\n      config.onPinch({\n        origin: [1, 2],\n        first: true,\n        last: true,\n        movement: [],\n        offset: [1, 2],\n      });\n      pinchConfig.pinch.scaleBounds();\n      pinchConfig.pinch.from();\n    }\n  }\n  return {\n    Gesture: MockGesture,\n  };\n});\nconst { startDrag } = PlaygroundDrag;\n\ndescribe('utils', () => {\n  let playgroundDrag: PlaygroundDrag<any>;\n  beforeEach(() => {\n    const onDragStart = vi.fn();\n    const onDrag = vi.fn();\n    const onDragEnd = vi.fn();\n    playgroundDrag = new PlaygroundDrag<any>({\n      onDragStart,\n      onDrag,\n      onDragEnd,\n    });\n  });\n  it('PlaygroundDrag', () => {\n    const playground = createPlayground();\n    playground.ready();\n    const entity =\n      playground.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n    // 绑定 _playgroundConfigEntity\n    playgroundDrag.start(0, 0, entity);\n    playgroundDrag.stop(100, 100);\n    expect(playgroundDrag.isStarted).toEqual(false);\n    playgroundDrag.dispose();\n    playgroundDrag.start(100, 100);\n    playgroundDrag.stop(100, 100);\n    // 触发滚动\n    playgroundDrag.fireScroll('scrollX', true);\n    const dispose = startDrag(20, 20, {\n      onDragStart: e => {\n        expect(e?.startPos).toEqual({ x: 20, y: 20 });\n      },\n      onDragEnd: e => {\n        expect(e?.startPos).toEqual({ x: 20, y: 20 });\n        expect(e?.endPos).toEqual({ x: 0, y: 0 });\n      },\n    });\n    dispose.dispose();\n  });\n\n  it('PlaygroundDrag with entity', () => {\n    const playground = createPlayground();\n    playgroundDrag.start(100, 100, playground.config);\n  });\n\n  it('mock mouse event', () => {\n    const playground = createPlayground();\n    playground.ready();\n    const entity =\n      playground.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n    // 绑定 _playgroundConfigEntity\n    playgroundDrag.start(0, 0, entity);\n    // mouse move\n    const mouseMoveEvent: MouseEvent = new MouseEvent('mousemove', <MouseEventInit>{\n      movementX: 1,\n      movementY: 2,\n    });\n    playgroundDrag.handleEvent(mouseMoveEvent);\n    // mouse up\n    const mouseUpEvent: MouseEvent = new MouseEvent('mouseup', <MouseEventInit>{\n      button: 1,\n    });\n    playgroundDrag.handleEvent(mouseUpEvent);\n    // key down\n    const keyDownEvent: MouseEvent = new MouseEvent('keydown', <MouseEventInit>{\n      keyCode: 27,\n    });\n    (keyDownEvent as MouseEvent & { keyCode: number }).keyCode = 27;\n    // expect(keyDownEvent.keyCode).toEqual(27)\n    playgroundDrag.handleEvent(keyDownEvent);\n    // contextmenu\n    const contextMenuEvent: MouseEvent = new MouseEvent('contextmenu', <MouseEventInit>{\n      bubbles: true,\n    });\n    playgroundDrag.handleEvent(contextMenuEvent);\n    // default\n    const defaultEvent: MouseEvent = new MouseEvent('click', <MouseEventInit>{\n      clientX: 100,\n      clientY: 100,\n    });\n    playgroundDrag.handleEvent(defaultEvent);\n  });\n\n  it('playground-gesture', () => {\n    const playground = createPlayground();\n    playground.ready();\n    const entity =\n      playground.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n    const target = domUtils.createDivWithClass('test-target');\n    new PlaygroundGesture(target, entity);\n    document.dispatchEvent(new Event('gesturestart'));\n  });\n\n  it('tween', () => {\n    const ret1 = scrollIntoViewWithTween({\n      getScrollParent: () => undefined,\n      getTargetNode: () => undefined,\n      duration: 300,\n    });\n    expect(ret1).toEqual(Disposable.NULL);\n    const parent = domUtils.createDivWithClass('test-parent');\n    const dom2 = domUtils.createDivWithClass('test-node');\n    const ret2 = scrollIntoViewWithTween({\n      getScrollParent: () => parent,\n      getTargetNode: () => dom2,\n      duration: 300,\n    });\n    expect(ret2).toEqual(Disposable.NULL);\n    parent.style.width = '300px';\n    parent.style.height = '300px';\n    parent.scrollTop = 20;\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/core\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/command\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@phosphor/messaging\": \"^1.3.0\",\n    \"@tweenjs/tween.js\": \"^18\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/config-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Disposable } from '@flowgram.ai/utils';\n\nimport type { EntityDataRegistry } from './entity-data';\nimport { EntityData } from './entity-data';\nimport { Entity, type EntityOpts } from './entity';\n\nexport interface ConfigEntityProps {}\n\n// let version = 0\n\nexport function createConfigDataRegistry<P>(entity: ConfigEntity<any>): EntityDataRegistry {\n  class ConfigData extends EntityData<P> {\n    getDefaultData(): P {\n      return entity.getDefaultConfig();\n    }\n\n    checkChanged(newProps: Partial<P>): boolean {\n      return entity.checkChanged(this.data, newProps);\n    }\n\n    toJSON(): object {\n      return super.toJSON();\n    }\n  }\n\n  Object.defineProperty(ConfigData, 'type', {\n    value: `_${entity.type}DataMixin`,\n    // value: `_${entity.type}DataMixin_${version++}`,\n  });\n\n  return ConfigData as any;\n}\n\n/**\n * 用于专门的数据配置，且是单例\n */\nexport class ConfigEntity<\n  P extends ConfigEntityProps = ConfigEntityProps,\n  O extends EntityOpts = EntityOpts,\n> extends Entity<O> {\n  static type = 'ConfigEntity';\n\n  protected ConfigDataRegistry: EntityDataRegistry;\n\n  constructor(opts: O) {\n    super(opts);\n    this.isInitialized = true;\n    this.ConfigDataRegistry = createConfigDataRegistry<P>(this);\n    this.addData(this.ConfigDataRegistry);\n    this.isInitialized = false;\n  }\n\n  getDefaultConfig(): P {\n    return {} as P;\n  }\n\n  /**\n   * 判断 config 数据是否变化\n   */\n  checkChanged(oldData: P, newData: Partial<P>): boolean {\n    return Entity.checkDataChanged(oldData, newData);\n  }\n\n  get config(): P {\n    return this.getData(this.ConfigDataRegistry)!.data as P;\n  }\n\n  updateConfig(props: Partial<P>): void {\n    this.updateData(this.ConfigDataRegistry, props);\n  }\n\n  onConfigChanged(fn: (data: P) => void): Disposable {\n    return this.getData<EntityData>(this.ConfigDataRegistry)!.onDataChange(d => fn(d.data as P));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/entity-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Compare, DisposableImpl, Emitter } from '@flowgram.ai/utils';\n\nimport { Entity } from './entity';\n\n/**\n * 实体的数据块\n */\nexport abstract class EntityData<\n  DATA = any | number | string,\n  OPTS extends {} = {},\n> extends DisposableImpl {\n  static type = 'EntityData';\n\n  protected onDataChangeEmitter = new Emitter<EntityData<DATA, OPTS>>();\n\n  protected onWillChangeEmitter = new Emitter<EntityData<DATA, OPTS>>();\n\n  protected _data: DATA;\n\n  private _changeLocked = false;\n\n  protected _version = 0;\n\n  declare entity: Entity;\n\n  /**\n   * 修改后触发\n   */\n  readonly onDataChange = this.onDataChangeEmitter.event;\n\n  /**\n   * 修改前触发\n   */\n  readonly onWillChange = this.onWillChangeEmitter.event;\n\n  /**\n   * 初始化数据\n   */\n  abstract getDefaultData(): DATA;\n\n  constructor(entity: Entity, readonly opts?: OPTS) {\n    super();\n    this.entity = entity;\n    this._data = this.getDefaultData();\n    this.toDispose.push(this.onDataChangeEmitter);\n    this.toDispose.push(this.onWillChangeEmitter);\n  }\n\n  /**\n   * data 类型\n   */\n  get type(): string {\n    if (!(this.constructor as any).type) {\n      throw new Error(`Entity Data Registry need a type: ${this.constructor.name}`);\n    }\n    return (this.constructor as any).type;\n  }\n\n  /**\n   * 当前数据\n   */\n  get data(): DATA {\n    return this._data;\n  }\n\n  /**\n   * 更新单个数据\n   */\n  update(props: Partial<DATA> | keyof DATA | DATA, value?: any): void {\n    if (arguments.length === 2) {\n      if (this._data[props as keyof DATA] !== value) {\n        this.fireWillChange();\n        this._data[props as keyof DATA] = value;\n        this.fireChange();\n      }\n    } else if (this.checkChanged(props as Partial<DATA>)) {\n      this.fireWillChange();\n      if (typeof props !== 'object') {\n        this._data = props as any;\n      } else {\n        this._data = { ...this._data, ...(props as Partial<DATA>) };\n      }\n      this.fireChange();\n    }\n  }\n\n  /**\n   * 更新全量数据\n   * @param props\n   */\n  fullyUpdate(props: DATA): void {\n    // 仅做一层的全量比较\n    if (Compare.isChanged(this._data, props, 1, false)) {\n      this.fireWillChange();\n      this._data = props;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * @deprecated\n   * 检测属性是否更改，默认采用浅比较\n   */\n  checkChanged(newProps: Partial<DATA> | DATA): boolean {\n    return Entity.checkDataChanged(this._data, newProps);\n  }\n\n  /**\n   * 存储数据，一般在关闭浏览器后需要暂时存到 localStorage\n   */\n  toJSON(): any {\n    return this.data as any;\n  }\n\n  /**\n   * 还原数据\n   */\n  fromJSON(data: object): void {\n    this.update(data);\n  }\n\n  get changeLocked(): boolean {\n    return this._changeLocked;\n  }\n\n  set changeLocked(p) {\n    this._changeLocked = p;\n  }\n\n  fireWillChange(): void {\n    this.onWillChangeEmitter.fire(this as EntityData<DATA, OPTS>);\n  }\n\n  fireChange(): void {\n    if (this._changeLocked) return;\n    this._version++;\n    /* istanbul ignore next */\n    if (this._version >= Number.MAX_SAFE_INTEGER) {\n      this._version = 0;\n    }\n    this.onDataChangeEmitter.fire(this as EntityData<DATA, OPTS>);\n  }\n\n  protected bindChange(data: EntityData, fn?: () => void): void {\n    this.toDispose.push(\n      data.onDataChange(() => {\n        if (fn) fn();\n        this.fireChange();\n      }),\n    );\n  }\n\n  get version() {\n    return this._version;\n  }\n}\n\nexport type EntityDataProps<E extends EntityData> = E['data'];\n\nexport interface EntityDataRegistry<E extends EntityData = EntityData> {\n  new (...args: any[]): E;\n\n  type: E['type'];\n}\n\nexport type EntityDataInjector = <OPTS extends {} = {}>() => OPTS;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/entity-manager-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type EntityManager } from './entity-manager';\n\nexport const EntityManagerContribution = Symbol('EntityManagerContribution');\n\nexport interface EntityManagerContribution {\n  registerEntityManager(entityManager: EntityManager): void;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/entity-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, interfaces, multiInject, optional, postConstruct } from 'inversify';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { PlaygroundSchedule } from './playground-schedule';\nimport {\n  injectPlaygroundContext,\n  PlaygroundContainerFactory,\n  PlaygroundContext,\n} from './playground-context';\nimport { EntityManagerContribution } from './entity-manager-contribution';\nimport { ConfigEntity } from './config-entity';\n// import { AbleManager } from './able-manager';\nimport type {\n  Entity,\n  EntityData,\n  EntityDataInjector,\n  EntityDataRegistry,\n  EntityJSON,\n  EntityOpts,\n  EntityRegistry,\n} from '.';\n\n/**\n * 让 entity 可以注入到类中\n *\n * @example\n * ```\n *    class SomeClass {\n *      @inject(PlaygroundConfigEntity) playgroundConfig: PlaygroundConfigEntity\n *    }\n * ```\n * @param bind\n * @param entityRegistry\n */\nexport function bindConfigEntity(bind: interfaces.Bind, entityRegistry: EntityRegistry): void {\n  bind(entityRegistry)\n    .toDynamicValue(\n      ctx =>\n        ctx.container.get<EntityManager>(EntityManager)!.createEntity(entityRegistry)! as never,\n    )\n    .inSingletonScope();\n}\n\n/**\n * TODO registry 改成 decorator\n * Entity 管理器，全局唯一\n */\n@injectable()\nexport class EntityManager implements Disposable {\n  readonly toDispose = new DisposableCollection();\n\n  protected onEntityChangeEmitter = new Emitter<string>();\n\n  protected onEntityLifeCycleEmitter = new Emitter<{\n    type: 'add' | 'update' | 'delete';\n    entity: Entity;\n  }>();\n\n  protected onEntityDataChangeEmitter = new Emitter<{\n    entityType: string;\n    entityDataType: string;\n  }>();\n\n  /**\n   *  Entity 的类缓存，便于在 fromJSON 时候查询对应的类\n   */\n  protected registryMap: Map<string, EntityRegistry> = new Map();\n\n  /**\n   * Entity 数据类缓存，便于 fromJSON 使用\n   */\n  protected dataRegistryMap: Map<string, EntityDataRegistry> = new Map();\n\n  /**\n   * Entity 数据类依赖注入器，可用于在EntityData构造器中注入第三方模块\n   */\n  protected dataInjectorMap: Map<string, EntityDataInjector> = new Map();\n\n  /**\n   * Entity 的所有实例缓存\n   */\n  protected entityInstanceMap: Map<string, Entity> = new Map(); // By entity id\n\n  /**\n   * entity 全局版本更新\n   * @protected\n   */\n  protected entityVersionMap: Map<string, number> = new Map();\n\n  /**\n   * data 全局版本更新\n   * @protected\n   */\n  protected entityDataVersionMap: Map<string, number> = new Map();\n\n  /**\n   * Entity 的实例按类型缓存，便于查询优化\n   */\n  protected entityInstanceMapByType: Map<string, Entity[]> = new Map(); // By entity type\n\n  /**\n   * 所有配置实体的缓存\n   */\n  protected configEntities: Map<string, ConfigEntity> = new Map();\n\n  /**\n   * 当对应的实体类型变化后触发\n   */\n  readonly onEntityChange = this.onEntityChangeEmitter.event;\n\n  /**\n   * entity data 数据变化\n   */\n  readonly onEntityDataChange = this.onEntityDataChangeEmitter.event;\n\n  /**\n   * Entity 生命周期变化\n   */\n  readonly onEntityLifeCycleChange = this.onEntityLifeCycleEmitter.event;\n\n  @multiInject(EntityManagerContribution)\n  @optional()\n  contributions: EntityManagerContribution[];\n\n  @injectPlaygroundContext() context: PlaygroundContext;\n\n  @inject(PlaygroundContainerFactory) @optional() protected containerFactory:\n    | PlaygroundContainerFactory\n    | undefined;\n\n  /**\n   * 暂停触发实体类型变化\n   */\n  changeEntityLocked = false;\n\n  constructor() {\n    this.toDispose.pushAll([this.onEntityChangeEmitter, this.schedule]);\n  }\n\n  @postConstruct()\n  init() {\n    this.contributions.forEach(contrib => contrib.registerEntityManager?.(this));\n  }\n\n  /**\n   * 创建实体\n   */\n  createEntity<T extends Entity>(\n    Registry: EntityRegistry,\n    opts?: Omit<T['__opts_type__'], 'entityManager'>,\n  ): T {\n    if (!Registry.type) {\n      throw new Error(`[EntityManager] createEntity need a type: ${Registry}`);\n    }\n    // this.registerEntity(Registry);\n    // ConfigEntity 默认为单例\n    if (this.configEntities.has(Registry.type)) {\n      return this.configEntities.get(Registry.type) as any;\n    }\n    const entityOpts: EntityOpts = {\n      entityManager: this,\n      savedInManager: true,\n      ...opts,\n    };\n    const entity = new Registry(entityOpts) as T;\n    if (entityOpts.savedInManager) {\n      this.saveEntity(entity);\n    }\n    return entity;\n  }\n\n  isConfigEntity(type: string): boolean {\n    return this.configEntities.has(type);\n  }\n\n  /**\n   * 批量删除实体\n   */\n  removeEntities(Registry: EntityRegistry): void {\n    for (const e of this.getEntities(Registry).values()) {\n      e.dispose();\n    }\n  }\n\n  removeEntityById(id: string): boolean {\n    const entity = this.getEntityById(id);\n    if (entity) {\n      entity.dispose();\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * 触发实体 reset\n   * @param registry\n   */\n  resetEntities(registry: EntityRegistry): void {\n    const entities = this.getEntities(registry);\n    entities.forEach(entity => {\n      entity.reset();\n    });\n  }\n\n  resetEntity(registry: EntityRegistry, autoCreate?: boolean): void {\n    const entity = this.getEntity(registry, autoCreate);\n    entity?.reset();\n  }\n\n  updateConfigEntity<E extends ConfigEntity>(\n    registry: EntityRegistry,\n    config: Partial<E['config']>,\n  ): void {\n    const entity = this.configEntities.get(registry.type);\n    if (entity) {\n      entity.updateConfig(config);\n    }\n  }\n\n  /**\n   * @param type\n   */\n  getRegistryByType(type: string): EntityRegistry | undefined {\n    return this.registryMap.get(type);\n  }\n\n  registerEntity(Registry: EntityRegistry): void {\n    if (!Registry.type) throw new Error(`Registry entity need a type: ${Registry.name}`);\n    const oldRegistry = this.registryMap.get(Registry.type);\n    if (oldRegistry) {\n      if (oldRegistry !== Registry) {\n        throw new Error(`Entity registry ${Registry.type} need a new type`);\n      }\n      return;\n    }\n    this.registryMap.set(Registry.type, Registry);\n  }\n\n  registerEntityData(Registry: EntityDataRegistry, injector?: EntityDataInjector): void {\n    if (!Registry.type) throw new Error(`Registry entity data need a type: ${Registry.name}`);\n    const oldRegistry = this.dataRegistryMap.get(Registry.type);\n    if (!oldRegistry) {\n      this.dataRegistryMap.set(Registry.type, Registry);\n      // if (oldRegistry !== Registry) {\n      //   throw new Error(`Entity data registry ${Registry.type} need a new type`)\n      // }\n      // return\n    }\n\n    const oldInjector = this.dataInjectorMap.get(Registry.type);\n    if (!oldInjector && injector) {\n      this.dataInjectorMap.set(Registry.type, injector);\n    }\n  }\n\n  getDataRegistryByType(type: string): EntityDataRegistry | undefined {\n    return this.dataRegistryMap.get(type);\n  }\n\n  getEntityById<T extends Entity>(id: string): T | undefined {\n    return this.entityInstanceMap.get(id) as T;\n  }\n\n  /**\n   * @param autoCreate 是否要自动创建，默认 false\n   */\n  getEntity<T extends Entity>(registry: EntityRegistry, autoCreate?: boolean): T | undefined {\n    const entity = this.getEntities<T>(registry)[0];\n    if (!entity && autoCreate) {\n      return this.createEntity<T>(registry);\n    }\n    return entity;\n  }\n\n  getEntities<T extends Entity>(registry: EntityRegistry): T[] {\n    // 获取当前 entities 的快照\n    return (this.entityInstanceMapByType.get(registry.type) as T[]) || [];\n  }\n\n  // getEntitiesByAble<T extends Entity>(registry: AbleRegistry): T[] {\n  //   return this.ableManager.getEntitiesByAble<T>(registry);\n  // }\n  //\n  // getEntitiesByAbles<T extends Entity>(...registries: AbleRegistry[]): T[] {\n  //   return this.ableManager.getEntitiesByAbles<T>(...registries);\n  // }\n  //\n  // getEntityByAble<T extends Entity>(registry: AbleRegistry): T | undefined {\n  //   return this.ableManager.getEntityByAble<T>(registry);\n  // }\n\n  getEntityDatas<T extends EntityData>(\n    entityRegistry: EntityRegistry,\n    dataRegistry: EntityDataRegistry<T>,\n  ): T[] {\n    return this.getEntities<any>(entityRegistry)\n      .map((e: Entity) => e.getData<T>(dataRegistry))\n      .filter(d => !!d) as T[];\n  }\n\n  hasEntity(registry: EntityRegistry): boolean {\n    return !!this.getEntity(registry);\n  }\n\n  /**\n   * 只存储 config 数据，忽略动态数据\n   */\n  storeState({ configOnly = true }: { configOnly?: boolean } = {}): EntityJSON[] {\n    const data: EntityJSON[] = [];\n    for (const e of this.entityInstanceMap.values()) {\n      if ((!configOnly || e instanceof ConfigEntity) && e.toJSON) {\n        if (e.toJSON) {\n          const d = e.toJSON();\n          if (d) {\n            data.push(d);\n          }\n        }\n      }\n    }\n    return data;\n  }\n\n  restoreState(data: EntityJSON[]): void {\n    if (!data || !Array.isArray(data)) return;\n    data.forEach((s: EntityJSON) => {\n      if (!s || !s.type || !s.id) return;\n      const register = this.getRegistryByType(s.type);\n      // 如果没有注册，则忽略掉\n      if (!register) {\n        console.warn(`Playground entity registry lost: ${s.type}`);\n        return;\n      }\n      const entity = this.createEntity(register, {\n        id: s.id,\n      });\n      if (entity.fromJSON) {\n        entity.fromJSON(s);\n      }\n    });\n  }\n\n  protected saveEntity(entity: Entity): void {\n    const { id } = entity;\n    // 无法重复创建\n    if (id && this.entityInstanceMap.has(id)) {\n      console.error(`Entity ${entity.type} ${id} is created before`);\n      return;\n    }\n\n    this.entityInstanceMap.set(entity.id, entity);\n    let entities = this.entityInstanceMapByType.get(entity.type);\n    if (!entities) {\n      entities = [];\n      this.entityInstanceMapByType.set(entity.type, entities);\n    }\n    if (entity instanceof ConfigEntity) {\n      this.configEntities.set(entity.type, entity);\n    }\n    entities.push(entity);\n    entity.onEntityChange(entity => {\n      this.fireEntityChanged(entity);\n      this.fireEntityLifeCycleChanged({ type: 'update', entity });\n    });\n    entity.onDataChange(e => {\n      this.fireEntityDataChanged(entity.type, e.data.type);\n    });\n    entity.toDispose.push(\n      Disposable.create(() => {\n        this.removeEntity(entity);\n        this.fireEntityLifeCycleChanged({ type: 'delete', entity });\n      }),\n    );\n    entity\n      .getDefaultDataRegistries()\n      .forEach(registry => this.fireEntityDataChanged(entity.type, registry.type));\n    this.fireEntityChanged(entity);\n    this.fireEntityLifeCycleChanged({ type: 'add', entity });\n  }\n\n  protected removeEntity(entity: Entity): void {\n    if (this.entityInstanceMap.has(entity.id) && this.entityInstanceMapByType.has(entity.type)) {\n      const entities = this.entityInstanceMapByType.get(entity.type)!;\n      const index = entities.indexOf(entity);\n      if (index !== -1) {\n        this.entityInstanceMapByType.set(\n          entity.type,\n          entities.filter(e => e !== entity),\n        );\n        this.entityInstanceMap.delete(entity.id);\n\n        if (this.configEntities.has(entity.type)) {\n          this.configEntities.delete(entity.type);\n        }\n\n        this.fireEntityChanged(entity);\n      }\n    }\n  }\n\n  /**\n   * 重制所有 entity 为初始化状态\n   */\n  reset(): void {\n    for (const entity of this.entityInstanceMap.values()) {\n      entity.reset();\n    }\n  }\n\n  private schedule = new PlaygroundSchedule();\n\n  fireEntityChanged = (entity: Entity | string) => {\n    const entityType = typeof entity === 'string' ? entity : entity.type;\n    let version = this.entityVersionMap.get(entityType) || 0;\n    /* istanbul ignore next */\n    if (version === Number.MAX_SAFE_INTEGER) {\n      version = 0;\n    }\n    this.entityVersionMap.set(entityType, version + 1);\n    if (this.changeEntityLocked) return;\n    this.schedule.push(entityType, () => {\n      this.onEntityChangeEmitter.fire(entityType);\n    });\n  };\n\n  fireEntityDataChanged = (entityType: string, entityDataType: string) => {\n    let version = this.entityDataVersionMap.get(entityDataType) || 0;\n    /* istanbul ignore next */\n    if (version === Number.MAX_SAFE_INTEGER) {\n      version = 0;\n    }\n    this.entityDataVersionMap.set(entityDataType, version + 1);\n    this.schedule.push(`${entityType}/${entityDataType}`, () => {\n      this.onEntityDataChangeEmitter.fire({ entityType, entityDataType });\n    });\n  };\n\n  fireEntityLifeCycleChanged = ({\n    type,\n    entity,\n  }: {\n    type: 'add' | 'update' | 'delete';\n    entity: Entity;\n  }) => {\n    this.schedule.push(`${type}/${entity.id}`, () => {\n      this.onEntityLifeCycleEmitter.fire({ type, entity });\n    });\n  };\n\n  getEntityVersion(registry: EntityRegistry | string): number {\n    return this.entityVersionMap.get(typeof registry === 'string' ? registry : registry.type) || 0;\n  }\n\n  getEntityDataVersion(registry: EntityDataRegistry | string): number {\n    return (\n      this.entityDataVersionMap.get(typeof registry === 'string' ? registry : registry.type) || 0\n    );\n  }\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  getDataInjector(registry: EntityDataRegistry | string) {\n    return this.dataInjectorMap.get(typeof registry === 'string' ? registry : registry.type);\n  }\n\n  getService<T>(identifier: interfaces.ServiceIdentifier<T>): T {\n    return this.containerFactory?.get<T>(identifier) as T;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { interfaces } from 'inversify';\nimport { Compare, Disposable, DisposableCollection, Emitter, type Event } from '@flowgram.ai/utils';\n\nimport { type PlaygroundContext } from './playground-context';\nimport type { EntityManager } from './entity-manager';\nimport type { EntityData, EntityDataProps, EntityDataRegistry } from './entity-data';\n// import type { AbleChangedEvent, EntityAbles } from './entity-ables';\n// import type { AbleManager } from './able-manager';\n// import type { Able, AbleRegistry } from './able';\n\n/**\n * 注册类\n */\nexport interface EntityRegistry<E extends Entity = Entity> {\n  new (opts: any): E;\n\n  readonly type: E['type'];\n}\n\n/**\n * 持久化数据\n */\nexport interface EntityJSON {\n  type: string;\n  id: string;\n  // ableList?: string[];\n  dataList: object[];\n}\n\n// eslint-disable-next-line no-proto\nconst ObjectProto = (Object as any).__proto__;\n\nexport interface EntityDataChangedEvent<T extends Entity = Entity> {\n  type: 'add' | 'delete' | 'update';\n  data: EntityData;\n  entity: T;\n}\n\nexport interface EntityOpts {\n  entityManager: EntityManager;\n  id?: string;\n  // ables?: AbleRegistry[]; // 添加的 able\n  datas?: { registry: EntityDataRegistry; data?: EntityDataProps<any> }[];\n  savedInManager?: boolean; // 是否存储到 manager 上，默认 true\n}\n\nlet _version = 0;\n\nexport class Entity<OPTS extends EntityOpts = EntityOpts> implements Disposable {\n  static type = 'Entity';\n\n  private readonly onEntityChangeEmitter = new Emitter<Entity>();\n\n  private readonly onDataChangeEmitter = new Emitter<EntityDataChangedEvent>();\n\n  private readonly initializeDataKeys: string[] = []; // 初始化的\n\n  protected readonly dataManager: Map<string, EntityData> = new Map(); // 存储的数据\n\n  // readonly onBeforeAbleDispatchedEmitter = new Emitter<Able>();\n  //\n  // readonly onAfterAbleDispatchedEmitter = new Emitter<Able>();\n\n  /**\n   * 销毁事件管理\n   */\n  readonly toDispose = new DisposableCollection();\n\n  /**\n   * 销毁前事件管理\n   */\n  readonly preDispose = new DisposableCollection();\n\n  // /**\n  //  * able 管理\n  //  */\n  // readonly ables: EntityAbles;\n\n  /**\n   * 修改会触发\n   */\n  readonly onEntityChange = this.onEntityChangeEmitter.event;\n\n  // /**\n  //  * able 触发之前\n  //  */\n  // readonly onBeforeAbleDispatched = this.onBeforeAbleDispatchedEmitter.event;\n\n  // /**\n  //  * able 触发之后\n  //  */\n  // readonly onAfterAbleDispatched = this.onAfterAbleDispatchedEmitter.event;\n\n  /**\n   * 数据更改事件\n   */\n  readonly onDataChange = this.onDataChangeEmitter.event;\n\n  // /**\n  //  * able 数据更改\n  //  */\n  // readonly onAbleChange: Event<AbleChangedEvent>;\n\n  // /**\n  //  * 默认初始化的 Able\n  //  */\n  // getDefaultAbleRegistries(): AbleRegistry[] {\n  //   return [];\n  // }\n\n  /**\n   * 默认初始化的 Data\n   */\n  getDefaultDataRegistries(): EntityDataRegistry[] {\n    return [];\n  }\n\n  private _changeLockedTimes = 0;\n\n  protected isInitialized = true;\n\n  private _id: string;\n\n  private _version: number = _version++; // 每次创建都有一个新 version，避免 id 相同的 entity 频繁创建销毁导致碰撞\n\n  private _savedInManager = true;\n\n  // readonly ableManager: AbleManager;\n\n  /**\n   * 暂停更新开关\n   * @protected\n   */\n  protected get changeLocked(): boolean {\n    return this._changeLockedTimes > 0;\n  }\n\n  protected set changeLocked(changeLocked) {\n    this._changeLockedTimes = changeLocked\n      ? this._changeLockedTimes + 1\n      : this._changeLockedTimes - 1;\n\n    /* istanbul ignore next */\n    if (this._changeLockedTimes < 0) this._changeLockedTimes = 0;\n  }\n\n  /**\n   * 实体类型\n   */\n  get type(): string {\n    if (!(this.constructor as any).type) {\n      throw new Error(`Entity Registry need a type: ${this.constructor.name}`);\n    }\n    return (this.constructor as any).type;\n  }\n\n  /**\n   * 全局的entity管理器\n   */\n  readonly entityManager: EntityManager;\n\n  get context(): PlaygroundContext {\n    return this.entityManager.context;\n  }\n\n  constructor(opts: OPTS) {\n    this.entityManager = opts.entityManager;\n    this._id = opts.id || nanoid();\n    this._savedInManager = opts.savedInManager === undefined ? true : opts.savedInManager;\n    // this.ableManager = this.entityManager.ableManager;\n    // this.context = this.entityManager.context;\n    this.isInitialized = true;\n    // this.ables = this.entityManager.ableManager.createAbleMap(this);\n    // this.ables.onAbleChange(event => {\n    //   // 只需要监听删除，add 和 update 都由 entityData 去监听\n    //   if (event.type === 'delete') {\n    //     this.fireChange();\n    //   }\n    // });\n    this.toDispose.push(this.onEntityChangeEmitter);\n    // this.toDispose.push(this.onBeforeAbleDispatchedEmitter);\n    // this.toDispose.push(this.onAfterAbleDispatchedEmitter);\n    this.toDispose.push(this.onDataChangeEmitter);\n    // this.toDispose.push(this.ables);\n    // this.onAbleChange = this.ables.onAbleChange;\n    this.register();\n    // if (opts.ables) {\n    //   opts.ables.forEach(able => this.ables.add(able));\n    // }\n    if (opts.datas) {\n      opts.datas.forEach((data) => this.addData(data.registry, data.data));\n    }\n    this.isInitialized = false;\n  }\n\n  addInitializeData(datas: EntityDataRegistry[], dataConfig?: any) {\n    this.isInitialized = true;\n    datas.forEach((data) => this.addData(data, dataConfig));\n    this.isInitialized = false;\n  }\n\n  /**\n   * 实体的版本\n   */\n  get version(): number {\n    return this._version;\n  }\n\n  /**\n   * 存储数据，用于持久化存储\n   */\n  toJSON(): EntityJSON | any {\n    const dataList: object[] = [];\n    for (const data of this.dataManager.values()) {\n      dataList.push({\n        type: data.type,\n        data: data.toJSON(),\n      });\n    }\n    return {\n      type: this.type,\n      id: this.id,\n      // ableList: this.ables.toJSON(),\n      dataList,\n    };\n  }\n\n  /**\n   * 还原数据\n   */\n  fromJSON(data?: EntityJSON | any): void {\n    if (!data || !data.id || !data.type) return;\n    this.changeLocked = true;\n    this.reset();\n    if (data.dataList) {\n      data.dataList.forEach((d: any) => {\n        const registry = this.entityManager.getDataRegistryByType(d.type);\n        if (registry) {\n          const dataEntity = this.addData(registry);\n          dataEntity.update(d.data);\n        }\n      });\n    }\n    this.changeLocked = false;\n    this.fireChange();\n  }\n\n  /**\n   * 实体 id\n   */\n  get id(): string {\n    return this._id;\n  }\n\n  /**\n   * 销毁实体\n   */\n  dispose(): void {\n    this.preDispose.dispose();\n    this.toDispose.dispose();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  /**\n   * 重制为初始化状态\n   */\n  reset(): void {\n    this.changeLocked = true;\n    for (const data of this.dataManager.values()) {\n      if (!this.initializeDataKeys.includes(data.type)) {\n        data.dispose();\n      }\n    }\n    // this.ables.reset();\n    this.register();\n    this.changeLocked = false;\n    this.fireChange();\n  }\n\n  /**\n   * 销毁事件\n   */\n  get onDispose(): Event<void> {\n    return this.toDispose.onDispose;\n  }\n\n  /**\n   * 触发实体更新\n   * @protected\n   */\n  protected fireChange(): void {\n    if (this.changeLocked || this.isInitialized || this.disposed) return;\n    this._version++;\n    /* istanbul ignore next */\n    if (this._version >= Number.MAX_SAFE_INTEGER) {\n      this._version = 0;\n    }\n    this.onEntityChangeEmitter.fire(this);\n  }\n\n  /**\n   * 添加数据\n   */\n  addData<D extends EntityData>(\n    Registry: EntityDataRegistry,\n    defaultProps?: EntityDataProps<D>\n  ): D {\n    this.entityManager.registerEntityData(Registry);\n    let entityData = this.dataManager.get(Registry.type) as D;\n\n    if (entityData) {\n      if (defaultProps) this.updateData(Registry, defaultProps);\n      return entityData;\n    }\n\n    // 是否存在EntityData依赖注入器\n    const injector = this.entityManager.getDataInjector(Registry);\n    entityData = new Registry(this, injector?.()) as D;\n\n    if (this.isInitialized) this.initializeDataKeys.push(entityData.type);\n    this.dataManager.set(Registry.type, entityData);\n    this.toDispose.push(entityData);\n    entityData.onDataChange(() => {\n      const event: EntityDataChangedEvent = {\n        type: 'update',\n        data: entityData,\n        entity: this,\n      };\n      this.onDataChangeEmitter.fire(event);\n      this.fireChange();\n    });\n    entityData.toDispose.push(\n      Disposable.create(() => {\n        // 初始化的 data 数据无法被删除\n        if (!this.initializeDataKeys.includes(Registry.type)) {\n          this.dataManager.delete(Registry.type);\n        }\n        const event: EntityDataChangedEvent = {\n          type: 'delete',\n          data: entityData,\n          entity: this,\n        };\n        this.onDataChangeEmitter.fire(event);\n        this.fireChange();\n      })\n    );\n    entityData.changeLocked = true;\n    this.updateData(Registry, defaultProps || entityData.getDefaultData());\n    entityData.changeLocked = false;\n    const event: EntityDataChangedEvent = {\n      type: 'add',\n      data: entityData,\n      entity: this,\n    };\n    this.onDataChangeEmitter.fire(event);\n    return entityData;\n  }\n\n  /**\n   * 是否存到全局 manager，默认 true\n   */\n  get savedInManager(): boolean {\n    return this._savedInManager;\n  }\n\n  /**\n   * 更新实体的数据\n   */\n  updateData<D extends EntityData>(\n    Registry: EntityDataRegistry<D>,\n    props: EntityDataProps<D>\n  ): void {\n    const entityData = this.dataManager.get(Registry.type);\n    if (entityData) {\n      entityData.update(props);\n    }\n  }\n\n  /**\n   * 获取 data 数据\n   */\n  getData<D extends EntityData>(Registry: EntityDataRegistry<D>): D {\n    return this.dataManager.get(Registry.type) as D;\n  }\n\n  /**\n   * 是否有指定数据\n   */\n  hasData(Registry: EntityDataRegistry): boolean {\n    return this.dataManager.has(Registry.type);\n  }\n\n  /**\n   * 删除数据，初始化状态注入的数据无法被删除\n   */\n  removeData<D extends EntityData>(Registry: EntityDataRegistry<D>): void {\n    // 初始化的数据无法被删除\n    if (this.initializeDataKeys.includes(Registry.type)) return;\n    const entityData = this.dataManager.get(Registry.type);\n    if (entityData) {\n      entityData.dispose();\n    }\n  }\n\n  /**\n   * 获取 IOC 服务\n   * @param identifier\n   */\n  getService<T>(identifier: interfaces.ServiceIdentifier<T>): T {\n    return this.entityManager.getService<T>(identifier);\n  }\n  // /**\n  //  * 添加 able\n  //  */\n  // addAbles(...ables: AbleRegistry[]): void {\n  //   ables.forEach(able => this.ables.add(able));\n  // }\n  //\n  // /**\n  //  * 删除 able\n  //  */\n  // removeAbles(...ables: AbleRegistry[]): void {\n  //   ables.forEach(able => this.ables.remove(able));\n  // }\n  //\n  // /**\n  //  * 是否有 able\n  //  */\n  // hasAble(able: AbleRegistry): boolean {\n  //   return this.ables.has(able);\n  // }\n  //\n  // hasAbles(...ables: AbleRegistry[]): boolean {\n  //   for (const able of ables) {\n  //     if (!this.ables.has(able)) return false;\n  //   }\n  //   return true;\n  // }\n\n  protected register(): void {\n    // 注册默认 able\n    // this.getDefaultAbleRegistries().forEach(Registry => this.ables.add(Registry));\n    // 注册默认 data\n    this.getDefaultDataRegistries().forEach((Registry) => this.addData(Registry));\n  }\n\n  declare __opts_type__: OPTS;\n}\n\nexport namespace Entity {\n  export function getType(registry: EntityRegistry): string {\n    return registry.type;\n  }\n\n  /**\n   * 默认数据比较，采用浅比较\n   */\n  export function checkDataChanged(oldProps: any, newProps: any): boolean {\n    return Compare.isChanged(oldProps, newProps);\n  }\n\n  export function isRegistryOf(target: any, Registry: any): boolean {\n    if (target === Registry) return true;\n    // eslint-disable-next-line no-proto\n    let proto = target.__proto__;\n    while (proto && proto !== ObjectProto) {\n      if (proto.prototype === Registry.prototype) return true;\n      // eslint-disable-next-line no-proto\n      proto = proto.__proto__;\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './schema';\nexport * from './utils';\n// export * from './able';\n// export * from './able-manager';\nexport * from './entity-manager';\nexport * from './config-entity';\nexport * from './entity';\n// export * from './entity-tree';\n// export * from './entity-ables';\nexport * from './entity-data';\nexport * from './entity-manager-contribution';\nexport * from './playground-context';\nexport * from './playground-decorator-helper';\nexport * from './playground-decorators';\nexport * from './playground-schedule';\nexport * from './protect-wheel-area';\nexport { bindContributions, ContributionProvider, bindContributionProvider } from '@flowgram.ai/utils';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/playground-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\n\nimport { injectByProvider } from '../core/utils';\n\n/**\n * 会被注入到 layer 层，可以在使用的时候替换它\n */\nexport const PlaygroundContext = Symbol('PlaygroundContext');\n\nexport type PlaygroundContext = any;\n\nexport const PlaygroundContextProvider = Symbol('PlaygroundContextProvider');\nexport type PlaygroundContextProvider = () => any;\nexport const injectPlaygroundContext = () => injectByProvider(PlaygroundContextProvider);\nexport const bindPlaygroundContextProvider = (bind: interfaces.Bind) => {\n  bind(PlaygroundContextProvider).toDynamicValue(ctx => () => {\n    if (ctx.container.isBound(PlaygroundContext)) {\n      return ctx.container.get(PlaygroundContext);\n    }\n    return undefined;\n  });\n};\n\nexport const PlaygroundContainerFactory = Symbol('PlaygroundContainerFactory');\n\nexport interface PlaygroundContainerFactory {\n  createChild: interfaces.Container['createChild'];\n  get: interfaces.Container['get'];\n  getAll: interfaces.Container['getAll'];\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/playground-decorator-helper.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import type { AbleRegistry } from './able';\nimport type { EntityDataRegistry, EntityRegistry } from '.';\n\ndeclare namespace Reflect {\n  export function getMetadata(key: string | symbol, target: any): any;\n  export function defineMetadata(key: string | symbol, value: any, target: any): any;\n}\n\n// export const ABLES_DECO_KEY = Symbol('AblesDecorator');\nexport const ENTITIES_DECO_KEY = Symbol('EntitiesDecorator');\n// export const PAYLOAD_DECO_KEY = Symbol('PayloadDecorator');\n// export const HANDLE_DECO_KEY = Symbol('HandleDecorator');\nexport const ENTITIES_BY_DATA_DECO_KEY = Symbol('EntitiesByDataDecorator');\n\nconst PROPERTEIS_INJECTED = Symbol('PropertiesInjected');\n\nexport interface RegistryValueGetter<T> {\n  (target: any, method: string | symbol): T;\n}\n\nexport interface RegistryInit {\n  (target: any, method: string | symbol): void;\n}\n\nexport function getRegistryMetadata(target: any, key: symbol): any[] {\n  return Reflect.getMetadata(key, target.prototype) || [];\n}\n\nfunction getRegistryInjectedProperties(target: any): string[] {\n  return Reflect.getMetadata(PROPERTEIS_INJECTED, target) || [];\n}\n\nfunction definePropertiesMetadata(target: any, property: string): void {\n  const properties = getRegistryInjectedProperties(target);\n  properties.push(property);\n  Reflect.defineMetadata(PROPERTEIS_INJECTED, properties, target);\n}\n\n/**\n *  在 rspack 场景编译ts文件时候\n *  decorator 注入的 property 会被当成 this 的属性, 导致 Reflect.metadata 失效\n */\nexport function removeInjectedProperties(instance: any): void {\n  if (typeof instance === 'object') {\n    const propertiesInjected = getRegistryInjectedProperties(instance.constructor.prototype);\n    propertiesInjected.forEach(propertyKey => {\n      // eslint-disable-next-line no-prototype-builtins\n      if (instance.hasOwnProperty(propertyKey) && instance[propertyKey] === undefined) {\n        delete instance[propertyKey];\n      }\n    });\n  }\n}\n\nexport function createRegistryDecorator(\n  key: symbol,\n  data: any,\n  getValue?: RegistryValueGetter<any>,\n  init?: RegistryInit,\n): any {\n  return (target: any, property: string): any => {\n    let registries = Reflect.getMetadata(key, target);\n    if (!registries) {\n      registries = [];\n      Reflect.defineMetadata(key, registries, target);\n    }\n    if (!Array.isArray(data)) {\n      data = [data];\n    }\n    data.forEach((registry: any) => {\n      if (!registries.includes(registry)) {\n        registries.push(registry);\n      }\n    });\n    if (init) init(target, property);\n    if (property && getValue) {\n      definePropertiesMetadata(target, property);\n      return {\n        enumerable: false,\n        configurable: false,\n        get(): any {\n          return getValue(this, property);\n        },\n      };\n    }\n  };\n}\n\n// export function getAbleMetadata(layer: any): AbleRegistry[] {\n//   return getRegistryMetadata(layer, ABLES_DECO_KEY);\n// }\n\nexport function getEntityMetadata(layer: any): EntityRegistry[] {\n  return getRegistryMetadata(layer, ENTITIES_DECO_KEY);\n}\nexport function getEntityDatasMetadata(\n  layer: any,\n): { entity: EntityRegistry; data: EntityDataRegistry }[] {\n  return getRegistryMetadata(layer, ENTITIES_BY_DATA_DECO_KEY);\n}\n\n// export function getPayloadMetadata(able: AbleRegistry): string | symbol {\n//   return getRegistryMetadata(able, PAYLOAD_DECO_KEY)[0];\n// }\n\n// export function getHandleParams(able: AbleRegistry): EntityDataRegistry[] {\n//   return getRegistryMetadata(able, HANDLE_DECO_KEY);\n// }\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/playground-decorators.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  // ABLES_DECO_KEY,\n  ENTITIES_DECO_KEY,\n  // HANDLE_DECO_KEY,\n  // PAYLOAD_DECO_KEY,\n  createRegistryDecorator,\n  ENTITIES_BY_DATA_DECO_KEY,\n  type RegistryValueGetter,\n} from './playground-decorator-helper';\n// import type { AbleRegistry } from './able';\nimport type { Entity, EntityDataRegistry, EntityRegistry } from '.';\n// import type { Layer } from './layer';\n\n// export function observeAble(registry: AbleRegistry): any {\n//   const getValue: RegistryValueGetter<Entity[]> = (target: any) =>\n//     target.observeManager.getEntitiesByAble(registry);\n//   return createRegistryDecorator(ABLES_DECO_KEY, [registry], getValue);\n// }\n\n// /**\n//  * @param andAbles - 多个 able，条件且\n//  * @param orAbles - 多个 able，条件或\n//  */\n// export function observeAbles(andAbles: AbleRegistry[], orAbles: AbleRegistry[] = []): any {\n//   const getValue: RegistryValueGetter<Entity[]> = (target: any) =>\n//     target.observeManager.getEntitiesByAbles(andAbles, orAbles);\n//   return createRegistryDecorator(ABLES_DECO_KEY, andAbles.concat(orAbles), getValue);\n// }\nexport function observeEntity(registry: EntityRegistry): any {\n  const getValue: RegistryValueGetter<Entity> = (target: any) =>\n    target.observeManager.get(registry)!;\n  return createRegistryDecorator(ENTITIES_DECO_KEY, registry, getValue);\n}\n\n/**\n * 监听 entity 变化\n * @param registry\n */\nexport function observeEntities(registry: EntityRegistry): any {\n  const getValue: RegistryValueGetter<Entity[]> = (target: any) =>\n    target.observeManager.getEntities(registry);\n  return createRegistryDecorator(ENTITIES_DECO_KEY, registry, getValue);\n}\n\n// export function payload(payloadKey: string | symbol): any {\n//   const check = (target: any, method: string) => {\n//     if (method !== 'payload')\n//       throw new Error(`@payload() should be used by \"payload\" method but get \"${method}\".`);\n//   };\n//   // @ts-ignore\n//   return createRegistryDecorator(PAYLOAD_DECO_KEY, payloadKey, undefined, check);\n// }\n\n// export function params(...registries: EntityDataRegistry[]): any {\n//   const check = (target: any, method: string) => {\n//     if (method !== 'handle')\n//       throw new Error(`@params() should be used by \"handle\" method but get \"${method}\".`);\n//   };\n//   // @ts-ignore\n//   return createRegistryDecorator(HANDLE_DECO_KEY, registries, undefined, check);\n// }\n\n/**\n * 监听 entity 对应的 data 数据变化\n *\n * @param entityRegistry\n * @param dataRegistry\n */\nexport function observeEntityDatas(\n  entityRegistry: EntityRegistry,\n  dataRegistry: EntityDataRegistry,\n): any {\n  const getValue: RegistryValueGetter<Entity[]> = (target: any) =>\n    target.observeManager.getEntityDatas(entityRegistry, dataRegistry);\n  return createRegistryDecorator(\n    ENTITIES_BY_DATA_DECO_KEY,\n    { entity: entityRegistry, data: dataRegistry },\n    getValue,\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/playground-schedule.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { throttle } from 'lodash-es';\nimport { type Disposable } from '@flowgram.ai/utils';\n\n// TODO 先用 throttle 替代\nexport class PlaygroundSchedule implements Disposable {\n  protected execMap: Map<any, () => void> = new Map();\n\n  push(key: any, fn: () => void): void {\n    const { execMap } = this;\n    if (process.env.NODE_ENV === 'test') {\n      fn();\n      return;\n    }\n    let schedule = execMap.get(key);\n    if (!schedule) {\n      schedule = throttle(fn, 0) as () => void;\n      execMap.set(key, schedule);\n    }\n    schedule();\n  }\n\n  dispose(): void {\n    this.execMap.clear();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/protect-wheel-area.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * 保护区域不被画布劫持滚动事件\n */\nexport const ProtectWheelArea = Symbol('ProtectWheelArea');\n\nexport type ProtectWheelArea = (dom: Element) => boolean;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node';\nexport * from './origin-schema';\nexport * from './opacity-schema';\nexport * from './position-schema';\nexport * from './rotation-schema';\nexport * from './scale-schema';\nexport * from './size-schema';\nexport * from './skew-schema';\nexport * from './transform-schema';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type TransformSchema } from './transform-schema';\n\nexport interface NodeSchema {\n  id: string;\n  name?: string;\n}\n\nexport interface TransformNodeSchema extends NodeSchema {\n  transform: TransformSchema;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/opacity-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OpacitySchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { OpacitySchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { OpacitySchema, OpacitySchemaDecoration };\n\nexport class OpacityData extends EntityData<OpacitySchema> {\n  static type = 'OpacityData';\n\n  getDefaultData(): OpacitySchema {\n    return Schema.createDefault<OpacitySchema>(OpacitySchemaDecoration);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/origin-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OriginSchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { OriginSchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { OriginSchema, OriginSchemaDecoration };\n\nexport class OriginData extends EntityData<OriginSchema> implements OriginSchema {\n  static type = 'OriginData';\n\n  getDefaultData(): OriginSchema {\n    return Schema.createDefault<OriginSchema>(OriginSchemaDecoration);\n  }\n\n  get x(): number {\n    return this.data.x;\n  }\n\n  get y(): number {\n    return this.data.y;\n  }\n\n  set x(x: number) {\n    this.update('x', x);\n  }\n\n  set y(y: number) {\n    this.update('y', y);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/position-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { PositionSchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { PositionSchema, PositionSchemaDecoration };\n\nexport class PositionData extends EntityData<PositionSchema> implements PositionSchema {\n  static type = 'PositionData';\n\n  getDefaultData(): PositionSchema {\n    return Schema.createDefault<PositionSchema>(PositionSchemaDecoration);\n  }\n\n  get x(): number {\n    return this.data.x;\n  }\n\n  get y(): number {\n    return this.data.y;\n  }\n\n  set x(x: number) {\n    this.update('x', x);\n  }\n\n  set y(y: number) {\n    this.update('y', y);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/rotation-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { RotationSchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { RotationSchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { RotationSchema, RotationSchemaDecoration };\n\nexport class RotationData extends EntityData<RotationSchema> {\n  static type = 'RotationData';\n\n  getDefaultData(): RotationSchema {\n    return Schema.createDefault<RotationSchema>(RotationSchemaDecoration);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/scale-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ScaleSchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { ScaleSchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { ScaleSchema, ScaleSchemaDecoration };\n\nexport class ScaleData extends EntityData<ScaleSchema> implements ScaleSchema {\n  static type = 'ScaleData';\n\n  getDefaultData(): ScaleSchema {\n    return Schema.createDefault(ScaleSchemaDecoration);\n  }\n\n  get x(): number {\n    return this.data.x;\n  }\n\n  get y(): number {\n    return this.data.y;\n  }\n\n  set x(x: number) {\n    this.update('x', x);\n  }\n\n  set y(y: number) {\n    this.update('y', y);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/size-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { SizeSchema, SizeSchemaDecoration, Schema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { SizeSchema, SizeSchemaDecoration };\n\nexport class SizeData extends EntityData<SizeSchema> implements SizeSchema {\n  static type = 'SizeData';\n\n  getDefaultData(): SizeSchema {\n    return Schema.createDefault<SizeSchema>(SizeSchemaDecoration);\n  }\n\n  get width(): number {\n    return this.data.width;\n  }\n\n  get height(): number {\n    return this.data.height;\n  }\n\n  set width(width: number) {\n    this.update('width', width);\n  }\n\n  set height(height: number) {\n    this.update('height', height);\n  }\n\n  get locked(): boolean {\n    return !!this.data.locked;\n  }\n\n  set locked(locked: boolean) {\n    this.update('locked', locked);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/skew-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { SkewSchemaDecoration, Schema } from '@flowgram.ai/utils';\nimport type { SkewSchema } from '@flowgram.ai/utils';\n\nimport { EntityData } from '../entity-data';\n\nexport { SkewSchema, SkewSchemaDecoration };\n\nexport class SkewData extends EntityData<SkewSchema> implements SkewSchema {\n  static type = 'SkewData';\n\n  getDefaultData(): SkewSchema {\n    return Schema.createDefault<SkewSchema>(SkewSchemaDecoration);\n  }\n\n  get x(): number {\n    return this.data.x;\n  }\n\n  get y(): number {\n    return this.data.y;\n  }\n\n  set x(x: number) {\n    this.update('x', x);\n  }\n\n  set y(y: number) {\n    this.update('y', y);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/schema/transform-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Angle,\n  Circle,\n  Disposable,\n  DisposableCollection,\n  Matrix,\n  PI_2,\n  RAD_TO_DEG,\n  Rectangle,\n  Schema,\n  TransformSchema,\n  TransformSchemaDecoration,\n} from '@flowgram.ai/utils';\n\nimport { Bounds } from '../utils/bounds';\nimport { EntityData } from '../entity-data';\nimport type { Entity } from '../entity';\nimport { SkewData, type SkewSchema } from './skew-schema';\nimport { SizeData, type SizeSchema } from './size-schema';\nimport { ScaleData, type ScaleSchema } from './scale-schema';\nimport { RotationData } from './rotation-schema';\nimport { PositionData, type PositionSchema } from './position-schema';\nimport { OriginData, type OriginSchema } from './origin-schema';\n\nexport { TransformSchemaDecoration, TransformSchema };\n\nexport class TransformData extends EntityData<TransformSchema> implements TransformSchema {\n  static type = 'TransformData';\n\n  protected _worldTransform: Matrix = new Matrix();\n\n  protected _localTransform: Matrix = new Matrix();\n\n  protected _children: TransformData[] | undefined;\n\n  protected mutationCache: Map<string, any> = new Map();\n\n  public sizeToScale = false; // 标记 size 转成 scale\n\n  get children(): TransformData[] {\n    return this._children || [];\n  }\n\n  clearChildren(): void {\n    if (this._children) {\n      this._children.slice().forEach((child) => {\n        child.setParent(undefined);\n      });\n    }\n  }\n\n  /**\n   * 容器选择框会动态计算子节点大小\n   */\n  get isContainer(): boolean {\n    return !!this._children && this._children.length > 0;\n  }\n\n  /**\n   * The X-coordinate value of the normalized local X axis,\n   * the first column of the local transformation matrix without a scale.\n   */\n  protected _cx = 1;\n\n  /**\n   * The Y-coordinate value of the normalized local X axis,\n   * the first column of the local transformation matrix without a scale.\n   */\n  protected _sx = 0;\n\n  /**\n   * The X-coordinate value of the normalized local Y axis,\n   * the second column of the local transformation matrix without a scale.\n   */\n  protected _cy = 0;\n\n  /**\n   * The Y-coordinate value of the normalized local Y axis,\n   * the second column of the local transformation matrix without a scale.\n   */\n  protected _sy = 1;\n\n  /**\n   * The locally unique ID of the local transform.\n   */\n  protected _localID = 0;\n\n  /**\n   * The locally unique ID of the local transform\n   * used to calculate the current local transformation matrix.\n   */\n  protected _currentLocalID = 0;\n\n  /**\n   * The locally unique ID of the world transform.\n   */\n  protected _worldID = 0;\n\n  /**\n   * The locally unique ID of the parent's world transform\n   * used to calculate the current world transformation matrix.\n   */\n  protected _parentID = 0;\n\n  /**\n   * The parent transform\n   */\n  protected _parent?: TransformData;\n\n  constructor(entity: Entity) {\n    super(entity);\n    // 默认添加\n    this.bindChange(this.entity.addData(PositionData));\n    this.bindChange(this.entity.addData(SizeData));\n    this.bindChange(this.entity.addData(OriginData));\n    this.bindChange(this.entity.addData(ScaleData));\n    this.bindChange(this.entity.addData(SkewData), () => this.updateSkew());\n    this.bindChange(this.entity.addData(RotationData), () => this.updateSkew());\n  }\n\n  fireChange(): void {\n    if (this.changeLocked) return;\n    this._localID++;\n    this.mutationCache.clear();\n    super.fireChange();\n  }\n\n  get localTransform(): Matrix {\n    this.updateLocalTransformMatrix();\n    return this._localTransform;\n  }\n\n  get worldTransform(): Matrix {\n    this.updateTransformMatrix();\n    return this._worldTransform;\n  }\n\n  getDefaultData(): TransformSchema {\n    return Schema.createDefault<TransformSchema>(TransformSchemaDecoration);\n  }\n\n  update(data: Partial<TransformSchema>): void {\n    if (data.position) {\n      this.entity.updateData(PositionData, data.position);\n    }\n    if (data.size) {\n      this.entity.updateData(SizeData, data.size);\n    }\n    if (data.origin) {\n      this.entity.updateData(OriginData, data.origin);\n    }\n    if (data.scale) {\n      this.entity.updateData(ScaleData, data.scale);\n    }\n    if (data.skew) {\n      this.entity.updateData(SkewData, data.skew);\n    }\n    if (data.rotation !== undefined) {\n      this.entity.updateData(RotationData, data.rotation);\n    }\n  }\n\n  get position(): PositionSchema {\n    return this.entity.getData<PositionData>(PositionData)!;\n  }\n\n  set position(position: PositionSchema) {\n    this.entity.updateData<PositionData>(PositionData, position);\n  }\n\n  get size(): SizeSchema {\n    return this.entity.getData<SizeData>(SizeData)!;\n  }\n\n  set size(size: SizeSchema) {\n    this.entity.updateData<SizeData>(SizeData, size);\n  }\n\n  get origin(): OriginSchema {\n    return this.entity.getData<OriginData>(OriginData)!;\n  }\n\n  set origin(origin: OriginSchema) {\n    this.entity.updateData<OriginData>(OriginData, origin);\n  }\n\n  get scale(): ScaleSchema {\n    return this.entity.getData<ScaleData>(ScaleData)!;\n  }\n\n  set scale(scale: ScaleSchema) {\n    this.entity.updateData<ScaleData>(ScaleData, scale);\n  }\n\n  get skew(): SkewSchema {\n    return this.entity.getData<SkewData>(SkewData)!;\n  }\n\n  set skew(skew: SkewSchema) {\n    this.entity.updateData<SkewData>(SkewData, skew);\n  }\n\n  get rotation(): number {\n    return this.entity.getData<RotationData>(RotationData)!.data;\n  }\n\n  set rotation(rotation: number) {\n    this.entity.updateData<RotationData>(RotationData, rotation);\n  }\n\n  get data(): TransformSchema {\n    return TransformSchema.toJSON(this);\n  }\n\n  /**\n   * Called when the skew or the rotation changes.\n   *\n   * @protected\n   */\n  protected updateSkew(): void {\n    const { rotation } = this;\n    this._cx = Math.cos(rotation + this.skew.y);\n    this._sx = Math.sin(rotation + this.skew.y);\n    this._cy = -Math.sin(rotation - this.skew.x); // cos, added PI/2\n    this._sy = Math.cos(rotation - this.skew.x); // sin, added PI/2\n\n    this._localID++;\n  }\n\n  /**\n   * Updates the local transformation matrix.\n   */\n  protected updateLocalTransformMatrix(): void {\n    const lt = this._localTransform;\n\n    if (this._localID !== this._currentLocalID) {\n      // get the matrix values of the displayobject based on its transform properties..\n      lt.a = this._cx * this.scale.x;\n      lt.b = this._sx * this.scale.x;\n      lt.c = this._cy * this.scale.y;\n      lt.d = this._sy * this.scale.y;\n\n      // TODO 删除这个 origin 偏移，不然会有一像素的偏差\n      lt.tx = this.position.x; //  - (this.origin.x * lt.a + this.origin.y * lt.c)\n      lt.ty = this.position.y; // - (this.origin.x * lt.b + this.origin.y * lt.d)\n      this._currentLocalID = this._localID;\n\n      // force an update..\n      this._parentID = -1;\n    }\n  }\n\n  get localID() {\n    return this._localID;\n  }\n\n  get worldID() {\n    return this._worldID;\n  }\n\n  /**\n   * Updates the local and the world transformation matrices.\n   *\n   */\n  protected updateTransformMatrix(): void {\n    const lt = this._localTransform;\n    this.updateLocalTransformMatrix();\n    let parentTransform: Matrix = Matrix.TEMP_MATRIX;\n    let worldId = 0;\n    if (this.parent) {\n      parentTransform = this.parent.worldTransform;\n      worldId = this.parent._worldID;\n    }\n    if (this._parentID !== worldId) {\n      // concat the parent matrix with the objects transform.\n      const pt = parentTransform;\n      const wt = this._worldTransform;\n\n      wt.a = lt.a * pt.a + lt.b * pt.c;\n      wt.b = lt.a * pt.b + lt.b * pt.d;\n      wt.c = lt.c * pt.a + lt.d * pt.c;\n      wt.d = lt.c * pt.b + lt.d * pt.d;\n      wt.tx = lt.tx * pt.a + lt.ty * pt.c + pt.tx;\n      wt.ty = lt.tx * pt.b + lt.ty * pt.d + pt.ty;\n      this._parentID = worldId;\n\n      // update the id of the transform..\n      this._worldID++;\n    }\n  }\n\n  /**\n   * Decomposes a matrix and sets the transforms properties based on it.\n   *\n   * matrix - The matrix to decompose\n   */\n  setFromMatrix(matrix: Matrix): void {\n    // sort out rotation / skew..\n    const { a, b, c, d } = matrix;\n\n    const skewX = -Math.atan2(-c, d);\n    const skewY = Math.atan2(b, a);\n\n    const delta = Math.abs(skewX + skewY);\n\n    if (delta < 0.00001 || Math.abs(PI_2 - delta) < 0.00001) {\n      this.rotation = skewY;\n      this.skew.x = this.skew.y = 0;\n    } else {\n      this.rotation = 0;\n      this.skew.x = skewX;\n      this.skew.y = skewY;\n    }\n\n    // next set scale\n    this.scale.x = Math.sqrt(a * a + b * b);\n    this.scale.y = Math.sqrt(c * c + d * d);\n\n    // next set position\n    this.position.x = matrix.tx;\n    this.position.y = matrix.ty;\n    this.fireChange();\n  }\n\n  /**\n   * 缓存计算, 缓存只能针对 local, world 加缓存会出问题\n   */\n  getMutationCache<T>(key: string, fn: () => T): T {\n    // 缓存设计有问题，先去掉\n    if (this.mutationCache.has(key)) return this.mutationCache.get(key) as T;\n    const item = fn();\n    this.mutationCache.set(key, item);\n    return item;\n  }\n\n  get bounds(): Rectangle {\n    if (this.isContainer) {\n      const children = this._children!;\n      return Rectangle.enlarge(children.map((c) => c.bounds));\n    }\n    return Bounds.getBounds(this, this.worldTransform);\n  }\n\n  /**\n   * 不旋转的 bounds\n   */\n  get boundsWithoutRotation(): Rectangle {\n    const { center } = this.bounds;\n    const { worldScale } = this;\n    // TODO 目前 container 计算有误差，需要解决\n    const size = this.localSize;\n    const width = worldScale.x * size.width;\n    const height = worldScale.y * size.height;\n    const leftTop = {\n      x: center.x - width / 2,\n      y: center.y - height / 2,\n    };\n    return new Rectangle(leftTop.x, leftTop.y, width, height);\n  }\n\n  /**\n   * 本身的大小\n   */\n  get localSize(): SizeSchema {\n    let { size } = this;\n    if (this.isContainer) {\n      const childrenBounds = Rectangle.enlarge(this.children.map((c) => c.localBounds));\n      size = {\n        width: childrenBounds.width,\n        height: childrenBounds.height,\n      };\n    }\n    return {\n      width: size.width,\n      height: size.height,\n    };\n  }\n\n  get worldSize(): SizeSchema {\n    const { localSize } = this;\n    const { worldScale } = this;\n    return {\n      width: localSize.width * worldScale.x,\n      height: localSize.height * worldScale.y,\n    };\n  }\n\n  /**\n   * 本地 bounds\n   */\n  get localBounds(): Rectangle {\n    if (this.isContainer) {\n      const children = this._children!;\n      const childrenBounds = Rectangle.enlarge(children.map((c) => c.localBounds));\n      // 投射 local\n      return Bounds.applyMatrix(childrenBounds, this.localTransform);\n    }\n    return this.getMutationCache<Rectangle>('localBounds', () =>\n      Bounds.getBounds(this, this.localTransform)\n    );\n  }\n\n  /**\n   * 判断是否包含点\n   * @param x\n   * @param y\n   * @param asCircle - 以圆形来算，TODO 目前不支持椭圆形\n   */\n  contains(x: number, y: number, asCircle?: boolean): boolean {\n    // Container 情况不支持 circle\n    if (this.isContainer) {\n      return this.bounds.contains(x, y);\n    }\n    const tempPoint = this.worldTransform.applyInverse({ x, y });\n    const { width, height } = this.size;\n    // 不包含空大小 TODO\n    if (width === 0 || height === 0) return false;\n    const x1 = -width * this.origin.x;\n    const y1 = -height * this.origin.y;\n    if (asCircle) {\n      const circle = new Circle(x1 + width / 2, y1 + height / 2, Math.min(width / 2, height / 2));\n      return circle.contains(tempPoint.x, tempPoint.y);\n    }\n    if (tempPoint.x >= x1 && tempPoint.x < x1 + width) {\n      if (tempPoint.y >= y1 && tempPoint.y < y1 + height) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  get parent(): TransformData | undefined {\n    return this._parent;\n  }\n\n  isParent(parent: TransformData): boolean {\n    let currentParent = this.parent;\n    while (currentParent) {\n      if (currentParent === parent) return true;\n      currentParent = currentParent.parent;\n    }\n    return false;\n  }\n\n  isParentTransform(parent?: TransformData): boolean {\n    let currentParent = this.parent;\n    while (currentParent) {\n      if (currentParent === parent) return true;\n      currentParent = currentParent.parent;\n    }\n    return false;\n  }\n\n  private _parentChangedDispose?: DisposableCollection;\n\n  private entityDispose?: Disposable;\n\n  setParent(parent: TransformData | undefined, listenParentData = true): void {\n    if (this._parent !== parent) {\n      if (this.entityDispose) {\n        this.entityDispose.dispose();\n        this.entityDispose = undefined;\n      }\n      if (this._parentChangedDispose) {\n        this._parentChangedDispose.dispose();\n        this._parentChangedDispose = undefined;\n      }\n      this._parentID = -1;\n      if (parent && listenParentData) {\n        if (!parent._children) parent._children = [];\n        parent._children.push(this);\n        this._parentChangedDispose = new DisposableCollection();\n        this.toDispose.push(this._parentChangedDispose);\n        this.entityDispose = this.entity.onDispose(() => {\n          parent.fireChange();\n        });\n        this._parentChangedDispose.pushAll([\n          parent.onDispose(() => {\n            this.setParent(undefined);\n          }),\n          Disposable.create(() => {\n            const index = parent._children!.indexOf(this);\n            if (index !== -1) {\n              parent._children!.splice(index, 1);\n            }\n          }),\n        ]);\n      }\n      this._parent = parent;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * 判断矩形碰撞\n   */\n  intersects(rect: Rectangle): boolean {\n    if (!this.isContainer && (this.size.width === 0 || this.size.height === 0)) return false;\n    return Rectangle.intersectsWithRotation(\n      this.boundsWithoutRotation,\n      this.worldRotation,\n      rect,\n      0\n    );\n  }\n\n  /**\n   * 全局的 scale\n   */\n  get worldScale(): ScaleSchema {\n    const { parent } = this;\n    const parentScale = parent ? parent.worldScale : { x: 1, y: 1 };\n    return {\n      x: this.scale.x * parentScale.x,\n      y: this.scale.y * parentScale.y,\n    };\n  }\n\n  /**\n   * 全局的 rotation\n   */\n  get worldRotation(): number {\n    const { parent } = this;\n    if (parent) {\n      return Angle.wrap(this.rotation + parent.worldRotation);\n    }\n    return Angle.wrap(this.rotation);\n  }\n\n  /**\n   * 全局的角度\n   */\n  get worldDegree(): number {\n    return Math.round(this.worldRotation * RAD_TO_DEG);\n  }\n\n  get localOrigin(): PositionSchema {\n    const matrix = this.localTransform;\n    const bounds = this.localBounds;\n    return matrix.apply({\n      x: this.origin.x * bounds.width,\n      y: this.origin.y * bounds.height,\n    });\n  }\n\n  /**\n   * 全局的原点位置\n   */\n  get worldOrigin(): PositionSchema {\n    const matrix = this.worldTransform;\n    const { bounds } = this;\n    return matrix.apply({\n      x: this.origin.x * bounds.width,\n      y: this.origin.y * bounds.height,\n    });\n  }\n\n  /**\n   * 宽转换成 scale，用于图片等无法修改大小的场景\n   * @param isWorldSize 是否为绝对大小\n   */\n  widthToScaleX(width: number, isWorldSize?: boolean): number {\n    const parentScaleX = isWorldSize && this.parent ? this.parent.worldScale.x : 1;\n    return width / parentScaleX / this.localSize.width;\n  }\n\n  /**\n   * 绝对高转换成 scale，用于图片等无法修改大小的场景\n   * @param isWorldSize 是否为绝对大小\n   */\n  heightToScaleY(height: number, isWorldSize?: boolean): number {\n    const parentScaleY = isWorldSize && this.parent ? this.parent.worldScale.y : 1;\n    return height / parentScaleY / this.localSize.height;\n  }\n\n  sizeToScaleValue(\n    size: { width: number; height: number },\n    isWorldSize?: boolean\n  ): { x: number; y: number } {\n    return {\n      x: this.widthToScaleX(size.width, isWorldSize),\n      y: this.heightToScaleY(size.height, isWorldSize),\n    };\n  }\n}\n\nexport namespace TransformData {\n  /**\n   * @param dragableEntities\n   * @param target\n   */\n  export function isParentOrChildrenTransform(dragableEntities: Entity[], target: Entity): boolean {\n    const targetTransform = target.getData<TransformData>(TransformData);\n    if (!targetTransform) return false;\n    for (const dragger of dragableEntities.values()) {\n      const draggerTransform = dragger.getData<TransformData>(TransformData);\n      // eslint-disable-next-line no-continue\n      if (!draggerTransform) continue;\n      if (\n        draggerTransform.isParent(targetTransform) ||\n        targetTransform.isParent(draggerTransform)\n      ) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/utils/bounds.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// nolint: cyclo_complexity,method_line\nimport { expect, describe, beforeEach, it } from 'vitest';\nimport { Container } from 'inversify';\nimport { Matrix, Rectangle } from '@flowgram.ai/utils';\n\nimport { Entity, EntityManager, PlaygroundContext, TransformData } from '..';\nimport { Bounds } from './bounds';\n\nfunction createContainer(): Container {\n  const child = new Container({ defaultScope: 'Singleton' });\n  // child.bind(AbleManager).toSelf();\n  child.bind(PlaygroundContext).toConstantValue({});\n  child.bind(EntityManager).toSelf();\n  return child;\n}\n\nconst container = createContainer();\n\nfunction createEntity(): Entity {\n  return container.get<EntityManager>(EntityManager).createEntity<Entity>(Entity);\n}\n\nfunction expectRectangle(target: Rectangle, arr: number[]): void {\n  expect(target.x).toBeCloseTo(arr[0]);\n  expect(target.y).toBeCloseTo(arr[1]);\n  expect(target.width).toBeCloseTo(arr[2]);\n  expect(target.height).toBeCloseTo(arr[3]);\n}\n\ndescribe('bounds', () => {\n  let target: TransformData;\n  beforeEach(() => {\n    target = new TransformData(createEntity());\n    target.size.width = 100;\n    target.size.height = 100;\n  });\n\n  it('get points and bounds', () => {\n    target.origin.x = 0;\n    target.origin.y = 0;\n    expect(Bounds.getCenter(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: 50,\n    });\n    expect(Bounds.getTopLeft(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: 0,\n    });\n    expect(Bounds.getTopCenter(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: 0,\n    });\n    expect(Bounds.getTopRight(target, target.worldTransform)).toEqual({\n      x: 100,\n      y: 0,\n    });\n    expect(Bounds.getLeftCenter(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: 50,\n    });\n    expect(Bounds.getRightCenter(target, target.worldTransform)).toEqual({\n      x: 100,\n      y: 50,\n    });\n    expect(Bounds.getBottomLeft(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: 100,\n    });\n    expect(Bounds.getBottomCenter(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: 100,\n    });\n    expect(Bounds.getBottomRight(target, target.worldTransform)).toEqual({\n      x: 100,\n      y: 100,\n    });\n    expect(Bounds.getBounds(target, target.worldTransform)).toEqual(new Rectangle(0, 0, 100, 100));\n\n    target.origin.x = 0.5;\n    target.origin.y = 0.5;\n    expect(Bounds.getCenter(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: 0,\n    });\n    expect(Bounds.getTopLeft(target, target.worldTransform)).toEqual({\n      x: -50,\n      y: -50,\n    });\n    expect(Bounds.getTopCenter(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: -50,\n    });\n    expect(Bounds.getTopRight(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: -50,\n    });\n    expect(Bounds.getLeftCenter(target, target.worldTransform)).toEqual({\n      x: -50,\n      y: 0,\n    });\n    expect(Bounds.getRightCenter(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: 0,\n    });\n    expect(Bounds.getBottomLeft(target, target.worldTransform)).toEqual({\n      x: -50,\n      y: 50,\n    });\n    expect(Bounds.getBottomCenter(target, target.worldTransform)).toEqual({\n      x: 0,\n      y: 50,\n    });\n    expect(Bounds.getBottomRight(target, target.worldTransform)).toEqual({\n      x: 50,\n      y: 50,\n    });\n    expect(Bounds.getBounds(target, target.worldTransform)).toEqual(\n      new Rectangle(-50, -50, 100, 100),\n    );\n  });\n\n  it('transform position', () => {\n    target.position.x = 10;\n    target.position.y = 10;\n    expect(Bounds.getTopLeft(target)).toEqual({ x: -50, y: -50 });\n    expect(Bounds.getTopLeft(target, target.localTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n    expect(Bounds.getTopLeft(target, target.worldTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n  });\n\n  it('transform position with parent', () => {\n    const parent = new TransformData(createEntity());\n    parent.position.x = 10;\n    parent.position.y = 10;\n    target.setParent(parent);\n    expect(Bounds.getTopLeft(target)).toEqual({\n      x: -50,\n      y: -50,\n    });\n    expect(Bounds.getTopLeft(target, target.localTransform)).toEqual({\n      x: -50,\n      y: -50,\n    });\n    expect(Bounds.getTopLeft(target, target.worldTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n    expect(Bounds.getLeftPointFromBounds(target, target.worldTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n    expect(Bounds.getTopPointFromBounds(target, target.worldTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n    expect(Bounds.getLeftPointFromBounds(parent, parent.worldTransform)).toEqual({\n      x: 10,\n      y: 10,\n    });\n    expect(Bounds.getTopPointFromBounds(parent, parent.worldTransform)).toEqual({\n      x: 10,\n      y: 10,\n    });\n\n    target.position.x = 10;\n    target.position.y = 10;\n    expect(Bounds.getTopLeft(target)).toEqual({\n      x: -50,\n      y: -50,\n    });\n    expect(Bounds.getTopLeft(target, target.localTransform)).toEqual({\n      x: -40,\n      y: -40,\n    });\n    expect(Bounds.getTopLeft(target, target.worldTransform)).toEqual({\n      x: -30,\n      y: -30,\n    });\n  });\n\n  it('transform rotation', () => {\n    // 中心点旋转 45 度\n    target.rotation = Math.PI / 4;\n    expectRectangle(\n      Bounds.getBounds(target, target.worldTransform),\n      [-70.71, -70.71, 141.42, 141.42],\n    );\n  });\n\n  it('trasnform scale', () => {\n    target.scale.x = 2;\n    const bounds = Bounds.getBounds(target, target.worldTransform);\n    expect(bounds.width).toEqual(200);\n    expect(bounds.height).toEqual(100);\n  });\n\n  it('applyMatrix - without rotation', () => {\n    target.position.x = 50;\n    target.position.y = 50;\n    expect(target.worldTransform).toEqual(new Matrix(1, 0, 0, 1, 50, 50));\n    expectRectangle(Bounds.getBounds(target, target.worldTransform), [0, 0, 100, 100]);\n    expectRectangle(\n      Bounds.applyMatrix(new Rectangle(-50, -50, 100, 100), target.worldTransform),\n      [0, 0, 100, 100],\n    );\n  });\n\n  it('applyMatrix - with rotation', () => {\n    target.position.x = 50;\n    target.position.y = 50;\n    target.scale.x = 4;\n    expect(target.worldTransform).toEqual(new Matrix(4, 0, 0, 1, 50, 50));\n    expectRectangle(Bounds.getBounds(target, target.worldTransform), [-150, 0, 400, 100]);\n    expectRectangle(\n      Bounds.applyMatrix(new Rectangle(-50, -50, 100, 100), target.worldTransform),\n      [-150, 0, 400, 100],\n    );\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/utils/bounds.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Matrix, Point, Rectangle } from '@flowgram.ai/utils';\n\nimport type { PositionSchema, TransformSchema } from '../schema';\n\ntype TransformOriginAndSize = Pick<TransformSchema, 'origin'> & Pick<TransformSchema, 'size'>;\n\nconst { fixZero } = Point;\n\nexport namespace Bounds {\n  /**\n   * 位置做矩阵偏移\n   */\n  export function getPointWithMatrix(output: PositionSchema, matrix?: Matrix): PositionSchema {\n    // if (target.rotation !== 0) {\n    // rotateAround(output, target.position.x, target.position.y, target.rotation);\n    // }\n    if (matrix) {\n      matrix.apply(output, output);\n    }\n    // fix: -0\n    fixZero(output);\n    return output;\n  }\n  /**\n   * 获取外围边界矩形\n   */\n  export function getBounds(target: TransformOriginAndSize, matrix?: Matrix): Rectangle {\n    const output = new Rectangle();\n    if (!matrix || matrix.isSimple()) {\n      const { size, origin } = target;\n      output.x = -(size.width * origin.x) + (matrix?.tx || 0);\n      output.y = -(size.height * origin.y) + (matrix?.ty || 0);\n      output.width = size.width;\n      output.height = size.height;\n      // fix: -0\n      fixZero(output);\n    } else {\n      const topLeft = getTopLeft(target, matrix);\n      const topRight = getTopRight(target, matrix);\n      const bottomLeft = getBottomLeft(target, matrix);\n      const bottomRight = getBottomRight(target, matrix);\n      output.x = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);\n      output.y = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);\n      output.width = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x) - output.x;\n      output.height = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y) - output.y;\n    }\n    return output;\n  }\n  export function applyMatrix(bounds: Rectangle, matrix: Matrix): Rectangle {\n    const output = new Rectangle();\n    if (matrix.isSimple()) {\n      output.x = bounds.x + matrix.tx;\n      output.y = bounds.y + matrix.ty;\n      output.width = bounds.width;\n      output.height = bounds.height;\n      // fix: -0\n      fixZero(output);\n    } else {\n      const topLeft = getPointWithMatrix(bounds.leftTop, matrix);\n      const topRight = getPointWithMatrix(bounds.rightTop, matrix);\n      const bottomLeft = getPointWithMatrix(bounds.leftBottom, matrix);\n      const bottomRight = getPointWithMatrix(bounds.rightBottom, matrix);\n      output.x = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);\n      output.y = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);\n      output.width = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x) - output.x;\n      output.height = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y) - output.y;\n    }\n    return output;\n  }\n\n  /**\n   * 找到边框中最左边的点\n   */\n  export function getLeftPointFromBounds(\n    target: TransformOriginAndSize,\n    matrix?: Matrix,\n  ): PositionSchema {\n    const topLeft = getTopLeft(target, matrix);\n    const topRight = getTopRight(target, matrix);\n    const bottomLeft = getBottomLeft(target, matrix);\n    const bottomRight = getBottomRight(target, matrix);\n    const items = [topLeft, topRight, bottomLeft, bottomRight].sort((p1, p2) => p1.x - p2.x);\n    return items[0];\n  }\n  /**\n   * 找到边框中最上边的点\n   */\n  export function getTopPointFromBounds(\n    target: TransformOriginAndSize,\n    matrix?: Matrix,\n  ): PositionSchema {\n    const topLeft = getTopLeft(target, matrix);\n    const topRight = getTopRight(target, matrix);\n    const bottomLeft = getBottomLeft(target, matrix);\n    const bottomRight = getBottomRight(target, matrix);\n    const items = [topLeft, topRight, bottomLeft, bottomRight].sort((p1, p2) => p1.y - p2.y);\n    return items[0];\n  }\n  export function getCenter(target: TransformSchema, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width / 2,\n      y: -(size.height * origin.y) + size.height / 2,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getTopLeft(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x),\n      y: -(size.height * origin.y),\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getTopCenter(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width / 2,\n      y: -(size.height * origin.y),\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getTopRight(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width,\n      y: -(size.height * origin.y),\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getLeftCenter(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x),\n      y: -(size.height * origin.y) + size.height / 2,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getRightCenter(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width,\n      y: -(size.height * origin.y) + size.height / 2,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getBottomLeft(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x),\n      y: -(size.height * origin.y) + size.height,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getBottomCenter(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width / 2,\n      y: -(size.height * origin.y) + size.height,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n  export function getBottomRight(target: TransformOriginAndSize, matrix?: Matrix): PositionSchema {\n    const { size, origin } = target;\n    const output = {\n      x: -(size.width * origin.x) + size.width,\n      y: -(size.height * origin.y) + size.height,\n    };\n    return getPointWithMatrix(output, matrix);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/common/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './bounds';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './pipeline';\nexport * from './layer';\nexport * from './utils';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/config/editor-state-config-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport { Disposable, Emitter } from '@flowgram.ai/utils';\nimport { ConfigEntity, EntityOpts } from '../../../common';\nimport type { PlaygroundConfigEntity } from './playground-config-entity';\n\n/**\n * 编辑态\n */\nexport interface EditorState {\n  id: string;\n  disabled?: boolean | ((config: PlaygroundConfigEntity) => boolean);\n  cursor?: string; // 光标类型\n  shortcut?: string; // 快捷键\n  shortcutAutoEsc?: boolean; // 点击快捷键后自动退出到默认\n  // 仅在状态发生变化时，当前快捷键才会生效\n  shortcutWorksOnlyOnStateChanged?: boolean;\n  handle?: (config: PlaygroundConfigEntity, e?: EditorStateChangeEvent) => void; // 触发逻辑\n  disableSelector?: boolean; // 切换后会把选择器隐藏了\n  cancelMode:\n    | 'esc' // 按住 esc 则退\n    | 'once' // 触发一次后则退出\n    | 'hold' // 点击会保持该状态\n  onEsc?: (config: PlaygroundConfigEntity, e?: KeyboardEvent) => void;\n}\n\nexport namespace EditorState {\n  export const STATE_SELECT: EditorState = {\n    id: 'STATE_SELECT',\n    cursor: '',\n    shortcut: '',\n    cancelMode: 'hold',\n  };\n\n  /** 鼠标友好模式状态 */\n  export const STATE_MOUSE_FRIENDLY_SELECT: EditorState = {\n    id: 'STATE_MOUSE_FRIENDLY_SELECT',\n    cursor: 'grab', // 初始为小手状态\n    shortcut: '',\n    cancelMode: 'hold',\n  };\n\n  export const STATE_GRAB: EditorState = {\n    id: 'STATE_GRAB',\n    cursor: 'grab',\n    shortcut: 'SPACE', // 如果是鼠标模式，这里不用按住 SPACE 就可以拖动\n    shortcutAutoEsc: true,\n    shortcutWorksOnlyOnStateChanged: true,\n    cancelMode: 'hold',\n  };\n}\nexport const EDITOR_STATE_DEFAULTS: EditorState[] = [\n  EditorState.STATE_SELECT,\n  EditorState.STATE_MOUSE_FRIENDLY_SELECT,\n  EditorState.STATE_GRAB,\n];\n\nexport interface EditorStateChangeEvent {\n  state: EditorState;\n  event?: React.MouseEvent;\n  lastState?: EditorState;\n}\n\n/**\n * 编辑状态管理\n */\nexport class EditorStateConfigEntity extends ConfigEntity {\n  static type = 'EditorStateConfigEntity';\n\n  private _isPressingSpaceBar: boolean = false;\n  private _isPressingShift: boolean = false;\n\n  protected states = EDITOR_STATE_DEFAULTS.slice();\n\n  protected selected: string = EditorState.STATE_SELECT.id;\n\n  protected onStateChangeEmitter = new Emitter<EditorStateChangeEvent>();\n\n  readonly onStateChange = this.onStateChangeEmitter.event;\n\n  constructor(opts: EntityOpts) {\n    super(opts);\n    this.toDispose.push(this.onStateChangeEmitter);\n  }\n\n  get isPressingSpaceBar(): boolean {\n    return this._isPressingSpaceBar;\n  }\n\n  set isPressingSpaceBar(isPressing: boolean) {\n    this._isPressingSpaceBar = isPressing;\n  }\n\n  get isPressingShift(): boolean {\n    return this._isPressingShift;\n  }\n\n  set isPressingShift(isPressing: boolean) {\n    this._isPressingShift = isPressing;\n  }\n\n  /**\n   * 取消指定状态后触发\n   * @param stateId\n   * @param fn\n   */\n  onCancel(stateId: string, fn: () => void): Disposable {\n    return this.onStateChange(e => {\n      if (e.lastState && e.lastState.id === stateId) {\n        fn();\n      }\n    });\n  }\n\n  getCurrentState(): EditorState | undefined {\n    return this.states.find(s => s.id === this.selected);\n  }\n\n  is(stateId: string): boolean {\n    return this.selected === stateId;\n  }\n\n  changeState(stateId: string, event?: React.MouseEvent): void {\n    const state = this.states.find(s => s.id === stateId);\n    if (!state) throw new Error(`Unknown editor state ${stateId}`);\n    if (this.selected !== stateId) {\n      const lastState = this.getCurrentState();\n      this.selected = stateId;\n      this.onStateChangeEmitter.fire({ state, event, lastState });\n      this.fireChange();\n    }\n  }\n\n  toDefaultState(): void {\n    this.changeState(EditorState.STATE_SELECT.id);\n  }\n\n  registerState(state: EditorState): void {\n    this.states.push(state);\n    // this.sortStates();\n    this.fireChange();\n  }\n\n  getStates(): EditorState[] {\n    return this.states;\n  }\n\n  /**\n   * 是否为鼠标友好模式\n   */\n  isMouseFriendlyMode(): boolean {\n    return this.getCurrentState() === EditorState.STATE_MOUSE_FRIENDLY_SELECT;\n  }\n\n\n  getStateFromShortcut(e: KeyboardEvent): EditorState | undefined {\n    return this.states.find(s => {\n      const shortcut =\n        s.shortcut === 'SPACE' ? ' ' : (s.shortcut || '').toLowerCase();\n      if (shortcut === e.key.toLowerCase()) {\n        return s;\n        // switch (e.key.toLowerCase()) {\n        //   case '=':\n        //   case '-':\n        //   case '0':\n        //     if (e.ctrlKey) {\n        //       return s;\n        //     }\n        //     break;\n        //   default:\n        //     return s;\n        // }\n      }\n      return undefined;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/config/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './editor-state-config-entity'\nexport * from './playground-config-entity'\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/config/playground-config-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable, domUtils, Emitter, PromiseDeferred, Rectangle } from '@flowgram.ai/utils'\nimport {\n  ConfigEntity,\n  Entity,\n  PositionData,\n  PositionSchema,\n  SizeData,\n  SizeSchema,\n  TransformData,\n} from '../../../common'\nimport { MouseTouchEvent, startTween } from '../../utils'\n// import { Selectable } from '../../able'\n\nexport interface PlaygroundConfigEntityData {\n  scrollX: number // 滚动 x\n  scrollY: number // 滚动 y\n  originX: number // 左上角默认开始的原点坐标\n  originY: number // 左上角默认开始原点坐标\n  width: number // 编辑区宽，在 onResize 触发后重制\n  height: number // 编辑区高，在 onResize 触发后重制\n  reverseScroll: boolean // 支持反方向滚动\n  overflowX: 'hidden' | 'scroll'\n  overflowY: 'hidden' | 'scroll'\n  minZoom: number // 最大\n  maxZoom: number //  最小\n  zoom: number // 缩放比\n  scrollLimitX?: number // 水平滚动限制\n  scrollLimitY?: number // 垂直滚动限制\n  mouseScrollDelta?: number | ((zoom: number) => number); // 鼠标滚动时的 delta 值\n  pageBounds?: { x: number; y: number; width: number; height: number } // 编辑的画布边框，用于处理外部对齐问题\n  disabled: boolean // 禁用状态\n  readonly: boolean // readonly 状态\n  scrollDisable: boolean\n  grabDisable: boolean // 禁用抓取拖拽画布能力\n  zoomDisable: boolean\n}\n\nexport interface PlaygroundConfigRevealOpts {\n  entities?: Entity[]\n  position?: PositionSchema // 滚动到指定位置，并居中\n  bounds?: Rectangle // 滚动的 bounds\n  // selection?: boolean 是否回到选择器所在位置，默认 true\n  scrollDelta?: PositionSchema\n  zoom?: number // 需要缩放的比例\n  easing?: boolean // 是否开启缓动，默认开启\n  easingDuration?: number // 默认 500 ms\n  scrollToCenter?: boolean // 是否滚动到中心\n}\nexport const SCALE_WIDTH = 0\n\n/** 鼠标缩放 delta */\nexport const MOUSE_SCROLL_DELTA = (zoom: number) => zoom / 20;\nexport type PlaygroundScrollLimitFn = (scroll: { scrollX: number; scrollY: number }) => {\n  scrollX: number\n  scrollY: number\n}\nexport type Cursors = Record<string, string>;\n/**\n * 全局画布的配置信息\n */\nexport class PlaygroundConfigEntity extends ConfigEntity<PlaygroundConfigEntityData> {\n  static type = 'PlaygroundConfigEntity'\n\n  public getCursors: (() => Cursors | undefined) | undefined;\n  private _loading = false\n  private _zoomEnable = true\n\n  private _scrollLimitFn?: PlaygroundScrollLimitFn\n\n  private _onReadonlyOrDisabledChangeEmitter = new Emitter<{ readonly: boolean, disabled: boolean }>()\n  private _onGrabDisableChangeEmitter = new Emitter<boolean>()\n  readonly onGrabDisableChange = this._onGrabDisableChangeEmitter.event;\n  readonly onReadonlyOrDisabledChange = this._onReadonlyOrDisabledChangeEmitter.event\n  playgroundDomNode: HTMLElement = document.createElement('div')\n\n  cursor = 'default'\n  constructor(opts: any) {\n    super(opts);\n    this.toDispose.push(this._onReadonlyOrDisabledChangeEmitter)\n  }\n\n  /**\n   * 是否禁用抓取拖拽画布能力\n   */\n  get grabDisable(): boolean {\n    return this.config.grabDisable;\n  }\n\n  /**\n   * 是否禁用抓取拖拽画布能力\n   */\n  set grabDisable(grabDisable: boolean) {\n    this.updateConfig({\n      grabDisable\n    })\n  }\n  get scrollDisable(): boolean {\n    return this.config.scrollDisable;\n  }\n  set scrollDisable(scrollDisable: boolean) {\n    this.updateConfig({\n      scrollDisable\n    })\n  }\n  get zoomDisable(): boolean {\n    return this.config.zoomDisable;\n  }\n  set zoomDisable(zoomDisable: boolean) {\n    this.updateConfig({\n      zoomDisable\n    })\n  }\n  getDefaultConfig(): PlaygroundConfigEntityData {\n    return {\n      scrollX: 0,\n      scrollY: 0,\n      originX: 0,\n      originY: 0,\n      width: 0,\n      height: 0,\n      minZoom: 0.25,\n      maxZoom: 2,\n      zoom: 1,\n      reverseScroll: true,\n      overflowX: 'scroll',\n      overflowY: 'scroll',\n      disabled: false,\n      readonly: false,\n      grabDisable: false,\n      scrollDisable: false,\n      zoomDisable: false,\n      mouseScrollDelta: MOUSE_SCROLL_DELTA\n    }\n  }\n\n  /**\n   * 添加滚动限制逻辑\n   * @param fn\n   */\n  addScrollLimit(fn: PlaygroundScrollLimitFn): void {\n    this._scrollLimitFn = fn\n  }\n\n  /**\n   * 更新实体配置\n   * @param props\n   */\n  updateConfig(props: Partial<PlaygroundConfigEntityData>): void {\n    if (props.zoom !== undefined) {\n      props = { ...props, zoom: this.normalizeZoom(props.zoom) }\n    }\n    props = { ...this.config, ...props }\n    if (!props.reverseScroll) {\n      if (props.scrollX! < this.config.originX) {\n        props.scrollX = this.config.originX\n      }\n      if (props.scrollY! < this.config.originY) {\n        props.scrollY = this.config.originY\n      }\n    }\n    if (props.scrollLimitX !== undefined && props.scrollX! < props.scrollLimitX) {\n      props.scrollX = props.scrollLimitX\n    }\n    if (props.scrollLimitY !== undefined && props.scrollY! < props.scrollLimitY) {\n      props.scrollY = props.scrollLimitY\n    }\n    if (props.overflowX === 'hidden') {\n      props.scrollX = this.config.originX\n    }\n    if (props.overflowY === 'hidden') {\n      props.scrollY = this.config.originY\n    }\n    const scrollDisable = props.scrollDisable || this.scrollDisable\n    const { readonly, disabled, grabDisable } = this\n    if (scrollDisable) {\n      props.scrollX = this.config.scrollX\n      props.scrollY = this.config.scrollY\n    }\n    super.updateConfig(\n      this._scrollLimitFn\n        ? { ...props, ...this._scrollLimitFn({ scrollX: props.scrollX!, scrollY: props.scrollY! }) }\n        : props\n    )\n    const readonlyOrDisableChanged = readonly !== this.readonly || disabled !== this.disabled\n    if (readonlyOrDisableChanged) this._onReadonlyOrDisabledChangeEmitter.fire({ readonly: this.readonly, disabled: this.disabled })\n    if (grabDisable !== this.grabDisable) this._onGrabDisableChangeEmitter.fire(this.grabDisable)\n  }\n\n  /**\n   * 缩放比例\n   * 使用 zoom 替代\n   * @deprecated\n   */\n  get finalScale(): number {\n    if (!this.zoomEnable) return 1\n    return this.config.zoom\n  }\n\n  /**\n   * 缩放比例\n   */\n  get zoom(): number {\n    if (!this.zoomEnable) return 1\n    return this.config.zoom\n  }\n\n  get scrollData(): { scrollX: number; scrollY: number } {\n    return {\n      scrollX: this.config.scrollX,\n      scrollY: this.config.scrollY,\n    }\n  }\n\n  protected normalizeZoom(zoom: number): number {\n    if (!this.zoomEnable) return 1\n    if (this.zoomDisable) return this.config.zoom;\n    if (zoom < this.config.minZoom) {\n      zoom = this.config.minZoom\n    } else if (zoom > this.config.maxZoom) {\n      zoom = this.config.maxZoom\n    }\n    return zoom\n  }\n\n  /**\n   * 修改画布光标\n   * @param cursor\n   */\n  updateCursor(cursor: string): void {\n    if (this.cursor !== cursor) {\n      this.cursor = cursor\n      this.fireChange()\n    }\n  }\n\n  /**\n   * 获取相对画布的位置\n   * @param event\n   * @param widthScale 是否要计算缩放\n   */\n  getPosFromMouseEvent(\n    event:\n    | MouseEvent\n    | TouchEvent\n    | {\n        clientX: number;\n        clientY: number;\n      },\n    withScale = true\n  ): PositionSchema {\n    const { config } = this\n    const scale = withScale ? this.zoom : 1\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(event)\n    const clientRect = this.playgroundDomNode.getBoundingClientRect()\n    return {\n      x: (clientX + config.scrollX - clientRect.x) / scale,\n      y: (clientY + config.scrollY - clientRect.y) / scale,\n    }\n  }\n  getClientBounds(): Rectangle {\n    const clientRect = this.playgroundDomNode.getBoundingClientRect()\n    return new Rectangle(clientRect.x, clientRect.y, clientRect.width, clientRect.height)\n  }\n  /**\n   * 将画布中的位置转成相对 window 的位置\n   * @param pos\n   */\n  toFixedPos(pos: PositionSchema): PositionSchema {\n    const { config } = this\n    const clientRect = this.playgroundDomNode.getBoundingClientRect()\n    return {\n      x: pos.x - config.scrollX + clientRect.x,\n      y: pos.y - config.scrollY + clientRect.y,\n    }\n  }\n\n  /**\n   * 获取可视区域\n   */\n  getViewport(withScale: boolean = true): Rectangle {\n    const { config } = this\n    const scale = withScale ? this.finalScale : 1\n    return new Rectangle(\n      config.scrollX / scale,\n      config.scrollY / scale,\n      config.width / scale,\n      config.height / scale\n    )\n  }\n\n  /**\n   * 判断矩形是否在可视区域，如果有擦边页代表在可是区域\n   * @param bounds\n   * @param rotation\n   * @param includeAll - 是否包含在里边，默认 false\n   */\n  isViewportVisible(bounds: Rectangle, rotation: number = 0, includeAll: boolean = false): boolean {\n    return Rectangle.isViewportVisible(bounds, this.getViewport(), rotation, includeAll)\n  }\n\n  /**\n   * 按下边顺序执行\n   * 1. 指定的 entity 位置或 pos 位置\n   * 3. 初始化位置\n   */\n  scrollToView(opts: PlaygroundConfigRevealOpts = {}): Promise<void> {\n    const {\n      scrollDelta,\n      position: pos,\n      // selection = true,\n      easing = true,\n      easingDuration = 300,\n      entities,\n    } = opts\n    const { config } = this\n    const scale = opts.zoom ? opts.zoom : this.finalScale\n    let bounds: Rectangle | undefined\n    if (entities && entities.length > 0) {\n      const entitiesBounds = entities\n        .map((e) => {\n          const transform = e.getData<TransformData>(TransformData)\n          if (transform) return transform.bounds\n          const position = e.getData<PositionData>(PositionData)\n          const size = e.getData<SizeData>(SizeData) || { width: 0, height: 0 }\n          if (!position) return\n          return new Rectangle(position.x, position.y, size.width, size.height || 0)\n        })\n        .filter((e) => !!e) as Rectangle[]\n      if (entitiesBounds.length > 0) {\n        bounds = Rectangle.enlarge(entitiesBounds)\n      }\n    } else if (pos) {\n      bounds = new Rectangle(pos.x, pos.y, 0, 0)\n    } else if (opts.bounds) {\n      bounds = opts.bounds\n    } // else if (selection) {\n      // bounds = Selectable.getSelectedBounds(this.entityManager)\n    // }\n    if (!bounds) {\n      const defaultConfig = this.getDefaultConfig()\n      bounds = new Rectangle(\n        (defaultConfig.scrollX + config.width / 2) / scale,\n        (defaultConfig.scrollY + config.height / 2) / scale,\n        0,\n        0\n      )\n    }\n    if (!opts.scrollToCenter) {\n      const boundsVisible = this.getViewport()\n      // 判断是否看得见\n      if (boundsVisible.containsRectangle(bounds)) {\n        return Promise.resolve()\n      }\n    }\n    // TODO 微调滚动，而不是直接滚动到中心\n    const toValues = {\n      scrollX:\n        (bounds.x + bounds.width / 2 + (scrollDelta ? scrollDelta.x : 0)) * scale -\n        config.width / 2,\n      scrollY:\n        (bounds.y + bounds.height / 2 + (scrollDelta ? scrollDelta.y : 0)) * scale -\n        config.height / 2,\n      zoom: opts.zoom,\n    }\n    return this.scroll(toValues, easing, easingDuration)\n  }\n\n  /**\n   * 这只画布边框，元素编辑的时候回吸附画布边框\n   * @param bounds\n   */\n  setPageBounds(bounds: Rectangle): void {\n    this.updateConfig({\n      pageBounds: {\n        x: bounds.x,\n        y: bounds.y,\n        width: bounds.width,\n        height: bounds.height,\n      },\n    })\n  }\n\n  getPageBounds(): Rectangle | undefined {\n    const { pageBounds } = this.config\n    if (pageBounds) {\n      return new Rectangle(pageBounds.x, pageBounds.y, pageBounds.width, pageBounds.height)\n    }\n  }\n\n  /**\n   * 滚动到画布中央\n   * @param zoomToFit 是否缩放并适配外围大小\n   * @param fitPadding 适配外围的留白\n   * @param easing 是否缓动\n   */\n  scrollPageBoundsToCenter(\n    zoomToFit: boolean = true,\n    fitPadding = 16,\n    easing = true\n  ): Promise<void> {\n    const pageBounds = this.getPageBounds()\n    if (pageBounds) {\n      let zoom: number | undefined\n      const fitPaddingDouble = fitPadding * 2\n      if (zoomToFit) {\n        const fixedScale = SizeSchema.fixSize(\n          {\n            width: pageBounds.width,\n            height: pageBounds.height,\n          },\n          {\n            width:\n              fitPaddingDouble > this.config.width\n                ? fitPaddingDouble\n                : this.config.width - fitPaddingDouble,\n            height:\n              fitPaddingDouble > this.config.height\n                ? fitPaddingDouble\n                : this.config.height - fitPaddingDouble,\n          }\n        )\n        zoom = fixedScale\n      }\n      return this.scrollToView({\n        bounds: pageBounds,\n        zoom,\n        scrollToCenter: true,\n        // selection: false,\n        easing,\n      })\n    }\n    return this.scrollToView({ easing })\n  }\n\n  private cancelScrollTeeen?: Disposable\n\n  /**\n   * 滚动\n   * @param scroll\n   * @param easing - 是否开启缓动，默认开启\n   * @param easingDuration - 滚动持续时间，默认 300ms\n   */\n  scroll(\n    scroll: Partial<{ scrollX: number; scrollY: number; zoom: number }>,\n    easing: boolean = true,\n    easingDuration = 300\n  ): Promise<void> {\n    const deferred = new PromiseDeferred<void>()\n    if (this.cancelScrollTeeen) this.cancelScrollTeeen.dispose()\n    if (easing) {\n      const fromValues = {\n        scrollX: this.config.scrollX,\n        scrollY: this.config.scrollY,\n        zoom: this.config.zoom,\n      }\n      this.cancelScrollTeeen = startTween({\n        from: fromValues,\n        to: {\n          ...fromValues,\n          ...scroll,\n        },\n        onUpdate: (v) => {\n          this.updateConfig(v)\n        },\n        onComplete: () => {\n          this.cancelScrollTeeen = undefined\n          deferred.resolve()\n        },\n        onDispose: () => {\n          deferred.resolve()\n        },\n        duration: easingDuration,\n      })\n    } else {\n      this.updateConfig(scroll)\n      deferred.resolve()\n    }\n    return deferred.promise\n  }\n\n  /**\n   * 让 layer 的 node 节点不随着画布滚动条滚动\n   * @param layerNode\n   */\n  fixLayerPosition(layerNode: HTMLElement): void {\n    domUtils.setStyle(layerNode, {\n      left: this.config.scrollX,\n      top: this.config.scrollY,\n    })\n  }\n\n  get loading(): boolean {\n    return this._loading\n  }\n\n  set loading(loading: boolean) {\n    if (this.loading !== loading) {\n      this._loading = loading\n      this.fireChange()\n    }\n  }\n\n  /**\n   * @deprecated use 'zoomDisable' instead\n   */\n  get zoomEnable(): boolean {\n    return this._zoomEnable\n  }\n\n  /**\n   * 开启缩放\n   * @deprecated use 'zoomDisable' instead\n   */\n  set zoomEnable(zoomEnable: boolean) {\n    if (this._zoomEnable !== zoomEnable) {\n      this._zoomEnable = zoomEnable\n      this.fireChange()\n    }\n  }\n\n  /**\n   * 放大\n   */\n  zoomin(easing?: boolean, easingDuration?: number): void {\n    const unit = this.config.zoom / 10\n    const newZoom = Math.ceil((this.config.zoom + unit) * 10) / 10\n    this.updateZoom(newZoom, easing, easingDuration)\n  }\n\n  /**\n   * 缩小\n   */\n  zoomout(easing?: boolean, easingDuration?: number): void {\n    const unit = this.config.zoom / 10\n    const newZoom = Math.floor((this.config.zoom - unit) * 10) / 10\n    this.updateZoom(newZoom, easing, easingDuration)\n  }\n\n  updateZoom(newZoom: number, easing: boolean = true, easingDuration = 200): void {\n    newZoom = this.normalizeZoom(newZoom)\n    const { center } = this.getViewport()\n    const oldScale = this.finalScale\n    const newScale = !this.zoomEnable ? oldScale : newZoom\n    if (newScale !== oldScale) {\n      const delta = {\n        x: center.x * newScale - center.x * oldScale,\n        y: center.y * newScale - center.y * oldScale,\n      }\n      this.scroll(\n        {\n          scrollX: this.config.scrollX + delta.x,\n          scrollY: this.config.scrollY + delta.y,\n          zoom: newZoom,\n        },\n        easing,\n        easingDuration\n      )\n    }\n  }\n  get disabled(): boolean {\n    return this.config.disabled\n  }\n  get readonly(): boolean {\n    return this.config.readonly\n  }\n  get readonlyOrDisabled(): boolean {\n    return this.config.readonly || this.config.disabled\n  }\n  set readonly(readonly) {\n    this.updateConfig({\n      readonly\n    })\n  }\n  set disabled(disabled) {\n    this.updateConfig({\n      disabled\n    })\n  }\n\n  /**\n   * 适应大小\n   * @param bounds 目标大小\n   * @param easing 是否开启动画，默认开启\n   * @param padding 边界空白\n   * @param easingDuration\n   */\n  fitView(bounds: Rectangle, easing = true, padding = 0, easingDuration = 300): Promise<void> {\n    const viewport = this.getViewport(false);\n    const zoom = SizeSchema.fixSize(bounds.clone().pad(padding, padding), viewport);\n    return this.scrollToView({\n      bounds,\n      zoom,\n      easing,\n      easingDuration,\n      scrollToCenter: true,\n    });\n\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './config';\nexport * from './layer';\nexport * from './playground-layer';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/layer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport {\n  type CacheManager,\n  type Disposable,\n  DisposableCollection,\n  type DOMCache,\n  domUtils,\n} from '@flowgram.ai/utils';\n\n// import { Adsorber } from '../utils/adsorber';\n// import { PlaygroundDrag, type PlaygroundDragEntitiesOpts } from '../utils';\nimport { type PipelineEntities } from '../pipeline/pipeline-entities';\nimport { type PipeEventName, type PipelineDimension, type PipeSupportEvent } from '../pipeline';\nimport {\n  EntityManager,\n  injectPlaygroundContext,\n  PlaygroundContext,\n  type PositionSchema,\n} from '../../common';\n// import { SelectionService } from '@flowgram.ai/application-common';\nimport { type PlaygroundConfigEntity } from './config';\n\nexport interface LayerOptions {}\n\nexport const LayerOptions = Symbol('LayerOptions');\n\n/**\n * 基础 layer\n */\n@injectable()\nexport class Layer<\n  OPT extends LayerOptions = any,\n  CONTEXT extends PlaygroundContext = PlaygroundContext\n> {\n  /**\n   * layer 的配置, 由 registerLayer(Layer, LayerOptions) 传入\n   */\n  @inject(LayerOptions) options: OPT;\n\n  protected readonly toDispose = new DisposableCollection();\n\n  /**\n   * layer 可能存在 dom 也可能没有，如果有，则会加入到 pipeline 的 dom 节点上\n   */\n  node: HTMLElement;\n\n  /**\n   * 父节点\n   */\n  pipelineNode: HTMLElement;\n\n  /**\n   * 画布根节点\n   */\n  playgroundNode: HTMLElement;\n\n  // /**\n  //  * 发送 payload\n  //  * @param payloadKey\n  //  * @param payloadValue\n  //  * @param cb - layer 触发 autorun 后的回调\n  //  */\n  // dispatch<P>(payloadKey: string | symbol, payloadValue: P, cb?: () => void): void {}\n\n  /**\n   * 当前 layer 的所有监听的实体数据\n   */\n  observeManager: PipelineEntities;\n\n  /**\n   * 实体管理器\n   */\n  @inject(EntityManager) readonly entityManager: EntityManager;\n\n  @injectPlaygroundContext() readonly context: CONTEXT;\n\n  /**\n   * 自动触发更新，在不需要 react 的时候用这个方法\n   */\n  autorun?(): void;\n\n  /**\n   * 绘制 react\n   */\n  render?(): JSX.Element;\n\n  /**\n   * 默认在渲染时候都会启用 react memo 进行隔离，这种情况就需要数据驱动更新\n   */\n  renderWithReactMemo = true;\n\n  /**\n   * 全局选择\n   */\n  // selectionService?: SelectionService;\n  /**\n   * 监听 playground 上的事件\n   * 规则：\n   *  1. 按 priority 排序，越高先执行\n   *  2. 没有提供，按 layer 的注册顺序，后注册先执行 (符合冒泡排序)\n   *  3. 执行返回 true，则阻止后续的执行\n   */\n  listenPlaygroundEvent: (\n    name: PipeEventName,\n    handle: (event: PipeSupportEvent) => boolean | void,\n    priority?: number,\n    options?: AddEventListenerOptions\n  ) => Disposable;\n\n  /**\n   * 监听 document 上的事件\n   * 规则：\n   *  1. 按 priority 排序，越高先执行\n   *  2. 没有提供，按 layer 的注册顺序，后注册先执行 (符合冒泡排序)\n   *  3. 执行返回 true，则阻止后续的执行\n   */\n  listenGlobalEvent: (\n    name: PipeEventName,\n    handle: (event: PipeSupportEvent) => boolean | void,\n    priority?: number,\n    options?: AddEventListenerOptions\n  ) => Disposable;\n\n  /**\n   * 初始化时候触发\n   */\n  onReady?(): void;\n\n  /**\n   * playground 大小变化时候会触发\n   */\n  onResize?(size: PipelineDimension): void;\n\n  /**\n   * playground focus 时候触发\n   */\n  onFocus?(): void;\n\n  /**\n   * playground blur 时候触发\n   */\n  onBlur?(): void;\n\n  /**\n   * 监听缩放\n   */\n  onZoom?(zoom: number): void;\n\n  /**\n   * 监听滚动\n   */\n  onScroll?(scroll: { scrollX: number; scrollY: number }): void;\n\n  /**\n   * viewport 更新触发\n   */\n  onViewportChange?(): void;\n\n  /**\n   * readonly 或 disable 状态变化\n   * @param state\n   */\n  onReadonlyOrDisabledChange?(state: { disabled: boolean; readonly: boolean }): void;\n\n  /**\n   * playground 是否 focused\n   */\n  readonly isFocused: boolean;\n\n  /**\n   * 销毁\n   */\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  /**\n   * 创建 dom 缓冲池\n   * @param className\n   */\n  createDOMCache<T extends DOMCache>(\n    className: string | (() => HTMLElement),\n    children?: string\n  ): CacheManager<T> {\n    if (!this.node) throw new Error('DomCache need a parent dom node.');\n    return domUtils.createDOMCache<T>(this.node, className, children);\n  }\n\n  /**\n   * 加载 layer 注册的实体数据，内部使用，不需要手动触发\n   * @return 数据是否变化\n   */\n  declare reloadEntities: () => boolean;\n\n  // /**\n  //  * 在画布上拖动实体\n  //  */\n  // startDrag<T>(\n  //   clientX: number,\n  //   clientY: number,\n  //   opts: PlaygroundDragEntitiesOpts<T> = {},\n  // ): Disposable {\n  //   const adsorbRefs = Adsorber.getRefsFromEntities(\n  //     this.entityManager,\n  //     opts.entities || [],\n  //     this.config,\n  //   );\n  //   return PlaygroundDrag.startDrag(clientX, clientY, {\n  //     ...opts,\n  //     adsorbRefs,\n  //     config: this.config,\n  //   });\n  // }\n\n  /**\n   * 全局画布配置\n   */\n  config: PlaygroundConfigEntity;\n\n  /**\n   * 获取鼠标在 Playground 的位置\n   */\n  getPosFromMouseEvent(\n    event: { clientX: number; clientY: number },\n    addScale = true\n  ): PositionSchema {\n    const pos = this.config.getPosFromMouseEvent(event, addScale);\n    return {\n      x: pos.x,\n      y: pos.y,\n    };\n  }\n\n  /**\n   * 可以用于获取别的 layer\n   */\n  getOtherLayer: <T extends Layer>(layerRegistry: LayerRegistry<T>) => T | undefined;\n}\n\nexport interface LayerRegistry<P extends Layer = Layer> {\n  new (): P;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/layer/playground-layer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, optional } from 'inversify';\nimport { Disposable, domUtils, PositionSchema } from '@flowgram.ai/utils';\n\nimport { Gesture } from '../utils/use-gesture';\nimport { PlaygroundGesture } from '../utils/playground-gesture';\nimport { MouseTouchEvent, PlaygroundDrag } from '../utils';\nimport { PipelineLayerPriority } from '../pipeline';\nimport { ProtectWheelArea } from '../../common/protect-wheel-area';\nimport { observeEntity } from '../../common';\nimport { Layer, LayerOptions } from './layer';\nimport {\n  EditorState,\n  type EditorStateChangeEvent,\n  EditorStateConfigEntity,\n  PlaygroundConfigEntity,\n  type PlaygroundConfigEntityData,\n} from './config';\n\n/**\n * MOUSE: 鼠标友好模式，鼠标左键拖动画布，滚动缩放 (适合 windows )\n * PAD: 双指同向移动拖动，双指张开捏合缩放 (适合 mac)\n */\nexport type PlaygroundInteractiveType = 'MOUSE' | 'PAD';\n\nexport interface PlaygroundLayerOptions extends LayerOptions {\n  /**\n   * 阻止浏览器默认的手势（苹果触摸板），包含：放大缩小、左右滑动翻页，默认为 false\n   */\n  preventGlobalGesture?: boolean;\n\n  ineractiveType?: PlaygroundInteractiveType;\n\n  /** 悬浮服务 */\n  hoverService?: {\n    /** 精确判断当前鼠标位置是否有元素存在 */\n    isSomeHovered: () => boolean;\n    updateHoverPosition: (position: PositionSchema, target?: HTMLElement) => void;\n    clearHovered: () => void;\n  };\n}\n\n/**\n * 基础层，控制画布缩放/滚动等操作\n */\n@injectable()\nexport class PlaygroundLayer extends Layer<PlaygroundLayerOptions> {\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  @optional()\n  @inject(ProtectWheelArea)\n  protectWheelArea?: ProtectWheelArea;\n\n  private cancelStateListen?: Disposable;\n\n  private lastShortcutState?: EditorState;\n\n  private currentGesture?: PlaygroundGesture;\n\n  private startGrabScroll: { scrollX: number; scrollY: number } = {\n    scrollX: 0,\n    scrollY: 0,\n  };\n\n  private cursorStyle: HTMLStyleElement = document.createElement('style');\n\n  private maskNode: HTMLDivElement = document.createElement('div');\n\n  onReady(): void {\n    this.options = {\n      preventGlobalGesture: false,\n      ...this.options,\n    };\n    /**\n     * 阻止默认的浏览器手势缩放\n     */\n    if (this.options.preventGlobalGesture) {\n      const gesturePreventGlobal = new Gesture(document.body, {\n        /* v8 ignore next 3 */\n        onPinch: () => {\n          // Do nothing\n        },\n      });\n      if (document.documentElement) {\n        document.documentElement.style.overscrollBehaviorX = 'none';\n      }\n      document.body.style.overscrollBehaviorX = 'none';\n      this.toDispose.push(Disposable.create(() => gesturePreventGlobal.destroy()));\n    }\n    this.toDispose.pushAll([\n      this.config.onGrabDisableChange((disable) => {\n        if (disable) {\n          this.grabDragger.stop(0, 0);\n        }\n      }),\n      /**\n       * 防止滚动事件被透出到业务层滚动\n       */\n      domUtils.addStandardDisposableListener(this.playgroundNode, 'wheel', (event: WheelEvent) => {\n        // 判断当前 scrollParent，有滚动条则停止滚动\n        if (this.getScrollParent(event.target as HTMLElement)) {\n          return;\n        }\n        event.preventDefault();\n        event.stopPropagation();\n      }),\n      /**\n       * 在父节点上监听滚动事件\n       */\n      this.listenPlaygroundEvent(\n        'wheel',\n        this.handleWheelEvent.bind(this),\n        PipelineLayerPriority.BASE_LAYER,\n        { passive: true }\n      ),\n      /**\n       * 监听触控拖动画布操作\n       */\n      this.listenPlaygroundEvent(\n        'touchstart',\n        (e: TouchEvent) => {\n          const { clientX: x, clientY: y } = MouseTouchEvent.getEventCoord(e);\n          if (!this.options?.hoverService) {\n            return;\n          }\n          this.options.hoverService.updateHoverPosition(\n            {\n              x,\n              y,\n            },\n            e.target as HTMLElement\n          );\n          const isSomeHovered = this.options.hoverService?.isSomeHovered();\n          if (isSomeHovered) {\n            return;\n          }\n          this.grabDragger.start(x, y);\n        },\n        // 这里必须监听 NORMAL_LAYER，该图层最先触发\n        PipelineLayerPriority.NORMAL_LAYER\n      ),\n      this.listenPlaygroundEvent('touchend', (e: TouchEvent) => {\n        this.options.hoverService?.clearHovered();\n      }),\n      this.listenPlaygroundEvent('touchcancel', (e: TouchEvent) => {\n        this.options.hoverService?.clearHovered();\n      }),\n      this.listenPlaygroundEvent(\n        'mousedown',\n        (e: MouseEvent) => {\n          const isMouseCenterButton = e.button === 1;\n\n          // 按住中键，进入拖拽模式，鼠标模式不支持\n          if (isMouseCenterButton && !this.isMouseMode()) {\n            this.editorStateConfig.changeState(EditorState.STATE_GRAB.id);\n          }\n\n          // 触控板模式下，目前支持按住 space 键或者鼠标中键后拖动\n          if (this.isGrab() && (this.editorStateConfig.isPressingSpaceBar || isMouseCenterButton)) {\n            this.grabDragger.start(e.clientX, e.clientY);\n          }\n        },\n        PipelineLayerPriority.BASE_LAYER\n      ),\n      this.listenPlaygroundEvent(\n        'mousedown',\n        (e: MouseEvent) => {\n          const isSomeHovered = this.options?.hoverService?.isSomeHovered();\n\n          // 如果是鼠标优先模式，当前位置不是节点，并且没有按下 shift，才启动拖拽\n          if (this.isMouseMode() && !isSomeHovered && !this.editorStateConfig.isPressingShift) {\n            this.grabDragger.start(e.clientX, e.clientY);\n          }\n        },\n        // 这里必须监听 NORMAL_LAYER，该图层最先触发\n        PipelineLayerPriority.NORMAL_LAYER\n      ),\n\n      this.editorStateConfig.onStateChange(this.onStateChanged.bind(this)),\n\n      // 单独监听 shift 按键\n      // 只有 keydown 能监听到 shift 按键，keypress 无法监听到\n      this.listenGlobalEvent(\n        'keydown',\n        (e: KeyboardEvent) => {\n          if (e.shiftKey) {\n            this.editorStateConfig.isPressingShift = true;\n\n            // 如果是鼠标优先，按住 shift 键需要更新鼠标为默认\n            if (this.isMouseMode()) {\n              this.config.updateCursor('');\n            }\n          }\n        },\n        PipelineLayerPriority.BASE_LAYER\n      ),\n\n      // 监听快捷键\n      this.listenGlobalEvent(\n        'keypress',\n        (e: KeyboardEvent) => {\n          if (!this.isFocused || e.target !== this.playgroundNode) return;\n\n          // PS: 如果是鼠标优先模式，不监听快捷键\n          if (this.isMouseMode()) {\n            return;\n          }\n\n          const state = this.editorStateConfig.getStateFromShortcut(e);\n\n          // 使用场景：\n          // 在按住空格时（进入 grab 模式），此时点击工具栏的手型工具，需禁止退出 grab 模式\n          // 需要让业务侧感知是否按住空格\n          if (e.key === ' ') {\n            this.editorStateConfig.isPressingSpaceBar = true;\n          }\n\n          // 部分状态不允许重复进入\n          if (\n            state?.shortcutWorksOnlyOnStateChanged === true &&\n            state === this.editorStateConfig.getCurrentState()\n          ) {\n            return;\n          }\n\n          this.lastShortcutState = state;\n          if (state) {\n            this.editorStateConfig.changeState(state.id);\n          }\n        },\n        PipelineLayerPriority.BASE_LAYER\n      ),\n      this.listenGlobalEvent('keyup', (e: KeyboardEvent) => {\n        if (e.key === ' ') {\n          this.editorStateConfig.isPressingSpaceBar = false;\n        }\n\n        this.editorStateConfig.isPressingShift = false;\n\n        if (this.lastShortcutState && this.lastShortcutState.shortcutAutoEsc) {\n          this.editorStateConfig.toDefaultState();\n        }\n\n        this.lastShortcutState = undefined;\n      }),\n      {\n        // 在进入 grab 模式后，此时后退页面，需清理样式\n        dispose: () => {\n          if (this.maskNode.parentNode) {\n            this.maskNode.parentNode.removeChild(this.maskNode);\n          }\n          if (this.cursorStyle.parentNode) {\n            this.cursorStyle.parentNode.removeChild(this.cursorStyle);\n          }\n        },\n      },\n    ]);\n    // 切换到鼠标模式\n    if (this.options.ineractiveType === 'MOUSE') {\n      this.editorStateConfig.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n    }\n  }\n\n  private getCursor(cursor: string | undefined) {\n    if (!cursor) {\n      return '';\n    }\n    return this.playgroundConfigEntity.getCursors?.()?.[cursor] ?? cursor;\n  }\n\n  /** 是否为鼠标优先模式 */\n  private isMouseMode() {\n    return this.editorStateConfig.isMouseFriendlyMode();\n  }\n\n  onStateChanged(e: EditorStateChangeEvent): void {\n    const { state } = e;\n    if (this.cancelStateListen) {\n      this.cancelStateListen.dispose();\n      this.cancelStateListen = undefined;\n    }\n    if (state.handle) {\n      state.handle(this.config, e);\n    }\n    if (state.cursor) {\n      this.playgroundConfigEntity.updateCursor(state.cursor);\n      if (this.currentGesture) {\n        this.playgroundNode.style.cursor = this.getCursor(state.cursor);\n      }\n    } else {\n      this.playgroundConfigEntity.updateCursor('');\n      this.playgroundNode.style.cursor = '';\n    }\n\n    // 避免触发控件交互\n    if (state.cursor === 'grab' || state.cursor === 'grabbing') {\n      // 在鼠标优先交互模式下，应该要允许控件交互，可以选择节点拖动\n      if (state === EditorState.STATE_MOUSE_FRIENDLY_SELECT) {\n        return;\n      }\n\n      this.maskNode.style.cssText = `\n        position: absolute;\n        width: 100%;\n        height: 100%;\n        z-index: 100;\n      `;\n      this.playgroundNode.appendChild(this.maskNode);\n    } else {\n      if (this.maskNode.parentNode) {\n        this.maskNode.parentNode.removeChild(this.maskNode);\n      }\n    }\n    // 按 esc 退出\n    if (state.cancelMode === 'esc') {\n      this.cancelStateListen = domUtils.addStandardDisposableListener(\n        document.body,\n        'keydown',\n        (keyboard: KeyboardEvent) => {\n          if (keyboard.key === 'Escape' || keyboard.key === 'Enter') {\n            this.editorStateConfig.toDefaultState();\n          }\n        },\n        true\n      );\n    } else if (state.cancelMode === 'once') {\n      // 只执行一次\n      this.editorStateConfig.toDefaultState();\n    }\n  }\n\n  protected grabDragger = new PlaygroundDrag({\n    onDragStart: (e) => {\n      if (this.config.grabDisable) return;\n      this.config.updateCursor('grabbing');\n      this.startGrabScroll = {\n        scrollX: this.config.config.scrollX,\n        scrollY: this.config.config.scrollY,\n      };\n    },\n    onDrag: (e) => {\n      if (this.config.grabDisable) return;\n      this.config.updateConfig({\n        scrollX: this.startGrabScroll.scrollX - e.endPos.x + e.startPos.x,\n        scrollY: this.startGrabScroll.scrollY - e.endPos.y + e.startPos.y,\n      });\n    },\n    onDragEnd: (e) => {\n      if (this.isGrab()) {\n        // 可能已经取消了\n        this.config.updateCursor('grab');\n      }\n\n      // 如果拖拽触发自中键，需从拖拽态退出，且重置光标\n      const isMouseCenterButton = e.button === 1;\n      if (isMouseCenterButton) {\n        if (this.isMouseMode()) {\n          this.editorStateConfig.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n          this.config.updateCursor('grab');\n        } else {\n          this.editorStateConfig.toDefaultState();\n          this.config.updateCursor('');\n        }\n      }\n    },\n  });\n\n  protected isGrab(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n\n    // STATE_GRAB 和 STATE_MOUSE_FRIENDLY_SELECT 都允许拖动\n    return (\n      currentState === EditorState.STATE_GRAB ||\n      currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT\n    );\n  }\n\n  createGesture(): void {\n    if (!this.currentGesture) {\n      this.currentGesture = new PlaygroundGesture(this.playgroundNode, this.config);\n      this.currentGesture.onDispose(() => {\n        this.currentGesture = undefined;\n      });\n      this.toDispose.push(this.currentGesture);\n    }\n  }\n\n  protected handleScrollEvent(event: WheelEvent): void {\n    const { playgroundConfigEntity } = this;\n    const scrollX = playgroundConfigEntity.config.scrollX + event.deltaX;\n    const scrollY = playgroundConfigEntity.config.scrollY + event.deltaY;\n    const state: Partial<PlaygroundConfigEntityData> = {\n      scrollX,\n      scrollY,\n    };\n    playgroundConfigEntity.updateConfig(state);\n  }\n\n  protected getMouseScaleDelta(): number {\n    const { mouseScrollDelta, zoom } = this.config.config;\n    if (typeof mouseScrollDelta === 'function') {\n      return mouseScrollDelta(zoom);\n    }\n    return mouseScrollDelta!;\n  }\n\n  /**\n   * 监听滚动事件\n   * @param event\n   */\n  protected handleWheelEvent(event: WheelEvent): void {\n    const e = event as any;\n    if ((this.currentGesture && this.currentGesture.pinching) || event.ctrlKey || event.metaKey)\n      return;\n\n    // 判断当前 scrollParent，有滚动条则停止滚动\n    if (this.getScrollParent(event.target as HTMLElement)) {\n      return;\n    }\n\n    // 鼠标优先模式，使用滚轮缩放，并且在当前鼠标位置放大缩小\n    if (this.isMouseMode()) {\n      // 这里没有使用 this.config.zoomin 和 zoomout 方法\n      // 因为这两个方法目前看没有实现居中缩放的效果，且体验有些卡顿\n      const { zoom, minZoom, maxZoom, scrollX, scrollY } = this.playgroundConfigEntity.config;\n\n      // 鼠标模式下，为了避免过快缩放，这里比例相对触控板模式缩小一倍，这个参数从业务侧传过来，同时提供默认值\n      const scaleStep = this.getMouseScaleDelta();\n      const scaleMin = minZoom;\n      const scaleMax = maxZoom;\n\n      // 处理横向和竖向滚轮\n      const getDelta = (wheelDelta: number): number => (wheelDelta > 0 ? -scaleStep : scaleStep);\n\n      // 优先使用垂直滚动，如果垂直滚动为0则使用水平滚动\n      const wheelDelta = Math.abs(e.deltaY) > 0 ? e.deltaY : e.deltaX;\n      const delta = getDelta(wheelDelta);\n\n      const oldScale = this.config.finalScale;\n      const originX = event.clientX;\n      const originY = event.clientY;\n\n      const newScale = Math.max(scaleMin, Math.min(scaleMax, zoom + delta));\n\n      const origin = this.config.getPosFromMouseEvent(\n        { clientX: originX, clientY: originY },\n        false\n      );\n\n      // 计算放大后的位置，鼠标位置居中缩放\n      // 参见 packages-ide-editor/common/core/src/core/utils/playground-gesture.ts\n      const finalPos = {\n        x: (origin.x / oldScale) * newScale,\n        y: (origin.y / oldScale) * newScale,\n      };\n      this.config.updateConfig({\n        scrollX: scrollX + finalPos.x - origin.x,\n        scrollY: scrollY + finalPos.y - origin.y,\n        zoom: newScale,\n      });\n      return;\n    }\n\n    this.handleScrollEvent(e);\n  }\n\n  /**\n   * 获取 wheel 事件滚动的父元素\n   * @param dom\n   */\n  protected getScrollParent(ele?: HTMLElement | null): HTMLElement | null {\n    if (!ele || ele === this.pipelineNode.parentElement) {\n      return null;\n    }\n\n    const hasScrollableXContent = ele.scrollWidth > ele.clientWidth;\n    const hasScrollableYContent = ele.scrollHeight > ele.clientHeight;\n    const overflowXStyle = window.getComputedStyle(ele).overflowX;\n    const overflowYStyle = window.getComputedStyle(ele).overflowY;\n    const isOverflowXScrollable = ['auto', 'scroll', 'overlay'].includes(overflowXStyle);\n    const isOverflowYScrollable = ['auto', 'scroll', 'overlay'].includes(overflowYStyle);\n\n    const hasScrollableContent =\n      (hasScrollableXContent && isOverflowXScrollable) ||\n      (hasScrollableYContent && isOverflowYScrollable);\n\n    if (hasScrollableContent || this.protectWheelArea?.(ele)) {\n      return ele;\n    }\n\n    return this.getScrollParent(ele.parentElement);\n  }\n\n  autorun(): void {\n    const playgroundConfig = this.playgroundConfigEntity.config;\n    const { cursor } = this.playgroundConfigEntity;\n    const finalCursor = this.getCursor(cursor);\n\n    // 创建手势\n    if (this.config.zoomEnable) {\n      this.createGesture();\n    } else if (this.currentGesture) {\n      this.currentGesture.dispose();\n    }\n    // // 设置 pipeline 的样式\n    // if (scaleVisible) {\n    //   domUtils.setStyle(this.pipelineNode, {\n    //     left: SCALE_WIDTH - playgroundConfig.scrollX,\n    //     top: SCALE_WIDTH - playgroundConfig.scrollY,\n    //     width: playgroundConfig.width,\n    //     height: playgroundConfig.height,\n    //   });\n    // } else {\n    // }\n    domUtils.setStyle(this.pipelineNode, {\n      left: -playgroundConfig.scrollX,\n      top: -playgroundConfig.scrollY,\n      width: playgroundConfig.width,\n      height: playgroundConfig.height,\n    });\n    this.playgroundNode.style.cursor = finalCursor;\n    // Note: 为什么要通过 style 注入样式\n    // 原因：在 pipelineNode.parentElement 上设置 style.cursor，子元素继承样式时 cursor 样式优先级不够（子元素自身也存在 cursor 配置）\n    if (cursor === 'grab' || cursor === 'grabbing') {\n      let classSelector = '';\n      this.playgroundNode.classList.forEach((className) => {\n        classSelector += `.${className}`;\n      });\n      this.cursorStyle.innerText = `.${classSelector} * { cursor: ${finalCursor} }`;\n      if (!this.cursorStyle.parentNode) {\n        document.head.appendChild(this.cursorStyle);\n      }\n    } else {\n      if (this.cursorStyle.parentNode) {\n        this.cursorStyle.parentNode.removeChild(this.cursorStyle);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './pipeline';\nexport * from './pipeline-renderer';\nexport * from './pipeline-registry';\nexport * from './pipeline-entities-selector';\nexport * from './pipeline-entities';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipeline-entities-selector.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport { type Layer } from '../layer';\nimport {\n  // AbleManager,\n  // type AbleRegistry,\n  type Entity,\n  type EntityData,\n  type EntityDataRegistry,\n  EntityManager,\n  type EntityRegistry,\n} from '../../common';\n\ntype SelectorVersion = Map<string, number>;\n\nexport interface LayerEntitiesSelector {\n  // lastAbleVersion?: SelectorVersion;\n  lastEntityVersion?: SelectorVersion;\n  lastDataVersion?: SelectorVersion;\n  entities: EntityRegistry[];\n  // ables: AbleRegistry[];\n  datas: [EntityRegistry, EntityDataRegistry][]; // entity-data\n}\n\n/**\n * 选择器用来在 pipeline 绘制之前，筛选并注入 entities\n */\n@injectable()\nexport class PipelineEntitiesSelector {\n  protected layerEntitiesSelectorMap: WeakMap<Layer, LayerEntitiesSelector> = new WeakMap();\n\n  readonly entityLayerMap: Map<string, Set<Layer>> = new Map();\n\n  readonly ableLayerMap: Map<string, Set<Layer>> = new Map();\n\n  // @inject(AbleManager) ableManager: AbleManager;\n\n  @inject(EntityManager) entityManager: EntityManager;\n\n  /**\n   * 订阅关联的 entity，会影响 autorun\n   */\n  subscribeEntities(layer: Layer, entities: EntityRegistry[]): void {\n    const selector = this.getSelector(layer);\n    entities.forEach(e => {\n      if (!selector.entities.includes(e)) selector.entities.push(e);\n      let layers = this.entityLayerMap.get(e.type);\n      if (!layers) {\n        layers = new Set();\n        this.entityLayerMap.set(e.type, layers);\n      }\n      layers.add(layer);\n    });\n  }\n\n  // /**\n  //  * 订阅关联的 able, 会影响 autorun\n  //  */\n  // subscribeAbles(layer: Layer, ables: AbleRegistry[]): void {\n  //   const selector = this.getSelector(layer);\n  //   ables.forEach(able => {\n  //     if (!selector.ables.includes(able)) selector.ables.push(able);\n  //     let layers = this.ableLayerMap.get(able.type);\n  //     if (!layers) {\n  //       layers = new Set();\n  //       this.ableLayerMap.set(able.type, layers);\n  //     }\n  //     layers.add(layer);\n  //   });\n  // }\n\n  /**\n   * 订阅 data 数据\n   * @param layer\n   * @param entity\n   * @param data\n   */\n  subscribleEntityByData(layer: Layer, entity: EntityRegistry, data: EntityDataRegistry): void {\n    const selector = this.getSelector(layer);\n    // Entity 和 layer 做关联\n    let layers = this.entityLayerMap.get(entity.type);\n    if (!layers) {\n      layers = new Set();\n      this.entityLayerMap.set(entity.type, layers);\n    }\n    layers.add(layer);\n    const item: [EntityRegistry, EntityDataRegistry] = [entity, data];\n    if (!selector.datas.find(i => i[0] === entity && i[1] === data)) selector.datas.push(item);\n  }\n\n  protected getSelector(layer: Layer): LayerEntitiesSelector {\n    let selector = this.layerEntitiesSelectorMap.get(layer);\n    if (!selector) {\n      selector = { entities: [], datas: [] };\n      this.layerEntitiesSelectorMap.set(layer, selector);\n    }\n    return selector;\n  }\n\n  /**\n   * 查询 layer 关联的实体\n   */\n  getLayerEntities(layer: Layer): { entities: Entity[]; changed: boolean } {\n    const selector = this.layerEntitiesSelectorMap.get(layer);\n    /* v8 ignore next 1 */\n    if (!selector) return { entities: [], changed: false };\n    const allEntities: Set<Entity> = new Set();\n    const entityVersion: SelectorVersion = new Map();\n    let entityChanged = false;\n    selector.entities.forEach(registry => {\n      const entities = this.entityManager.getEntities(registry);\n      const version = this.entityManager.getEntityVersion(registry);\n      entityVersion.set(registry.type, version);\n      for (const item of entities) {\n        allEntities.add(item);\n      }\n    });\n    // selector.ables.forEach(registry => {\n    //   const entities = this.ableManager.getEntitiesByAble(registry);\n    //   for (const item of entities) {\n    //     if (!entityVersion.has(item.type)) {\n    //       const version = this.entityManager.getEntityVersion(item.type);\n    //       entityVersion.set(item.type, version);\n    //     }\n    //     allEntities.add(item);\n    //   }\n    // });\n    // To array\n    const result: Entity[] = [];\n    for (const item of allEntities.values()) {\n      result.push(item);\n    }\n    /**\n     * 检查版本变化\n     */\n    if (checkChanged(entityVersion, selector.lastEntityVersion)) {\n      selector.lastEntityVersion = entityVersion;\n      entityChanged = true;\n    }\n    return {\n      entities: result,\n      changed: entityChanged,\n    };\n  }\n\n  getLayerEntityDatas(layer: Layer): { datas: EntityData[]; changed: boolean } {\n    const selector = this.layerEntitiesSelectorMap.get(layer);\n    /* v8 ignore next 1 */\n    if (!selector) return { datas: [], changed: false };\n    const allDatas: EntityData[] = [];\n    const dataVersion: SelectorVersion = new Map();\n    let dataChanged = false;\n    selector.datas.forEach(registries => {\n      const [entityRegistry, entityDataRegistry] = registries;\n      const entityDatas = this.entityManager.getEntityDatas(entityRegistry, entityDataRegistry);\n      const version = this.entityManager.getEntityDataVersion(entityDataRegistry);\n      dataVersion.set(entityDataRegistry.type, version);\n      /* v8 ignore next 3 */\n      for (const item of entityDatas) {\n        allDatas.push(item);\n      }\n    });\n    if (checkChanged(dataVersion, selector.lastDataVersion)) {\n      selector.lastDataVersion = dataVersion;\n      dataChanged = true;\n    }\n    return {\n      datas: allDatas,\n      changed: dataChanged,\n    };\n  }\n\n  getLayerData(layer: Layer): {\n    observeEntities: Entity[];\n    observeDatas: EntityData[];\n    changed: boolean;\n  } {\n    const entitiesSelector = this.getLayerEntities(layer);\n    const datasSelector = this.getLayerEntityDatas(layer);\n    return {\n      observeEntities: entitiesSelector.entities,\n      observeDatas: datasSelector.datas,\n      changed: datasSelector.changed || entitiesSelector.changed,\n    };\n  }\n}\n\nfunction checkChanged(v1: SelectorVersion = new Map(), v2: SelectorVersion = new Map()): boolean {\n  if (v1.size !== v2.size) return true;\n  for (const key of v1.keys()) {\n    /* v8 ignore next 1 */\n    if (v1.get(key) !== v2.get(key)) return true;\n  }\n  return false;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipeline-entities.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  // type AbleRegistry,\n  type ConfigEntity,\n  type Entity,\n  type EntityData,\n  type EntityManager,\n  type EntityRegistry,\n} from '../../common';\n\n/**\n * 注入到 Layer 中的实体选择器\n */\nexport interface PipelineEntities extends Iterable<Entity> {\n  /**\n   * 获取单个实体，如果该实体是单例且被注册过，则会自动创建\n   * @param registry\n   */\n  get<T extends Entity>(registry: EntityRegistry, id?: string): T | undefined;\n  /**\n   * 获取多个实体\n   * @param registry\n   */\n  getEntities<T extends Entity>(registry: EntityRegistry): T[];\n\n  // /**\n  //  * 通过 Able 获取多个实体\n  //  * @param registry\n  //  */\n  // getEntitiesByAble<T extends Entity>(registry: AbleRegistry): T[];\n  // /**\n  //  * 通过多个 Ables 获取多个实体\n  //  * @param andAbles - 多个 able，条件且\n  //  * @param orAbles - 多个 able，条件或\n  //  */\n  // getEntitiesByAbles<T extends Entity>(andAbles: AbleRegistry[], orAbles?: AbleRegistry[]): T[];\n\n  /**\n   * 获取 entity\n   * @param entityRegistry\n   * @param dataRegistry\n   */\n  getEntityDatas<T extends EntityData>(entityRegistry: EntityRegistry, dataRegistry: T): T[];\n  /**\n   * 是否存在\n   * @param registry\n   */\n  has(registry: EntityRegistry): boolean;\n  /**\n   * 获取配置信息\n   * @param registry\n   */\n  getConfig<E extends ConfigEntity>(registry: EntityRegistry): E['config'] | undefined;\n  /**\n   * 更新配置数据\n   */\n  updateConfig<E extends ConfigEntity>(registry: EntityRegistry, props: Partial<E['config']>): void;\n\n  /**\n   * 创建实体\n   */\n  createEntity: <E extends Entity>(\n    registry: EntityRegistry,\n    opts?: Omit<E['__opts_type__'], 'entityManager'>,\n  ) => E;\n  /**\n   * 批量删除实体\n   */\n  removeEntities: (registry: EntityRegistry) => void;\n  /**\n   * 当前画布订阅的实体数目\n   */\n  readonly size: number;\n}\n\nexport class PipelineEntitiesImpl implements PipelineEntities {\n  protected observeEntities: Entity[] = [];\n\n  protected observeDatas: EntityData[] = [];\n\n  // @Action 这里要加多个缓存的原因是，每次 decorator 触发都会频繁调用获取方法，\"this.xxx\" 也会触发 decorator 方法\n  protected entitiesTypeCache: Map<EntityRegistry, Entity[]> = new Map();\n\n  protected entitiesAbleCache: Map<string, Entity[]> = new Map();\n\n  protected entitiyDataCache: Map<string, EntityData[]> = new Map();\n\n  constructor(protected readonly entityManager: EntityManager) {}\n\n  get size(): number {\n    return this.observeEntities.length;\n  }\n\n  /**\n   * 加载订阅数据，会缓存到 layer 内部，layer 只能拿到订阅数据的子集\n   * @param observeEntites\n   * @param observeDatas\n   */\n  load(observeEntites: Entity[], observeDatas: EntityData[]): void {\n    this.observeEntities = observeEntites;\n    this.observeDatas = observeDatas;\n    // clear cache\n    this.entitiesTypeCache.clear();\n    this.entitiesAbleCache.clear();\n    this.entitiyDataCache.clear();\n  }\n\n  get<T extends Entity>(registry: EntityRegistry, id?: string): T | undefined {\n    const entities = this.getEntities(registry) as T[];\n    if (id !== undefined) {\n      return entities.find(e => e.id === id);\n    }\n    return entities[0];\n  }\n\n  has(registy: EntityRegistry): boolean {\n    return !!this.get(registy);\n  }\n\n  getEntities<T extends Entity>(registry: EntityRegistry): T[] {\n    let result = this.entitiesTypeCache.get(registry) as T[];\n    // 缓存查询结果\n    if (!result) {\n      result = [];\n      this.observeEntities.forEach(e => {\n        if (e.type === registry.type) result!.push(e as T);\n      });\n      this.entitiesTypeCache.set(registry, result);\n    }\n    // 可能会出现延迟更新\n    return result.filter(r => !r.disposed);\n  }\n\n  getEntityDatas<T extends EntityData = EntityData>(\n    entityRegistry: EntityRegistry,\n    dataRegistry: T,\n  ): T[] {\n    const dataKey = `${entityRegistry.type}:${dataRegistry.type}`;\n    let result = this.entitiyDataCache.get(dataKey) as T[];\n    if (result) {\n      return result;\n    }\n    result = this.observeDatas.filter(\n      data => data.type === dataRegistry.type && data.entity.type === entityRegistry.type,\n    ) as T[];\n    this.entitiyDataCache.set(dataKey, result);\n    return result;\n  }\n\n  // getEntitiesByAble<T extends Entity = Entity>(able: AbleRegistry): T[] {\n  //   return this.getEntitiesByAbles([able]);\n  // }\n\n  // getEntitiesByAbles<T extends Entity = Entity>(\n  //   andAbles: AbleRegistry[] = [],\n  //   orAbles: AbleRegistry[] = [],\n  // ): T[] {\n  //   const ableKey = `${andAbles.map(a => a.type).join(':')}_${orAbles.map(a => a.type).join(':')}`;\n  //   let result = this.entitiesAbleCache.get(ableKey) as T[];\n  //   // 缓存查询结果\n  //   if (!result) {\n  //     result = [];\n  //     this.observeEntities.forEach(entity => {\n  //       const checkAnd = andAbles.length === 0 || !andAbles.find(able => !entity.ables.has(able));\n  //       const checkOr = orAbles.length === 0 || orAbles.find(able => entity.ables.has(able));\n  //       if (checkAnd && checkOr) {\n  //         result.push(entity as T);\n  //       }\n  //     });\n  //     this.entitiesAbleCache.set(ableKey, result);\n  //   }\n  //   // 可能会出现延迟更新\n  //   return result.filter(r => !r.disposed);\n  // }\n\n  updateConfig<E extends ConfigEntity>(\n    registry: EntityRegistry,\n    props: Partial<E['config']>,\n  ): void {\n    const entity = this.get(registry) as ConfigEntity;\n    /* v8 ignore next 3 */\n    if (entity && entity.updateConfig) {\n      entity.updateConfig(props);\n    }\n  }\n\n  getConfig<E extends ConfigEntity>(registry: EntityRegistry): E['config'] | undefined {\n    const entity = this.get(registry) as ConfigEntity;\n    /* v8 ignore next 3 */\n    if (entity) {\n      return entity.config;\n    }\n  }\n\n  /**\n   * 创建实体\n   */\n  createEntity<E extends Entity>(\n    registry: EntityRegistry,\n    opts?: Omit<E['__opts_type__'], 'entityManager'>,\n  ): E {\n    return this.entityManager.createEntity<E>(registry, opts);\n  }\n\n  /**\n   * 批量删除实体\n   */\n  removeEntities(registry: EntityRegistry): void {\n    this.entityManager.removeEntities(registry);\n  }\n\n  [Symbol.iterator](): Iterator<Entity> {\n    let index = 0;\n    const len = this.observeEntities.length;\n    return {\n      next: () => {\n        const current = index++;\n        const done = current === len;\n        return {\n          value: this.observeEntities[current],\n          done,\n        };\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipeline-registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport {\n  ConflatableMessage,\n  type IMessageHandler,\n  type Message,\n  MessageLoop,\n} from '@phosphor/messaging';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { type Layer, type LayerRegistry, PlaygroundConfigEntity } from '../layer';\nimport {\n  // AbleManager,\n  ConfigEntity,\n  Entity,\n  EntityManager,\n  // getAbleMetadata,\n  getEntityDatasMetadata,\n  getEntityMetadata,\n  injectPlaygroundContext,\n  PlaygroundContext,\n} from '../../common';\nimport { PipelineRenderer } from './pipeline-renderer';\nimport { PipelineEntitiesSelector } from './pipeline-entities-selector';\nimport { PipelineEntitiesImpl } from './pipeline-entities';\n// import { PipelineDispatcher } from './pipeline-dispatcher';\nimport {\n  type PipeEventName,\n  type PipelineDimension,\n  type PipelineEventHandler,\n  type PipelineEventRegsiter,\n  PipelineLayerFactory,\n  type PipeSupportEvent,\n} from './pipeline';\n\nexport enum PipelineMessage {\n  ZOOM = 'PIPELINE_ZOOM',\n  SCROLL = 'PIPELINE_SCROLL',\n}\nconst zoomMessage = new ConflatableMessage(PipelineMessage.ZOOM);\nconst scrollMessage = new ConflatableMessage(PipelineMessage.SCROLL);\n/**\n * pipeline 注册器，用于注册一些事件\n */\n@injectable()\nexport class PipelineRegistry implements Disposable, IMessageHandler {\n  private _isFocused = false;\n\n  protected toDispose = new DisposableCollection();\n\n  protected allLayersMap = new Map<LayerRegistry, Layer>();\n\n  readonly onResizeEmitter = new Emitter<PipelineDimension>();\n\n  readonly onFocusEmitter = new Emitter<void>();\n\n  readonly onBlurEmitter = new Emitter<void>();\n\n  readonly onZoomEmitter = new Emitter<number>();\n\n  readonly onScrollEmitter = new Emitter<{ scrollX: number; scrollY: number }>();\n\n  readonly onFocus = this.onFocusEmitter.event;\n\n  readonly onBlur = this.onBlurEmitter.event;\n\n  readonly onZoom = this.onZoomEmitter.event;\n\n  readonly onScroll = this.onScrollEmitter.event;\n\n  constructor() {\n    this.toDispose.pushAll([\n      this.onResizeEmitter,\n      this.onFocusEmitter,\n      this.onZoomEmitter,\n      this.onBlurEmitter,\n      this.onScrollEmitter,\n    ]);\n    this.onFocusEmitter.event(() => {\n      this._isFocused = true;\n    });\n    this.onBlurEmitter.event(() => {\n      this._isFocused = false;\n    });\n  }\n\n  // @inject(PipelineDispatcher) dispatcher: PipelineDispatcher;\n\n  @inject(PipelineRenderer) renderer: PipelineRenderer;\n\n  @inject(PipelineEntitiesSelector) selector: PipelineEntitiesSelector;\n\n  @inject(EntityManager) entityManager: EntityManager;\n\n  // @inject(AbleManager) ableManager: AbleManager;\n\n  @injectPlaygroundContext() context: PlaygroundContext;\n\n  @inject(PipelineLayerFactory) layerFactory: PipelineLayerFactory;\n\n  // @inject(SelectionService) @optional() selectionService?: SelectionService;\n  protected playgroundEvents: {\n    [key: string]: { handlers: PipelineEventRegsiter[] } & Disposable;\n  } = {};\n\n  protected globalEvents: {\n    [key: string]: { handlers: PipelineEventRegsiter[] } & Disposable;\n  } = {};\n\n  _listenEvent(\n    name: PipeEventName,\n    handle: PipelineEventHandler,\n    isGlobal: boolean,\n    priority = 0,\n    options?: AddEventListenerOptions\n  ): Disposable {\n    const eventsCache = isGlobal ? this.globalEvents : this.playgroundEvents;\n    const domNode = isGlobal ? document : this.renderer.node.parentNode!;\n    let eventRegister = eventsCache[name];\n    if (!eventRegister) {\n      const realHandler = {\n        handleEvent: (e: PipeSupportEvent) => {\n          const list = eventRegister.handlers;\n          for (let i = 0, len = list.length; i < len; i++) {\n            const prevent = list[i].handle(e);\n            /* v8 ignore next 1 */\n            if (prevent) return;\n          }\n        },\n      };\n      domNode.addEventListener(name, realHandler, options);\n      eventRegister = eventsCache[name] = {\n        handlers: [],\n        dispose: () => {\n          domNode.removeEventListener(name, realHandler);\n          delete eventsCache[name];\n        },\n      };\n    }\n    const { handlers } = eventRegister;\n    const item = { handle, priority };\n    /**\n     * handlers 排序：\n     * 1. 后注册先执行 (符合冒泡规则)\n     * 2. 按 priority 排序\n     */\n    handlers.unshift(item);\n    handlers.sort((a, b) => b.priority - a.priority);\n    const dispose = Disposable.create(() => {\n      const index = eventRegister.handlers.indexOf(item);\n      if (index !== -1) eventRegister.handlers.splice(index, 1);\n      if (eventRegister.handlers.length === 0) {\n        eventRegister.dispose();\n      }\n    });\n    this.toDispose.push(dispose);\n    return dispose;\n  }\n\n  /**\n   * 监听画布上的浏览器事件\n   */\n  listenPlaygroundEvent(\n    name: PipeEventName,\n    handle: (event: PipeSupportEvent) => boolean | undefined,\n    priority?: number,\n    options?: AddEventListenerOptions\n  ): Disposable {\n    return this._listenEvent(name, handle, false, priority, options);\n  }\n\n  /**\n   * 监听全局的事件\n   * @param name\n   * @param handle\n   */\n  listenGlobalEvent(\n    name: PipeEventName,\n    handle: (event: PipeSupportEvent) => boolean | undefined,\n    priority?: number,\n    options?: AddEventListenerOptions\n  ): Disposable {\n    return this._listenEvent(name, handle, true, priority, options);\n  }\n\n  /**\n   * 注册 layer\n   * @param layerRegistry\n   * @param layerOptions 配置\n   */\n  registerLayer<P extends Layer = Layer>(\n    layerRegistry: LayerRegistry<P>,\n    layerOptions?: P['options']\n  ): void {\n    // layer 不允许重复添加\n    if (this.allLayersMap.has(layerRegistry)) return;\n    const layer = this.layerFactory(layerRegistry, layerOptions);\n    this.allLayersMap.set(layerRegistry, layer);\n    // const ableRegistries = getAbleMetadata(layerRegistry);\n    const entityRegistries = getEntityMetadata(layerRegistry);\n    const entityDataRegistries = getEntityDatasMetadata(layerRegistry);\n    // ableRegistries.forEach(r => this.ableManager.registerAble(r));\n    entityRegistries.forEach((r) => {\n      this.entityManager.registerEntity(r);\n      if (Entity.isRegistryOf(r, ConfigEntity)) {\n        // 自动创建注册的 entity\n        this.entityManager.createEntity(r);\n      }\n    });\n    entityDataRegistries.forEach((r) => {\n      this.entityManager.registerEntity(r.entity);\n      this.entityManager.registerEntityData(r.data);\n    });\n    // this.selector.subscribeAVbles(layer, ableRegistries);\n    this.selector.subscribeEntities(layer, entityRegistries);\n    entityDataRegistries.forEach((r) =>\n      this.selector.subscribleEntityByData(layer, r.entity, r.data)\n    );\n    layer.observeManager = new PipelineEntitiesImpl(this.entityManager);\n    // layer.commands = this.commands;\n    // layer.menus = this.menus;\n    // layer.keybindings = this.keybindings;\n    // layer.selectionService = this.selectionService;\n    layer.reloadEntities = () => {\n      const result = this.selector.getLayerData(layer);\n      if (result.changed) {\n        (layer.observeManager as PipelineEntitiesImpl).load(\n          result.observeEntities,\n          result.observeDatas\n        );\n      }\n      return result.changed;\n    };\n    // layer.dispatch = (payloadKey: string | symbol, payloadValue: object, cb?: () => void) => {\n    //   const changedEntities = this.dispatcher.dispatch(payloadKey, payloadValue);\n    //   if (cb) {\n    //     if (changedEntities.length > 0) {\n    //       const changed = layer.reloadEntities();\n    //       if (changed && layer.autorun) layer.autorun();\n    //     }\n    //     cb();\n    //   }\n    // };\n    // @ts-ignore\n    layer.listenPlaygroundEvent = this.listenPlaygroundEvent.bind(this);\n    // @ts-ignore\n    layer.listenGlobalEvent = this.listenGlobalEvent.bind(this);\n    layer.config = this.configEntity;\n    layer.getOtherLayer = this.getLayer.bind(this);\n    Object.defineProperty(layer, 'isFocused', {\n      get: () => this._isFocused,\n    });\n    if (layer.onResize) {\n      this.onResize(layer.onResize.bind(layer));\n    }\n    if (layer.onBlur) {\n      this.onBlurEmitter.event(layer.onBlur.bind(layer));\n    }\n    if (layer.onFocus) {\n      this.onFocusEmitter.event(layer.onFocus.bind(layer));\n    }\n    if (layer.onZoom) {\n      this.onZoomEmitter.event(layer.onZoom.bind(layer));\n    }\n    if (layer.onScroll) {\n      this.onScrollEmitter.event(layer.onScroll.bind(layer));\n    }\n    if (layer.onViewportChange) {\n      const viewportChange = layer.onViewportChange.bind(layer);\n      this.onResize(viewportChange);\n      this.onZoomEmitter.event(viewportChange);\n      this.onScrollEmitter.event(viewportChange);\n    }\n    if (layer.onReadonlyOrDisabledChange) {\n      this.configEntity.onReadonlyOrDisabledChange(layer.onReadonlyOrDisabledChange.bind(layer));\n    }\n    this.renderer.addLayer(layer);\n  }\n\n  /**\n   * 获取 layer\n   */\n  getLayer<T extends Layer>(layerRegistry: LayerRegistry<T>): T | undefined {\n    return this.allLayersMap.get(layerRegistry) as T;\n  }\n\n  get configEntity(): PlaygroundConfigEntity {\n    return this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity, true)!;\n  }\n\n  ready(): void {\n    const config = this.configEntity;\n    let lastScale = config.finalScale;\n    let lastScroll = config.scrollData;\n    // 监听 zoom\n    config.onConfigChanged(() => {\n      /* v8 ignore next 10 */\n      const newScale = config.finalScale;\n      const newScroll = config.scrollData;\n      if (newScale !== lastScale) {\n        lastScale = newScale;\n        if (process.env.NODE_ENV === 'test') {\n          this.processMessage(zoomMessage);\n        } else {\n          MessageLoop.postMessage(this, zoomMessage);\n        }\n      }\n      if (lastScroll.scrollX !== newScroll.scrollX || lastScroll.scrollY !== newScroll.scrollY) {\n        lastScroll = newScroll;\n        if (process.env.NODE_ENV === 'test') {\n          this.processMessage(scrollMessage);\n        } else {\n          MessageLoop.postMessage(this, scrollMessage);\n        }\n      }\n    });\n  }\n\n  processMessage(msg: Message): void {\n    const config = this.configEntity;\n    switch (msg.type) {\n      case PipelineMessage.SCROLL:\n        this.onScrollEmitter.fire(config.scrollData);\n        break;\n      case PipelineMessage.ZOOM:\n        this.onZoomEmitter.fire(config.finalScale);\n        break;\n      default:\n    }\n  }\n\n  /**\n   * pipline 大小变化时候会触发\n   */\n  readonly onResize = this.onResizeEmitter.event;\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipeline-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport {\n  ConflatableMessage,\n  type IMessageHandler,\n  type Message,\n  MessageLoop,\n} from '@phosphor/messaging';\nimport { type Disposable, DisposableCollection, domUtils, Emitter } from '@flowgram.ai/utils';\n\nimport { type Layer } from '../layer';\nimport { LoggerService } from '../../services';\nimport { EntityManager } from '../../common';\nimport { createLayerReactAutorun } from './pipline-react-utils';\nimport { PipelineEntitiesSelector } from './pipeline-entities-selector';\nimport { type PipelineEntitiesImpl } from './pipeline-entities';\n\nexport const FLUSH_LAYER_REQUEST = 'flush-layer-request';\n\nlet id = 0;\n\nexport class FlushLayerMessage extends ConflatableMessage {\n  constructor(readonly layer: Layer) {\n    super(`${FLUSH_LAYER_REQUEST}_layer${id++}`);\n  }\n}\n\n/**\n * pipeline 渲染器\n */\n@injectable()\nexport class PipelineRenderer implements Disposable, IMessageHandler {\n  public isReady = false;\n\n  protected onAllLayersRenderedEmitter = new Emitter<void>();\n\n  protected toDispose = new DisposableCollection();\n\n  readonly layers: Layer[] = [];\n\n  protected forceUpdates: Set<Layer> = new Set();\n\n  readonly layerAutorunMap: Map<Layer, () => void> = new Map();\n\n  readonly layerRenderedMap: Map<Layer, boolean> = new Map();\n\n  readonly layerFlushMessages: Map<Layer, FlushLayerMessage> = new Map();\n\n  protected reactPortals: React.FunctionComponent[] = [];\n\n  readonly node = domUtils.createDivWithClass('gedit-playground-pipeline');\n\n  /**\n   * 所有 Layer 第一次渲染完成后触发\n   */\n  readonly onAllLayersRendered = this.onAllLayersRenderedEmitter.event;\n\n  @inject(LoggerService) protected readonly loggerService: LoggerService;\n\n  constructor(\n    @inject(PipelineEntitiesSelector)\n    protected readonly selector: PipelineEntitiesSelector,\n    @inject(EntityManager) entityManager: EntityManager\n    // @inject(AbleManager) ableManager: AbleManager,\n  ) {\n    /**\n     * entity 修改触发 layer 更新\n     */\n    this.toDispose.push(\n      entityManager.onEntityChange((entityType: string) => {\n        const layers = this.selector.entityLayerMap.get(entityType);\n        if (layers) layers.forEach((layer) => this.updateLayer(layer));\n      })\n    );\n    this.toDispose.push(this.onAllLayersRenderedEmitter);\n  }\n\n  reportLayerRendered(layer: Layer): void {\n    this.layerRenderedMap.set(layer, true);\n    const allLayersRendered = Array.from(this.layerRenderedMap.values()).every((v) => v);\n    if (allLayersRendered) {\n      // logger 事件点位上报\n      this.loggerService.onAllLayersRendered();\n      this.onAllLayersRenderedEmitter.fire();\n      // e2e 性能测试时会注入以下全局变量\n      if ((window as any).REPORT_TTI_FOR_E2E) {\n        (window as any).REPORT_TTI_FOR_E2E(\n          // 由于 e2e 环境，performance 耗时即为 tti 的完整耗时。\n          performance.now(),\n          performance.getEntriesByType('resource')\n        );\n      }\n    }\n  }\n\n  addLayer(layer: Layer): void {\n    this.layers.push(layer);\n    this.toDispose.push(layer);\n    this.layerFlushMessages.set(layer, new FlushLayerMessage(layer));\n    layer.pipelineNode = this.node;\n    layer.playgroundNode = this.node.parentElement!;\n    // Auto create node\n    if ((layer.autorun || layer.render) && !layer.node) {\n      layer.node = document.createElement('div');\n    }\n    // 把 layer 加到父亲节点上\n    if (layer.node) {\n      this.node.appendChild(layer.node);\n      layer.node.classList.add('gedit-playground-layer');\n    }\n    if (layer.autorun) {\n      const autorun = layer.autorun.bind(layer);\n      this.layerAutorunMap.set(layer, autorun);\n      // 重载 layer autorun\n      layer.autorun = () => {\n        this.updateLayer(layer, true);\n      };\n    } else if (layer.render) {\n      this.layerRenderedMap.set(layer, false);\n      const render = layer.render.bind(layer);\n      const autorun = createLayerReactAutorun(\n        layer,\n        render,\n        this.reportLayerRendered.bind(this),\n        this\n      );\n      this.reactPortals.push(autorun.portal);\n      this.layerAutorunMap.set(layer, autorun.autorun);\n      if (process.env.NODE_ENV === 'test') {\n        // 不对外暴露_render 字段\n        (layer as any)._render = layer.render;\n      }\n      // 重载 layer autorun\n      layer.render = (() => {\n        this.updateLayer(layer, true);\n      }) as () => JSX.Element;\n    }\n  }\n\n  flush(forceUpdate?: boolean): void {\n    this.layers.forEach((layer) => {\n      this.updateLayer(layer, forceUpdate);\n    });\n  }\n\n  ready(): void {\n    this.layers.forEach((layer) => {\n      // 先加载一次数据，保证 ready 时候能运行\n      this.loadLayerEntities(layer);\n      if (layer.onReady) layer.onReady();\n    });\n    this.isReady = true;\n    // 第一次先渲染一次\n    this.flush(true);\n  }\n\n  dispose(): void {\n    this.toDispose.dispose();\n    this.node.remove();\n  }\n\n  processMessage(msg: Message): void {\n    if (msg instanceof FlushLayerMessage) {\n      this.onFlushRequest(msg.layer);\n    }\n  }\n\n  protected loadLayerEntities(layer: Layer): boolean {\n    const result = this.selector.getLayerData(layer);\n    if (result.changed) {\n      // 这里更新 layer 的 entities\n      (layer.observeManager as PipelineEntitiesImpl).load(\n        result.observeEntities,\n        result.observeDatas\n      );\n    }\n    return result.changed;\n  }\n\n  protected onFlushRequest(layer: Layer): boolean {\n    // 只有 ready 之后才能执行 autorun\n    if (!this.isReady || this.toDispose.disposed) return false;\n    const startRenderTime = performance.now();\n    const trackRenderPerformance = () => {\n      const renderDuration = performance.now() - startRenderTime;\n      // 小于 4ms 的渲染时间不需要记录\n      if (renderDuration < 4) {\n        return;\n      }\n      this.loggerService.onFlushRequest(renderDuration);\n    };\n    const autorun = this.layerAutorunMap.get(layer);\n    const changed = this.loadLayerEntities(layer);\n    // 只有修改或强制刷新才会刷新\n    if (autorun && (changed || this.forceUpdates.has(layer))) {\n      this.forceUpdates.delete(layer);\n      try {\n        // console.time(`layer ${layer.constructor.name}`)\n        autorun();\n        // console.timeEnd(`layer ${layer.constructor.name}`)\n        /* v8 ignore next 3 */\n      } catch (e) {\n        console.error(e);\n      }\n      trackRenderPerformance();\n      return true;\n      /* v8 ignore next 2 */\n    }\n    trackRenderPerformance();\n    return false;\n  }\n\n  /**\n   * 1. PostMessage: 会将消息在 nextTick 执行\n   * 2. ConflatableMessage: 当多个消息进来会在下一个 nextTick 做合并\n   * 3. 图层相互隔离，即时一层挂了也不受影响\n   */\n  updateLayer(layer: Layer, forceUpdate?: boolean): void {\n    if (forceUpdate) {\n      this.forceUpdates.add(layer);\n    }\n    if (process.env.NODE_ENV === 'test') {\n      this.onFlushRequest(layer);\n    } else {\n      MessageLoop.postMessage(this, this.layerFlushMessages.get(layer)!);\n    }\n  }\n\n  private reactComp?: React.FC;\n\n  /**\n   * 转成 react\n   */\n  toReactComponent(): React.FC {\n    if (this.reactComp) return this.reactComp;\n    const portals = this.reactPortals;\n    const comp = () => (\n      <>\n        {/* eslint-disable-next-line react/no-array-index-key */}\n        {portals.map((Portal, key) => (\n          <Portal key={key} />\n        ))}\n      </>\n    );\n    this.reactComp = comp;\n    return comp;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipeline.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Layer, type LayerRegistry } from '../layer';\n\nexport type PipeSupportEvent = MouseEvent | DragEvent | KeyboardEvent | UIEvent | TouchEvent | any;\nexport type PipeEventName = string;\nexport interface PipelineDimension {\n  width: number;\n  height: number;\n}\n\nexport type PipelineEventHandler = (event: PipeSupportEvent) => boolean | undefined;\n\nexport interface PipelineEventRegsiter {\n  handle: PipelineEventHandler;\n  priority: number;\n}\n\nexport enum PipelineLayerPriority {\n  BASE_LAYER = -2, // 优先级最低\n  TOOL_LAYER = -1, // 工具层\n  NORMAL_LAYER, // 使用层\n}\nexport const PipelineLayerFactory = Symbol('PipelineLayerFactory');\nexport type PipelineLayerFactory = (layerRegistry: LayerRegistry, layerOptions?: any) => Layer;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/pipeline/pipline-react-utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React, { useEffect, useState, useCallback } from 'react';\n\nimport { NOOP } from '@flowgram.ai/utils';\n\nimport type { Layer } from '../layer';\nimport { type PipelineRenderer } from './pipeline-renderer';\n\ninterface LayerReactAutorun {\n  autorun: () => void; // autorun function\n  portal: () => JSX.Element;\n}\n\nfunction OriginComp({\n  originRenderer,\n  renderedCb,\n}: {\n  originRenderer: () => JSX.Element | void;\n  renderedCb: () => void;\n}): JSX.Element | null {\n  useEffect(() => {\n    renderedCb();\n  }, []);\n  return originRenderer() || null;\n}\n\nexport function createLayerReactAutorun(\n  layer: Layer,\n  originRenderer: () => JSX.Element | void,\n  renderedCb: (layer: Layer) => void,\n  pipelineRenderer: PipelineRenderer,\n): LayerReactAutorun {\n  let update = NOOP;\n  function PlaygroundReactLayerPortal(): JSX.Element {\n    const [, refresh] = useState({});\n    const handleRendered = useCallback(() => {\n      renderedCb(layer);\n    }, [layer]);\n    useEffect(() => {\n      update = () => refresh({});\n      return () => {\n        update = NOOP;\n      };\n    });\n\n    let result: any;\n    try {\n      result = !pipelineRenderer.isReady ? (\n        <></>\n      ) : (\n        <OriginComp originRenderer={originRenderer} renderedCb={handleRendered} />\n      );\n    } catch (e) {\n      console.error(`Render Layer \"${layer.constructor.name}\" error `, e);\n      result = <></>;\n    }\n    return ReactDOM.createPortal(result, layer.node!);\n  }\n  return {\n    autorun: () => update(),\n    // 这里使用了 memo 缓存隔离，这样做的前提 layer 的刷新完全交给 entity，不受外部干扰\n    portal: layer.renderWithReactMemo\n      ? (React.memo(PlaygroundReactLayerPortal) as any)\n      : PlaygroundReactLayerPortal,\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './playground-drag';\n// export * from './adsorber';\nexport * from './tween';\nexport * from './playground-gesture';\nexport { injectByProvider } from './inject-provider-decorators';\nexport { lazyInject, LazyInjectContext } from './lazy-inject-decorators';\nexport { MouseTouchEvent } from './mouse-touch-event';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/inject-provider-decorators.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, interfaces, optional } from 'inversify';\nimport 'reflect-metadata';\n\nexport const injectByProvider = (provider: interfaces.ServiceIdentifier) =>\n  function (target: any, propertyKey: string) {\n    const providerPropertyKey = `${propertyKey}Provider`;\n\n    inject(provider)(target, providerPropertyKey);\n    optional()(target, providerPropertyKey);\n\n    // decorate 会依赖 reflect-metadata，因此解除其依赖\n    // decorate(inject(provider), target, providerPropertyKey);\n    // decorate(optional(), target, providerPropertyKey);\n\n    return {\n      get() {\n        return this[providerPropertyKey]?.();\n      },\n      configurable: true,\n      enumerable: true,\n    } as any;\n  };\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/lazy-inject-decorators.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, interfaces } from 'inversify';\n\nexport const LazyInjectContext = Symbol('LazyInjectContext');\nexport const IS_LAZY_INJECT_CONTEXT_INJECTED = Symbol('IS_LAZY_INJECT_CONTEXT_INJECTED');\n\nexport const lazyInject = (serviceIdentifier: interfaces.ServiceIdentifier) =>\n  function (target: any, propertyKey: string) {\n    if (!serviceIdentifier) {\n      throw new Error(\n        `ServiceIdentifier ${serviceIdentifier} in @lazyInject is Empty, it might be caused by file circular dependency, please check it.`,\n      );\n    }\n\n    // 只依赖注入一次\n    if (!Reflect.hasMetadata(IS_LAZY_INJECT_CONTEXT_INJECTED, target)) {\n      inject(LazyInjectContext)(target, LazyInjectContext);\n      Reflect.defineMetadata(IS_LAZY_INJECT_CONTEXT_INJECTED, true, target);\n    }\n\n    const descriptor = {\n      get() {\n        const ctx = this[LazyInjectContext];\n        return ctx.get(serviceIdentifier);\n      },\n      set() {},\n      configurable: true,\n      enumerable: true,\n    } as any;\n\n    // Object.defineProperty(target, propertyKey, descriptor);\n\n    return descriptor;\n  };\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/mouse-touch-event.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport namespace MouseTouchEvent {\n  export const isTouchEvent = (event: TouchEvent | React.TouchEvent): event is TouchEvent =>\n    'touches' in event;\n\n  export const touchToMouseEvent = (event: Event): MouseEvent | Event => {\n    if (!isTouchEvent(event as TouchEvent)) {\n      return event as MouseEvent;\n    }\n    const touchEvent = event as TouchEvent;\n\n    // 默认获取第一个触摸点\n    const touch = touchEvent.touches[0] || touchEvent.changedTouches[0];\n\n    if (touchEvent.type === 'touchmove') {\n      preventDefault(touchEvent);\n    }\n\n    // 确定对应的鼠标事件类型\n    const mouseEventType = {\n      touchstart: 'mousedown',\n      touchmove: 'mousemove',\n      touchend: 'mouseup',\n      touchcancel: 'mouseup',\n    }[touchEvent.type];\n\n    if (!mouseEventType) {\n      throw new Error(`Unknown touch event type: ${touchEvent.type}`);\n    }\n\n    // 创建新的鼠标事件\n    const mouseEvent = new MouseEvent(mouseEventType, {\n      bubbles: touchEvent.bubbles,\n      cancelable: touchEvent.cancelable,\n      view: touchEvent.view,\n      // 复制触摸点的位置信息\n      clientX: touch.clientX,\n      clientY: touch.clientY,\n      screenX: touch.screenX,\n      screenY: touch.screenY,\n      // 复制修饰键状态\n      ctrlKey: touchEvent.ctrlKey,\n      altKey: touchEvent.altKey,\n      shiftKey: touchEvent.shiftKey,\n      metaKey: touchEvent.metaKey,\n    });\n\n    return mouseEvent;\n  };\n  export const getEventCoord = (\n    e:\n      | MouseEvent\n      | TouchEvent\n      | {\n          clientX: number;\n          clientY: number;\n        }\n  ): {\n    clientX: number;\n    clientY: number;\n  } => {\n    if (isTouchEvent(e as TouchEvent)) {\n      const touchEvent = e as TouchEvent;\n      if (touchEvent.touches.length === 0) {\n        return {\n          clientX: 0,\n          clientY: 0,\n        };\n      }\n      return {\n        clientX: touchEvent.touches[0].clientX,\n        clientY: touchEvent.touches[0].clientY,\n      };\n    } else if (e instanceof MouseEvent) {\n      return {\n        clientX: e.clientX,\n        clientY: e.clientY,\n      };\n    }\n    return {\n      clientX: (e as MouseEvent).clientX,\n      clientY: (e as MouseEvent).clientY,\n    };\n  };\n\n  export const preventDefault = (\n    e: Event | MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent\n  ) => {\n    if (e.cancelable) {\n      e.preventDefault();\n    }\n  };\n\n  export const onTouched = (\n    touchStartEvent: React.TouchEvent,\n    callback: (e: MouseEvent) => void\n  ) => {\n    const startTouch = touchStartEvent.changedTouches[0];\n\n    const handleTouchEnd = (touchEndEvent: TouchEvent) => {\n      const endTouch = touchEndEvent.changedTouches[0];\n      const deltaX = endTouch.clientX - startTouch.clientX;\n      const deltaY = endTouch.clientY - startTouch.clientY;\n      // 判断是拖拽还是点击\n      const delta = 5;\n      if (Math.abs(deltaX) < delta && Math.abs(deltaY) < delta) {\n        // 触发回调\n        const mouseEvent = touchToMouseEvent(touchEndEvent) as MouseEvent;\n        callback(mouseEvent);\n      }\n      document.removeEventListener('touchend', handleTouchEnd);\n      document.removeEventListener('touchcancel', handleTouchEnd);\n    };\n\n    document.addEventListener('touchend', handleTouchEnd);\n    document.addEventListener('touchcancel', handleTouchEnd);\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/playground-drag.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Disposable,\n  Emitter,\n  generateLocalId,\n  type LocalId,\n  type Rectangle,\n} from '@flowgram.ai/utils';\n\nimport type { PlaygroundConfigEntity } from '../layer/config';\nimport type { PositionSchema } from '../../common/schema/position-schema';\nimport { MouseTouchEvent } from './mouse-touch-event';\n\n/* istanbul ignore next */\nconst SCROLL_DELTA = 4;\n/* istanbul ignore next */\nconst SCROLL_AUTO_DISTANCE = 20; // 自动滚动到边缘距离\n/* istanbul ignore next */\nconst SCROLL_INTERVAL = 6;\n\n/* istanbul ignore next */\nfunction createMouseEvent(type: string, clientX: number, clientY: number): MouseEvent {\n  const event = document.createEvent('MouseEvent');\n  event.initMouseEvent(\n    type,\n    true,\n    true,\n    // @ts-ignore\n    undefined,\n    0,\n    0,\n    0,\n    clientX,\n    clientY,\n    false,\n    false,\n    false,\n    false,\n    0,\n    null\n  );\n  return event;\n}\n\nexport interface PlaygroundDragEvent extends MouseEvent {\n  id: LocalId;\n  startPos: PositionSchema;\n  endPos: PositionSchema;\n  movingDelta: PositionSchema; // 移动的偏移量\n  scale: number;\n  isMoving: boolean;\n  isStart: boolean;\n}\n\nexport interface PlaygroundDragOptions<T> {\n  onDragStart?: (e: PlaygroundDragEvent, context?: T) => void;\n  onDrag?: (e: PlaygroundDragEvent, context?: T) => void;\n  onDragEnd?: (e: PlaygroundDragEvent, context?: T) => void;\n  stopGlobalEventNames?: string[];\n}\n\n/* istanbul ignore next */\nexport class PlaygroundDrag<T = undefined> implements Disposable {\n  private onDragStartEmitter = new Emitter<PlaygroundDragEvent>();\n\n  private onDragEndEmitter = new Emitter<PlaygroundDragEvent>();\n\n  private onDragEmitter = new Emitter<PlaygroundDragEvent>();\n\n  private readonly _stopGlobalEventNames = [\n    'mouseenter',\n    'mouseleave',\n    'mouseover',\n    'mouseout',\n    'contextmenu',\n  ];\n\n  private localId: LocalId;\n\n  protected context?: T;\n\n  readonly onDrag = this.onDragEmitter.event;\n\n  readonly onDragStart = this.onDragStartEmitter.event;\n\n  readonly onDragEnd = this.onDragEndEmitter.event;\n\n  constructor(options: PlaygroundDragOptions<T> = {}) {\n    if (options.onDragStart) this.onDragStart((e) => options.onDragStart!(e, this.context));\n    if (options.onDrag) this.onDrag((e) => options.onDrag!(e, this.context));\n    if (options.onDragEnd) this.onDragEnd((e) => options.onDragEnd!(e, this.context));\n    if (options.stopGlobalEventNames) this._stopGlobalEventNames = options.stopGlobalEventNames;\n  }\n\n  get isStarted(): boolean {\n    return !!this._promise;\n  }\n\n  start(\n    clientX: number,\n    clientY: number,\n    entity?: PlaygroundConfigEntity,\n    context?: T\n  ): Promise<void> {\n    if (this._disposed) {\n      return Promise.resolve();\n    }\n    if (this._promise) {\n      return this._promise;\n    }\n    this.context = context;\n    this.localId = generateLocalId();\n    this._addListeners();\n    this._promise = new Promise((resolve) => {\n      this._resolve = resolve;\n    });\n    this._playgroundConfigEntity = entity;\n    const mousedown = createMouseEvent('mousedown', clientX, clientY);\n    this._startPos = this.getRelativePos(mousedown);\n    this.onDragStartEmitter.fire(this.getDragEvent(mousedown));\n    return this._promise;\n  }\n\n  stop(clientX: number, clientY: number): void {\n    if (this._disposed || !this._promise) {\n      return;\n    }\n    const mouseup = createMouseEvent('mouseup', clientX, clientY);\n    this.handleEvent(mouseup);\n  }\n\n  dispose(): void {\n    if (this._disposed) return;\n    this._stopScrollX();\n    this._stopScrollY();\n    this._disposed = true;\n    this.onDragEmitter.dispose();\n    this.onDragStartEmitter.dispose();\n    this.onDragEndEmitter.dispose();\n    this._finalize();\n  }\n\n  handleEvent(_event: Event): void {\n    const event = MouseTouchEvent.touchToMouseEvent(_event);\n    switch (event.type) {\n      case 'mousemove':\n        this._evtMouseMove(event as MouseEvent);\n        break;\n      case 'mouseup':\n        this._stopScrollX();\n        this._stopScrollY();\n        this._evtMouseUp(event as MouseEvent);\n        break;\n      case 'keydown':\n        this._evtKeyDown(event as KeyboardEvent);\n        break;\n      // TODO 暂时屏蔽右键菜单\n      case 'contextmenu':\n        const mouseup = createMouseEvent(\n          'mouseup',\n          (event as MouseEvent).clientX,\n          (event as MouseEvent).clientY\n        );\n        this._evtMouseUp(mouseup);\n        break;\n      default:\n        // Stop all other events during drag-drop.\n        MouseTouchEvent.preventDefault(event);\n        event.stopPropagation();\n        break;\n    }\n  }\n\n  get scale(): number {\n    return this._playgroundConfigEntity ? this._playgroundConfigEntity.finalScale : 1;\n  }\n\n  protected getRelativePos(event: MouseEvent): PositionSchema {\n    if (this._playgroundConfigEntity) {\n      return this._playgroundConfigEntity.getPosFromMouseEvent(event, false);\n    }\n    return {\n      x: event.clientX,\n      y: event.clientY,\n    };\n  }\n\n  private _lastPos: PositionSchema = { x: 0, y: 0 };\n\n  protected getDragEvent(event: MouseEvent): PlaygroundDragEvent {\n    const startPos = this._startPos!;\n    const { scale } = this;\n    switch (event.type) {\n      case 'mousedown':\n        this._lastPos = startPos;\n        return Object.assign(event, {\n          id: this.localId,\n          startPos,\n          endPos: startPos,\n          scale,\n          movingDelta: { x: 0, y: 0 },\n          isStart: true,\n          isMoving: false,\n        });\n      case 'mousemove':\n        const endPos = this.getRelativePos(event);\n        const movingDelta = {\n          x: endPos.x - this._lastPos.x,\n          y: endPos.y - this._lastPos.y,\n        };\n        this._lastPos = endPos;\n        return Object.assign(event, {\n          id: this.localId,\n          startPos,\n          endPos,\n          scale,\n          isStart: true,\n          movingDelta,\n          isMoving: true,\n        });\n      case 'mouseup':\n        this._lastPos = { x: 0, y: 0 };\n        return Object.assign(event, {\n          id: this.localId,\n          startPos,\n          endPos: this.getRelativePos(event),\n          movingDelta: { x: 0, y: 0 },\n          scale,\n          isStart: false,\n          isMoving: false,\n        });\n      default:\n        throw new Error('unknown event');\n    }\n  }\n\n  private _finalize(): void {\n    const resolve = this._resolve;\n    this._removeListeners();\n    this._startPos = undefined;\n    this._promise = undefined;\n    this._resolve = undefined;\n    if (resolve) {\n      resolve();\n    }\n  }\n\n  // 这个用于自动滚动时候使用\n  private _lastMouseMoveEvent?: MouseEvent;\n\n  /**\n   * Handle the `'mousemove'` event for the drag object.\n   */\n  private _evtMouseMove(event: MouseEvent): void {\n    // Stop all input events during drag-drop.\n    event.preventDefault();\n    event.stopPropagation();\n    this._lastMouseMoveEvent = event;\n    const dragEvent = this.getDragEvent(event);\n    // Update the playground scroller.\n    this._updateDragScroll(dragEvent);\n    this.onDragEmitter.fire(dragEvent);\n  }\n\n  /**\n   * Handle the `'mouseup'` event for the drag object.\n   */\n  private _evtMouseUp(event: MouseEvent): void {\n    this._lastMouseMoveEvent = undefined;\n    // Stop all input events during drag-drop.\n    event.preventDefault();\n    event.stopPropagation();\n\n    // Do nothing if the left or center button is not released.\n    if (event.button !== 0 && event.button !== 1) {\n      return;\n    }\n    this.onDragEndEmitter.fire(this.getDragEvent(event));\n    this._finalize();\n  }\n\n  /**\n   * Handle the `'keydown'` event for the drag object.\n   */\n  private _evtKeyDown(event: KeyboardEvent): void {\n    // Stop all input events during drag-drop.\n    event.preventDefault();\n    event.stopPropagation();\n\n    // Cancel the drag if `Escape` is pressed.\n    if (event.keyCode === 27) {\n      this.stop(NaN, NaN);\n    }\n  }\n\n  /**\n   * Add the document event listeners for the drag object.\n   */\n  private _addListeners(): void {\n    // mouse\n    document.addEventListener('mousedown', this, true);\n    document.addEventListener('mousemove', this, true);\n    document.addEventListener('mouseup', this, true);\n\n    // touch\n    document.addEventListener('touchstart', this, true);\n    document.addEventListener('touchmove', this, { passive: false });\n    document.addEventListener('touchend', this, true);\n    document.addEventListener('touchcancel', this, true);\n\n    this._stopGlobalEventNames.forEach((_event) => {\n      document.addEventListener(_event, this, true);\n    });\n  }\n\n  /**\n   * Remove the document event listeners for the drag object.\n   */\n  private _removeListeners(): void {\n    // mouse\n    document.removeEventListener('mousedown', this, true);\n    document.removeEventListener('mousemove', this, true);\n    document.removeEventListener('mouseup', this, true);\n\n    // touch\n    document.removeEventListener('touchstart', this, true);\n    document.removeEventListener('touchmove', this);\n    document.removeEventListener('touchend', this, true);\n    document.removeEventListener('touchcancel', this, true);\n\n    this._stopGlobalEventNames.forEach((_event) => {\n      document.removeEventListener(_event, this, true);\n    });\n  }\n\n  /**\n   * 自动滚动画布\n   */\n  private _updateDragScroll = (event: PlaygroundDragEvent): void => {\n    if (!this._playgroundConfigEntity) return;\n    const playgroundConfig = this._playgroundConfigEntity.config;\n    const dragPos = event.endPos;\n    const { scrollX, width, height, scrollY } = playgroundConfig;\n    if (dragPos.x > width + scrollX - SCROLL_AUTO_DISTANCE) {\n      this._startScrollX(scrollX, true);\n    } else if (dragPos.x < scrollX + SCROLL_AUTO_DISTANCE) {\n      this._startScrollX(scrollX, false);\n    } else {\n      this._stopScrollX();\n    }\n    if (dragPos.y > height + scrollY - SCROLL_AUTO_DISTANCE) {\n      this._startScrollY(scrollY, true);\n    } else if (dragPos.y < scrollY + SCROLL_AUTO_DISTANCE) {\n      this._startScrollY(scrollY, false);\n    } else {\n      this._stopScrollY();\n    }\n  };\n\n  private _scrollXInterval: { interval: number; origin: number } | undefined;\n\n  private _scrollYInterval: { interval: number; origin: number } | undefined;\n\n  private _startScrollX(origin: number, added: boolean): void {\n    if (this._scrollXInterval) {\n      return;\n    }\n    const interval = window.setInterval(() => {\n      const current = this._scrollXInterval!;\n      if (!current) return;\n      this.fireScroll('scrollX', added);\n    }, SCROLL_INTERVAL);\n    this._scrollXInterval = { interval, origin };\n  }\n\n  private _stopScrollX(): void {\n    if (this._scrollXInterval) {\n      clearInterval(this._scrollXInterval.interval);\n      this._scrollXInterval = undefined;\n    }\n  }\n\n  private _startScrollY(origin: number, added: boolean): void {\n    if (this._scrollYInterval) {\n      return;\n    }\n    const interval = window.setInterval(() => {\n      this.fireScroll('scrollY', added);\n    }, SCROLL_INTERVAL);\n    this._scrollYInterval = { interval, origin };\n  }\n\n  private _stopScrollY(): void {\n    if (this._scrollYInterval) {\n      clearInterval(this._scrollYInterval.interval);\n      this._scrollYInterval = undefined;\n    }\n  }\n\n  /**\n   * 触发滚动\n   * @param scrollKey\n   * @param added\n   */\n  fireScroll(scrollKey: 'scrollY' | 'scrollX', added: boolean): void {\n    const current = scrollKey === 'scrollY' ? this._scrollYInterval : this._scrollXInterval;\n    if (!current) return;\n    const value = (current.origin = added\n      ? current.origin + SCROLL_DELTA\n      : current.origin - SCROLL_DELTA);\n    const oldScroll = this._playgroundConfigEntity!.config[scrollKey];\n    this._playgroundConfigEntity!.updateConfig({\n      [scrollKey]: value,\n    });\n    const newScroll = this._playgroundConfigEntity!.config[scrollKey];\n    if (newScroll !== oldScroll) {\n      const lastMouseMoveEvent = this._lastMouseMoveEvent!;\n      const delta = {\n        x: scrollKey === 'scrollX' ? newScroll - current.origin : 0,\n        y: scrollKey === 'scrollY' ? newScroll - current.origin : 0,\n      };\n      const mouseMove = createMouseEvent(\n        'mousemove',\n        lastMouseMoveEvent.clientX + delta.x,\n        lastMouseMoveEvent.clientY + delta.y\n      );\n      const dragEvent = this.getDragEvent(mouseMove);\n      this.onDragEmitter.fire(dragEvent);\n    }\n  }\n\n  private _disposed = false;\n\n  private _promise?: Promise<void>;\n\n  private _resolve?: () => void;\n\n  private _startPos?: PositionSchema;\n\n  private _playgroundConfigEntity?: PlaygroundConfigEntity;\n}\n\nlet dragCache: PlaygroundDrag<any> | undefined;\n\nexport interface PlaygroundDragEntitiesOpts<T> extends PlaygroundDragOptions<T> {\n  // entities?: Entity[]; // 需要拖动的实体，会自动修改 position\n  context?: T; // 上下文\n  config?: PlaygroundConfigEntity;\n  adsorbRefs?: Rectangle[]; // 需要吸附的矩形\n  // adsorbLines?: Adsorber.Line[]; // 需要吸附的线\n}\n\n/* istanbul ignore next */\nexport namespace PlaygroundDrag {\n  /**\n   * 拖拽实体\n   */\n  export function startDrag<T>(\n    clientX: number,\n    clientY: number,\n    opts: PlaygroundDragEntitiesOpts<T> = {}\n  ): Disposable {\n    if (dragCache) {\n      dragCache.stop(NaN, NaN);\n    }\n    // const { entities } = opts;\n    // const ableManager = entities && entities.length >= 1 ? entities[0].ableManager : undefined;\n    const dragger = (dragCache = new PlaygroundDrag<T>({\n      onDragStart(e, ctx): void {\n        // if (ableManager) {\n        //   // 添加拖拽能力\n        //   entities!.forEach(n => n.addAbles(Dragable));\n        //   ableManager.dispatch<DragablePayload>(DragablePayload, e);\n        // }\n        if (opts.onDragStart) opts.onDragStart(e, ctx);\n      },\n      onDrag(e, ctx): void {\n        // if (ableManager) {\n        //   ableManager.dispatch<DragablePayload>(DragablePayload, {\n        //     ...e,\n        //     adsorbRefs: opts.adsorbRefs,\n        //     adsorbLines: opts.adsorbLines,\n        //   });\n        // }\n        if (opts.onDrag) opts.onDrag(e, ctx);\n      },\n      onDragEnd(e, ctx): void {\n        // if (ableManager) {\n        //   ableManager.dispatch<DragablePayload>(DragablePayload, e);\n        //   // 去除拖拽能力\n        //   entities!.forEach(n => n.removeAbles(Dragable));\n        // }\n        if (opts.onDragEnd) opts.onDragEnd(e, ctx);\n        dragger.dispose();\n        if (dragCache === dragger) dragCache = undefined;\n      },\n    }));\n    dragger.start(clientX, clientY, opts.config, opts.context);\n    return Disposable.create(() => {\n      dragger.stop(0, 0);\n      dragger.dispose();\n      if (dragCache === dragger) {\n        dragCache = undefined;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/playground-gesture.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, vi } from 'vitest';\n\nimport { PlaygroundGesture } from './playground-gesture';\n\nvi.mock('@use-gesture/vanilla', () => {\n  class Gesture {}\n  return {\n    Gesture,\n  };\n});\n\ndescribe('PlaygroundGesture', () => {\n  const el = document.createElement('div');\n  const playgroundGesture = new PlaygroundGesture(el, {\n    config: {\n      minZoom: 0.4,\n      maxZoom: 2.2,\n      resolution: 1,\n    },\n  } as any);\n  it('getScaleBounds', () => {\n    expect(playgroundGesture.getScaleBounds()).toEqual({\n      min: 0.4,\n      max: 2.2,\n    });\n  });\n  it('pinching', () => {\n    expect(playgroundGesture.pinching).toBeFalsy();\n  });\n});\n\ndescribe('PlaygroundGesture', () => {\n  let el: HTMLElement;\n  let config: any;\n  let playgroundGesture: PlaygroundGesture;\n\n  beforeEach(() => {\n    el = document.createElement('div');\n    config = {\n      config: {\n        minZoom: 0.4,\n        maxZoom: 2.2,\n        resolution: 1,\n        scrollX: 0,\n        scrollY: 0,\n      },\n      finalScale: 1,\n      getPosFromMouseEvent: vi.fn().mockReturnValue({ x: 50, y: 50 }),\n      updateConfig: vi.fn(),\n    };\n    playgroundGesture = new PlaygroundGesture(el, config);\n  });\n\n  describe('PlaygroundGesture handlePinch', () => {\n    it('should not update config when newScale is NaN', () => {\n      playgroundGesture.handlePinch({\n        first: true,\n        last: false,\n        originX: 100,\n        originY: 100,\n        newScale: NaN,\n      });\n\n      expect(config.updateConfig).not.toHaveBeenCalled();\n    });\n\n    it('should set _pinching to true on first pinch', () => {\n      playgroundGesture.handlePinch({\n        first: true,\n        last: false,\n        originX: 100,\n        originY: 100,\n        newScale: 1.5,\n      });\n\n      expect(playgroundGesture.pinching).toBe(true);\n    });\n\n    it('should set _pinching to false on last pinch', () => {\n      playgroundGesture.handlePinch({\n        first: false,\n        last: true,\n        originX: 100,\n        originY: 100,\n        newScale: 1.5,\n      });\n\n      expect(playgroundGesture.pinching).toBe(false);\n    });\n\n    it('should call updateConfig with correct parameters', () => {\n      playgroundGesture.handlePinch({\n        first: false,\n        last: false,\n        originX: 100,\n        originY: 100,\n        newScale: 2,\n      });\n\n      expect(config.updateConfig).toHaveBeenCalledWith(\n        expect.objectContaining({\n          scrollX: expect.any(Number),\n          scrollY: expect.any(Number),\n          zoom: 2,\n        })\n      );\n    });\n\n    it('should calculate correct scroll values', () => {\n      config.getPosFromMouseEvent.mockReturnValue({ x: 100, y: 100 });\n      config.finalScale = 1;\n      config.config.scrollX = 0;\n      config.config.scrollY = 0;\n\n      playgroundGesture.handlePinch({\n        first: false,\n        last: false,\n        originX: 100,\n        originY: 100,\n        newScale: 2,\n      });\n\n      expect(config.updateConfig).toHaveBeenCalledWith({\n        scrollX: 100,\n        scrollY: 100,\n        zoom: 2,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/playground-gesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable, DisposableImpl } from '@flowgram.ai/utils';\n\nimport { type PlaygroundConfigEntity } from '../layer/config/playground-config-entity';\nimport { Gesture } from './use-gesture';\n\n/* istanbul ignore next */\nexport class PlaygroundGesture extends DisposableImpl {\n  private _pinching = false;\n\n  constructor(\n    public readonly target: HTMLElement,\n    protected readonly config: PlaygroundConfigEntity\n  ) {\n    super();\n    this.preventDefault();\n    const gesture = new Gesture(\n      target,\n      {\n        // onDrag: ({pinching, cancel, offset: [x, y], ...rest}) => {\n        //   if (pinching) return cancel();\n        //   onChange({ ...style, x, y })\n        //   api.start({ x, y })\n        // },\n        onPinch: ({\n          origin: [originX, originY],\n          first,\n          last,\n          movement: [ms],\n          offset: [newScale, a],\n        }) => {\n          this.handlePinch({ first, last, originX, originY, newScale });\n        },\n      },\n      {\n        // drag: { from: () => [startState.x, startState.y] },\n        pinch: {\n          scaleBounds: () => this.getScaleBounds(),\n          from: () => [this.config.finalScale, 0],\n          /**\n           * 支持 command 和 ctrl\n           */\n          modifierKey: ['metaKey', 'ctrlKey'],\n          // rubberband: true\n        },\n      }\n    );\n    this.toDispose.push(\n      Disposable.create(() => {\n        gesture.destroy();\n      })\n    );\n  }\n\n  handlePinch(params: {\n    first: boolean;\n    last: boolean;\n    originX: number;\n    originY: number;\n    newScale: number;\n  }) {\n    const { first, last, originX, originY, newScale } = params;\n    if (Number.isNaN(params.newScale)) {\n      // 防止画布无法缩放\n      return;\n    }\n    if (first) {\n      this._pinching = true;\n    }\n    if (last) {\n      this._pinching = false;\n    }\n    const oldScale = this.config.finalScale;\n    const origin = this.config.getPosFromMouseEvent({ clientX: originX, clientY: originY }, false);\n    // 放大后的位置\n    const finalPos = {\n      x: (origin.x / oldScale) * newScale,\n      y: (origin.y / oldScale) * newScale,\n    };\n    this.config.updateConfig({\n      scrollX: this.config.config.scrollX + finalPos.x - origin.x,\n      scrollY: this.config.config.scrollY + finalPos.y - origin.y,\n      zoom: newScale,\n    });\n  }\n\n  getScaleBounds(): { min: number; max: number } {\n    return {\n      min: this.config.config.minZoom,\n      max: this.config.config.maxZoom,\n    };\n  }\n\n  protected preventDefault(): void {\n    // 阻止默认手势\n    const handler = (e: MouseEvent) => e.preventDefault();\n    // @ts-ignore\n    document.addEventListener('gesturestart', handler);\n    // @ts-ignore\n    document.addEventListener('gesturechange', handler);\n    this.toDispose.push(\n      Disposable.create(() => {\n        // @ts-ignore\n        document.removeEventListener('gesturestart', handler);\n        // @ts-ignore\n        document.removeEventListener('gesturechange', handler);\n      })\n    );\n  }\n\n  get pinching(): boolean {\n    return this._pinching;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/tween.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport TWEEN from '@tweenjs/tween.js';\nimport { Disposable } from '@flowgram.ai/utils';\n\nlet started = 0;\n\n// Setup the animation loop.\nfunction startTweenLoop(): void {\n  started++;\n  function animate(time: number): void {\n    if (started <= 0) return;\n    requestAnimationFrame(animate);\n    TWEEN.update(time);\n  }\n  requestAnimationFrame(animate);\n}\n\nfunction stopTweenLoop(): void {\n  started--;\n}\n\ninterface TweenValues {\n  [key: string]: number;\n}\n\nexport interface TweenOpts<V> {\n  from: V;\n  to: V;\n  onUpdate?: (v: V) => void;\n  onComplete?: (v: V) => void;\n  onDispose?: (v: V) => void;\n  easing?: (num: number) => number;\n  duration: number;\n}\n\nexport function startTween<V extends TweenValues = TweenValues>(opts: TweenOpts<V>): Disposable {\n  startTweenLoop();\n  let stopped = false;\n  const tween = new TWEEN.Tween(opts.from)\n    .to(opts.to, opts.duration)\n    .easing(opts.easing || TWEEN.Easing.Quadratic.Out)\n    .onUpdate(() => {\n      if (stopped) return;\n      if (opts.onUpdate) opts.onUpdate(opts.from);\n    })\n    .onComplete(() => {\n      if (stopped) return;\n      stopped = true;\n      stopTweenLoop();\n      if (opts.onComplete) opts.onComplete(opts.from);\n    })\n    .start();\n  return Disposable.create(() => {\n    if (stopped) return;\n    stopped = true;\n    stopTweenLoop();\n    tween.stop();\n    if (opts.onDispose) opts.onDispose(opts.from);\n  });\n}\n\nexport interface ScrollIntoViewOpts {\n  getScrollParent(): HTMLElement | undefined;\n\n  getTargetNode(): HTMLElement | undefined;\n\n  duration?: number;\n  scrollY?: boolean; // 默认 true\n  scrollX?: boolean; // 默认 true\n}\n\nconst defaultScrollIntoViewOpts = {\n  duration: 300,\n  scrollY: true,\n  scrollX: true,\n};\n\nconst preTweenMap = new WeakMap<HTMLElement, Disposable>();\n\n/**\n * 滚动到可视区域\n * @param opts\n */\nexport function scrollIntoViewWithTween(opts: ScrollIntoViewOpts): Disposable {\n  opts = { ...defaultScrollIntoViewOpts, ...opts };\n  const parentNode = opts.getScrollParent();\n  const targetNode = opts.getTargetNode();\n  if (!parentNode || !targetNode) return Disposable.NULL;\n  // 销毁上一次的\n  if (preTweenMap.has(parentNode)) {\n    preTweenMap.get(parentNode)!.dispose();\n  }\n  const startScrollTop = parentNode.scrollTop;\n  const startScrollLeft = parentNode.scrollLeft;\n  let endScrollTop = startScrollTop;\n  let endScrollLeft = startScrollLeft;\n  const parentBound = parentNode.getBoundingClientRect();\n  const targetBound = targetNode.getBoundingClientRect();\n  const top = targetBound.top - parentBound.top + startScrollTop;\n  const left = targetBound.left - parentBound.left + startScrollLeft;\n  if (startScrollTop > top) {\n    endScrollTop = top;\n  } else {\n    const bottom = top + targetNode.clientHeight - parentNode.clientHeight;\n    if (startScrollTop < bottom && targetNode.clientHeight < parentNode.clientHeight) {\n      endScrollTop = bottom;\n    }\n  }\n  if (startScrollLeft > left) {\n    endScrollLeft = left;\n  } else {\n    const right = left + targetNode.clientWidth - parentNode.clientWidth;\n    if (startScrollLeft < right && targetNode.clientWidth < parentNode.clientWidth) {\n      endScrollLeft = right;\n    }\n  }\n  if (startScrollTop !== endScrollTop || startScrollLeft !== endScrollLeft) {\n    const from: { scrollTop?: number; scrollLeft?: number } = {};\n    const to: { scrollTop?: number; scrollLeft?: number } = {};\n    if (opts.scrollY) {\n      from.scrollTop = startScrollTop;\n      to.scrollTop = endScrollTop;\n    }\n    if (opts.scrollX) {\n      from.scrollLeft = startScrollLeft;\n      to.scrollLeft = endScrollLeft;\n    }\n    const scrollTween = startTween<{ scrollTop?: number; scrollLeft?: number }>({\n      from,\n      to,\n      onUpdate: v => {\n        if (v.scrollTop !== undefined) {\n          parentNode.scrollTop = v.scrollTop;\n        }\n        if (v.scrollLeft !== undefined) {\n          parentNode.scrollLeft = v.scrollLeft;\n        }\n      },\n      onComplete: () => {\n        toDispose.dispose();\n      },\n      duration: opts.duration!,\n    });\n    const toDispose = Disposable.create(() => {\n      scrollTween.dispose();\n      preTweenMap.delete(parentNode);\n    });\n    preTweenMap.set(parentNode, toDispose);\n    return toDispose;\n  }\n  return Disposable.NULL;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/Controller.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { chain } from './utils/fn';\nimport { isTouch, parseProp, toHandlerProp, touchIds } from './utils/events';\nimport {\n  GestureKey,\n  InternalConfig,\n  InternalHandlers,\n  NativeHandlers,\n  State,\n  UserGestureConfig,\n} from './types';\nimport { TimeoutStore } from './TimeoutStore';\nimport { EventStore } from './EventStore';\nimport { parse } from './config/resolver';\nimport { EngineMap } from './actions';\n\nexport class Controller {\n  /**\n   * The list of gestures handled by the Controller.\n   */\n  public gestures = new Set<GestureKey>();\n\n  /**\n   * The event store that keeps track of the config.target listeners.\n   */\n  private _targetEventStore = new EventStore(this);\n\n  /**\n   * Object that keeps track of all gesture event listeners.\n   */\n  public gestureEventStores: { [key in GestureKey]?: EventStore } = {};\n\n  public gestureTimeoutStores: { [key in GestureKey]?: TimeoutStore } = {};\n\n  public handlers: InternalHandlers = {};\n\n  private nativeHandlers?: NativeHandlers;\n\n  public config = {} as InternalConfig;\n\n  public pointerIds = new Set<number>();\n\n  public touchIds = new Set<number>();\n\n  public state = {\n    shared: {\n      shiftKey: false,\n      metaKey: false,\n      ctrlKey: false,\n      altKey: false,\n    },\n  } as State;\n\n  constructor(handlers: InternalHandlers) {\n    resolveGestures(this, handlers);\n  }\n\n  /**\n   * Sets pointer or touch ids based on the event.\n   * @param event\n   */\n  setEventIds(event: TouchEvent | PointerEvent) {\n    if (isTouch(event)) {\n      this.touchIds = new Set(touchIds(event as TouchEvent));\n      return this.touchIds;\n    } else if ('pointerId' in event) {\n      if (event.type === 'pointerup' || event.type === 'pointercancel')\n        this.pointerIds.delete(event.pointerId);\n      else if (event.type === 'pointerdown') this.pointerIds.add(event.pointerId);\n      return this.pointerIds;\n    }\n  }\n\n  /**\n   * Attaches handlers to the controller.\n   * @param handlers\n   * @param nativeHandlers\n   */\n  applyHandlers(handlers: InternalHandlers, nativeHandlers?: NativeHandlers) {\n    this.handlers = handlers;\n    this.nativeHandlers = nativeHandlers;\n  }\n\n  /**\n   * Compute and attaches a config to the controller.\n   * @param config\n   * @param gestureKey\n   */\n  applyConfig(config: UserGestureConfig, gestureKey?: GestureKey) {\n    this.config = parse(config, gestureKey, this.config);\n  }\n\n  /**\n   * Cleans all side effects (listeners, timeouts). When the gesture is\n   * destroyed (in React, when the component is unmounted.)\n   */\n  clean() {\n    this._targetEventStore.clean();\n    for (const key of this.gestures) {\n      this.gestureEventStores[key]!.clean();\n      this.gestureTimeoutStores[key]!.clean();\n    }\n  }\n\n  /**\n   * Executes side effects (attaching listeners to a `config.target`). Ran on\n   * each render.\n   */\n  effect() {\n    if (this.config.shared.target) this.bind();\n    return () => this._targetEventStore.clean();\n  }\n\n  /**\n   * The bind function that can be returned by the gesture handler (a hook in\n   * React for example.)\n   * @param args\n   */\n  bind(...args: any[]) {\n    const sharedConfig = this.config.shared;\n    const props: any = {};\n\n    let target;\n    if (sharedConfig.target) {\n      target = sharedConfig.target();\n      // if target is undefined let's stop\n      if (!target) return;\n    }\n\n    if (sharedConfig.enabled) {\n      // Adding gesture handlers\n      for (const gestureKey of this.gestures) {\n        const gestureConfig = this.config[gestureKey]!;\n        const bindFunction = bindToProps(props, gestureConfig.eventOptions, !!target);\n        if (gestureConfig.enabled) {\n          const Engine = EngineMap.get(gestureKey)!;\n          // @ts-ignore\n          new Engine(this, args, gestureKey).bind(bindFunction);\n        }\n      }\n\n      // Adding native handlers\n      const nativeBindFunction = bindToProps(props, sharedConfig.eventOptions, !!target);\n      for (const eventKey in this.nativeHandlers) {\n        nativeBindFunction(\n          eventKey,\n          '',\n          // @ts-ignore\n          event => this.nativeHandlers[eventKey]({ ...this.state.shared, event, args }),\n          undefined,\n          true,\n        );\n      }\n    }\n\n    // If target isn't set, we return an object that contains gesture handlers\n    // mapped to props handler event keys.\n    for (const handlerProp in props) {\n      props[handlerProp] = chain(...props[handlerProp]);\n    }\n\n    // When target isn't specified then return hanlder props.\n    if (!target) return props;\n\n    // When target is specified, then add listeners to the controller target\n    // store.\n    for (const handlerProp in props) {\n      const { device, capture, passive } = parseProp(handlerProp);\n      this._targetEventStore.add(target, device, '', props[handlerProp], { capture, passive });\n    }\n  }\n}\n\nfunction setupGesture(ctrl: Controller, gestureKey: GestureKey) {\n  ctrl.gestures.add(gestureKey);\n  ctrl.gestureEventStores[gestureKey] = new EventStore(ctrl, gestureKey);\n  ctrl.gestureTimeoutStores[gestureKey] = new TimeoutStore();\n}\n\nfunction resolveGestures(ctrl: Controller, internalHandlers: InternalHandlers) {\n  // make sure hover handlers are added first to prevent bugs such as #322\n  // where the hover pointerLeave handler is removed before the move\n  // pointerLeave, which prevents hovering: false to be fired.\n  if (internalHandlers.drag) setupGesture(ctrl, 'drag');\n  if (internalHandlers.wheel) setupGesture(ctrl, 'wheel');\n  if (internalHandlers.scroll) setupGesture(ctrl, 'scroll');\n  if (internalHandlers.move) setupGesture(ctrl, 'move');\n  if (internalHandlers.pinch) setupGesture(ctrl, 'pinch');\n  if (internalHandlers.hover) setupGesture(ctrl, 'hover');\n}\n\nconst bindToProps =\n  (props: any, eventOptions: AddEventListenerOptions, withPassiveOption: boolean) =>\n  (\n    device: string,\n    action: string,\n    handler: (event: any) => void,\n    options: AddEventListenerOptions = {},\n    isNative = false,\n  ) => {\n    const capture = options.capture ?? eventOptions.capture;\n    const passive = options.passive ?? eventOptions.passive;\n    // a native handler is already passed as a prop like \"onMouseDown\"\n    let handlerProp = isNative ? device : toHandlerProp(device, action, capture);\n    if (withPassiveOption && passive) handlerProp += 'Passive';\n    props[handlerProp] = props[handlerProp] || [];\n    props[handlerProp].push(handler);\n  };\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/EventStore.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { toDomEventType } from './utils/events';\nimport { GestureKey } from './types';\nimport type { Controller } from './Controller';\n\nexport class EventStore {\n  private _listeners = new Set<() => void>();\n\n  private _ctrl: Controller;\n\n  private _gestureKey?: GestureKey;\n\n  constructor(ctrl: Controller, gestureKey?: GestureKey) {\n    this._ctrl = ctrl;\n    this._gestureKey = gestureKey;\n  }\n\n  add(\n    element: EventTarget,\n    device: string,\n    action: string,\n    handler: (event: any) => void,\n    options?: AddEventListenerOptions,\n  ) {\n    const listeners = this._listeners;\n    const type = toDomEventType(device, action);\n    const _options = this._gestureKey ? this._ctrl.config[this._gestureKey]!.eventOptions : {};\n    const eventOptions = { ..._options, ...options };\n    element.addEventListener(type, handler, eventOptions);\n    const remove = () => {\n      element.removeEventListener(type, handler, eventOptions);\n      listeners.delete(remove);\n    };\n    listeners.add(remove);\n    return remove;\n  }\n\n  clean() {\n    this._listeners.forEach(remove => remove());\n    this._listeners.clear(); // just for safety\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/TimeoutStore.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport class TimeoutStore {\n  private _timeouts = new Map<string, number>();\n\n  add<FunctionType extends (...args: any[]) => any>(\n    key: string,\n    callback: FunctionType,\n    ms = 140,\n    ...args: Parameters<FunctionType>\n  ) {\n    this.remove(key);\n    this._timeouts.set(key, window.setTimeout(callback, ms, ...args));\n  }\n\n  remove(key: string) {\n    const timeout = this._timeouts.get(key);\n    if (timeout) window.clearTimeout(timeout);\n  }\n\n  clean() {\n    this._timeouts.forEach(timeout => void window.clearTimeout(timeout));\n    this._timeouts.clear();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/actions.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { GestureKey, EngineClass, Action } from './types';\nimport { WheelEngine } from './engines/WheelEngine';\nimport { ScrollEngine } from './engines/ScrollEngine';\nimport { PinchEngine } from './engines/PinchEngine';\nimport { MoveEngine } from './engines/MoveEngine';\nimport { HoverEngine } from './engines/HoverEngine';\nimport { DragEngine } from './engines/DragEngine';\nimport { wheelConfigResolver } from './config/wheelConfigResolver';\nimport { scrollConfigResolver } from './config/scrollConfigResolver';\nimport { ResolverMap } from './config/resolver';\nimport { pinchConfigResolver } from './config/pinchConfigResolver';\nimport { moveConfigResolver } from './config/moveConfigResolver';\nimport { hoverConfigResolver } from './config/hoverConfigResolver';\nimport { dragConfigResolver } from './config/dragConfigResolver';\n\nexport const EngineMap = new Map<GestureKey, EngineClass<any>>();\nexport const ConfigResolverMap = new Map<GestureKey, ResolverMap>();\n\nexport function registerAction(action: Action) {\n  EngineMap.set(action.key, action.engine);\n  ConfigResolverMap.set(action.key, action.resolver);\n}\n\nexport const dragAction: Action = {\n  key: 'drag',\n  engine: DragEngine as any,\n  resolver: dragConfigResolver,\n};\n\nexport const hoverAction: Action = {\n  key: 'hover',\n  engine: HoverEngine as any,\n  resolver: hoverConfigResolver,\n};\n\nexport const moveAction: Action = {\n  key: 'move',\n  engine: MoveEngine as any,\n  resolver: moveConfigResolver,\n};\n\nexport const pinchAction: Action = {\n  key: 'pinch',\n  engine: PinchEngine as any,\n  resolver: pinchConfigResolver,\n};\n\nexport const scrollAction: Action = {\n  key: 'scroll',\n  engine: ScrollEngine as any,\n  resolver: scrollConfigResolver,\n};\n\nexport const wheelAction: Action = {\n  key: 'wheel',\n  engine: WheelEngine as any,\n  resolver: wheelConfigResolver,\n};\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/commonConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { InternalGestureOptions } from '../types'\nimport { Vector2, State, GenericOptions } from '../types'\nimport { V } from '../utils/maths'\n\nexport const identity = (v: Vector2) => v\nexport const DEFAULT_RUBBERBAND = 0.15\n\nexport const commonConfigResolver = {\n  enabled(value = true) {\n    return value\n  },\n  eventOptions(value: AddEventListenerOptions | undefined, _k: string, config: { shared: GenericOptions }) {\n    return { ...config.shared.eventOptions, ...value }\n  },\n  preventDefault(value = false) {\n    return value\n  },\n  triggerAllEvents(value = false) {\n    return value\n  },\n  rubberband(value: number | boolean | Vector2 = 0): Vector2 {\n    switch (value) {\n      case true:\n        return [DEFAULT_RUBBERBAND, DEFAULT_RUBBERBAND]\n      case false:\n        return [0, 0]\n      default:\n        return V.toVector(value)\n    }\n  },\n  from(value: number | Vector2 | ((s: State) => Vector2)) {\n    if (typeof value === 'function') return value\n    // eslint-disable-next-line eqeqeq\n    if (value != null) return V.toVector(value)\n  },\n  transform(this: InternalGestureOptions, value: any, _k: string, config: { shared: GenericOptions }) {\n    const transform = value || config.shared.transform\n    this.hasCustomTransform = !!transform\n\n    if (process.env.NODE_ENV === 'development') {\n      const originalTransform = transform || identity\n      return (v: Vector2) => {\n        const r = originalTransform(v)\n        if (!isFinite(r[0]) || !isFinite(r[1])) {\n          // eslint-disable-next-line no-console\n          console.warn(`[@use-gesture]: config.transform() must produce a valid result, but it was: [${r[0]},${[1]}]`)\n        }\n        return r\n      }\n    }\n    return transform || identity\n  },\n  threshold(value: any) {\n    return V.toVector(value, 0)\n  }\n}\n\nif (process.env.NODE_ENV === 'development') {\n  Object.assign(commonConfigResolver, {\n    domTarget(value: any) {\n      if (value !== undefined) {\n        throw Error(`[@use-gesture]: \\`domTarget\\` option has been renamed to \\`target\\`.`)\n      }\n      return NaN\n    },\n    lockDirection(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`lockDirection\\` option has been merged with \\`axis\\`. Use it as in \\`{ axis: 'lock' }\\``\n        )\n      }\n      return NaN\n    },\n    initial(value: any) {\n      if (value !== undefined) {\n        throw Error(`[@use-gesture]: \\`initial\\` option has been renamed to \\`from\\`.`)\n      }\n      return NaN\n    }\n  })\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/coordinatesConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { commonConfigResolver } from './commonConfigResolver'\nimport { InternalCoordinatesOptions, CoordinatesConfig, Bounds, DragBounds, State, Vector2 } from '../types'\n\nconst DEFAULT_AXIS_THRESHOLD = 0\n\nexport const coordinatesConfigResolver = {\n  ...commonConfigResolver,\n  axis(\n    this: InternalCoordinatesOptions,\n    _v: any,\n    _k: string,\n    { axis }: CoordinatesConfig\n  ): InternalCoordinatesOptions['axis'] {\n    this.lockDirection = axis === 'lock'\n    if (!this.lockDirection) return axis as any\n  },\n  axisThreshold(value = DEFAULT_AXIS_THRESHOLD) {\n    return value\n  },\n  bounds(\n    value: DragBounds | ((state: State) => DragBounds) = {}\n  ): (() => EventTarget | null) | HTMLElement | [Vector2, Vector2] {\n    if (typeof value === 'function') {\n      // @ts-ignore\n      return (state: State) => coordinatesConfigResolver.bounds(value(state))\n    }\n\n    if ('current' in value) {\n      return () => value.current\n    }\n\n    if (typeof HTMLElement === 'function' && value instanceof HTMLElement) {\n      return value\n    }\n\n    const { left = -Infinity, right = Infinity, top = -Infinity, bottom = Infinity } = value as Bounds\n\n    return [\n      [left, right],\n      [top, bottom]\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/dragConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PointerType } from '../types'\nimport { DragConfig, InternalDragOptions, Vector2 } from '../types'\nimport { V } from '../utils/maths'\nimport { coordinatesConfigResolver } from './coordinatesConfigResolver'\nimport { SUPPORT } from './support'\n\nexport const DEFAULT_PREVENT_SCROLL_DELAY = 250\nexport const DEFAULT_DRAG_DELAY = 180\nexport const DEFAULT_SWIPE_VELOCITY = 0.5\nexport const DEFAULT_SWIPE_DISTANCE = 50\nexport const DEFAULT_SWIPE_DURATION = 250\nexport const DEFAULT_KEYBOARD_DISPLACEMENT = 10\n\nconst DEFAULT_DRAG_AXIS_THRESHOLD: Record<PointerType, number> = { mouse: 0, touch: 0, pen: 8 }\n\nexport const dragConfigResolver = {\n  ...coordinatesConfigResolver,\n  device(\n    this: InternalDragOptions,\n    _v: any,\n    _k: string,\n    { pointer: { touch = false, lock = false, mouse = false } = {} }: DragConfig\n  ) {\n    this.pointerLock = lock && SUPPORT.pointerLock\n    if (SUPPORT.touch && touch) return 'touch'\n    if (this.pointerLock) return 'mouse'\n    if (SUPPORT.pointer && !mouse) return 'pointer'\n    if (SUPPORT.touch) return 'touch'\n    return 'mouse'\n  },\n  preventScrollAxis(this: InternalDragOptions, value: 'x' | 'y' | 'xy', _k: string, { preventScroll }: DragConfig) {\n    this.preventScrollDelay =\n      typeof preventScroll === 'number'\n        ? preventScroll\n        : preventScroll || (preventScroll === undefined && value)\n        ? DEFAULT_PREVENT_SCROLL_DELAY\n        : undefined\n    if (!SUPPORT.touchscreen || preventScroll === false) return undefined\n    return value ? value : preventScroll !== undefined ? 'y' : undefined\n  },\n  pointerCapture(\n    this: InternalDragOptions,\n    _v: any,\n    _k: string,\n    { pointer: { capture = true, buttons = 1, keys = true } = {} }\n  ) {\n    this.pointerButtons = buttons\n    this.keys = keys\n    return !this.pointerLock && this.device === 'pointer' && capture\n  },\n  threshold(\n    this: InternalDragOptions,\n    value: number | Vector2,\n    _k: string,\n    { filterTaps = false, tapsThreshold = 3, axis = undefined }\n  ) {\n    // TODO add warning when value is 0 and filterTaps or axis is set\n    const threshold = V.toVector(value, filterTaps ? tapsThreshold : axis ? 1 : 0)\n    this.filterTaps = filterTaps\n    this.tapsThreshold = tapsThreshold\n    return threshold\n  },\n  swipe(\n    this: InternalDragOptions,\n    { velocity = DEFAULT_SWIPE_VELOCITY, distance = DEFAULT_SWIPE_DISTANCE, duration = DEFAULT_SWIPE_DURATION } = {}\n  ) {\n    return {\n      velocity: this.transform(V.toVector(velocity)),\n      distance: this.transform(V.toVector(distance)),\n      duration\n    }\n  },\n  delay(value: number | boolean = 0) {\n    switch (value) {\n      case true:\n        return DEFAULT_DRAG_DELAY\n      case false:\n        return 0\n      default:\n        return value\n    }\n  },\n  axisThreshold(value: Record<PointerType, number>) {\n    if (!value) return DEFAULT_DRAG_AXIS_THRESHOLD\n    return { ...DEFAULT_DRAG_AXIS_THRESHOLD, ...value }\n  },\n  keyboardDisplacement(value: number = DEFAULT_KEYBOARD_DISPLACEMENT) {\n    return value\n  }\n}\n\nif (process.env.NODE_ENV === 'development') {\n  Object.assign(dragConfigResolver, {\n    useTouch(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`useTouch\\` option has been renamed to \\`pointer.touch\\`. Use it as in \\`{ pointer: { touch: true } }\\`.`\n        )\n      }\n      return NaN\n    },\n    experimental_preventWindowScrollY(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`experimental_preventWindowScrollY\\` option has been renamed to \\`preventScroll\\`.`\n        )\n      }\n      return NaN\n    },\n    swipeVelocity(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`swipeVelocity\\` option has been renamed to \\`swipe.velocity\\`. Use it as in \\`{ swipe: { velocity: 0.5 } }\\`.`\n        )\n      }\n      return NaN\n    },\n    swipeDistance(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`swipeDistance\\` option has been renamed to \\`swipe.distance\\`. Use it as in \\`{ swipe: { distance: 50 } }\\`.`\n        )\n      }\n      return NaN\n    },\n    swipeDuration(value: any) {\n      if (value !== undefined) {\n        throw Error(\n          `[@use-gesture]: \\`swipeDuration\\` option has been renamed to \\`swipe.duration\\`. Use it as in \\`{ swipe: { duration: 250 } }\\`.`\n        )\n      }\n      return NaN\n    }\n  })\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/hoverConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { coordinatesConfigResolver } from './coordinatesConfigResolver'\n\nexport const hoverConfigResolver = {\n  ...coordinatesConfigResolver,\n  mouseOnly: (value = true) => value\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/moveConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { coordinatesConfigResolver } from './coordinatesConfigResolver'\n\nexport const moveConfigResolver = {\n  ...coordinatesConfigResolver,\n  mouseOnly: (value = true) => value\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/pinchConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ModifierKey } from '../types'\nimport { PinchConfig, GenericOptions, InternalPinchOptions, State, Vector2 } from '../types'\nimport { call, assignDefault } from '../utils/fn'\nimport { V } from '../utils/maths'\nimport { commonConfigResolver } from './commonConfigResolver'\nimport { SUPPORT } from './support'\n\nexport const pinchConfigResolver = {\n  ...commonConfigResolver,\n  device(\n    this: InternalPinchOptions,\n    _v: any,\n    _k: string,\n    { shared, pointer: { touch = false } = {} }: { shared: GenericOptions } & PinchConfig\n  ) {\n    // Only try to use gesture events when they are supported and domTarget is set\n    // as React doesn't support gesture handlers.\n    const sharedConfig = shared\n    if (sharedConfig.target && !SUPPORT.touch && SUPPORT.gesture) return 'gesture'\n    if (SUPPORT.touch && touch) return 'touch'\n    if (SUPPORT.touchscreen) {\n      if (SUPPORT.pointer) return 'pointer'\n      if (SUPPORT.touch) return 'touch'\n    }\n    // device is undefined and that's ok, we're going to use wheel to zoom.\n  },\n  bounds(_v: any, _k: string, { scaleBounds = {}, angleBounds = {} }: PinchConfig) {\n    const _scaleBounds = (state?: State) => {\n      const D = assignDefault(call(scaleBounds, state), { min: -Infinity, max: Infinity })\n      return [D.min, D.max]\n    }\n\n    const _angleBounds = (state?: State) => {\n      const A = assignDefault(call(angleBounds, state), { min: -Infinity, max: Infinity })\n      return [A.min, A.max]\n    }\n\n    if (typeof scaleBounds !== 'function' && typeof angleBounds !== 'function') return [_scaleBounds(), _angleBounds()]\n\n    return (state: State) => [_scaleBounds(state), _angleBounds(state)]\n  },\n  threshold(this: InternalPinchOptions, value: number | Vector2, _k: string, config: PinchConfig) {\n    this.lockDirection = config.axis === 'lock'\n    const threshold = V.toVector(value, this.lockDirection ? [0.1, 3] : 0)\n    return threshold\n  },\n  modifierKey(value: ModifierKey | ModifierKey[]) {\n    if (value === undefined) return 'ctrlKey'\n    return value\n  },\n  pinchOnWheel(value = true) {\n    return value\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/resolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { sharedConfigResolver } from './sharedConfigResolver'\nimport { ConfigResolverMap } from '../actions'\nimport { GestureKey, InternalConfig, UserGestureConfig } from '../types'\n\nexport type Resolver = (x: any, key: string, obj: any) => any\nexport type ResolverMap = { [k: string]: Resolver | ResolverMap | boolean }\n\nexport function resolveWith<T extends { [k: string]: any }, V extends { [k: string]: any }>(\n  config: Partial<T> = {},\n  resolvers: ResolverMap\n): V {\n  const result: any = {}\n\n  for (const [key, resolver] of Object.entries(resolvers)) {\n    switch (typeof resolver) {\n      case 'function':\n        if (process.env.NODE_ENV === 'development') {\n          const r = resolver.call(result, config[key], key, config)\n          // prevents deprecated resolvers from applying in dev mode\n          if (!Number.isNaN(r)) result[key] = r\n        } else {\n          result[key] = resolver.call(result, config[key], key, config)\n        }\n        break\n      case 'object':\n        result[key] = resolveWith(config[key], resolver)\n        break\n      case 'boolean':\n        if (resolver) result[key] = config[key]\n        break\n    }\n  }\n\n  return result\n}\n\nexport function parse(newConfig: UserGestureConfig, gestureKey?: GestureKey, _config: any = {}): InternalConfig {\n  const { target, eventOptions, window, enabled, transform, ...rest } = newConfig as any\n\n  _config.shared = resolveWith({ target, eventOptions, window, enabled, transform }, sharedConfigResolver)\n\n  if (gestureKey) {\n    const resolver = ConfigResolverMap.get(gestureKey)!\n    _config[gestureKey] = resolveWith({ shared: _config.shared, ...rest }, resolver)\n  } else {\n    for (const key in rest) {\n      const resolver = ConfigResolverMap.get(key as GestureKey)!\n\n      if (resolver) {\n        _config[key] = resolveWith({ shared: _config.shared, ...rest[key] }, resolver)\n      } else if (process.env.NODE_ENV === 'development') {\n        if (!['drag', 'pinch', 'scroll', 'wheel', 'move', 'hover'].includes(key)) {\n          if (key === 'domTarget') {\n            throw Error(`[@use-gesture]: \\`domTarget\\` option has been renamed to \\`target\\`.`)\n          }\n          // eslint-disable-next-line no-console\n          console.warn(\n            `[@use-gesture]: Unknown config key \\`${key}\\` was used. Please read the documentation for further information.`\n          )\n        }\n      }\n    }\n  }\n  return _config\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/scrollConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { coordinatesConfigResolver } from './coordinatesConfigResolver'\n\nexport const scrollConfigResolver = coordinatesConfigResolver\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/sharedConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Target } from '../types'\nimport { SUPPORT } from './support'\n\nexport const sharedConfigResolver = {\n  target(value: Target) {\n    if (value) {\n      return () => ('current' in value ? value.current : value)\n    }\n    return undefined\n  },\n  enabled(value = true) {\n    return value\n  },\n  window(value = SUPPORT.isBrowser ? window : undefined) {\n    return value\n  },\n  eventOptions({ passive = true, capture = false } = {}) {\n    return { passive, capture }\n  },\n  transform(value: any) {\n    return value\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/support.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst isBrowser = typeof window !== 'undefined' && window.document && window.document.createElement\n\nfunction supportsTouchEvents(): boolean {\n  return isBrowser && 'ontouchstart' in window\n}\n\nfunction isTouchScreen(): boolean {\n  return supportsTouchEvents() || (isBrowser && window.navigator.maxTouchPoints > 1)\n}\n\nfunction supportsPointerEvents(): boolean {\n  return isBrowser && 'onpointerdown' in window\n}\n\nfunction supportsPointerLock(): boolean {\n  return isBrowser && 'exitPointerLock' in window.document\n}\n\nfunction supportsGestureEvents(): boolean {\n  try {\n    // TODO [TS] possibly find GestureEvent definitions?\n    // @ts-ignore: no type definitions for webkit GestureEvents\n    return 'constructor' in GestureEvent\n  } catch (e) {\n    return false\n  }\n}\n\nexport const SUPPORT = {\n  isBrowser,\n  gesture: supportsGestureEvents(),\n  /**\n   * It looks from https://github.com/pmndrs/use-gesture/discussions/421 that\n   * some touchscreens using webkits don't have 'ontouchstart' in window. So\n   * we're considering that browsers support TouchEvent if they have\n   * `maxTouchPoints > 1`\n   *\n   * Update 16/09/2023: This generates failure on other Windows systems, so reverting\n   * back to detecting TouchEvent support only.\n   * https://github.com/pmndrs/use-gesture/issues/626\n   */\n  touch: supportsTouchEvents(),\n  // touch: isTouchScreen(),\n  touchscreen: isTouchScreen(),\n  pointer: supportsPointerEvents(),\n  pointerLock: supportsPointerLock()\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/config/wheelConfigResolver.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { coordinatesConfigResolver } from './coordinatesConfigResolver'\n\nexport const wheelConfigResolver = coordinatesConfigResolver\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/CoordinatesEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V } from '../utils/maths';\nimport { getPointerType } from '../utils/events';\nimport { CoordinatesKey, Vector2 } from '../types';\nimport { Engine } from './Engine';\n\nfunction selectAxis([dx, dy]: Vector2, threshold: number) {\n  const absDx = Math.abs(dx);\n  const absDy = Math.abs(dy);\n\n  if (absDx > absDy && absDx > threshold) {\n    return 'x';\n  }\n  if (absDy > absDx && absDy > threshold) {\n    return 'y';\n  }\n  return undefined;\n}\n\nexport abstract class CoordinatesEngine<Key extends CoordinatesKey> extends Engine<Key> {\n  aliasKey = 'xy';\n\n  reset() {\n    super.reset();\n    this.state.axis = undefined;\n  }\n\n  init() {\n    this.state.offset = [0, 0];\n    this.state.lastOffset = [0, 0];\n  }\n\n  computeOffset() {\n    this.state.offset = V.add(this.state.lastOffset, this.state.movement);\n  }\n\n  computeMovement() {\n    this.state.movement = V.sub(this.state.offset, this.state.lastOffset);\n  }\n\n  axisIntent(event?: UIEvent) {\n    const state = this.state;\n    const config = this.config;\n\n    if (!state.axis && event) {\n      const threshold =\n        typeof config.axisThreshold === 'object'\n          ? config.axisThreshold[getPointerType(event)]\n          : config.axisThreshold;\n\n      state.axis = selectAxis(state._movement, threshold);\n    }\n\n    // We block the movement if either:\n    // - config.lockDirection or config.axis was set but axis isn't detected yet\n    // - config.axis was set but is different than detected axis\n    state._blocked =\n      ((config.lockDirection || !!config.axis) && !state.axis) ||\n      (!!config.axis && config.axis !== state.axis);\n  }\n\n  restrictToAxis(v: Vector2) {\n    if (this.config.axis || this.config.lockDirection) {\n      switch (this.state.axis) {\n        case 'x':\n          v[1] = 0;\n          break; // [ x, 0 ]\n        case 'y':\n          v[0] = 0;\n          break; // [ 0, y ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/DragEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V } from '../utils/maths';\nimport { pointerId, getPointerType, pointerValues } from '../utils/events';\nimport { Vector2 } from '../types';\nimport { coordinatesConfigResolver } from '../config/coordinatesConfigResolver';\nimport { CoordinatesEngine } from './CoordinatesEngine';\n\nconst KEYS_DELTA_MAP = {\n  ArrowRight: (displacement: number, factor: number = 1) => [displacement * factor, 0],\n  ArrowLeft: (displacement: number, factor: number = 1) => [-1 * displacement * factor, 0],\n  ArrowUp: (displacement: number, factor: number = 1) => [0, -1 * displacement * factor],\n  ArrowDown: (displacement: number, factor: number = 1) => [0, displacement * factor],\n};\n\nexport class DragEngine extends CoordinatesEngine<'drag'> {\n  ingKey = 'dragging' as const;\n\n  // superseeds generic Engine reset call\n  reset(this: DragEngine) {\n    super.reset();\n    const state = this.state;\n    state._pointerId = undefined;\n    state._pointerActive = false;\n    state._keyboardActive = false;\n    state._preventScroll = false;\n    state._delayed = false;\n    state.swipe = [0, 0];\n    state.tap = false;\n    state.canceled = false;\n    state.cancel = this.cancel.bind(this);\n  }\n\n  setup() {\n    const state = this.state;\n\n    if (state._bounds instanceof HTMLElement) {\n      const boundRect = state._bounds.getBoundingClientRect();\n      const targetRect = (state.currentTarget as HTMLElement).getBoundingClientRect();\n      const _bounds = {\n        left: boundRect.left - targetRect.left + state.offset[0],\n        right: boundRect.right - targetRect.right + state.offset[0],\n        top: boundRect.top - targetRect.top + state.offset[1],\n        bottom: boundRect.bottom - targetRect.bottom + state.offset[1],\n      };\n      state._bounds = coordinatesConfigResolver.bounds(_bounds) as [Vector2, Vector2];\n    }\n  }\n\n  cancel() {\n    const state = this.state;\n    if (state.canceled) return;\n    state.canceled = true;\n    state._active = false;\n    setTimeout(() => {\n      // we run compute with no event so that kinematics won't be computed\n      this.compute();\n      this.emit();\n    }, 0);\n  }\n\n  setActive() {\n    this.state._active = this.state._pointerActive || this.state._keyboardActive;\n  }\n\n  // superseeds Engine clean function\n  clean() {\n    this.pointerClean();\n    this.state._pointerActive = false;\n    this.state._keyboardActive = false;\n    super.clean();\n  }\n\n  pointerDown(event: PointerEvent) {\n    const config = this.config;\n    const state = this.state;\n\n    if (\n      event.buttons != null &&\n      // If the user submits an array as pointer.buttons, don't start the drag\n      // if event.buttons isn't included inside that array.\n      (Array.isArray(config.pointerButtons)\n        ? !config.pointerButtons.includes(event.buttons)\n        : // If the user submits a number as pointer.buttons, refuse the drag if\n          // config.pointerButtons is different than `-1` and if event.buttons\n          // doesn't match the combination.\n          config.pointerButtons !== -1 && config.pointerButtons !== event.buttons)\n    )\n      return;\n\n    const ctrlIds = this.ctrl.setEventIds(event);\n    // We need to capture all pointer ids so that we can keep track of them when\n    // they're released off the target\n    if (config.pointerCapture) {\n      (event.target as HTMLElement).setPointerCapture(event.pointerId);\n    }\n\n    if (\n      // in some situations (https://github.com/pmndrs/use-gesture/issues/494#issuecomment-1127584116)\n      // like when a new browser tab is opened during a drag gesture, the drag\n      // can be interrupted mid-way, and can stall. This happens because the\n      // pointerId that initiated the gesture is lost, and since the drag\n      // persists until that pointerId is lifted with pointerup, it never ends.\n      //\n      // Therefore, when we detect that only one pointer is pressing the screen,\n      // we consider that the gesture can proceed.\n      ctrlIds &&\n      ctrlIds.size > 1 &&\n      state._pointerActive\n    )\n      return;\n\n    this.start(event);\n    this.setupPointer(event);\n\n    state._pointerId = pointerId(event);\n    state._pointerActive = true;\n\n    this.computeValues(pointerValues(event));\n    this.computeInitial();\n\n    if (config.preventScrollAxis && getPointerType(event) !== 'mouse') {\n      // when preventScrollAxis is set we don't consider the gesture active\n      // until it's deliberate\n      state._active = false;\n      this.setupScrollPrevention(event);\n    } else if (config.delay > 0) {\n      this.setupDelayTrigger(event);\n      // makes sure we emit all events when `triggerAllEvents` flag is `true`\n      if (config.triggerAllEvents) {\n        this.compute(event);\n        this.emit();\n      }\n    } else {\n      this.startPointerDrag(event);\n    }\n  }\n\n  startPointerDrag(event: PointerEvent) {\n    const state = this.state;\n    state._active = true;\n    state._preventScroll = true;\n    state._delayed = false;\n\n    this.compute(event);\n    this.emit();\n  }\n\n  pointerMove(event: PointerEvent) {\n    const state = this.state;\n    const config = this.config;\n\n    if (!state._pointerActive) return;\n\n    const id = pointerId(event);\n    if (state._pointerId !== undefined && id !== state._pointerId) return;\n    const _values = pointerValues(event);\n\n    if (document.pointerLockElement === event.target) {\n      state._delta = [event.movementX, event.movementY];\n    } else {\n      state._delta = V.sub(_values, state._values);\n      this.computeValues(_values);\n    }\n\n    V.addTo(state._movement, state._delta);\n    this.compute(event);\n\n    // if the gesture is delayed but deliberate, then we can start it\n    // immediately.\n    if (state._delayed && state.intentional) {\n      this.timeoutStore.remove('dragDelay');\n      // makes sure `first` is still true when moving for the first time after a\n      // delay.\n      state.active = false;\n      this.startPointerDrag(event);\n      return;\n    }\n\n    if (config.preventScrollAxis && !state._preventScroll) {\n      if (state.axis) {\n        if (state.axis === config.preventScrollAxis || config.preventScrollAxis === 'xy') {\n          state._active = false;\n          this.clean();\n          return;\n        } else {\n          this.timeoutStore.remove('startPointerDrag');\n          this.startPointerDrag(event);\n          return;\n        }\n      } else {\n        return;\n      }\n    }\n\n    this.emit();\n  }\n\n  pointerUp(event: PointerEvent) {\n    this.ctrl.setEventIds(event);\n    // We release the pointer id if it has pointer capture\n    try {\n      if (\n        this.config.pointerCapture &&\n        (event.target as HTMLElement).hasPointerCapture(event.pointerId)\n      ) {\n        // this shouldn't be necessary as it should be automatic when releasing the pointer\n        (event.target as HTMLElement).releasePointerCapture(event.pointerId);\n      }\n    } catch {\n      if (process.env.NODE_ENV === 'development') {\n        // eslint-disable-next-line no-console\n        console.warn(\n          `[@use-gesture]: If you see this message, it's likely that you're using an outdated version of \\`@react-three/fiber\\`. \\n\\nPlease upgrade to the latest version.`,\n        );\n      }\n    }\n\n    const state = this.state;\n    const config = this.config;\n\n    if (!state._active || !state._pointerActive) return;\n\n    const id = pointerId(event);\n    if (state._pointerId !== undefined && id !== state._pointerId) return;\n\n    this.state._pointerActive = false;\n    this.setActive();\n    this.compute(event);\n\n    const [dx, dy] = state._distance;\n    state.tap = dx <= config.tapsThreshold && dy <= config.tapsThreshold;\n\n    if (state.tap && config.filterTaps) {\n      state._force = true;\n    } else {\n      const [_dx, _dy] = state._delta;\n      const [_mx, _my] = state._movement;\n      const [svx, svy] = config.swipe.velocity;\n      const [sx, sy] = config.swipe.distance;\n      const sdt = config.swipe.duration;\n\n      if (state.elapsedTime < sdt) {\n        const _vx = Math.abs(_dx / state.timeDelta);\n        const _vy = Math.abs(_dy / state.timeDelta);\n\n        if (_vx > svx && Math.abs(_mx) > sx) state.swipe[0] = Math.sign(_dx);\n        if (_vy > svy && Math.abs(_my) > sy) state.swipe[1] = Math.sign(_dy);\n      }\n    }\n\n    this.emit();\n  }\n\n  pointerClick(event: MouseEvent) {\n    // event.detail indicates the number of buttons being pressed. When it's\n    // null, it's likely to be a keyboard event from the Enter Key that could\n    // be used for accessibility, and therefore shouldn't be prevented.\n    // See https://github.com/pmndrs/use-gesture/issues/530\n    if (!this.state.tap && event.detail > 0) {\n      event.preventDefault();\n      event.stopPropagation();\n    }\n  }\n\n  setupPointer(event: PointerEvent) {\n    const config = this.config;\n    const device = config.device;\n\n    if (process.env.NODE_ENV === 'development') {\n      try {\n        if (device === 'pointer' && config.preventScrollDelay === undefined) {\n          const currentTarget =\n            // @ts-ignore (warning for r3f)\n            'uv' in event ? event.sourceEvent.currentTarget : event.currentTarget;\n          const style = window.getComputedStyle(currentTarget);\n          if (style.touchAction === 'auto') {\n            // eslint-disable-next-line no-console\n            console.warn(\n              `[@use-gesture]: The drag target has its \\`touch-action\\` style property set to \\`auto\\`. It is recommended to add \\`touch-action: 'none'\\` so that the drag gesture behaves correctly on touch-enabled devices. For more information read this: https://use-gesture.netlify.app/docs/extras/#touch-action.\\n\\nThis message will only show in development mode. It won't appear in production. If this is intended, you can ignore it.`,\n              currentTarget,\n            );\n          }\n        }\n      } catch {}\n    }\n\n    if (config.pointerLock) {\n      (event.currentTarget as HTMLElement).requestPointerLock();\n    }\n\n    if (!config.pointerCapture) {\n      this.eventStore.add(this.sharedConfig.window, device, 'change', this.pointerMove.bind(this));\n      this.eventStore.add(this.sharedConfig.window, device, 'end', this.pointerUp.bind(this));\n      this.eventStore.add(this.sharedConfig.window, device, 'cancel', this.pointerUp.bind(this));\n    }\n  }\n\n  pointerClean() {\n    if (this.config.pointerLock && document.pointerLockElement === this.state.currentTarget) {\n      document.exitPointerLock();\n    }\n  }\n\n  preventScroll(event: PointerEvent) {\n    if (this.state._preventScroll && event.cancelable) {\n      event.preventDefault();\n    }\n  }\n\n  setupScrollPrevention(event: PointerEvent) {\n    // fixes https://github.com/pmndrs/use-gesture/issues/497\n    this.state._preventScroll = false;\n    persistEvent(event);\n    // we add window listeners that will prevent the scroll when the user has started dragging\n    const remove = this.eventStore.add(\n      this.sharedConfig.window,\n      'touch',\n      'change',\n      this.preventScroll.bind(this),\n      {\n        passive: false,\n      },\n    );\n    this.eventStore.add(this.sharedConfig.window, 'touch', 'end', remove);\n    this.eventStore.add(this.sharedConfig.window, 'touch', 'cancel', remove);\n    this.timeoutStore.add(\n      'startPointerDrag',\n      this.startPointerDrag.bind(this),\n      this.config.preventScrollDelay!,\n      event,\n    );\n  }\n\n  setupDelayTrigger(event: PointerEvent) {\n    this.state._delayed = true;\n    this.timeoutStore.add(\n      'dragDelay',\n      () => {\n        // forces drag to start no matter the threshold when delay is reached\n        this.state._step = [0, 0];\n        this.startPointerDrag(event);\n      },\n      this.config.delay,\n    );\n  }\n\n  keyDown(event: KeyboardEvent) {\n    // @ts-ignore\n    const deltaFn = KEYS_DELTA_MAP[event.key];\n    if (deltaFn) {\n      const state = this.state;\n      const factor = event.shiftKey ? 10 : event.altKey ? 0.1 : 1;\n\n      this.start(event);\n\n      state._delta = deltaFn(this.config.keyboardDisplacement, factor);\n      state._keyboardActive = true;\n      V.addTo(state._movement, state._delta);\n\n      this.compute(event);\n      this.emit();\n    }\n  }\n\n  keyUp(event: KeyboardEvent) {\n    if (!(event.key in KEYS_DELTA_MAP)) return;\n\n    this.state._keyboardActive = false;\n    this.setActive();\n    this.compute(event);\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    const device = this.config.device;\n\n    bindFunction(device, 'start', this.pointerDown.bind(this));\n\n    if (this.config.pointerCapture) {\n      bindFunction(device, 'change', this.pointerMove.bind(this));\n      bindFunction(device, 'end', this.pointerUp.bind(this));\n      bindFunction(device, 'cancel', this.pointerUp.bind(this));\n      bindFunction('lostPointerCapture', '', this.pointerUp.bind(this));\n    }\n\n    if (this.config.keys) {\n      bindFunction('key', 'down', this.keyDown.bind(this));\n      bindFunction('key', 'up', this.keyUp.bind(this));\n    }\n    if (this.config.filterTaps) {\n      bindFunction('click', '', this.pointerClick.bind(this), { capture: true, passive: false });\n    }\n  }\n}\n\nfunction persistEvent(event: PointerEvent) {\n  // @ts-ignore\n  'persist' in event && typeof event.persist === 'function' && event.persist();\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/Engine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V, computeRubberband } from '../utils/maths';\nimport { call } from '../utils/fn';\nimport { getEventDetails } from '../utils/events';\nimport { GestureKey, IngKey, State, Vector2 } from '../types';\nimport { NonUndefined } from '../types';\nimport { Controller } from '../Controller';\n\n/**\n * The lib doesn't compute the kinematics on the last event of the gesture\n * (i.e. for a drag gesture, the `pointerup` coordinates will generally match the\n * last `pointermove` coordinates which would result in all drags ending with a\n * `[0,0]` velocity). However, when the timestamp difference between the last\n * event (ie pointerup) and the before last event (ie pointermove) is greater\n * than BEFORE_LAST_KINEMATICS_DELAY, the kinematics are computed (which would\n * mean that if you release your drag after stopping for more than\n * BEFORE_LAST_KINEMATICS_DELAY, the velocity will be indeed 0).\n *\n * See https://github.com/pmndrs/use-gesture/issues/332 for more details.\n */\n\nconst BEFORE_LAST_KINEMATICS_DELAY = 32;\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport interface Engine<Key extends GestureKey> {\n  /**\n   * Function that some gestures can use to add initilization\n   * properties to the state when it is created.\n   */\n  init?(): void;\n  /**\n   * Setup function that some gestures can use to set additional properties of\n   * the state when the gesture starts.\n   */\n  setup?(): void;\n  /**\n   * Function used by some gestures to determine the intentionality of a\n   * a movement depending on thresholds. The intent function can change the\n   * `state._active` or `state._blocked` flags if the gesture isn't intentional.\n   * @param event\n   */\n  axisIntent?(event?: UIEvent): void;\n\n  restrictToAxis?(movement: Vector2): void;\n}\n\nexport abstract class Engine<Key extends GestureKey> {\n  /**\n   * The Controller handling state.\n   */\n  ctrl: Controller;\n\n  /**\n   * The gesture key ('drag' | 'pinch' | 'wheel' | 'scroll' | 'move' | 'hover')\n   */\n  readonly key: Key;\n\n  /**\n   * The key representing the active state of the gesture in the shared state.\n   * ('dragging' | 'pinching' | 'wheeling' | 'scrolling' | 'moving' | 'hovering')\n   */\n  abstract readonly ingKey: IngKey;\n  /**\n   * The arguments passed to the `bind` function.\n   */\n\n  /**\n   * State prop that aliases state values (`xy` or `da`).\n   */\n  abstract readonly aliasKey: string;\n\n  args: any[];\n\n  constructor(ctrl: Controller, args: any[], key: Key) {\n    this.ctrl = ctrl;\n    this.args = args;\n    this.key = key;\n\n    if (!this.state) {\n      this.state = {} as any;\n      this.computeValues([0, 0]);\n      this.computeInitial();\n\n      if (this.init) this.init();\n      this.reset();\n    }\n  }\n\n  /**\n   * Function implemented by gestures that compute the offset from the state\n   * movement.\n   */\n  abstract computeOffset(): void;\n\n  /**\n   * Function implemented by the gestures that compute the movement from the\n   * corrected offset (after bounds and potential rubberbanding).\n   */\n  abstract computeMovement(): void;\n\n  /**\n   * Executes the bind function so that listeners are properly set by the\n   * Controller.\n   * @param bindFunction\n   */\n  abstract bind(\n    bindFunction: (\n      device: string,\n      action: string,\n      handler: (event: any) => void,\n      options?: AddEventListenerOptions,\n    ) => void,\n  ): void;\n\n  /**\n   * Shortcut to the gesture state read from the Controller.\n   */\n  get state() {\n    return this.ctrl.state[this.key]!;\n  }\n\n  set state(state) {\n    this.ctrl.state[this.key] = state;\n  }\n\n  /**\n   * Shortcut to the shared state read from the Controller\n   */\n  get shared() {\n    return this.ctrl.state.shared;\n  }\n\n  /**\n   * Shortcut to the gesture event store read from the Controller.\n   */\n  get eventStore() {\n    return this.ctrl.gestureEventStores[this.key]!;\n  }\n\n  /**\n   * Shortcut to the gesture timeout store read from the Controller.\n   */\n  get timeoutStore() {\n    return this.ctrl.gestureTimeoutStores[this.key]!;\n  }\n\n  /**\n   * Shortcut to the gesture config read from the Controller.\n   */\n  get config() {\n    return this.ctrl.config[this.key]!;\n  }\n\n  /**\n   * Shortcut to the shared config read from the Controller.\n   */\n  get sharedConfig() {\n    return this.ctrl.config.shared;\n  }\n\n  /**\n   * Shortcut to the gesture handler read from the Controller.\n   */\n  get handler() {\n    return this.ctrl.handlers[this.key]!;\n  }\n\n  reset() {\n    const { state, shared, ingKey, args } = this;\n    shared[ingKey] = state._active = state.active = state._blocked = state._force = false;\n    state._step = [false, false];\n    state.intentional = false;\n    state._movement = [0, 0];\n    state._distance = [0, 0];\n    state._direction = [0, 0];\n    state._delta = [0, 0];\n    // prettier-ignore\n    state._bounds = [[-Infinity, Infinity], [-Infinity, Infinity]]\n    state.args = args;\n    state.axis = undefined;\n    state.memo = undefined;\n    state.elapsedTime = state.timeDelta = 0;\n    state.direction = [0, 0];\n    state.distance = [0, 0];\n    state.overflow = [0, 0];\n    state._movementBound = [false, false];\n    state.velocity = [0, 0];\n    state.movement = [0, 0];\n    state.delta = [0, 0];\n    state.timeStamp = 0;\n  }\n\n  /**\n   * Function ran at the start of the gesture.\n   * @param event\n   */\n  start(event: NonUndefined<State[Key]>['event']) {\n    const state = this.state;\n    const config = this.config;\n    if (!state._active) {\n      this.reset();\n      this.computeInitial();\n\n      state._active = true;\n      state.target = event.target!;\n      state.currentTarget = event.currentTarget!;\n      state.lastOffset = config.from ? call(config.from, state) : state.offset;\n      state.offset = state.lastOffset;\n      state.startTime = state.timeStamp = event.timeStamp;\n    }\n  }\n\n  /**\n   * Assign raw values to `state._values` and transformed values to\n   * `state.values`.\n   * @param values\n   */\n  computeValues(values: Vector2) {\n    const state = this.state;\n    state._values = values;\n    // transforming values into user-defined coordinates (#402)\n    state.values = this.config.transform(values);\n  }\n\n  /**\n   * Assign `state._values` to `state._initial` and transformed `state.values` to\n   * `state.initial`.\n   * @param values\n   */\n  computeInitial() {\n    const state = this.state;\n    state._initial = state._values;\n    state.initial = state.values;\n  }\n\n  /**\n   * Computes all sorts of state attributes, including kinematics.\n   * @param event\n   */\n  compute(event?: NonUndefined<State[Key]>['event']) {\n    const { state, config, shared } = this;\n    state.args = this.args;\n\n    let dt = 0;\n\n    if (event) {\n      // sets the shared state with event properties\n      state.event = event;\n      // if config.preventDefault is true, then preventDefault\n      if (config.preventDefault && event.cancelable) state.event.preventDefault();\n      state.type = event.type;\n      shared.touches = this.ctrl.pointerIds.size || this.ctrl.touchIds.size;\n      shared.locked = !!document.pointerLockElement;\n      Object.assign(shared, getEventDetails(event));\n      shared.down = shared.pressed = shared.buttons % 2 === 1 || shared.touches > 0;\n\n      // sets time stamps\n      dt = event.timeStamp - state.timeStamp;\n      state.timeStamp = event.timeStamp;\n      state.elapsedTime = state.timeStamp - state.startTime;\n    }\n\n    // only compute _distance if the state is active otherwise we might compute it\n    // twice when the gesture ends because state._delta wouldn't have changed on\n    // the last frame.\n    if (state._active) {\n      const _absoluteDelta = state._delta.map(Math.abs) as Vector2;\n      V.addTo(state._distance, _absoluteDelta);\n    }\n\n    // let's run intentionality check.\n    if (this.axisIntent) this.axisIntent(event);\n\n    // _movement is calculated by each gesture engine\n    const [_m0, _m1] = state._movement;\n    const [t0, t1] = config.threshold;\n\n    const { _step, values } = state;\n\n    if (config.hasCustomTransform) {\n      // When the user is using a custom transform, we're using `_step` to store\n      // the first value passing the threshold.\n      if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && values[0];\n      if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && values[1];\n    } else {\n      // `_step` will hold the threshold at which point the gesture was triggered.\n      // The threshold is signed depending on which direction triggered it.\n      if (_step[0] === false) _step[0] = Math.abs(_m0) >= t0 && Math.sign(_m0) * t0;\n      if (_step[1] === false) _step[1] = Math.abs(_m1) >= t1 && Math.sign(_m1) * t1;\n    }\n\n    state.intentional = _step[0] !== false || _step[1] !== false;\n\n    if (!state.intentional) return;\n\n    const movement: Vector2 = [0, 0];\n\n    if (config.hasCustomTransform) {\n      const [v0, v1] = values;\n      movement[0] = _step[0] !== false ? v0 - _step[0] : 0;\n      movement[1] = _step[1] !== false ? v1 - _step[1] : 0;\n    } else {\n      movement[0] = _step[0] !== false ? _m0 - _step[0] : 0;\n      movement[1] = _step[1] !== false ? _m1 - _step[1] : 0;\n    }\n\n    if (this.restrictToAxis && !state._blocked) this.restrictToAxis(movement);\n\n    const previousOffset = state.offset;\n\n    const gestureIsActive = (state._active && !state._blocked) || state.active;\n\n    if (gestureIsActive) {\n      state.first = state._active && !state.active;\n      state.last = !state._active && state.active;\n      state.active = shared[this.ingKey] = state._active;\n\n      if (event) {\n        if (state.first) {\n          if ('bounds' in config) state._bounds = call(config.bounds, state);\n          if (this.setup) this.setup();\n        }\n\n        state.movement = movement;\n        this.computeOffset();\n      }\n    }\n\n    const [ox, oy] = state.offset;\n    const [[x0, x1], [y0, y1]] = state._bounds;\n    state.overflow = [ox < x0 ? -1 : ox > x1 ? 1 : 0, oy < y0 ? -1 : oy > y1 ? 1 : 0];\n\n    // _movementBound will store the latest _movement value\n    // before it went off bounds.\n    state._movementBound[0] = state.overflow[0]\n      ? state._movementBound[0] === false\n        ? state._movement[0]\n        : state._movementBound[0]\n      : false;\n\n    state._movementBound[1] = state.overflow[1]\n      ? state._movementBound[1] === false\n        ? state._movement[1]\n        : state._movementBound[1]\n      : false;\n\n    // @ts-ignore\n    const rubberband: Vector2 = state._active ? config.rubberband || [0, 0] : [0, 0];\n    state.offset = computeRubberband(state._bounds, state.offset, rubberband);\n    state.delta = V.sub(state.offset, previousOffset);\n\n    this.computeMovement();\n\n    if (gestureIsActive && (!state.last || dt > BEFORE_LAST_KINEMATICS_DELAY)) {\n      state.delta = V.sub(state.offset, previousOffset);\n      const absoluteDelta = state.delta.map(Math.abs) as Vector2;\n\n      V.addTo(state.distance, absoluteDelta);\n      state.direction = state.delta.map(Math.sign) as Vector2;\n      state._direction = state._delta.map(Math.sign) as Vector2;\n\n      // calculates kinematics unless the gesture starts or ends or if the\n      // dt === 0 (which can happen on high frame rate monitors, see issue #581)\n      // because of privacy protection:\n      // https://developer.mozilla.org/en-US/docs/Web/API/Event/timeStamp#reduced_time_precision\n      if (!state.first && dt > 0) {\n        state.velocity = [absoluteDelta[0] / dt, absoluteDelta[1] / dt];\n        state.timeDelta = dt;\n      }\n    }\n  }\n\n  /**\n   * Fires the gesture handler.\n   */\n  emit() {\n    const state = this.state;\n    const shared = this.shared;\n    const config = this.config;\n\n    if (!state._active) this.clean();\n\n    // we don't trigger the handler if the gesture is blocked or non intentional,\n    // unless the `_force` flag was set or the `triggerAllEvents` option was set\n    // to true in the config.\n    if ((state._blocked || !state.intentional) && !state._force && !config.triggerAllEvents) return;\n\n    // @ts-ignore\n    const memo = this.handler({ ...shared, ...state, [this.aliasKey]: state.values });\n\n    // Sets memo to the returned value of the handler (unless it's  undefined)\n    if (memo !== undefined) state.memo = memo;\n  }\n\n  /**\n   * Cleans the gesture timeouts and event listeners.\n   */\n  clean() {\n    this.eventStore.clean();\n    this.timeoutStore.clean();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/HoverEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V } from '../utils/maths';\nimport { pointerValues } from '../utils/events';\nimport { CoordinatesEngine } from './CoordinatesEngine';\n\nexport class HoverEngine extends CoordinatesEngine<'hover'> {\n  ingKey = 'hovering' as const;\n\n  enter(event: PointerEvent) {\n    if (this.config.mouseOnly && event.pointerType !== 'mouse') return;\n    this.start(event);\n    this.computeValues(pointerValues(event));\n\n    this.compute(event);\n    this.emit();\n  }\n\n  leave(event: PointerEvent) {\n    if (this.config.mouseOnly && event.pointerType !== 'mouse') return;\n\n    const state = this.state;\n    if (!state._active) return;\n\n    state._active = false;\n    const values = pointerValues(event);\n    state._movement = state._delta = V.sub(values, state._values);\n\n    this.computeValues(values);\n    this.compute(event);\n\n    state.delta = state.movement;\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    bindFunction('pointer', 'enter', this.enter.bind(this));\n    bindFunction('pointer', 'leave', this.leave.bind(this));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/MoveEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V } from '../utils/maths';\nimport { pointerValues } from '../utils/events';\nimport { CoordinatesEngine } from './CoordinatesEngine';\n\nexport class MoveEngine extends CoordinatesEngine<'move'> {\n  ingKey = 'moving' as const;\n\n  move(event: PointerEvent) {\n    if (this.config.mouseOnly && event.pointerType !== 'mouse') return;\n    if (!this.state._active) this.moveStart(event);\n    else this.moveChange(event);\n    this.timeoutStore.add('moveEnd', this.moveEnd.bind(this));\n  }\n\n  moveStart(event: PointerEvent) {\n    this.start(event);\n    this.computeValues(pointerValues(event));\n    this.compute(event);\n    this.computeInitial();\n    this.emit();\n  }\n\n  moveChange(event: PointerEvent) {\n    if (!this.state._active) return;\n    const values = pointerValues(event);\n    const state = this.state;\n    state._delta = V.sub(values, state._values);\n    V.addTo(state._movement, state._delta);\n\n    this.computeValues(values);\n\n    this.compute(event);\n    this.emit();\n  }\n\n  moveEnd(event?: PointerEvent) {\n    if (!this.state._active) return;\n    this.state._active = false;\n    this.compute(event);\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    bindFunction('pointer', 'change', this.move.bind(this));\n    bindFunction('pointer', 'leave', this.moveEnd.bind(this));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/PinchEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { clampStateInternalMovementToBounds } from '../utils/state';\nimport { V } from '../utils/maths';\nimport { touchDistanceAngle, distanceAngle, wheelValues } from '../utils/events';\nimport { Vector2, WebKitGestureEvent } from '../types';\nimport { Engine } from './Engine';\n\nconst SCALE_ANGLE_RATIO_INTENT_DEG = 30;\nconst PINCH_WHEEL_RATIO = 100;\n\nexport class PinchEngine extends Engine<'pinch'> {\n  ingKey = 'pinching' as const;\n\n  aliasKey = 'da';\n\n  init() {\n    this.state.offset = [1, 0];\n    this.state.lastOffset = [1, 0];\n    this.state._pointerEvents = new Map();\n  }\n\n  // superseeds generic Engine reset call\n  reset() {\n    super.reset();\n    const state = this.state;\n    state._touchIds = [];\n    state.canceled = false;\n    state.cancel = this.cancel.bind(this);\n    state.turns = 0;\n  }\n\n  computeOffset() {\n    const { type, movement, lastOffset } = this.state;\n    if (type === 'wheel') {\n      this.state.offset = V.add(movement, lastOffset);\n    } else {\n      this.state.offset = [(1 + movement[0]) * lastOffset[0], movement[1] + lastOffset[1]];\n    }\n  }\n\n  computeMovement() {\n    const { offset, lastOffset } = this.state;\n    this.state.movement = [offset[0] / lastOffset[0], offset[1] - lastOffset[1]];\n  }\n\n  axisIntent() {\n    const state = this.state;\n    const [_m0, _m1] = state._movement;\n    if (!state.axis) {\n      const axisMovementDifference = Math.abs(_m0) * SCALE_ANGLE_RATIO_INTENT_DEG - Math.abs(_m1);\n      if (axisMovementDifference < 0) state.axis = 'angle';\n      else if (axisMovementDifference > 0) state.axis = 'scale';\n    }\n  }\n\n  restrictToAxis(v: Vector2) {\n    if (this.config.lockDirection) {\n      if (this.state.axis === 'scale') v[1] = 0;\n      else if (this.state.axis === 'angle') v[0] = 0;\n    }\n  }\n\n  cancel() {\n    const state = this.state;\n    if (state.canceled) return;\n    setTimeout(() => {\n      state.canceled = true;\n      state._active = false;\n      // we run compute with no event so that kinematics won't be computed\n      this.compute();\n      this.emit();\n    }, 0);\n  }\n\n  touchStart(event: TouchEvent) {\n    this.ctrl.setEventIds(event);\n    const state = this.state;\n    const ctrlTouchIds = this.ctrl.touchIds;\n\n    if (state._active) {\n      // check that the touchIds that initiated the gesture are still enabled\n      // This is useful for when the page loses track of the pointers (minifying\n      // gesture on iPad).\n      if (state._touchIds.every(id => ctrlTouchIds.has(id))) return;\n      // The gesture is still active, but probably didn't have the opportunity to\n      // end properly, so we restart the pinch.\n    }\n\n    if (ctrlTouchIds.size < 2) return;\n\n    this.start(event);\n    state._touchIds = Array.from(ctrlTouchIds).slice(0, 2) as [number, number];\n\n    const payload = touchDistanceAngle(event, state._touchIds);\n\n    if (!payload) return;\n    this.pinchStart(event, payload);\n  }\n\n  pointerStart(event: PointerEvent) {\n    if (event.buttons != null && event.buttons % 2 !== 1) return;\n    this.ctrl.setEventIds(event);\n    (event.target as HTMLElement).setPointerCapture(event.pointerId);\n    const state = this.state;\n    const _pointerEvents = state._pointerEvents;\n    const ctrlPointerIds = this.ctrl.pointerIds;\n\n    if (state._active) {\n      // see touchStart comment\n      if (Array.from(_pointerEvents.keys()).every(id => ctrlPointerIds.has(id))) return;\n    }\n\n    if (_pointerEvents.size < 2) {\n      _pointerEvents.set(event.pointerId, event);\n    }\n\n    if (state._pointerEvents.size < 2) return;\n\n    this.start(event);\n\n    // @ts-ignore\n    const payload = distanceAngle(...Array.from(_pointerEvents.values()));\n\n    if (!payload) return;\n    this.pinchStart(event, payload);\n  }\n\n  pinchStart(\n    event: PointerEvent | TouchEvent,\n    payload: { distance: number; angle: number; origin: Vector2 },\n  ) {\n    const state = this.state;\n    state.origin = payload.origin;\n    this.computeValues([payload.distance, payload.angle]);\n    this.computeInitial();\n\n    this.compute(event);\n    this.emit();\n  }\n\n  touchMove(event: TouchEvent) {\n    if (!this.state._active) return;\n    const payload = touchDistanceAngle(event, this.state._touchIds);\n\n    if (!payload) return;\n    this.pinchMove(event, payload);\n  }\n\n  pointerMove(event: PointerEvent) {\n    const _pointerEvents = this.state._pointerEvents;\n    if (_pointerEvents.has(event.pointerId)) {\n      _pointerEvents.set(event.pointerId, event);\n    }\n    if (!this.state._active) return;\n    // @ts-ignore\n    const payload = distanceAngle(...Array.from(_pointerEvents.values()));\n\n    if (!payload) return;\n    this.pinchMove(event, payload);\n  }\n\n  pinchMove(\n    event: PointerEvent | TouchEvent,\n    payload: { distance: number; angle: number; origin: Vector2 },\n  ) {\n    const state = this.state;\n    const prev_a = state._values[1];\n    const delta_a = payload.angle - prev_a;\n\n    let delta_turns = 0;\n    if (Math.abs(delta_a) > 270) delta_turns += Math.sign(delta_a);\n\n    this.computeValues([payload.distance, payload.angle - 360 * delta_turns]);\n\n    state.origin = payload.origin;\n    state.turns = delta_turns;\n    state._movement = [\n      state._values[0] / state._initial[0] - 1,\n      state._values[1] - state._initial[1],\n    ];\n\n    this.compute(event);\n    this.emit();\n  }\n\n  touchEnd(event: TouchEvent) {\n    this.ctrl.setEventIds(event);\n    if (!this.state._active) return;\n\n    if (this.state._touchIds.some(id => !this.ctrl.touchIds.has(id))) {\n      this.state._active = false;\n\n      this.compute(event);\n      this.emit();\n    }\n  }\n\n  pointerEnd(event: PointerEvent) {\n    const state = this.state;\n    this.ctrl.setEventIds(event);\n    try {\n      // @ts-ignore r3f\n      event.target.releasePointerCapture(event.pointerId);\n    } catch {}\n\n    if (state._pointerEvents.has(event.pointerId)) {\n      state._pointerEvents.delete(event.pointerId);\n    }\n\n    if (!state._active) return;\n\n    if (state._pointerEvents.size < 2) {\n      state._active = false;\n      this.compute(event);\n      this.emit();\n    }\n  }\n\n  gestureStart(event: WebKitGestureEvent) {\n    if (event.cancelable) event.preventDefault();\n    const state = this.state;\n\n    if (state._active) return;\n\n    this.start(event);\n    this.computeValues([event.scale, event.rotation]);\n    state.origin = [event.clientX, event.clientY];\n    this.compute(event);\n\n    this.emit();\n  }\n\n  gestureMove(event: WebKitGestureEvent) {\n    if (event.cancelable) event.preventDefault();\n\n    if (!this.state._active) return;\n\n    const state = this.state;\n\n    this.computeValues([event.scale, event.rotation]);\n    state.origin = [event.clientX, event.clientY];\n    const _previousMovement = state._movement;\n    state._movement = [event.scale - 1, event.rotation];\n    state._delta = V.sub(state._movement, _previousMovement);\n    this.compute(event);\n    this.emit();\n  }\n\n  gestureEnd(event: WebKitGestureEvent) {\n    if (!this.state._active) return;\n\n    this.state._active = false;\n\n    this.compute(event);\n    this.emit();\n  }\n\n  wheel(event: WheelEvent) {\n    const modifierKey = this.config.modifierKey;\n    if (\n      modifierKey &&\n      (Array.isArray(modifierKey) ? !modifierKey.find(k => event[k]) : !event[modifierKey])\n    )\n      return;\n    if (!this.state._active) this.wheelStart(event);\n    else this.wheelChange(event);\n    this.timeoutStore.add('wheelEnd', this.wheelEnd.bind(this));\n  }\n\n  wheelStart(event: WheelEvent) {\n    this.start(event);\n    this.wheelChange(event);\n  }\n\n  wheelChange(event: WheelEvent) {\n    const isR3f = 'uv' in event;\n    if (!isR3f) {\n      if (event.cancelable) {\n        event.preventDefault();\n      }\n      if (process.env.NODE_ENV === 'development' && !event.defaultPrevented) {\n        // eslint-disable-next-line no-console\n        console.warn(\n          `[@use-gesture]: To properly support zoom on trackpads, try using the \\`target\\` option.\\n\\nThis message will only appear in development mode.`,\n        );\n      }\n    }\n    const state = this.state;\n    /**\n     * 原版的计算对鼠标滚轮不友好\n     * 1. 基数问题。鼠标滚轮的 delta 像素级在 0 - 300+ 浮动不等。相对的触摸板在 0 - 10+ 左右，所以相对会觉得触摸板缩放比较稳定\n     * 2. 步进问题。一次缩放的增量 = 基数 * 上一次缩放。本身是为了营造平滑加速的感觉，但对于基数较大的滚轮加速曲线会特别快\n     * 3. 所以最终步进值限制在 0.1（触摸板在 0.01 - 0.08 左右，无影响），用来约束太快的滚轮连续滚动。该值先写死（受限于 maxOffset）\n     * 4. 计算公式 offset = lastOffset + movement - delta * offset\n     */\n    // state._delta = [(-wheelValues(event)[1] / PINCH_WHEEL_RATIO) * state.offset[0], 0]\n    let stepValue = (-wheelValues(event)[1] / PINCH_WHEEL_RATIO) * state.offset[0];\n    if (Math.abs(stepValue) > 0.1) {\n      stepValue = 0.1 * Math.sign(stepValue);\n    }\n\n    state._delta = [stepValue, 0];\n    V.addTo(state._movement, state._delta);\n\n    // _movement rolls back to when it passed the bounds.\n    clampStateInternalMovementToBounds(state);\n\n    this.state.origin = [event.clientX, event.clientY];\n\n    this.compute(event);\n    this.emit();\n  }\n\n  wheelEnd() {\n    if (!this.state._active) return;\n    this.state._active = false;\n    this.compute();\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    const device = this.config.device;\n    if (!!device) {\n      // @ts-ignore\n      bindFunction(device, 'start', this[device + 'Start'].bind(this));\n      // @ts-ignore\n      bindFunction(device, 'change', this[device + 'Move'].bind(this));\n      // @ts-ignore\n      bindFunction(device, 'end', this[device + 'End'].bind(this));\n      // @ts-ignore\n      bindFunction(device, 'cancel', this[device + 'End'].bind(this));\n      // @ts-ignore\n      bindFunction('lostPointerCapture', '', this[device + 'End'].bind(this));\n    }\n    // we try to set a passive listener, knowing that in any case React will\n    // ignore it.\n    if (this.config.pinchOnWheel) {\n      bindFunction('wheel', '', this.wheel.bind(this), { passive: false });\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/ScrollEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { V } from '../utils/maths';\nimport { scrollValues } from '../utils/events';\nimport { CoordinatesEngine } from './CoordinatesEngine';\n\nexport class ScrollEngine extends CoordinatesEngine<'scroll'> {\n  ingKey = 'scrolling' as const;\n\n  scroll(event: UIEvent) {\n    if (!this.state._active) this.start(event);\n    this.scrollChange(event);\n    this.timeoutStore.add('scrollEnd', this.scrollEnd.bind(this));\n  }\n\n  scrollChange(event: UIEvent) {\n    if (event.cancelable) event.preventDefault();\n    const state = this.state;\n    const values = scrollValues(event);\n    state._delta = V.sub(values, state._values);\n    V.addTo(state._movement, state._delta);\n\n    this.computeValues(values);\n    this.compute(event);\n\n    this.emit();\n  }\n\n  scrollEnd() {\n    if (!this.state._active) return;\n    this.state._active = false;\n    this.compute();\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    bindFunction('scroll', '', this.scroll.bind(this));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/engines/WheelEngine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { clampStateInternalMovementToBounds } from '../utils/state';\nimport { V } from '../utils/maths';\nimport { wheelValues } from '../utils/events';\nimport { CoordinatesEngine } from './CoordinatesEngine';\n\nexport interface WheelEngine extends CoordinatesEngine<'wheel'> {\n  wheel(this: WheelEngine, event: WheelEvent): void;\n  wheelChange(this: WheelEngine, event: WheelEvent): void;\n  wheelEnd(this: WheelEngine): void;\n}\n\nexport class WheelEngine extends CoordinatesEngine<'wheel'> {\n  ingKey = 'wheeling' as const;\n\n  wheel(event: WheelEvent) {\n    if (!this.state._active) this.start(event);\n    this.wheelChange(event);\n    this.timeoutStore.add('wheelEnd', this.wheelEnd.bind(this));\n  }\n\n  wheelChange(event: WheelEvent) {\n    const state = this.state;\n    state._delta = wheelValues(event);\n    V.addTo(state._movement, state._delta);\n\n    // _movement rolls back to when it passed the bounds.\n    clampStateInternalMovementToBounds(state);\n\n    this.compute(event);\n    this.emit();\n  }\n\n  wheelEnd() {\n    if (!this.state._active) return;\n    this.state._active = false;\n    this.compute();\n    this.emit();\n  }\n\n  bind(bindFunction: any) {\n    bindFunction('wheel', '', this.wheel.bind(this));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Controller } from './Controller';\nexport { parseMergedHandlers } from './parser';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/parser.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FullGestureState,\n  GestureHandlers,\n  GestureKey,\n  InternalHandlers,\n  UserGestureConfig,\n} from './types';\nimport { EngineMap } from './actions';\n\nconst RE_NOT_NATIVE = /^on(Drag|Wheel|Scroll|Move|Pinch|Hover)/;\n\nfunction sortHandlers(_handlers: GestureHandlers) {\n  const native: any = {};\n  const handlers: InternalHandlers = {};\n  const actions = new Set();\n\n  for (let key in _handlers) {\n    if (RE_NOT_NATIVE.test(key)) {\n      actions.add(RegExp.lastMatch);\n      // @ts-ignore\n      handlers[key] = _handlers[key];\n    } else {\n      // @ts-ignore\n      native[key] = _handlers[key];\n    }\n  }\n\n  return [handlers, native, actions];\n}\n\ntype HandlerKey = 'onDrag' | 'onPinch' | 'onWheel' | 'onMove' | 'onScroll' | 'onHover';\n\nfunction registerGesture(\n  actions: Set<unknown>,\n  handlers: GestureHandlers,\n  handlerKey: HandlerKey,\n  key: GestureKey,\n  internalHandlers: any,\n  config: any,\n) {\n  if (!actions.has(handlerKey)) return;\n\n  if (!EngineMap.has(key)) {\n    if (process.env.NODE_ENV === 'development') {\n      // eslint-disable-next-line no-console\n      console.warn(\n        `[@use-gesture]: You've created a custom handler that that uses the \\`${key}\\` gesture but isn't properly configured.\\n\\nPlease add \\`${key}Action\\` when creating your handler.`,\n      );\n    }\n    return;\n  }\n\n  const startKey = handlerKey + 'Start';\n  const endKey = handlerKey + 'End';\n\n  const fn = (state: FullGestureState<GestureKey>) => {\n    let memo = undefined;\n    // @ts-ignore\n    if (state.first && startKey in handlers) handlers[startKey](state);\n    // @ts-ignore\n    if (handlerKey in handlers) memo = handlers[handlerKey](state);\n    // @ts-ignore\n    if (state.last && endKey in handlers) handlers[endKey](state);\n    return memo;\n  };\n\n  internalHandlers[key] = fn;\n  config[key] = config[key] || {};\n}\n\nexport function parseMergedHandlers(\n  mergedHandlers: GestureHandlers,\n  mergedConfig: UserGestureConfig,\n) {\n  const [handlers, nativeHandlers, actions] = sortHandlers(mergedHandlers);\n\n  const internalHandlers = {};\n\n  registerGesture(actions, handlers, 'onDrag', 'drag', internalHandlers, mergedConfig);\n  registerGesture(actions, handlers, 'onWheel', 'wheel', internalHandlers, mergedConfig);\n  registerGesture(actions, handlers, 'onScroll', 'scroll', internalHandlers, mergedConfig);\n  registerGesture(actions, handlers, 'onPinch', 'pinch', internalHandlers, mergedConfig);\n  registerGesture(actions, handlers, 'onMove', 'move', internalHandlers, mergedConfig);\n  registerGesture(actions, handlers, 'onHover', 'hover', internalHandlers, mergedConfig);\n\n  return { handlers: internalHandlers, config: mergedConfig, nativeHandlers };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/action.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Engine } from '../engines/Engine';\nimport type { Controller } from '../Controller';\nimport type { ResolverMap } from '../config/resolver';\nimport { GestureKey } from './config';\n\nexport type EngineClass<Key extends GestureKey> = {\n  new (controller: Controller, args: any[], key: Key): Engine<Key>;\n};\n\nexport type Action = {\n  key: GestureKey;\n  engine: EngineClass<GestureKey>;\n  resolver: ResolverMap;\n};\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Vector2, Target, PointerType, NonUndefined } from './utils';\nimport { State } from './state';\n\nexport type GestureKey = Exclude<keyof State, 'shared'>;\nexport type CoordinatesKey = Exclude<GestureKey, 'pinch'>;\n\nexport type GenericOptions = {\n  /**\n   * Lets you specify a dom node or ref you want to attach the gesture to.\n   */\n  target?: Target;\n  /**\n   * Lets you specify which window element the gesture should bind events to\n   * (only relevant for the drag gesture).\n   */\n  window?: EventTarget;\n  /**\n   * Lets you customize if you want events to be passive or captured.\n   */\n  eventOptions?: AddEventListenerOptions;\n  /**\n   * When set to false none of the handlers will be fired.\n   */\n  enabled?: boolean;\n  /**\n   * A function that you can use to transform movement and offset values. Useful\n   * to map your screen coordinates to custom space coordinates such as a\n   * canvas.\n   */\n  transform?: (v: Vector2) => Vector2;\n};\n\nexport type GestureOptions<T extends GestureKey> = GenericOptions & {\n  /**\n   * Whether the gesture is enabled.\n   */\n  enabled?: boolean;\n  /**\n   * Lets you customize if you want events to be passive or captured.\n   */\n  eventOptions?: AddEventListenerOptions;\n  /**\n   * The position `offset` will start from.\n   */\n  from?: Vector2 | ((state: NonUndefined<State[T]>) => Vector2);\n  /**\n   * The handler will fire only when the gesture displacement is greater than\n   * the threshold.\n   */\n  threshold?: number | Vector2;\n  /**\n   * The handler will preventDefault all events when `true`.\n   */\n  preventDefault?: boolean;\n  /**\n   * Forces the handler to fire even for non intentional displacement (ignores\n   * the threshold). In that case, the intentional attribute from state will\n   * remain false until the threshold is reached.\n   */\n  triggerAllEvents?: boolean;\n  /**\n   * The elasticity coefficient of the gesture when going out of bounds. When\n   * set to true, the elasticiy coefficient will be defaulted to 0.15\n   */\n  rubberband?: boolean | number | Vector2;\n  /**\n   * A function that you can use to transform movement and offset values. Useful\n   * to map your screen coordinates to custom space coordinates such as a\n   * canvas.\n   */\n  transform?: (v: Vector2) => Vector2;\n};\n\nexport type Bounds = {\n  top?: number;\n  bottom?: number;\n  left?: number;\n  right?: number;\n};\n\nexport type CoordinatesConfig<Key extends CoordinatesKey = CoordinatesKey> = GestureOptions<Key> & {\n  /**\n   * The handler will only trigger if a movement is detected on the specified\n   * axis.\n   */\n  axis?: 'x' | 'y' | 'lock';\n  /**\n   * Limits the gesture `offset` to the specified bounds.\n   */\n  bounds?: Bounds | ((state: State[Key]) => Bounds);\n  /**\n   * Determines the number of pixels in one direction needed for axises to be\n   * calculated.\n   */\n  axisThreshold?: number;\n};\n\nexport type PinchBounds = { min?: number; max?: number };\nexport type ModifierKey = 'ctrlKey' | 'altKey' | 'metaKey' | null;\n\nexport type PinchConfig = GestureOptions<'pinch'> & {\n  pointer?: {\n    /**\n     * If true, pinch will use touch events on touch-enabled devices.\n     */\n    touch?: boolean;\n  };\n  /**\n   * Limits the scale `offset` to the specified bounds.\n   */\n  scaleBounds?: PinchBounds | ((state: State['pinch']) => PinchBounds);\n  /**\n   * Limits the angle `offset` to the specified bounds.\n   */\n  angleBounds?: PinchBounds | ((state: State['pinch']) => PinchBounds);\n  /**\n   * Scales OR rotates when set to 'lock'.\n   */\n  axis?: 'lock' | undefined;\n  /**\n   * Key that triggers scale when using the wheel. Defaults to `'ctrlKey'`.\n   */\n  modifierKey?: ModifierKey | NonNullable<ModifierKey>[];\n  /**\n   * Whether wheel should trigger a pinch at all.\n   */\n  pinchOnWheel?: boolean;\n};\n\nexport type DragBounds = Bounds | HTMLElement | { current: HTMLElement | null };\n\ntype MoveAndHoverMouseOnly = {\n  /**\n   * If false, onMove or onHover handlers will also fire on touch devices.\n   */\n  mouseOnly?: boolean;\n};\n\nexport type MoveConfig = CoordinatesConfig<'move'> & MoveAndHoverMouseOnly;\n\nexport type HoverConfig = MoveAndHoverMouseOnly;\n\nexport type DragConfig = Omit<CoordinatesConfig<'drag'>, 'axisThreshold' | 'bounds'> & {\n  /**\n   * If true, the component won't trigger your drag logic if the user just clicked on the component.\n   */\n  filterTaps?: boolean;\n  /**\n   * The maximum total displacement a tap can have\n   */\n  tapsThreshold?: number;\n  /**\n   * Set this option to true when using with @react-three/fiber objects.\n   */\n  /**\n   * Limits the gesture `offset` to the specified bounds. Can be a ref or a dom\n   * node.\n   */\n  bounds?: DragBounds | ((state: State['drag']) => DragBounds);\n  pointer?: {\n    /**\n     * The buttons combination that would trigger the drag. Use `-1` to allow\n     * for any button combination to start the drag.\n     */\n    buttons?: number | number[];\n    /**\n     * If true, drag will use touch events on touch-enabled devices.\n     */\n    touch?: boolean;\n    /**\n     * If true, drag will use touch events on touch-enabled devices, and use\n     * mouse events on non touch devices.\n     */\n    mouse?: boolean;\n    /**\n     * If false, will disable KeyboardEvents that would otherwise trigger the\n     * drag gesture when the element is focused. Defaults to true.\n     */\n    keys?: boolean;\n    /**\n     * Doesn't use setPointerCapture when false and delegate drag handling to\n     * window\n     */\n    capture?: boolean;\n    /**\n     * Will perform a pointer lock when drag starts, and exit pointer lock when\n     * drag ends,\n     */\n    lock?: boolean;\n  };\n  swipe?: {\n    /**\n     * The minimum velocity per axis (in pixels / ms) the drag gesture needs to\n     * reach before the pointer is released.\n     */\n    velocity?: number | Vector2;\n    /**\n     * The minimum distance per axis (in pixels) the drag gesture needs to\n     * travel to trigger a swipe. Defaults to 50.\n     */\n    distance?: number | Vector2;\n    /**\n     * The maximum duration in milliseconds that a swipe is detected. Defaults\n     * to 250.\n     */\n    duration?: number;\n  };\n  /**\n   * If set, the drag will be triggered after the duration of the delay (in ms).\n   * When set to true, delay is defaulted to 250ms.\n   */\n  preventScroll?: boolean | number;\n  /**\n   * If set, the drag will allow scrolling in the direction of this axis until\n   * the preventScroll duration has elapsed. Defaults to only 'y'.\n   */\n  preventScrollAxis?: 'x' | 'y' | 'xy';\n  /**\n   * If set, the handler will be delayed for the duration of the delay (in ms)\n   * — or if the user starts moving. When set to true, delay is defaulted\n   * to 180ms.\n   */\n  delay?: boolean | number;\n  /**\n   * Key-number record that determines for each device (`'mouse'`, `'touch'`,\n   * `'pen'`) the number of pixels of drag in one direction needed for axises to\n   * be calculated.\n   */\n  axisThreshold?: Partial<Record<PointerType, number>>;\n  /**\n   * The distance (in pixels) emulated by arrow keys.\n   */\n  keyboardDisplacement?: number;\n};\n\nexport type UserDragConfig = GenericOptions & DragConfig;\nexport type UserPinchConfig = GenericOptions & PinchConfig;\nexport type UserWheelConfig = GenericOptions & CoordinatesConfig<'wheel'>;\nexport type UserScrollConfig = GenericOptions & CoordinatesConfig<'scroll'>;\nexport type UserMoveConfig = GenericOptions & MoveConfig;\nexport type UserHoverConfig = GenericOptions & HoverConfig;\n\nexport type UserGestureConfig = GenericOptions & {\n  drag?: DragConfig;\n  wheel?: CoordinatesConfig<'wheel'>;\n  scroll?: CoordinatesConfig<'scroll'>;\n  move?: MoveConfig;\n  pinch?: PinchConfig;\n  hover?: { enabled?: boolean } & HoverConfig;\n};\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/handlers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DOMHandlers, EventHandler } from './utils';\nimport { FullGestureState, State, EventTypes } from './state';\nimport { GestureKey } from './config';\n\nexport type Handler<Key extends GestureKey, EventType = EventTypes[Key]> = (\n  state: Omit<FullGestureState<Key>, 'event'> & { event: EventType },\n) => any | void;\n\n// if no type is provided in the user generic for a given key\n// then return the default EventTypes that key\ntype check<T extends AnyHandlerEventTypes, Key extends GestureKey> = undefined extends T[Key]\n  ? EventTypes[Key]\n  : T[Key];\n\nexport type UserHandlers<T extends AnyHandlerEventTypes = EventTypes> = {\n  onDrag: Handler<'drag', check<T, 'drag'>>;\n  onDragStart: Handler<'drag', check<T, 'drag'>>;\n  onDragEnd: Handler<'drag', check<T, 'drag'>>;\n  onPinch: Handler<'pinch', check<T, 'pinch'>>;\n  onPinchStart: Handler<'pinch', check<T, 'pinch'>>;\n  onPinchEnd: Handler<'pinch', check<T, 'pinch'>>;\n  onWheel: Handler<'wheel', check<T, 'wheel'>>;\n  onWheelStart: Handler<'wheel', check<T, 'wheel'>>;\n  onWheelEnd: Handler<'wheel', check<T, 'wheel'>>;\n  onMove: Handler<'move', check<T, 'move'>>;\n  onMoveStart: Handler<'move', check<T, 'move'>>;\n  onMoveEnd: Handler<'move', check<T, 'move'>>;\n  onScroll: Handler<'scroll', check<T, 'scroll'>>;\n  onScrollStart: Handler<'scroll', check<T, 'scroll'>>;\n  onScrollEnd: Handler<'scroll', check<T, 'scroll'>>;\n  onHover: Handler<'hover', check<T, 'hover'>>;\n};\n\ntype NativeHandlersKeys = keyof Omit<DOMHandlers, keyof UserHandlers>;\n\ntype GetEventType<Key extends NativeHandlersKeys> = DOMHandlers[Key] extends\n  | EventHandler<infer EventType>\n  | undefined\n  ? EventType\n  : UIEvent;\n\nexport type NativeHandlers<T extends AnyHandlerEventTypes = {}> = {\n  [key in NativeHandlersKeys]?: (\n    state: State['shared'] & {\n      event: undefined extends T[key] ? GetEventType<key> : T[key];\n      args: any;\n    },\n    ...args: any\n  ) => void;\n};\n\n// allows overriding the event type from the returned state in handlers\nexport type AnyHandlerEventTypes = Partial<\n  {\n    drag: any;\n    wheel: any;\n    scroll: any;\n    move: any;\n    pinch: any;\n    hover: any;\n  } & { [key in NativeHandlersKeys]: any }\n>;\n\nexport type GestureHandlers<HandlerType extends AnyHandlerEventTypes = EventTypes> = Partial<\n  NativeHandlers<HandlerType> & UserHandlers<HandlerType>\n>;\n\nexport type InternalHandlers = { [Key in GestureKey]?: Handler<Key, any> };\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './config';\nexport * from './internalConfig';\nexport * from './state';\nexport * from './utils';\nexport * from './handlers';\nexport * from './action';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/internalConfig.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PointerType, Vector2 } from './utils';\nimport { State } from './state';\nimport { GestureKey, CoordinatesKey, ModifierKey } from './config';\n\nexport type InternalGenericOptions = {\n  target?: () => EventTarget;\n  eventOptions: AddEventListenerOptions;\n  window: EventTarget;\n  enabled: boolean;\n  transform?: (v: Vector2) => Vector2;\n};\n\nexport type InternalGestureOptions<Key extends GestureKey = GestureKey> = {\n  enabled: boolean;\n  eventOptions: AddEventListenerOptions;\n  from: Vector2 | ((state: State[Key]) => Vector2);\n  threshold: Vector2;\n  preventDefault: boolean;\n  triggerAllEvents: boolean;\n  rubberband: Vector2;\n  bounds: [Vector2, Vector2] | ((state: State[Key]) => [Vector2, Vector2]);\n  hasCustomTransform: boolean;\n  transform: (v: Vector2) => Vector2;\n};\n\nexport type InternalCoordinatesOptions<Key extends CoordinatesKey = CoordinatesKey> =\n  InternalGestureOptions<Key> & {\n    axis?: 'x' | 'y';\n    lockDirection: boolean;\n    axisThreshold: number;\n  };\n\nexport type InternalDragOptions = Omit<InternalCoordinatesOptions<'drag'>, 'axisThreshold'> & {\n  filterTaps: boolean;\n  tapsThreshold: number;\n  pointerButtons: number | number[];\n  pointerCapture: boolean;\n  preventScrollDelay?: number;\n  preventScrollAxis?: 'x' | 'y' | 'xy';\n  pointerLock: boolean;\n  keys: boolean;\n  device: 'pointer' | 'touch' | 'mouse';\n  swipe: {\n    velocity: Vector2;\n    distance: Vector2;\n    duration: number;\n  };\n  delay: number;\n  axisThreshold: Record<PointerType, number>;\n  keyboardDisplacement: number;\n};\n\nexport type InternalPinchOptions = InternalGestureOptions<'pinch'> & {\n  /**\n   * When device is undefined, we'll be using wheel to zoom.\n   */\n  device: 'gesture' | 'pointer' | 'touch' | undefined;\n  lockDirection: boolean;\n  modifierKey: ModifierKey | NonNullable<ModifierKey>[];\n  pinchOnWheel: boolean;\n};\n\ntype MoveAndHoverMouseOnly = {\n  mouseOnly: boolean;\n};\n\nexport type InternalConfig = {\n  shared: InternalGenericOptions;\n  drag?: InternalDragOptions;\n  wheel?: InternalCoordinatesOptions<'wheel'>;\n  scroll?: InternalCoordinatesOptions<'scroll'>;\n  move?: InternalCoordinatesOptions<'move'> & MoveAndHoverMouseOnly;\n  hover?: InternalCoordinatesOptions<'hover'> & MoveAndHoverMouseOnly;\n  pinch?: InternalPinchOptions;\n};\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NonUndefined, Vector2, WebKitGestureEvent } from './utils';\nimport { GestureKey } from './config';\n\nexport type IngKey = 'dragging' | 'wheeling' | 'moving' | 'hovering' | 'scrolling' | 'pinching';\n\nexport type SharedGestureState = {\n  /**\n   * True if the element is being dragged.\n   */\n  dragging?: boolean;\n  /**\n   * True if the element is being wheeled.\n   */\n  wheeling?: boolean;\n  /**\n   * True if the element is being moved.\n   */\n  moving?: boolean;\n  /**\n   * True if the element is being hovered.\n   */\n  hovering?: boolean;\n  /**\n   * True if the element is being scrolled.\n   */\n  scrolling?: boolean;\n  /**\n   * True if the element is being pinched.\n   */\n  pinching?: boolean;\n  /**\n   * Number of fingers touching the screen.\n   */\n  touches: number;\n  /**\n   * True when the main mouse button or touch is pressed.\n   */\n  pressed: boolean;\n  /**\n   * Alias for pressed.\n   */\n  down: boolean;\n  /**\n   * True if the document is in lock mode.\n   */\n  locked: boolean;\n  /**\n   * Indicates which buttons are pressed (https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons).\n   */\n  buttons: number;\n  /**\n   * True when the Shift key is pressed.\n   */\n  shiftKey: boolean;\n  /**\n   * True when the Alt key is pressed.\n   */\n  altKey: boolean;\n  /**\n   * True when the Meta key is pressed.\n   */\n  metaKey: boolean;\n  /**\n   * True when the Control key is pressed.\n   */\n  ctrlKey: boolean;\n};\n\nexport type CommonGestureState = {\n  _active: boolean;\n  _blocked: boolean;\n  _force: boolean;\n  _step: [false | number, false | number];\n  _movementBound: [false | number, false | number];\n  _values: Vector2;\n  _initial: Vector2;\n  _movement: Vector2;\n  _distance: Vector2;\n  _direction: Vector2;\n  _delta: Vector2;\n  _bounds: [Vector2, Vector2];\n  /**\n   * The event triggering the gesture.\n   */\n  event: UIEvent;\n  /**\n   * The event target.\n   */\n  target: EventTarget;\n  /**\n   * The event current target.\n   */\n  currentTarget: EventTarget;\n  /**\n   * True when the gesture is intentional (passed the threshold).\n   */\n  intentional: boolean;\n  /**\n   * Cumulative distance of the gesture. Deltas are summed with their absolute\n   * values.\n   */\n  distance: Vector2;\n  /**\n   * Displacement of the current gesture.\n   */\n  movement: Vector2;\n  /**\n   * Difference between the current movement and the previous movement.\n   */\n  delta: Vector2;\n  /**\n   * Cumulative displacements of all gestures (sum of all movements triggered\n   * by the handler)\n   */\n  offset: Vector2;\n  /**\n   * Offset when the gesture started.\n   */\n  lastOffset: Vector2;\n  /**\n   * Velocity vector.\n   */\n  velocity: Vector2;\n  /**\n   * Current raw values of the gesture. Can be coordinates or distance / angle\n   * depending on the gesture.\n   */\n  values: Vector2;\n  /**\n   * Raw values when the gesture started.\n   */\n  initial: Vector2;\n  /**\n   * Direction per axis. `-1` when going down, `1` when going up, `0` when still.\n   */\n  direction: Vector2;\n  /**\n   * Bound overflow per axis. `-1` when overflowing bounds to the left/top, `1` when overflowing bounds to the right/bottom.\n   */\n  overflow: Vector2;\n  /**\n   * True when it's the first event of the active gesture.\n   */\n  first: boolean;\n  /**\n   * True when it's the last event of the active gesture.\n   */\n  last: boolean;\n  /**\n   * True when the gesture is active.\n   */\n  active: boolean;\n  /**\n   * The timestamp (ms) of when the gesture started.\n   */\n  startTime: number;\n  /**\n   * The timestamp (ms) of the current event.\n   */\n  timeStamp: number;\n  /**\n   * Elapsed time (ms) of the current gesture.\n   */\n  elapsedTime: number;\n  /**\n   * Time delta (ms) with the previous event.\n   */\n  timeDelta: number;\n  /**\n   * Event type.\n   */\n  type: string;\n  /**\n   * Value returned by your handler on its previous run.\n   */\n  memo?: any;\n  /**\n   * The arguments passed to the bind function (only relevant in React when\n   * using `<div {...bind(someArgument)} />`)\n   */\n  args?: any;\n};\n\nexport type CoordinatesState = CommonGestureState & {\n  /**\n   * The initial axis (x or y) of the gesture.\n   */\n  axis: 'x' | 'y' | undefined;\n  /**\n   * Pointer coordinates (alias to values)\n   */\n  xy: Vector2;\n};\n\nexport type DragState = CoordinatesState & {\n  _pointerId?: number;\n  _pointerActive: boolean;\n  _keyboardActive: boolean;\n  _preventScroll: boolean;\n  _delayed: boolean;\n  /**\n   * True when the drag gesture has been canceled by the `cancel` function.\n   */\n  canceled: boolean;\n  /**\n   * Function that can be called to cancel the drag.\n   */\n  cancel(): void;\n  /**\n   * True if the drag gesture is recognized as a tap (ie when the displacement\n   * is lower than 3px per axis).\n   */\n  tap: boolean;\n  /**\n   * [swipeX, swipeY] is [0, 0] if no swipe detected, -1 or 1 otherwise.\n   */\n  swipe: Vector2;\n};\n\nexport interface PinchState extends CommonGestureState {\n  _pointerEvents: Map<number, PointerEvent>;\n  _touchIds: [] | [number, number];\n  /**\n   * Distance and angle raw values (alias to values).\n   */\n  da: Vector2;\n  /**\n   * The initial axis (scale or angle) of the gesture.\n   */\n  axis: 'scale' | 'angle' | undefined;\n  /**\n   * Coordinates of the center of touch events, or the cursor when using wheel\n   * to pinch.\n   */\n  origin: Vector2;\n  /**\n   * The number of full rotation the current gesture has performed.\n   */\n  turns: number;\n  /**\n   * True when the pinch gesture has been canceled by the `cancel` function.\n   */\n  canceled: boolean;\n  /**\n   * Function that can be called to cancel the pinch.\n   */\n  cancel(): void;\n}\n\nexport type EventTypes = {\n  drag: PointerEvent | TouchEvent | MouseEvent | KeyboardEvent;\n  wheel: WheelEvent;\n  scroll: UIEvent;\n  move: PointerEvent;\n  hover: PointerEvent;\n  pinch: PointerEvent | TouchEvent | WheelEvent | WebKitGestureEvent;\n};\n\nexport interface State {\n  shared: SharedGestureState;\n  drag?: DragState & { event: EventTypes['drag'] };\n  wheel?: CoordinatesState & { event: EventTypes['wheel'] };\n  scroll?: CoordinatesState & { event: EventTypes['scroll'] };\n  move?: CoordinatesState & { event: EventTypes['move'] };\n  hover?: CoordinatesState & { event: EventTypes['hover'] };\n  pinch?: PinchState & { event: EventTypes['pinch'] };\n}\n\nexport type FullGestureState<Key extends GestureKey> = SharedGestureState &\n  NonUndefined<State[Key]>;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type Vector2 = [number, number];\nexport type WebKitGestureEvent = PointerEvent & { scale: number; rotation: number };\nexport type Target = EventTarget | { current: EventTarget | null };\nexport type PointerType = 'mouse' | 'touch' | 'pen';\n\n// replaces NonUndefined from 4.7 and inferior versions\nexport type NonUndefined<T> = T extends undefined ? never : T;\nexport type EventHandler<E extends Event = Event> = (event: E) => void;\n\n// rip off from React types\nexport interface DOMHandlers {\n  // Clipboard Events\n  onCopy?: EventHandler<ClipboardEvent>;\n  onCopyCapture?: EventHandler<ClipboardEvent>;\n  onCut?: EventHandler<ClipboardEvent>;\n  onCutCapture?: EventHandler<ClipboardEvent>;\n  onPaste?: EventHandler<ClipboardEvent>;\n  onPasteCapture?: EventHandler<ClipboardEvent>;\n\n  // Composition Events\n  onCompositionEnd?: EventHandler<CompositionEvent>;\n  onCompositionEndCapture?: EventHandler<CompositionEvent>;\n  onCompositionStart?: EventHandler<CompositionEvent>;\n  onCompositionStartCapture?: EventHandler<CompositionEvent>;\n  onCompositionUpdate?: EventHandler<CompositionEvent>;\n  onCompositionUpdateCapture?: EventHandler<CompositionEvent>;\n\n  // Focus Events\n  onFocus?: EventHandler<FocusEvent>;\n  onFocusCapture?: EventHandler<FocusEvent>;\n  onBlur?: EventHandler<FocusEvent>;\n  onBlurCapture?: EventHandler<FocusEvent>;\n\n  // Form Events\n  onChange?: EventHandler<FormDataEvent>;\n  onChangeCapture?: EventHandler<FormDataEvent>;\n  onBeforeInput?: EventHandler<FormDataEvent>;\n  onBeforeInputCapture?: EventHandler<FormDataEvent>;\n  onInput?: EventHandler<FormDataEvent>;\n  onInputCapture?: EventHandler<FormDataEvent>;\n  onReset?: EventHandler<FormDataEvent>;\n  onResetCapture?: EventHandler<FormDataEvent>;\n  onSubmit?: EventHandler<FormDataEvent>;\n  onSubmitCapture?: EventHandler<FormDataEvent>;\n  onInvalid?: EventHandler<FormDataEvent>;\n  onInvalidCapture?: EventHandler<FormDataEvent>;\n\n  // Image Events\n  onLoad?: EventHandler;\n  onLoadCapture?: EventHandler;\n  onError?: EventHandler; // also a Media Event\n  onErrorCapture?: EventHandler; // also a Media Event\n\n  // Keyboard Events\n  onKeyDown?: EventHandler<KeyboardEvent>;\n  onKeyDownCapture?: EventHandler<KeyboardEvent>;\n  onKeyUp?: EventHandler<KeyboardEvent>;\n  onKeyUpCapture?: EventHandler<KeyboardEvent>;\n\n  // Media Events\n  onAbort?: EventHandler;\n  onAbortCapture?: EventHandler;\n  onCanPlay?: EventHandler;\n  onCanPlayCapture?: EventHandler;\n  onCanPlayThrough?: EventHandler;\n  onCanPlayThroughCapture?: EventHandler;\n  onDurationChange?: EventHandler;\n  onDurationChangeCapture?: EventHandler;\n  onEmptied?: EventHandler;\n  onEmptiedCapture?: EventHandler;\n  onEncrypted?: EventHandler;\n  onEncryptedCapture?: EventHandler;\n  onEnded?: EventHandler;\n  onEndedCapture?: EventHandler;\n  onLoadedData?: EventHandler;\n  onLoadedDataCapture?: EventHandler;\n  onLoadedMetadata?: EventHandler;\n  onLoadedMetadataCapture?: EventHandler;\n  onLoadStart?: EventHandler;\n  onLoadStartCapture?: EventHandler;\n  onPause?: EventHandler;\n  onPauseCapture?: EventHandler;\n  onPlay?: EventHandler;\n  onPlayCapture?: EventHandler;\n  onPlaying?: EventHandler;\n  onPlayingCapture?: EventHandler;\n  onProgress?: EventHandler;\n  onProgressCapture?: EventHandler;\n  onRateChange?: EventHandler;\n  onRateChangeCapture?: EventHandler;\n  onSeeked?: EventHandler;\n  onSeekedCapture?: EventHandler;\n  onSeeking?: EventHandler;\n  onSeekingCapture?: EventHandler;\n  onStalled?: EventHandler;\n  onStalledCapture?: EventHandler;\n  onSuspend?: EventHandler;\n  onSuspendCapture?: EventHandler;\n  onTimeUpdate?: EventHandler;\n  onTimeUpdateCapture?: EventHandler;\n  onVolumeChange?: EventHandler;\n  onVolumeChangeCapture?: EventHandler;\n  onWaiting?: EventHandler;\n  onWaitingCapture?: EventHandler;\n\n  // MouseEvents\n  onAuxClick?: EventHandler<MouseEvent>;\n  onAuxClickCapture?: EventHandler<MouseEvent>;\n  onClick?: EventHandler<MouseEvent>;\n  onClickCapture?: EventHandler<MouseEvent>;\n  onContextMenu?: EventHandler<MouseEvent>;\n  onContextMenuCapture?: EventHandler<MouseEvent>;\n  onDoubleClick?: EventHandler<MouseEvent>;\n  onDoubleClickCapture?: EventHandler<MouseEvent>;\n  onDrag?: EventHandler<DragEvent>;\n  onDragCapture?: EventHandler<DragEvent>;\n  onDragEnd?: EventHandler<DragEvent>;\n  onDragEndCapture?: EventHandler<DragEvent>;\n  onDragEnter?: EventHandler<DragEvent>;\n  onDragEnterCapture?: EventHandler<DragEvent>;\n  onDragExit?: EventHandler<DragEvent>;\n  onDragExitCapture?: EventHandler<DragEvent>;\n  onDragLeave?: EventHandler<DragEvent>;\n  onDragLeaveCapture?: EventHandler<DragEvent>;\n  onDragOver?: EventHandler<DragEvent>;\n  onDragOverCapture?: EventHandler<DragEvent>;\n  onDragStart?: EventHandler<DragEvent>;\n  onDragStartCapture?: EventHandler<DragEvent>;\n  onDrop?: EventHandler<DragEvent>;\n  onDropCapture?: EventHandler<DragEvent>;\n  onMouseDown?: EventHandler<MouseEvent>;\n  onMouseDownCapture?: EventHandler<MouseEvent>;\n  onMouseEnter?: EventHandler<MouseEvent>;\n  onMouseLeave?: EventHandler<MouseEvent>;\n  onMouseMove?: EventHandler<MouseEvent>;\n  onMouseMoveCapture?: EventHandler<MouseEvent>;\n  onMouseOut?: EventHandler<MouseEvent>;\n  onMouseOutCapture?: EventHandler<MouseEvent>;\n  onMouseOver?: EventHandler<MouseEvent>;\n  onMouseOverCapture?: EventHandler<MouseEvent>;\n  onMouseUp?: EventHandler<MouseEvent>;\n  onMouseUpCapture?: EventHandler<MouseEvent>;\n\n  // Selection Events\n  onSelect?: EventHandler;\n  onSelectCapture?: EventHandler;\n\n  // Touch Events\n  onTouchCancel?: EventHandler<TouchEvent>;\n  onTouchCancelCapture?: EventHandler<TouchEvent>;\n  onTouchEnd?: EventHandler<TouchEvent>;\n  onTouchEndCapture?: EventHandler<TouchEvent>;\n  onTouchMove?: EventHandler<TouchEvent>;\n  onTouchMoveCapture?: EventHandler<TouchEvent>;\n  onTouchStart?: EventHandler<TouchEvent>;\n  onTouchStartCapture?: EventHandler<TouchEvent>;\n\n  // Pointer Events\n  onPointerDown?: EventHandler<PointerEvent>;\n  onPointerDownCapture?: EventHandler<PointerEvent>;\n  onPointerMove?: EventHandler<PointerEvent>;\n  onPointerMoveCapture?: EventHandler<PointerEvent>;\n  onPointerUp?: EventHandler<PointerEvent>;\n  onPointerUpCapture?: EventHandler<PointerEvent>;\n  onPointerCancel?: EventHandler<PointerEvent>;\n  onPointerCancelCapture?: EventHandler<PointerEvent>;\n  onPointerEnter?: EventHandler<PointerEvent>;\n  onPointerEnterCapture?: EventHandler<PointerEvent>;\n  onPointerLeave?: EventHandler<PointerEvent>;\n  onPointerLeaveCapture?: EventHandler<PointerEvent>;\n  onPointerOver?: EventHandler<PointerEvent>;\n  onPointerOverCapture?: EventHandler<PointerEvent>;\n  onPointerOut?: EventHandler<PointerEvent>;\n  onPointerOutCapture?: EventHandler<PointerEvent>;\n  onGotPointerCapture?: EventHandler<PointerEvent>;\n  onGotPointerCaptureCapture?: EventHandler<PointerEvent>;\n  onLostPointerCapture?: EventHandler<PointerEvent>;\n  onLostPointerCaptureCapture?: EventHandler<PointerEvent>;\n\n  // UI Events\n  onScroll?: EventHandler<UIEvent>;\n  onScrollCapture?: EventHandler<UIEvent>;\n\n  // Wheel Events\n  onWheel?: EventHandler<WheelEvent>;\n  onWheelCapture?: EventHandler<WheelEvent>;\n\n  // Animation Events\n  onAnimationStart?: EventHandler<AnimationEvent>;\n  onAnimationStartCapture?: EventHandler<AnimationEvent>;\n  onAnimationEnd?: EventHandler<AnimationEvent>;\n  onAnimationEndCapture?: EventHandler<AnimationEvent>;\n  onAnimationIteration?: EventHandler<AnimationEvent>;\n  onAnimationIterationCapture?: EventHandler<AnimationEvent>;\n\n  // Transition Events\n  onTransitionEnd?: EventHandler<TransitionEvent>;\n  onTransitionEndCapture?: EventHandler<TransitionEvent>;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// type exports for core\n\nexport * from './types/index';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/utils/events.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PointerType } from '../types';\nimport { Vector2 } from '../types';\n\nconst EVENT_TYPE_MAP: any = {\n  pointer: { start: 'down', change: 'move', end: 'up' },\n  mouse: { start: 'down', change: 'move', end: 'up' },\n  touch: { start: 'start', change: 'move', end: 'end' },\n  gesture: { start: 'start', change: 'change', end: 'end' },\n};\n\nfunction capitalize(string: string) {\n  if (!string) return '';\n  return string[0].toUpperCase() + string.slice(1);\n}\n\nconst actionsWithoutCaptureSupported = ['enter', 'leave'];\n\nfunction hasCapture(capture = false, actionKey: string) {\n  return capture && !actionsWithoutCaptureSupported.includes(actionKey);\n}\n\nexport function toHandlerProp(device: string, action = '', capture: boolean = false) {\n  const deviceProps = EVENT_TYPE_MAP[device];\n  const actionKey = deviceProps ? deviceProps[action] || action : action;\n  return (\n    'on' +\n    capitalize(device) +\n    capitalize(actionKey) +\n    (hasCapture(capture, actionKey) ? 'Capture' : '')\n  );\n}\n\nconst pointerCaptureEvents = ['gotpointercapture', 'lostpointercapture'];\n\nexport function parseProp(prop: string) {\n  let eventKey = prop.substring(2).toLowerCase();\n  const passive = !!~eventKey.indexOf('passive');\n  if (passive) eventKey = eventKey.replace('passive', '');\n\n  const captureKey = pointerCaptureEvents.includes(eventKey) ? 'capturecapture' : 'capture';\n  // capture = true\n  const capture = !!~eventKey.indexOf(captureKey);\n  // pointermovecapture => pointermove\n  if (capture) eventKey = eventKey.replace('capture', '');\n  return { device: eventKey, capture, passive };\n}\n\nexport function toDomEventType(device: string, action = '') {\n  const deviceProps = EVENT_TYPE_MAP[device];\n  const actionKey = deviceProps ? deviceProps[action] || action : action;\n  return device + actionKey;\n}\n\nexport function isTouch(event: UIEvent) {\n  return 'touches' in event;\n}\n\nexport function getPointerType(event: UIEvent): PointerType {\n  if (isTouch(event)) return 'touch';\n  if ('pointerType' in event) return (event as PointerEvent).pointerType as PointerType;\n  return 'mouse';\n}\n\nfunction getCurrentTargetTouchList(event: TouchEvent) {\n  return Array.from(event.touches).filter(\n    e =>\n      e.target === event.currentTarget ||\n      (event.currentTarget as Node)?.contains?.(e.target as Node),\n  );\n}\n\nfunction getTouchList(event: TouchEvent) {\n  return event.type === 'touchend' || event.type === 'touchcancel'\n    ? event.changedTouches\n    : event.targetTouches;\n}\n\nfunction getValueEvent<EventType extends TouchEvent | PointerEvent>(\n  event: EventType,\n): EventType extends TouchEvent ? Touch : PointerEvent {\n  return (isTouch(event) ? getTouchList(event as TouchEvent)[0] : event) as any;\n}\n\nexport function distanceAngle(P1: Touch | PointerEvent, P2: Touch | PointerEvent) {\n  // add a try catch\n  // attempt to fix https://github.com/pmndrs/use-gesture/issues/551\n  try {\n    const dx = P2.clientX - P1.clientX;\n    const dy = P2.clientY - P1.clientY;\n    const cx = (P2.clientX + P1.clientX) / 2;\n    const cy = (P2.clientY + P1.clientY) / 2;\n\n    const distance = Math.hypot(dx, dy);\n    const angle = -(Math.atan2(dx, dy) * 180) / Math.PI;\n    const origin = [cx, cy] as Vector2;\n    return { angle, distance, origin };\n  } catch {}\n  return null;\n}\n\nexport function touchIds(event: TouchEvent) {\n  return getCurrentTargetTouchList(event).map(touch => touch.identifier);\n}\n\nexport function touchDistanceAngle(event: TouchEvent, ids: number[]) {\n  const [P1, P2] = Array.from(event.touches).filter(touch => ids.includes(touch.identifier));\n  return distanceAngle(P1, P2);\n}\n\nexport function pointerId(event: PointerEvent | TouchEvent) {\n  const valueEvent = getValueEvent(event);\n  return isTouch(event) ? (valueEvent as Touch).identifier : (valueEvent as PointerEvent).pointerId;\n}\n\nexport function pointerValues(event: PointerEvent | TouchEvent): Vector2 {\n  // if ('spaceX' in event) return [event.spaceX, event.spaceY]\n  const valueEvent = getValueEvent(event);\n  return [valueEvent.clientX, valueEvent.clientY];\n}\n\n// wheel delta defaults from https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js\nconst LINE_HEIGHT = 40;\nconst PAGE_HEIGHT = 800;\n\nexport function wheelValues(event: WheelEvent): Vector2 {\n  let { deltaX, deltaY, deltaMode } = event;\n  // normalize wheel values, especially for Firefox\n  if (deltaMode === 1) {\n    deltaX *= LINE_HEIGHT;\n    deltaY *= LINE_HEIGHT;\n  } else if (deltaMode === 2) {\n    deltaX *= PAGE_HEIGHT;\n    deltaY *= PAGE_HEIGHT;\n  }\n  return [deltaX, deltaY];\n}\n\nexport function scrollValues(event: UIEvent): Vector2 {\n  // If the currentTarget is the window then we return the scrollX/Y position.\n  // If not (ie the currentTarget is a DOM element), then we return scrollLeft/Top\n  const { scrollX, scrollY, scrollLeft, scrollTop } = event.currentTarget as Element & Window;\n  return [scrollX ?? scrollLeft ?? 0, scrollY ?? scrollTop ?? 0];\n}\n\nexport function getEventDetails(event: any) {\n  const payload: any = {};\n  if ('buttons' in event) payload.buttons = event.buttons;\n  if ('shiftKey' in event) {\n    const { shiftKey, altKey, metaKey, ctrlKey } = event;\n    Object.assign(payload, { shiftKey, altKey, metaKey, ctrlKey });\n  }\n  return payload;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/utils/fn.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function call<T>(v: T | ((...args: any[]) => T), ...args: any[]): T {\n  if (typeof v === 'function') {\n    // @ts-ignore\n    return v(...args);\n  } else {\n    return v;\n  }\n}\n\nexport function noop() {}\n\nexport function chain(...fns: Function[]): Function {\n  if (fns.length === 0) return noop;\n  if (fns.length === 1) return fns[0];\n\n  return function (this: any) {\n    let result;\n    for (const fn of fns) {\n      result = fn.apply(this, arguments) || result;\n    }\n    return result;\n  };\n}\n\nexport function assignDefault<T extends Object>(value: Partial<T> | undefined, fallback: T): T {\n  return Object.assign({}, fallback, value || {});\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/utils/maths.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Vector2 } from '../types';\n\nexport function clamp(v: number, min: number, max: number) {\n  return Math.max(min, Math.min(v, max));\n}\n\nexport const V = {\n  toVector<T>(v: T | [T, T] | undefined, fallback?: T | [T, T]): [T, T] {\n    if (v === undefined) v = fallback as T | [T, T];\n    return Array.isArray(v) ? v : [v, v];\n  },\n  add(v1: Vector2, v2: Vector2): Vector2 {\n    return [v1[0] + v2[0], v1[1] + v2[1]];\n  },\n  sub(v1: Vector2, v2: Vector2): Vector2 {\n    return [v1[0] - v2[0], v1[1] - v2[1]];\n  },\n  addTo(v1: Vector2, v2: Vector2) {\n    v1[0] += v2[0];\n    v1[1] += v2[1];\n  },\n  subTo(v1: Vector2, v2: Vector2) {\n    v1[0] -= v2[0];\n    v1[1] -= v2[1];\n  },\n};\n\n// Based on @aholachek ;)\n// https://twitter.com/chpwn/status/285540192096497664\n// iOS constant = 0.55\n\n// https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5\n\nfunction rubberband(distance: number, dimension: number, constant: number) {\n  if (dimension === 0 || Math.abs(dimension) === Infinity) return Math.pow(distance, constant * 5);\n  return (distance * dimension * constant) / (dimension + constant * distance);\n}\n\nexport function rubberbandIfOutOfBounds(\n  position: number,\n  min: number,\n  max: number,\n  constant = 0.15,\n) {\n  if (constant === 0) return clamp(position, min, max);\n  if (position < min) return -rubberband(min - position, max - min, constant) + min;\n  if (position > max) return +rubberband(position - max, max - min, constant) + max;\n  return position;\n}\n\nexport function computeRubberband(\n  bounds: [Vector2, Vector2],\n  [Vx, Vy]: Vector2,\n  [Rx, Ry]: Vector2,\n): Vector2 {\n  const [[X0, X1], [Y0, Y1]] = bounds;\n  return [rubberbandIfOutOfBounds(Vx, X0, X1, Rx), rubberbandIfOutOfBounds(Vy, Y0, Y1, Ry)];\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/utils/state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CommonGestureState } from '../types';\n\n// _movement rolls back to when it passed the bounds.\n/**\n * @note code is currently used in WheelEngine and PinchEngine.\n */\nexport function clampStateInternalMovementToBounds(state: CommonGestureState) {\n  const [ox, oy] = state.overflow;\n  const [dx, dy] = state._delta;\n  const [dirx, diry] = state._direction;\n\n  if ((ox < 0 && dx > 0 && dirx < 0) || (ox > 0 && dx < 0 && dirx > 0)) {\n    state._movement[0] = state._movementBound[0] as number;\n  }\n\n  if ((oy < 0 && dy > 0 && diry < 0) || (oy > 0 && dy < 0 && diry > 0)) {\n    state._movement[1] = state._movementBound[1] as number;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/core/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// additional core exports\n\nexport { rubberbandIfOutOfBounds } from './utils/maths';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './vanilla';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/DragGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EventTypes, Handler, UserDragConfig } from '../core/types';\nimport { registerAction, dragAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface DragGestureConstructor {\n  new <EventType = EventTypes['drag']>(\n    target: EventTarget,\n    handler: Handler<'drag', EventType>,\n    config?: UserDragConfig,\n  ): DragGesture;\n}\n\nexport interface DragGesture extends Recognizer<'drag'> {}\n\nexport const DragGesture: DragGestureConstructor = function <EventType = EventTypes['drag']>(\n  target: EventTarget,\n  handler: Handler<'drag', EventType>,\n  config?: UserDragConfig,\n) {\n  registerAction(dragAction);\n  return new Recognizer(target, { drag: handler }, config || {}, 'drag');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/Gesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  AnyHandlerEventTypes,\n  EventTypes,\n  GestureHandlers,\n  UserGestureConfig,\n} from '../core/types';\nimport {\n  dragAction,\n  pinchAction,\n  scrollAction,\n  wheelAction,\n  moveAction,\n  hoverAction,\n} from '../core/actions';\nimport { Recognizer } from './Recognizer';\nimport { createGesture } from './createGesture';\n\ninterface GestureConstructor {\n  new <HandlerTypes extends AnyHandlerEventTypes = EventTypes>(\n    target: EventTarget,\n    handlers: GestureHandlers<HandlerTypes>,\n    config?: UserGestureConfig,\n  ): Gesture;\n}\n\nexport interface Gesture extends Recognizer {}\n\nexport const Gesture: GestureConstructor = function <\n  HandlerTypes extends AnyHandlerEventTypes = EventTypes,\n>(target: EventTarget, handlers: GestureHandlers<HandlerTypes>, config?: UserGestureConfig) {\n  const gestureFunction = createGesture([\n    dragAction,\n    pinchAction,\n    scrollAction,\n    wheelAction,\n    moveAction,\n    hoverAction,\n  ]);\n  return gestureFunction(target, handlers, config || ({} as UserGestureConfig));\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/HoverGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EventTypes, UserHoverConfig, Handler } from '../core/types';\nimport { registerAction, hoverAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface HoverGestureConstructor {\n  new <EventType = EventTypes['hover']>(\n    target: EventTarget,\n    handler: Handler<'hover', EventType>,\n    config?: UserHoverConfig,\n  ): HoverGesture;\n}\n\nexport interface HoverGesture extends Recognizer<'hover'> {}\n\nexport const HoverGesture: HoverGestureConstructor = function <EventType = EventTypes['hover']>(\n  target: EventTarget,\n  handler: Handler<'hover', EventType>,\n  config?: UserHoverConfig,\n) {\n  registerAction(hoverAction);\n  return new Recognizer(target, { hover: handler }, config || {}, 'hover');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/MoveGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { UserMoveConfig, Handler, EventTypes } from '../core/types';\nimport { registerAction, moveAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface MoveGestureConstructor {\n  new <EventType = EventTypes['move']>(\n    target: EventTarget,\n    handler: Handler<'move', EventType>,\n    config?: UserMoveConfig,\n  ): MoveGesture;\n}\n\nexport interface MoveGesture extends Recognizer<'move'> {}\n\nexport const MoveGesture: MoveGestureConstructor = function <EventType = EventTypes['move']>(\n  target: EventTarget,\n  handler: Handler<'move', EventType>,\n  config?: UserMoveConfig,\n) {\n  registerAction(moveAction);\n  return new Recognizer(target, { move: handler }, config || {}, 'move');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/PinchGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { UserPinchConfig, Handler, EventTypes } from '../core/types';\nimport { registerAction, pinchAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface PinchGestureConstructor {\n  new <EventType = EventTypes['pinch']>(\n    target: EventTarget,\n    handler: Handler<'pinch', EventType>,\n    config?: UserPinchConfig,\n  ): PinchGesture;\n}\n\nexport interface PinchGesture extends Recognizer<'pinch'> {}\n\nexport const PinchGesture: PinchGestureConstructor = function <EventType = EventTypes['pinch']>(\n  target: EventTarget,\n  handler: Handler<'pinch', EventType>,\n  config?: UserPinchConfig,\n) {\n  registerAction(pinchAction);\n  return new Recognizer(target, { pinch: handler }, config || {}, 'pinch');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/Recognizer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { GestureKey, InternalHandlers, NativeHandlers, UserGestureConfig } from '../core/types';\nimport { Controller } from '../core';\n\nexport class Recognizer<GK extends GestureKey | undefined = undefined> {\n  private _gestureKey?: GK;\n\n  private _ctrl: Controller;\n\n  private _target: EventTarget;\n\n  constructor(\n    target: EventTarget,\n    handlers: InternalHandlers,\n    config: GK extends keyof UserGestureConfig ? UserGestureConfig[GK] : UserGestureConfig,\n    gestureKey?: GK,\n    nativeHandlers?: NativeHandlers,\n  ) {\n    this._target = target;\n    this._gestureKey = gestureKey;\n    this._ctrl = new Controller(handlers);\n    this._ctrl.applyHandlers(handlers, nativeHandlers);\n    this._ctrl.applyConfig({ ...config, target }, gestureKey);\n\n    this._ctrl.effect();\n  }\n\n  destroy() {\n    this._ctrl.clean();\n  }\n\n  setConfig(\n    config: GK extends keyof UserGestureConfig ? UserGestureConfig[GK] : UserGestureConfig,\n  ) {\n    this._ctrl.clean();\n    this._ctrl.applyConfig({ ...config, target: this._target }, this._gestureKey);\n    this._ctrl.effect();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/ScrollGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { UserScrollConfig, Handler, EventTypes } from '../core/types';\nimport { registerAction, scrollAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface ScrollGestureConstructor {\n  new <EventType = EventTypes['scroll']>(\n    target: EventTarget,\n    handler: Handler<'scroll', EventType>,\n    config?: UserScrollConfig,\n  ): ScrollGesture;\n}\n\nexport interface ScrollGesture extends Recognizer<'scroll'> {}\n\nexport const ScrollGesture: ScrollGestureConstructor = function <EventType = EventTypes['scroll']>(\n  target: EventTarget,\n  handler: Handler<'scroll', EventType>,\n  config?: UserScrollConfig,\n) {\n  registerAction(scrollAction);\n  return new Recognizer(target, { scroll: handler }, config || {}, 'scroll');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/WheelGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { UserWheelConfig, Handler, EventTypes } from '../core/types';\nimport { registerAction, wheelAction } from '../core/actions';\nimport { Recognizer } from './Recognizer';\n\ninterface WheelGestureConstructor {\n  new <EventType = EventTypes['wheel']>(\n    target: EventTarget,\n    handler: Handler<'wheel', EventType>,\n    config?: UserWheelConfig,\n  ): WheelGesture;\n}\n\nexport interface WheelGesture extends Recognizer<'wheel'> {}\n\nexport const WheelGesture: WheelGestureConstructor = function <EventType = EventTypes['wheel']>(\n  target: EventTarget,\n  handler: Handler<'wheel', EventType>,\n  config?: UserWheelConfig,\n) {\n  registerAction(wheelAction);\n  return new Recognizer(target, { wheel: handler }, config || {}, 'wheel');\n} as any;\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/createGesture.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Action, GestureHandlers, UserGestureConfig } from '../core/types';\nimport { registerAction } from '../core/actions';\nimport { parseMergedHandlers } from '../core';\nimport { Recognizer } from './Recognizer';\n\nexport function createGesture(actions: Action[]) {\n  actions.forEach(registerAction);\n\n  return function (target: EventTarget, _handlers: GestureHandlers, _config?: UserGestureConfig) {\n    const { handlers, nativeHandlers, config } = parseMergedHandlers(_handlers, _config || {});\n    return new Recognizer(target, handlers, config, undefined, nativeHandlers);\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/core/utils/use-gesture/vanilla/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { DragGesture } from './DragGesture';\nexport { PinchGesture } from './PinchGesture';\nexport { WheelGesture } from './WheelGesture';\nexport { ScrollGesture } from './ScrollGesture';\nexport { MoveGesture } from './MoveGesture';\nexport { HoverGesture } from './HoverGesture';\nexport { Gesture } from './Gesture';\n\nexport { createGesture } from './createGesture';\n\nexport * from '../core/utils';\nexport * from '../core/types';\nexport * from '../core/actions';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './common';\nexport * from './core';\nexport * from './react';\nexport * from './react-hooks';\nexport * from './services';\nexport * from './plugin';\nexport * from './playground';\nexport * from './playground-config';\nexport * from './playground-contribution';\nexport * from './playground-container';\nexport * from './playground-mock-tools';\nexport { CommandService, CommandRegistry, Command } from '@flowgram.ai/command';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/playground-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import {\n//   PlaygroundConfigEntity,\n//   SelectorBoxConfigEntity,\n//   SnaplineConfigEntity,\n//   EditorStateConfigEntity,\n// } from './core/layer/config';\nimport {\n  // AlignLayer,\n  type EditorState,\n  type LayerRegistry,\n  // SelectorBoxLayer,\n  // SelectorLayer,\n  // SnaplineLayer,\n} from './core/layer';\n// import { SelectableEntity } from './core/entity';\n// import { Selectable } from './core/able/selectable';\n// import { Dragable, Resizable } from './core/able';\nimport { type ConfigEntity, type EntityRegistry } from './common';\n\nexport const PlaygroundConfig = Symbol('PlaygroundConfig');\n\n/**\n * 画布配置\n */\nexport interface PlaygroundConfig {\n  layers?: LayerRegistry[]; // 渲染分层\n  // ables?: AbleRegistry[]; // 能力\n  // entities?: EntityRegistry[]; // 数据实体\n  editorStates?: EditorState[]; // 编辑态切换\n  width?: number; // 画布的初始宽度\n  height?: number; // 画布的初始高度\n  node?: HTMLElement; // 画布的容器节点\n  autoFocus?: boolean; // 自动 focus 及 blur，默认 true\n  autoResize?: boolean; // 监听变化\n  zoomEnable?: boolean; // 是否支持缩放，默认 true\n  contextMenuPath?: string[]; // 右键菜单路径\n  entityConfigs?: Map<EntityRegistry<ConfigEntity>, any>;\n  context?: any; // 注入到 layer 中的上下文数据\n}\n\n/**\n * 默认配置\n */\nexport function createDefaultPlaygroundConfig(): PlaygroundConfig {\n  return {\n    autoFocus: true,\n    autoResize: true,\n    zoomEnable: true,\n    layers: [\n      // PlaygroundLayer, // 基础配置\n      // SelectorLayer, // 节点选择器\n      // SelectorBoxLayer, // 选择框\n      // SnaplineLayer, // 参考线\n      // AlignLayer, // 对齐线\n    ],\n    // ables: [Dragable, Selectable, Resizable],\n    // entities: [\n    //   SelectableEntity,\n    //   SnaplineConfigEntity,\n    //   PlaygroundConfigEntity,\n    //   SelectorBoxConfigEntity,\n    //   EditorStateConfigEntity,\n    // ],\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/playground-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container, ContainerModule, type interfaces } from 'inversify';\nimport { CommandService, CommandContainerModule } from '@flowgram.ai/command';\n\nimport { ContextMenuService } from './services/context-menu-service';\nimport {\n  ClipboardService,\n  DefaultClipboardService,\n  LocalStorageService,\n  SelectionService,\n  StorageService,\n  LoggerService,\n} from './services';\nimport { PluginContext } from './plugin';\nimport { PlaygroundContribution, PlaygroundRegistry } from './playground-contribution';\nimport { createDefaultPlaygroundConfig, PlaygroundConfig } from './playground-config';\nimport { Playground } from './playground';\nimport {\n  type LayerRegistry,\n  LayerOptions,\n  // PipelineDispatcher,\n  PipelineEntitiesSelector,\n  PipelineLayerFactory,\n  PipelineRegistry,\n  PipelineRenderer,\n  PlaygroundConfigEntity,\n  LazyInjectContext,\n} from './core';\nimport {\n  // AbleManager,\n  bindConfigEntity,\n  bindContributionProvider,\n  bindPlaygroundContextProvider,\n  EntityManager,\n  PlaygroundContainerFactory,\n  PlaygroundContext,\n  removeInjectedProperties,\n} from './common';\n\nexport function createPluginContextDefault(container: interfaces.Container): PluginContext {\n  return {\n    container,\n    playground: container.get(Playground),\n    get<T>(identifier: interfaces.ServiceIdentifier): T {\n      return container.get(identifier) as T;\n    },\n    getAll<T>(identifier: interfaces.ServiceIdentifier): T[] {\n      return container.getAll(identifier) as T[];\n    },\n  };\n}\n\nexport function createPlaygroundLayerDefault(\n  container: interfaces.Container,\n  layerRegistry: LayerRegistry,\n  options: any = {},\n) {\n  const layerContainer = container.createChild();\n  layerContainer.bind(layerRegistry).toSelf().inSingletonScope();\n  layerContainer.bind(LayerOptions).toConstantValue(options);\n  const layerInstance = layerContainer.get(layerRegistry);\n  removeInjectedProperties(layerInstance);\n  return layerInstance;\n}\n\nexport const PlaygroundContainerModule = new ContainerModule(bind => {\n  // bind(AbleManager).toSelf().inSingletonScope();\n  bind(EntityManager).toSelf().inSingletonScope();\n  bind(PipelineRenderer).toSelf().inSingletonScope();\n  bind(PlaygroundRegistry).toSelf().inSingletonScope();\n  bind(Playground).toSelf().inSingletonScope();\n  bind(PipelineEntitiesSelector).toSelf().inSingletonScope();\n  bind(PipelineLayerFactory)\n    .toDynamicValue(\n      (context: interfaces.Context) => (layerRegistry: LayerRegistry, options?: any) =>\n        createPlaygroundLayerDefault(context.container, layerRegistry, options),\n    )\n    .inSingletonScope();\n  // bind(PipelineDispatcher).toSelf().inSingletonScope();\n  bind(PipelineRegistry).toSelf().inSingletonScope();\n  bind(PlaygroundContainerFactory)\n    .toDynamicValue(ctx => ctx.container)\n    .inSingletonScope();\n  bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n  bind(PlaygroundContext).toConstantValue({});\n  bindPlaygroundContextProvider(bind);\n\n  bind(LoggerService).toSelf().inSingletonScope();\n\n  bind(ContextMenuService).toSelf().inSingletonScope();\n  bind(SelectionService).toSelf().inSingletonScope();\n\n  // 默认采用 LocalStorageService\n  bind(StorageService).to(LocalStorageService).inSingletonScope();\n  bind(ClipboardService).to(DefaultClipboardService).inSingletonScope();\n  bindConfigEntity(bind, PlaygroundConfigEntity);\n  bindContributionProvider(bind, PlaygroundContribution);\n  bind(PluginContext)\n    .toDynamicValue(ctx => createPluginContextDefault(ctx.container))\n    .inSingletonScope();\n\n  bind(LazyInjectContext).toService(PluginContext);\n});\n\nexport function createPlaygroundContainer(\n  config?: PlaygroundConfig,\n  parent?: interfaces.Container,\n  container?: interfaces.Container,\n): interfaces.Container {\n  const child = container || new Container({ defaultScope: 'Singleton' });\n  if (parent) {\n    child.parent = parent;\n  }\n  child.load(PlaygroundContainerModule);\n  if (!child.isBound(CommandService)) {\n    child.load(CommandContainerModule);\n  }\n  if (config) {\n    child.rebind(PlaygroundConfig).toConstantValue(config);\n    if (config.context) {\n      child.rebind(PlaygroundContext).toConstantValue(config.context);\n    }\n  }\n  return child;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/playground-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport { PlaygroundConfig } from './playground-config';\nimport { type Playground } from './playground';\nimport {\n  PipelineRegistry,\n  type EditorState,\n  EditorStateConfigEntity,\n  type LayerRegistry,\n} from './core';\nimport { EntityManager, type EntityRegistry } from './common';\n\nexport const PlaygroundContribution = Symbol('PlaygroundContribution');\n\nexport interface PlaygroundContribution {\n  /**\n   * 注册 Layer/Entity/Able 相关\n   * @param registry\n   * @deprecated\n   */\n  registerPlayground?(registry: PlaygroundRegistry): void;\n  /**\n   * 初始化画布 (onReady 之前)\n   * @param playground\n   */\n  onInit?(playground: Playground): void;\n  /**\n   * 初始化 entity 完毕后触发\n   * @param playground\n   */\n  onReady?(playground: Playground): void;\n  /**\n   * 销毁\n   * @param playground\n   */\n  onDispose?(playground: Playground): void;\n\n  /**\n   * 所有 Layer 第一次渲染完毕后触发\n   * @param playground\n   */\n  onAllLayersRendered?(playground: Playground): void;\n}\n\n@injectable()\nexport class PlaygroundRegistry {\n  @inject(PipelineRegistry) protected readonly pipeline: PipelineRegistry;\n\n  // @inject(AbleManager) readonly ableManager: AbleManager;\n\n  @inject(EntityManager) readonly entityManager: EntityManager;\n\n  @inject(PlaygroundConfig) readonly playgroundConfig: PlaygroundConfig;\n\n  config(config: Partial<PlaygroundConfig>): void {\n    Object.assign(this.playgroundConfig, config);\n  }\n\n  registerLayer(layerRegistry: LayerRegistry): void {\n    this.pipeline.registerLayer(layerRegistry);\n  }\n\n  registerEntity(entityRegistry: EntityRegistry): void {\n    this.entityManager.registerEntity(entityRegistry);\n  }\n\n  // registerAble(ableRegistry: AbleRegistry): void {\n  //   this.ableManager.registerAble(ableRegistry);\n  // }\n\n  registerEditorState(state: EditorState): void {\n    const stateConfig =\n      this.entityManager.getEntity<EditorStateConfigEntity>(EditorStateConfigEntity);\n    stateConfig?.registerState(state);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/playground-mock-tools.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\n\nimport { createPlaygroundContainer, createPlaygroundLayerDefault } from './playground-container';\nimport { Playground } from './playground';\nimport { LayerRegistry, Layer, PipelineLayerFactory } from './core';\n\n/**\n * 画布测试工具\n *\n * @example\n *\n *  监听 layer 的执行\n *     const layerState = PlaygroundMockTools.createLayerTestState(PlaygroundLayer)\n *     layerState.playground.ready()\n *     expect(layerState.onReady.mock.calls.length).toEqual(1)\n *     expect(layerState.autorun.mock.calls.length).toEqual(1)\n *     layerState.playground.config.updateConfig({\n *       scrollX: 100\n *     })\n *     expect(layerState.onReady.mock.calls.length).toEqual(1)\n *     expect(layerState.autorun.mock.calls.length).toEqual(2)\n */\nexport namespace PlaygroundMockTools {\n  const LayerStateProvider = Symbol('LayerStateProvider');\n  type LayerStateProvider = WeakMap<LayerRegistry, LayerTestState>;\n\n  interface SpyInstance {\n    mock: {\n      calls: any[][];\n      instances: any[];\n      invocationCallOrder: any;\n      results: { type: string; value: any }[];\n      lastCall: any[];\n    };\n  }\n  export class LayerTestState<T extends Layer = Layer> {\n    autorun: SpyInstance;\n\n    render: SpyInstance;\n\n    onReady: SpyInstance;\n\n    onResize: SpyInstance;\n\n    onFocus: SpyInstance;\n\n    onBlur: SpyInstance;\n\n    onZoom: SpyInstance;\n\n    onScroll: SpyInstance;\n\n    onViewportChange: SpyInstance;\n\n    onReadonlyOrDisabledChange: SpyInstance;\n\n    constructor(\n      readonly instance: T,\n      readonly playground: Playground,\n      readonly container: interfaces.Container\n    ) {\n      this.hijackMethod(instance, 'autorun');\n      this.hijackMethod(instance, 'render');\n      this.hijackMethod(instance, 'onReady');\n      this.hijackMethod(instance, 'onResize');\n      this.hijackMethod(instance, 'onFocus');\n      this.hijackMethod(instance, 'onBlur');\n      this.hijackMethod(instance, 'onZoom');\n      this.hijackMethod(instance, 'onScroll');\n      this.hijackMethod(instance, 'onViewportChange');\n      this.hijackMethod(instance, 'onReadonlyOrDisabledChange');\n    }\n\n    private hijackMethod(layer: Layer, layerMethod: keyof Layer & keyof LayerTestState): void {\n      if (typeof layer[layerMethod] === 'function') {\n        // vi should be global\n        // @ts-ignore\n        (this as any)[layerMethod] = vi.spyOn(layer as any, layerMethod);\n      }\n    }\n  }\n\n  export function createContainer(modules?: interfaces.ContainerModule[]): interfaces.Container {\n    const container = createPlaygroundContainer();\n    container.bind(LayerStateProvider).toConstantValue(new WeakMap());\n    container\n      .rebind(PipelineLayerFactory)\n      .toDynamicValue((context) => (layerRegistry: LayerRegistry, options?: any) => {\n        const layerInstance = createPlaygroundLayerDefault(\n          context.container,\n          layerRegistry,\n          options\n        );\n        context.container\n          .get<LayerStateProvider>(LayerStateProvider)\n          .set(\n            layerRegistry,\n            new LayerTestState(layerInstance, container.get<Playground>(Playground), container)\n          );\n        return layerInstance;\n      });\n    if (modules) {\n      modules.forEach((module) => container.load(module));\n    }\n    return container;\n  }\n\n  export function createPlayground(modules?: interfaces.ContainerModule[]): Playground {\n    return createContainer(modules).get(Playground);\n  }\n\n  export function getLayerTestState<T extends Layer = Layer>(\n    container: interfaces.Container,\n    layerRegistry: LayerRegistry<T>\n  ): LayerTestState<T> {\n    return container\n      .get<LayerStateProvider>(LayerStateProvider)\n      .get(layerRegistry) as LayerTestState<T>;\n  }\n\n  /**\n   * 创建layer, 并记录layer的回调数据\n   * @param Layer\n   * @param opts\n   */\n  export function createLayerTestState<T extends Layer = Layer>(\n    layerRegistry: LayerRegistry<T>,\n    opts?: any,\n    modules?: interfaces.ContainerModule[]\n  ): LayerTestState<T> {\n    const container = createContainer(modules);\n    const playground = container.get<Playground>(Playground);\n    playground.registerLayer(layerRegistry, opts);\n    playground.init();\n    playground.ready();\n    return getLayerTestState(container, layerRegistry);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/playground.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { inject, injectable, optional, named } from 'inversify';\nimport { Disposable, DisposableCollection, domUtils, type Event } from '@flowgram.ai/utils';\nimport { CommandService } from '@flowgram.ai/command';\n\nimport { SelectionService } from './services';\nimport { PlaygroundContribution, PlaygroundRegistry } from './playground-contribution';\nimport { PlaygroundConfig } from './playground-config';\nimport {\n  type PipelineDimension,\n  // PipelineDispatcher,\n  PipelineRegistry,\n  PipelineRenderer,\n} from './core/pipeline';\nimport {\n  EditorStateConfigEntity,\n  PlaygroundConfigEntity,\n  type PlaygroundConfigRevealOpts,\n} from './core/layer/config';\nimport { Layer, LayerRegistry } from './core';\nimport {\n  // type AbleDispatchEvent,\n  // AbleManager,\n  type ConfigEntity,\n  EntityManager,\n  type EntityRegistry,\n  PlaygroundContext,\n  ContributionProvider,\n  PlaygroundContextProvider,\n} from './common';\n// import { PlaygroundCommandRegistry, PlaygroundId, toContextMenuPath } from './playground-registries';\n\n@injectable()\nexport class Playground<CONTEXT = PlaygroundContext> implements Disposable {\n  readonly toDispose = new DisposableCollection();\n\n  readonly node: HTMLElement;\n\n  private _focused = false;\n\n  readonly onBlur: Event<void>;\n\n  readonly onFocus: Event<void>;\n\n  readonly onZoom: Event<number>;\n\n  readonly onScroll: Event<{ scrollX: number; scrollY: number }>;\n\n  get onResize() {\n    return this.pipelineRegistry.onResizeEmitter.event;\n  }\n\n  // 唯一 className，适配画布多实例场景\n  private playgroundClassName = nanoid();\n\n  constructor(\n    // @inject(PlaygroundId) readonly id: PlaygroundId,\n    @inject(EntityManager) readonly entityManager: EntityManager,\n    @inject(PlaygroundRegistry) readonly registry: PlaygroundRegistry,\n    @inject(PlaygroundContextProvider)\n    @optional()\n    readonly contextProvider: PlaygroundContextProvider,\n    @inject(PipelineRenderer)\n    readonly pipelineRenderer: PipelineRenderer,\n    // @inject(PlaygroundCommandRegistry) protected readonly commands: PlaygroundCommandRegistry,\n    @inject(PipelineRegistry)\n    readonly pipelineRegistry: PipelineRegistry,\n    // @inject(AbleManager) readonly ableManager: AbleManager,\n    // @inject(PipelineDispatcher) protected readonly dispatcher: PipelineDispatcher,\n    @inject(PlaygroundConfig)\n    protected readonly playgroundConfig: PlaygroundConfig,\n    @inject(ContributionProvider)\n    @named(PlaygroundContribution)\n    @optional()\n    protected readonly contributionProvider: ContributionProvider<PlaygroundContribution>,\n    /**\n     * 用于管理画布命令\n     */\n    @inject(CommandService) readonly commandService: CommandService,\n    /**\n     * 用于管理画布选择\n     */\n    @inject(SelectionService) readonly selectionService: SelectionService\n  ) {\n    this.toDispose.pushAll([\n      this.pipelineRenderer,\n      this.pipelineRegistry,\n      this.entityManager,\n      // this.ableManager,\n      this.commandService,\n      this.selectionService,\n      Disposable.create(() => {\n        this.node.remove();\n      }),\n      pipelineRenderer.onAllLayersRendered(() => {\n        this.contributions.forEach((contrib) => contrib.onAllLayersRendered?.(this));\n      }),\n    ]);\n    // Deafult entities added\n    const editStates =\n      this.entityManager.createEntity<EditorStateConfigEntity>(EditorStateConfigEntity);\n    this.entityManager.createEntity(PlaygroundConfigEntity);\n    this.node = playgroundConfig.node || document.createElement('div');\n    this.config.playgroundDomNode = this.node;\n    this.toDispose.pushAll([\n      // 浏览器原生的 scrollIntoView 会导致页面的滚动\n      // 需要禁用这种操作，否则会引发画布 viewport 计算问题\n      domUtils.addStandardDisposableListener(this.node, 'scroll', (event: UIEvent) => {\n        this.node.scrollTop = 0;\n        this.node.scrollLeft = 0;\n        event.preventDefault();\n        event.stopPropagation();\n      }),\n    ]);\n    this.node.classList.add('gedit-playground');\n    if (process.env.NODE_ENV !== 'test') {\n      this.node.classList.add(this.playgroundClassName);\n    }\n    this.node.dataset.testid = 'sdk.workflow.canvas';\n    if (playgroundConfig.layers)\n      playgroundConfig.layers.forEach((layer) => this.registry.registerLayer(layer));\n    // if (playgroundConfig.ables)\n    //   playgroundConfig.ables.forEach(able => this.ableManager.registerAble(able));\n    // if (playgroundConfig.entities)\n    //   playgroundConfig.entities.forEach(entity => this.entityManager.registerEntity(entity));\n    if (playgroundConfig.editorStates)\n      playgroundConfig.editorStates.forEach((state) => editStates.registerState(state));\n    if (playgroundConfig.zoomEnable !== undefined) this.zoomEnable = playgroundConfig.zoomEnable;\n    if (playgroundConfig.entityConfigs) {\n      for (const [k, v] of playgroundConfig.entityConfigs) {\n        const entity = this.entityManager.getEntity<ConfigEntity>(k, true);\n        entity?.updateConfig(v);\n      }\n    }\n    this.node.addEventListener('blur', () => {\n      this.blur();\n    });\n    this.node.addEventListener('focus', () => {\n      this.focus();\n    });\n    this.node.tabIndex = 0;\n    this.node.appendChild(this.pipelineRenderer.node);\n    this.onBlur = this.pipelineRegistry.onBlurEmitter.event;\n    this.onFocus = this.pipelineRegistry.onFocusEmitter.event;\n    this.onZoom = this.pipelineRegistry.onZoomEmitter.event;\n    this.onScroll = this.pipelineRegistry.onScrollEmitter.event;\n  }\n\n  get context(): CONTEXT {\n    return this.contextProvider?.();\n  }\n\n  protected get contributions(): PlaygroundContribution[] {\n    return this.contributionProvider.getContributions();\n  }\n\n  init(): void {\n    const { contributions } = this;\n    for (const contrib of contributions) {\n      if (contrib.registerPlayground) contrib.registerPlayground(this.registry);\n    }\n    for (const contrib of contributions) {\n      if (contrib.onInit) contrib.onInit(this);\n    }\n  }\n\n  get pipelineNode(): HTMLDivElement {\n    return this.pipelineRenderer.node;\n  }\n\n  setParent(parent: HTMLElement): void {\n    parent.appendChild(this.node);\n    this.resize();\n  }\n\n  // get onDispatch(): Event<AbleDispatchEvent> {\n  //   return this.ableManager.onAbleDispatch;\n  // }\n\n  /**\n   * 对应的右键菜单路径\n   */\n  // get contextMenuPath(): string[] {\n  //   return this.playgroundConfig.contextMenuPath ? this.playgroundConfig.contextMenuPath : toContextMenuPath(this.id);\n  // }\n  get zoomEnable(): boolean {\n    return this.config.zoomEnable;\n  }\n\n  set zoomEnable(zoomEnable) {\n    this.config.zoomEnable = zoomEnable;\n  }\n\n  /**\n   * 转换为内部的命令 id\n   * @param commandId\n   */\n  // toPlaygroundCommandId(commandId: string): string {\n  //   return this.registry.commands.toCommandId(commandId);\n  // }\n  /**\n   * 通知所有关联 able 的 entity\n   */\n  // dispatch<P>(payloadKey: string | symbol, payload: P): string[] {\n  //   return this.ableManager.dispatch(payloadKey, payload);\n  // }\n\n  /**\n   * 刷新所有 layer\n   */\n  flush(): void {\n    this.pipelineRenderer.flush();\n  }\n\n  /**\n   * 执行命令\n   * @param commandId\n   * @param args\n   */\n  // execCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {\n  //   return this.commands.executeCommand<T>(commandId, ...args);\n  // }\n  private isReady = false;\n\n  ready(): void {\n    if (this.isReady) return;\n    this.isReady = true;\n    if (this.playgroundConfig.autoResize) {\n      const resize = () => {\n        if (this.disposed) return;\n        this.resize();\n      };\n      if (typeof ResizeObserver !== 'undefined') {\n        const resizeObserver = new ResizeObserver(resize);\n        resizeObserver.observe(this.node);\n        this.toDispose.push(\n          Disposable.create(() => {\n            resizeObserver.disconnect();\n          })\n        );\n      } else {\n        this.toDispose.push(\n          domUtils.addStandardDisposableListener(window.document.body, 'resize', resize, {\n            passive: true,\n          })\n        );\n      }\n      this.resize();\n    }\n    this.pipelineRegistry.ready();\n    this.pipelineRenderer.ready();\n    const { contributions } = this;\n    for (const contrib of contributions) {\n      if (contrib.onReady) contrib.onReady(this);\n    }\n  }\n\n  /**\n   * 按下边顺序执行\n   * 1. 指定的 entity 位置或 pos 位置\n   * 2. selection 位置\n   * 3. 初始化位置\n   */\n  scrollToView(opts?: PlaygroundConfigRevealOpts): Promise<void> {\n    const playgroundEntity =\n      this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n    return playgroundEntity.scrollToView(opts);\n  }\n\n  /**\n   * 这里会由 widget 透传进来\n   * @param msg\n   */\n  resize(msg?: PipelineDimension, scrollToCenter = true): boolean {\n    if (!msg) {\n      const boundingRect = this.node.getBoundingClientRect();\n      msg = {\n        width: boundingRect.width,\n        height: boundingRect.height,\n      };\n    }\n    // 页面宽度变更 触发滚动偏移\n    const { width, height } = this.config.config;\n    if (msg.width === 0 || msg.height === 0) {\n      return false;\n    }\n    const oldConfig = this.config.config;\n    let { scrollX, scrollY } = this.config.config;\n    // 这个在处理滚动\n    if (scrollToCenter && width && Math.round(msg.width) !== width) {\n      scrollX += (width - msg.width) / 2;\n    }\n    if (scrollToCenter && height && Math.round(msg.height) !== height) {\n      scrollY += (height - msg.height) / 2;\n    }\n    if (\n      Math.round(msg.width) !== width ||\n      Math.round(msg.height) !== height ||\n      oldConfig.scrollX !== scrollX ||\n      oldConfig.scrollY !== scrollY\n    ) {\n      this.config.updateConfig({ ...msg, scrollX, scrollY });\n      this.pipelineRegistry.onResizeEmitter.fire(msg);\n    }\n    return true;\n  }\n\n  /**\n   * 触发 focus\n   */\n  protected focus(): void {\n    if (this._focused) return;\n    this._focused = true;\n    this.pipelineRegistry.onFocusEmitter.fire();\n  }\n\n  /**\n   * 触发 blur\n   */\n  protected blur(): void {\n    if (!this._focused) return;\n    this._focused = false;\n    this.pipelineRegistry.onBlurEmitter.fire();\n  }\n\n  get focused(): boolean {\n    return this._focused;\n  }\n\n  /**\n   * 画布配置数据\n   */\n  get config(): PlaygroundConfigEntity {\n    return this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n  }\n\n  /**\n   * 画布编辑状态管理\n   */\n  get editorState(): EditorStateConfigEntity {\n    return this.entityManager.getEntity<EditorStateConfigEntity>(EditorStateConfigEntity)!;\n  }\n\n  getConfigEntity<T extends ConfigEntity>(r: EntityRegistry<T>): T {\n    return this.entityManager.getEntity<T>(r, true) as T;\n  }\n\n  dispose(): void {\n    if (this.disposed) return;\n    const { contributions } = this;\n    for (const contrib of contributions) {\n      if (contrib.onDispose) contrib.onDispose(this);\n    }\n    this.toDispose.dispose();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  /**\n   * 转换成 react 组件\n   */\n  toReactComponent(): React.FC {\n    return this.pipelineRenderer.toReactComponent();\n  }\n\n  /**\n   * 注册 layer\n   */\n  registerLayer<P extends Layer = Layer>(\n    layerRegistry: LayerRegistry<P>,\n    layerOptions?: P['options']\n  ): void {\n    this.pipelineRegistry.registerLayer<P>(layerRegistry, layerOptions);\n  }\n\n  /**\n   * 注册 多个 layer\n   */\n  registerLayers(...layerRegistries: LayerRegistry[]): void {\n    layerRegistries.forEach((layer) => this.pipelineRegistry.registerLayer(layer));\n  }\n\n  /**\n   * 获取 layer\n   */\n  getLayer<T extends Layer>(layerRegistry: LayerRegistry<T>): T | undefined {\n    return this.pipelineRegistry.getLayer<T>(layerRegistry);\n  }\n\n  get onAllLayersRendered(): Event<void> {\n    return this.pipelineRenderer.onAllLayersRendered;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './plugin';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/plugin/plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule, interfaces } from 'inversify';\n\nimport { PlaygroundContribution } from '../playground-contribution';\nimport { type Playground } from '../playground';\n\nexport interface PluginContext {\n  /**\n   * 画布实例\n   */\n  playground: Playground;\n  /**\n   * IOC 容器\n   */\n  container: interfaces.Container;\n  /**\n   * 获取 IOC 容器的 单例模块\n   * @param identifier\n   */\n  get<T>(identifier: interfaces.ServiceIdentifier<T>): T;\n\n  /**\n   * 获取 IOC 容器的 多例模块\n   */\n  getAll<T>(identifier: interfaces.ServiceIdentifier<T>): T[];\n}\n\nexport const PluginContext = Symbol('PluginContext');\nexport interface PluginBindConfig {\n  bind: interfaces.Bind;\n  unbind: interfaces.Unbind;\n  isBound: interfaces.IsBound;\n  rebind: interfaces.Rebind;\n}\n\nexport interface PluginConfig<Opts, CTX extends PluginContext = PluginContext> {\n  /**\n   * 插件 IOC 注册, 等价于 containerModule\n   * @param ctx\n   */\n  onBind?: (bindConfig: PluginBindConfig, opts: Opts) => void;\n  /**\n   * 画布注册阶段\n   */\n  onInit?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布准备阶段，一般用于 dom 事件注册等\n   */\n  onReady?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布销毁阶段\n   */\n  onDispose?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * 画布所有 layer 渲染结束\n   */\n  onAllLayersRendered?: (ctx: CTX, opts: Opts) => void;\n  /**\n   * IOC 模块，用于更底层的插件扩展\n   */\n  containerModules?: interfaces.ContainerModule[];\n}\n\nexport const Plugin = Symbol('Plugin');\n\nexport type Plugin<Options = any> = {\n  options: Options;\n  pluginId: string;\n  singleton: boolean;\n  initPlugin: () => void;\n  contributionKeys?: interfaces.ServiceIdentifier[];\n  containerModules?: interfaces.ContainerModule[];\n};\n\nexport interface PluginsProvider<CTX extends PluginContext = PluginContext> {\n  (ctx: CTX): Plugin[];\n}\n\nexport type PluginCreator<Options> = (opts: Options) => Plugin<Options>;\n\nexport function loadPlugins(plugins: Plugin[], container: interfaces.Container): void {\n  const pluginInitSet = new Set<string>();\n  const singletonPluginIds = new Set<string>();\n  const modules: interfaces.ContainerModule[] = plugins\n    .reduceRight((result: Plugin[], plugin: Plugin) => {\n      const shouldSkip = plugin.singleton && singletonPluginIds.has(plugin.pluginId);\n      if (plugin.singleton) {\n        singletonPluginIds.add(plugin.pluginId);\n      }\n      return shouldSkip ? result : [plugin, ...result];\n    }, [])\n    .reduce((res, plugin) => {\n      if (!pluginInitSet.has(plugin.pluginId)) {\n        plugin.initPlugin();\n        pluginInitSet.add(plugin.pluginId);\n      }\n      if (plugin.containerModules && plugin.containerModules.length > 0) {\n        for (let module of plugin.containerModules) {\n          // 去重\n          if (!res.includes(module)) {\n            res.push(module);\n          }\n        }\n        return res;\n      }\n      return res;\n    }, [] as interfaces.ContainerModule[]);\n  modules.forEach((module) => container.load(module));\n  plugins.forEach((plugin) => {\n    if (plugin.contributionKeys) {\n      for (const contribution of plugin.contributionKeys) {\n        container.bind(contribution).toConstantValue(plugin.options);\n      }\n    }\n  });\n}\n\nfunction toPlaygroundContainerModule<Options, CTX extends PluginContext = PluginContext>(\n  config: {\n    onInit?: (ctx: CTX, opts: Options) => void;\n    onDispose?: (ctx: CTX, opts: Options) => void;\n    onReady?: (ctx: CTX, opts: Options) => void;\n    onAllLayersRendered?: (ctx: CTX, opts: Options) => void;\n  },\n  opts: Options\n): interfaces.ContainerModule {\n  return new ContainerModule((bind) => {\n    bind(PlaygroundContribution).toDynamicValue((ctx) => {\n      const pluginContext = ctx.container.get<CTX>(PluginContext);\n      return {\n        onInit: () => {\n          config.onInit?.(pluginContext, opts);\n        },\n        onReady: () => {\n          config.onReady?.(pluginContext, opts);\n        },\n        onDispose: () => {\n          config.onDispose?.(pluginContext, opts);\n        },\n        onAllLayersRendered: () => {\n          config.onAllLayersRendered?.(pluginContext, opts);\n        },\n      };\n    });\n  });\n}\n\nlet pluginIndex = 0;\nexport function definePluginCreator<Options, CTX extends PluginContext = PluginContext>(\n  config: {\n    containerModules?: interfaces.ContainerModule[];\n    contributionKeys?: interfaces.ServiceIdentifier[];\n    singleton?: boolean;\n  } & PluginConfig<Options, CTX>\n): PluginCreator<Options> {\n  const { contributionKeys, singleton = false } = config;\n  pluginIndex += 1;\n  const pluginId = `Playground_${pluginIndex}`;\n  return (opts: Options) => {\n    const containerModules: interfaces.ContainerModule[] = [];\n    let isInit = false;\n\n    return {\n      pluginId,\n      singleton,\n      initPlugin: () => {\n        // 防止 plugin 被上层业务多次 init\n        if (isInit) {\n          return;\n        }\n        isInit = true;\n\n        if (config.containerModules) {\n          containerModules.push(...config.containerModules);\n        }\n        if (config.onBind) {\n          containerModules.push(\n            new ContainerModule((bind, unbind, isBound, rebind) => {\n              config.onBind!(\n                {\n                  bind,\n                  unbind,\n                  isBound,\n                  rebind,\n                },\n                opts\n              );\n            })\n          );\n        }\n        if (config.onInit || config.onDispose || config.onReady || config.onAllLayersRendered) {\n          containerModules.push(toPlaygroundContainerModule<Options, CTX>(config, opts));\n        }\n      },\n      options: opts,\n      contributionKeys,\n      containerModules,\n    };\n  };\n}\n\n/**\n * @example\n * createPlaygroundPlugin({\n *    // IOC 注册\n *    onBind(bind) {\n *      bind('xxx').toSelf().inSingletonScope()\n *    },\n *    // 画布初始化\n *    onInit(ctx) {\n *      ctx.playground.registerLayer(MyLayer)\n *    },\n *    // 画布销毁\n *    onDispose(ctx) {\n *    },\n *    // IOC 模块\n *    containerModules: [new ContainerModule(() => {})]\n * })\n */\nexport const createPlaygroundPlugin = <CTX extends PluginContext = PluginContext>(\n  options: PluginConfig<undefined, CTX>\n) => definePluginCreator<undefined, CTX>(options)(undefined);\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './playground-react-context';\nexport * from './playground-react-provider';\nexport * from './playground-react-renderer';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react/playground-react-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const PlaygroundReactContext = React.createContext<any>({});\n\nexport const PlaygroundReactContainerContext = React.createContext<any>({});\n\nexport const PlaygroundReactRefContext = React.createContext<any>({});\n\n/**\n * 当前 entity\n */\nexport const PlaygroundEntityContext = React.createContext<any>(undefined);\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react/playground-react-provider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useImperativeHandle, forwardRef, useRef, useEffect } from 'react';\n\nimport { type interfaces } from 'inversify';\n\nimport { PluginContext, loadPlugins, type PluginsProvider } from '../plugin';\nimport { createPlaygroundContainer } from '../playground-container';\nimport { Playground } from '../playground';\nimport { PlaygroundContext } from '../common';\nimport {\n  PlaygroundReactContainerContext,\n  PlaygroundReactContext,\n  PlaygroundReactRefContext,\n} from './playground-react-context';\n\nexport interface PlaygroundReactProviderProps {\n  containerModules?: interfaces.ContainerModule[]; // 注入的 IOC 包\n  parentContainer?: interfaces.Container;\n  playgroundContainer?: interfaces.Container; // 由外部加载 playgroundContainer, 和 container 不能共存\n  playgroundContext?: any; // 注入到画布中的 context, 所有的 layer 都能拿到\n  autoFocus?: boolean; // 是否会自动触发画布 focus，默认 true\n  autoResize?: boolean; // 是否允许根据浏览器自动 resize, 默认 true\n  zoomEnable?: boolean; // 是否允许缩放，默认 true\n  plugins?: PluginsProvider<any>;\n  customPluginContext?: (container: interfaces.Container) => PluginContext; // 自定义插件的上下文\n  children?: React.ReactNode;\n}\n\n/**\n * Playground react 组件\n * @param props\n */\nexport const PlaygroundReactProvider = forwardRef<\n  PlaygroundContext | any,\n  PlaygroundReactProviderProps\n>(function PlaygroundReactProvider(props, ref) {\n  const {\n    containerModules,\n    playgroundContext,\n    parentContainer: fromContainer,\n    playgroundContainer,\n    plugins,\n    customPluginContext,\n    ...others\n  } = props;\n\n  /**\n   * 创建 IOC 包\n   */\n  const container = useMemo(() => {\n    let flowContainer: interfaces.Container;\n    // 忽略所有 containerModules, 由外部加载 container,\n    if (playgroundContainer) {\n      flowContainer = playgroundContainer;\n    } else {\n      flowContainer = createPlaygroundContainer(\n        {\n          autoFocus: true,\n          autoResize: true,\n          zoomEnable: true,\n          ...others,\n        },\n        fromContainer\n      );\n      if (playgroundContext) {\n        flowContainer.rebind(PlaygroundContext).toConstantValue(playgroundContext);\n      }\n      if (containerModules) {\n        containerModules.forEach((module) => flowContainer.load(module));\n      }\n    }\n    return flowContainer;\n    // @action 这里 props 数据如果更改不会触发刷新，不允许修改\n  }, []);\n\n  const playground = useMemo(() => {\n    const playground = container.get(Playground);\n    let ctx: PluginContext;\n    if (customPluginContext) {\n      ctx = customPluginContext(container);\n      container.rebind(PluginContext).toConstantValue(ctx);\n    } else {\n      ctx = container.get<PluginContext>(PluginContext);\n    }\n    if (plugins) {\n      loadPlugins(plugins(ctx), container);\n    }\n    playground.init();\n    return playground;\n  }, []);\n\n  const effectSignalRef = useRef<number>(0);\n\n  useEffect(() => {\n    effectSignalRef.current += 1;\n    return () => {\n      // 开发环境下延迟处理 dispose，防止 React>=18 useEffect 初始化卸载（在生产构建时，这个条件分支会被完全移除）\n      if (process.env.NODE_ENV === 'development') {\n        const FRAME = 16;\n        setTimeout(() => {\n          effectSignalRef.current -= 1;\n          if (effectSignalRef.current === 0) {\n            playground.dispose();\n          }\n        }, FRAME);\n        return;\n      }\n      playground.dispose();\n    };\n  }, []);\n\n  useImperativeHandle(ref, () => container.get<PluginContext>(PluginContext), []);\n\n  return (\n    <PlaygroundReactContainerContext.Provider value={container}>\n      <PlaygroundReactRefContext.Provider value={playground}>\n        <PlaygroundReactContext.Provider value={playgroundContext}>\n          {props.children}\n        </PlaygroundReactContext.Provider>\n      </PlaygroundReactRefContext.Provider>\n    </PlaygroundReactContainerContext.Provider>\n  );\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react/playground-react-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React, { useEffect, useRef } from 'react';\n\nimport { usePlayground } from '../react-hooks/use-playground';\nimport { useService } from '../react-hooks';\nimport { PlaygroundConfig } from '../playground-config';\n\nexport interface PlaygroundReactRendererProps {\n  /**\n   * 这个会放到 playground node 下边\n   */\n  children?: React.ReactNode;\n  className?: string;\n  style?: React.CSSProperties;\n}\n\nexport const PlaygroundReactRenderer: React.FC<PlaygroundReactRendererProps> = (props) => {\n  const playground = usePlayground();\n  const playgroundConfig = useService<PlaygroundConfig>(PlaygroundConfig);\n  const ref = useRef<any>();\n  /**\n   * 初始化画布\n   */\n  useEffect(() => {\n    playground.setParent(ref.current);\n    playground.ready();\n    if (playgroundConfig.autoFocus) {\n      playground.node.focus();\n    }\n  }, []);\n  const PlaygroundComp = playground.toReactComponent();\n  return (\n    <>\n      <div\n        ref={ref}\n        className={`gedit-playground-container${props.className ? ` ${props.className}` : ''}`}\n        style={props.style}\n      />\n      <PlaygroundComp />\n      {props.children ? ReactDOM.createPortal(<>{props.children}</>, playground.node) : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './use-entities';\nexport * from './use-entity-data-from-context';\nexport * from './use-entity-from-context';\nexport * from './use-listen-events';\nexport * from './use-playground';\nexport * from './use-playground-container';\nexport * from './use-playground-context';\nexport * from './use-service';\nexport * from './use-refresh';\nexport * from './use-config-entity';\nexport * from './use-playground-drag';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-config-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { ConfigEntity, EntityManager, EntityRegistry } from '../common';\nimport { useRefresh } from './use-refresh';\nimport { usePlaygroundContainer } from './use-playground-container';\n\n/**\n * 获取 config entity\n */\nexport function useConfigEntity<T extends ConfigEntity>(\n  entityRegistry: EntityRegistry<T>,\n  listenChange = false,\n): T {\n  const entityManager = usePlaygroundContainer().get(EntityManager);\n  const entity = entityManager.getEntity<T>(entityRegistry, true) as T;\n  const refresh = useRefresh(entity.version);\n  useLayoutEffect(() => {\n    const dispose = listenChange\n      ? entity.onEntityChange(() => {\n          refresh(entity.version);\n        })\n      : Disposable.NULL;\n    return () => dispose.dispose();\n  }, [entityManager, refresh, entity, listenChange]);\n  return entity;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-entities.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { Entity, EntityManager, EntityRegistry } from '../common';\nimport { useRefresh } from './use-refresh';\nimport { usePlaygroundContainer } from './use-playground-container';\n\n/**\n * 获取 entities 并监听变化\n * @deprecated\n */\nexport function useEntities<T extends Entity>(entityRegistry: EntityRegistry): T[] {\n  const entityManager = usePlaygroundContainer().get(EntityManager);\n  const refresh = useRefresh();\n  useLayoutEffect(() => {\n    const dispose = entityManager.onEntityChange(entityKey => {\n      if (entityKey === entityRegistry.type) {\n        refresh();\n      }\n    });\n    return () => dispose.dispose();\n  }, [entityManager, refresh]);\n  return entityManager.getEntities<T>(entityRegistry);\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-entity-data-from-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { EntityData } from '../common';\nimport { EntityDataRegistry, EntityManager } from '../';\nimport { useRefresh } from './use-refresh';\n// 循环引用会导致单测访问不到 usePlaygroundContainer，这里改正路径。\nimport { usePlaygroundContainer } from './use-playground-container';\nimport { useEntityFromContext } from './use-entity-from-context';\n\n/**\n * 从上下 PlaygroundEntityContext 获取 entity data 并监听变化 (默认不监听)\n *\n * */\nexport function useEntityDataFromContext<T extends EntityData>(\n  dataRegistry: EntityDataRegistry<any>,\n  listenChange = false,\n): T {\n  const entityManager = usePlaygroundContainer().get(EntityManager);\n  const entityData = useEntityFromContext().getData<T>(dataRegistry)!;\n  if (!entityData) {\n    throw new Error(\n      `[useEntityDataFromContext] Unknown entity Data ${dataRegistry.name} from \"PlaygroundEntityContext\".`,\n    );\n  }\n  const refresh = useRefresh(entityData.version);\n  useLayoutEffect(() => {\n    const dispose = entityData.onDataChange(() => {\n      if (listenChange) refresh(entityData.version);\n    });\n    return () => dispose.dispose();\n  }, [entityManager, refresh, entityData, listenChange]);\n  return entityData;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-entity-from-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext, useLayoutEffect } from 'react';\n\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { PlaygroundEntityContext } from '../react';\nimport { Entity, EntityManager } from '../common';\nimport { useRefresh } from './use-refresh';\nimport { usePlaygroundContainer } from './use-playground-container';\n\n/**\n * 从上下 PlaygroundEntityContext 获取 entity 并监听变化(默认不监听)\n */\nexport function useEntityFromContext<T extends Entity>(listenChange = false): T {\n  const entityManager = usePlaygroundContainer().get(EntityManager);\n  const entity: T = useContext<T>(PlaygroundEntityContext);\n  if (!entity) {\n    throw new Error('[useEntityFromContext] Unknown entity from \"PlaygroundEntityContext\"');\n  }\n  const refresh = useRefresh(entity.version);\n  useLayoutEffect(() => {\n    let dispose: Disposable | undefined;\n    if (listenChange) {\n      dispose = entity.onEntityChange(() => refresh(entity.version));\n    }\n    return () => dispose?.dispose();\n  }, [entityManager, refresh, entity, listenChange]);\n  return entity;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-listen-events.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { DisposableCollection, type Event } from '@flowgram.ai/utils';\n\nimport { useRefresh } from './use-refresh';\n\n/**\n * 监听 event 事件变化\n */\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function useListenEvents(...events: Event<any>[]): void {\n  const refresh = useRefresh();\n  useLayoutEffect(() => {\n    const collection = new DisposableCollection();\n    collection.pushAll(events.map(e => e(() => refresh())));\n    return () => collection.dispose();\n  }, [events, refresh]);\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-playground-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { type interfaces } from 'inversify';\n\nimport { PlaygroundReactContainerContext } from '../react/playground-react-context';\n\n/**\n * 获取 playground inversify container\n */\nexport function usePlaygroundContainer(): interfaces.Container {\n  return React.useContext(PlaygroundReactContainerContext);\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-playground-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { PlaygroundReactContext } from '../react/playground-react-context';\n\n/**\n * 获取 playground context 数据\n */\nexport function usePlaygroundContext<T>(): T {\n  return React.useContext(PlaygroundReactContext);\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-playground-drag.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { PlaygroundDragOptions, PlaygroundDrag } from '../core';\nimport { usePlayground } from './use-playground';\n\ninterface UsePlaygroundDragReturn {\n  start<T = undefined>(\n    e: { clientX: number; clientY: number },\n    opts: PlaygroundDragOptions<T> & { context?: T },\n  ): Disposable;\n}\n\nexport function usePlaygroundDrag(): UsePlaygroundDragReturn {\n  const playground = usePlayground();\n  return useMemo(\n    () => ({\n      start<T>(\n        e: { clientX: number; clientY: number },\n        opts: PlaygroundDragOptions<T> & { context?: T },\n      ): Disposable {\n        return PlaygroundDrag.startDrag(e.clientX, e.clientY, {\n          ...opts,\n          config: playground.config,\n        });\n      },\n    }),\n    [],\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-playground.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { PlaygroundReactRefContext } from '../react/playground-react-context';\nimport { Playground } from '../playground';\n\n/**\n * 获取 playground\n */\nexport function usePlayground(): Playground {\n  return React.useContext(PlaygroundReactRefContext);\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-refresh.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useRefresh } from '@flowgram.ai/utils';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/react-hooks/use-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type interfaces } from 'inversify';\n\nimport { usePlaygroundContainer } from './use-playground-container';\n\n/**\n * 获取画布的 IOC 模块\n * @param identifier\n */\nexport function useService<T>(identifier: interfaces.ServiceIdentifier<T>): T {\n  const container = usePlaygroundContainer();\n  return container.get?.(identifier) as T;\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/clipboard-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Emitter, type Event, type MaybePromise } from '@flowgram.ai/utils';\n\nexport const ClipboardService = Symbol('ClipboardService');\n\nexport interface ClipboardService {\n  onClipboardChanged: Event<string>;\n  readText(): MaybePromise<string>;\n\n  writeText(value: string): MaybePromise<void>;\n}\n\n/**\n * 剪贴板服务，一般用于管理临时的复制黏贴数据\n * TODO: 后续可以支持调用浏览器\n */\n@injectable()\nexport class DefaultClipboardService implements ClipboardService {\n  private _currentData: string;\n\n  protected readonly onClipboardChangedEmitter = new Emitter<string>();\n\n  readonly onClipboardChanged: Event<string> = this.onClipboardChangedEmitter.event;\n\n  readText(): string {\n    return this._currentData;\n  }\n\n  writeText(value: string): void {\n    if (this._currentData !== value) {\n      this._currentData = value;\n      this.onClipboardChangedEmitter.fire(value);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/context-menu-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\n/**\n * 圈选右键菜单相关 service\n */\n@injectable()\nexport class ContextMenuService {\n  /**\n   * 右键面板是否展示，展示的时候为 true\n   */\n  private isRightPanelVisible: boolean;\n\n  get rightPanelVisible(): boolean {\n    return this.isRightPanelVisible;\n  }\n\n  set rightPanelVisible(visible: boolean) {\n    this.isRightPanelVisible = visible;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './selection-service';\nexport * from './storage-service';\nexport * from './clipboard-service';\nexport * from './context-menu-service';\nexport * from './logger-service';\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/logger-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { type Disposable, Emitter, type Event } from '@flowgram.ai/utils';\n\nexport interface LoggerProps {\n  event: LoggerEvent;\n  props?: Record<string, any>;\n}\n\nexport enum LoggerEvent {\n  CANVAS_TTI, // Time To Interactive，画布可交互时间\n  CANVAS_FPS, // Frame Per Second，画布渲染帧率\n}\n\n/**\n * 画布全局的选择器，可以放任何东西\n */\n@injectable()\nexport class LoggerService implements Disposable {\n  protected readonly onLoggerEmitter = new Emitter<LoggerProps>();\n\n  // plugin 内注册：loggerService.onLogger(() => {})\n  readonly onLogger: Event<any> = this.onLoggerEmitter.event;\n\n  onAllLayersRendered() {\n    this.onLoggerEmitter.fire({\n      event: LoggerEvent.CANVAS_TTI,\n    });\n  }\n\n  onFlushRequest(renderFrameInterval: number) {\n    if (renderFrameInterval <= 0) {\n      return;\n    }\n    const fps = 1000 / renderFrameInterval;\n    this.onLoggerEmitter.fire({\n      event: LoggerEvent.CANVAS_FPS,\n      props: { rfi: renderFrameInterval, fps },\n    });\n  }\n\n  dispose() {\n    this.onLoggerEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/selection-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Compare, type Disposable, Emitter, type Event } from '@flowgram.ai/utils';\n\nimport { type Entity } from '../common';\n\n/**\n * 画布全局的选择器，可以放任何东西\n */\n@injectable()\nexport class SelectionService implements Disposable {\n  protected readonly onSelectionChangedEmitter = new Emitter<Entity[]>();\n\n  readonly onSelectionChanged: Event<any> = this.onSelectionChangedEmitter.event;\n\n  private currentSelection: Entity[] = [];\n\n  private disposers: Disposable[] = [];\n\n  get selection(): Entity[] {\n    return this.currentSelection;\n  }\n\n  isEmpty(): boolean {\n    return this.currentSelection.length === 0;\n  }\n\n  set selection(selection: Entity<any>[]) {\n    if (!Compare.isArrayShallowChanged(this.currentSelection, selection)) {\n      return;\n    }\n    this.disposers.forEach((disposer) => disposer.dispose());\n    this.changeSelection(selection);\n    this.disposers = this.currentSelection.map((selection) =>\n      selection.onDispose(() => {\n        const newSelection = this.currentSelection.filter((n) => n !== selection);\n        this.changeSelection(newSelection);\n      })\n    );\n  }\n\n  private changeSelection(selection: Entity<any>[]) {\n    this.currentSelection = selection;\n    this.onSelectionChangedEmitter.fire(this.currentSelection);\n  }\n\n  dispose() {\n    this.onSelectionChangedEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/src/services/storage-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, postConstruct } from 'inversify';\n\nexport const StorageService = Symbol('StorageService');\n\n/**\n * 存储数据到缓存\n */\nexport interface StorageService {\n  /**\n   * Stores the given data under the given key.\n   */\n  setData<T>(key: string, data: T): void;\n\n  /**\n   * Returns the data stored for the given key or the provided default value if nothing is stored for the given key.\n   */\n  getData<T>(key: string, defaultValue: T): T;\n\n  getData<T>(key: string): T | undefined;\n}\n\ninterface LocalStorage {\n  [key: string]: any;\n}\n\n@injectable()\nexport class LocalStorageService implements StorageService {\n  private storage: LocalStorage;\n\n  private _prefix = '__gedit:';\n\n  setData<T>(key: string, data: T): void {\n    this.storage[this.prefix(key)] = JSON.stringify(data);\n  }\n\n  getData<T>(key: string, defaultValue?: T): T {\n    const result = this.storage[this.prefix(key)];\n    if (result === undefined) {\n      return defaultValue as any;\n    }\n    return JSON.parse(result);\n  }\n\n  prefix(key: string) {\n    return `${this._prefix}${key}`;\n  }\n\n  setPrefix(prefix: string) {\n    this._prefix = prefix;\n  }\n\n  @postConstruct()\n  protected init(): void {\n    if (typeof window !== 'undefined' && window.localStorage) {\n      this.storage = window.localStorage;\n    } else {\n      this.storage = {};\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\", \"node\"],\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/canvas-engine/core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__/**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n    coverage: {\n      exclude: ['**/use-gesture/**']\n    }\n  },\n});\n"
  },
  {
    "path": "packages/canvas-engine/core/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/__snapshots__/flow-document.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-document > add split node 1`] = `\n\"root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|-------- noop_0\n|------ new_split2\n|-------- $blockIcon$new_split2\n|-------- $inlineBlocks$new_split2\n|---------- nb1\n|------------ $blockOrderIcon$nb1\n|---------- nb2\n|------------ $blockOrderIcon$nb2\n|-- new_split\n|---- $blockIcon$new_split\n|---- $inlineBlocks$new_split\n|------ b1\n|-------- $blockOrderIcon$b1\n|------ b2\n|-------- $blockOrderIcon$b2\n|-- end_0\"\n`;\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/datas/__snapshots__/flow-node-transition-data.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-node-transition-data > get lines and labels for all nodes 1`] = `\n{\n  \"$blockIcon$dynamicSplit_0\": {\n    \"labels\": [\n      {\n        \"offset\": {\n          \"x\": 0,\n          \"y\": 168,\n        },\n        \"type\": 0,\n      },\n    ],\n    \"lines\": [\n      {\n        \"from\": {\n          \"x\": 0,\n          \"y\": 152,\n        },\n        \"to\": {\n          \"x\": 0,\n          \"y\": 184,\n        },\n        \"type\": 0,\n      },\n    ],\n  },\n  \"$blockOrderIcon$block_0\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"$blockOrderIcon$block_1\": {\n    \"labels\": [\n      {\n        \"offset\": {\n          \"x\": 0,\n          \"y\": 260,\n        },\n        \"type\": 0,\n      },\n    ],\n    \"lines\": [\n      {\n        \"from\": {\n          \"x\": 0,\n          \"y\": 244,\n        },\n        \"to\": {\n          \"x\": 0,\n          \"y\": 276,\n        },\n        \"type\": 0,\n      },\n    ],\n  },\n  \"$blockOrderIcon$block_2\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"$inlineBlocks$dynamicSplit_0\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"block_0\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"block_1\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"block_2\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"dynamicSplit_0\": {\n    \"labels\": [\n      {\n        \"offset\": {\n          \"x\": 0,\n          \"y\": 352,\n        },\n        \"type\": 0,\n      },\n    ],\n    \"lines\": [\n      {\n        \"from\": {\n          \"x\": 0,\n          \"y\": 336,\n        },\n        \"to\": {\n          \"x\": 0,\n          \"y\": 368,\n        },\n        \"type\": 0,\n      },\n    ],\n  },\n  \"end_0\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"noop_0\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"root\": {\n    \"labels\": [],\n    \"lines\": [],\n  },\n  \"start_0\": {\n    \"labels\": [\n      {\n        \"offset\": {\n          \"x\": 0,\n          \"y\": 76,\n        },\n        \"type\": 0,\n      },\n    ],\n    \"lines\": [\n      {\n        \"from\": {\n          \"x\": 0,\n          \"y\": 60,\n        },\n        \"to\": {\n          \"x\": 0,\n          \"y\": 92,\n        },\n        \"type\": 0,\n      },\n    ],\n  },\n}\n`;\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/datas/flow-node-render-data.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { baseMock } from '../flow.mock';\nimport { createDocumentContainer } from '../flow-document-container.mock';\nimport { FlowDocument } from '../../src/flow-document';\nimport { FlowNodeRenderData } from '../../src/datas';\n\ndescribe('flow-node-render-data', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  const originSetTimeout = setTimeout;\n  beforeEach(() => {\n    vi.stubGlobal('setTimeout', (fn: any) => {\n      fn();\n      return 1;\n    });\n    container = createDocumentContainer();\n    document = container.get<FlowDocument>(FlowDocument);\n    document.fromJSON(baseMock, true);\n    document.transformer.refresh();\n  });\n  afterEach(() => {\n    vi.stubGlobal('setTimeout', originSetTimeout);\n    document.dispose();\n  });\n  it('expanded and adable', () => {\n    const renderData = document.getNode('end_0')!.getData(FlowNodeRenderData)!;\n    expect(!!renderData.addable).toEqual(false);\n    expect(renderData.expandable).toEqual(true);\n    renderData.toggleExpand();\n    expect(renderData.expanded).toEqual(true);\n  });\n  it('get dom node', () => {\n    const renderData = document.getNode('end_0')!.getData<FlowNodeRenderData>(FlowNodeRenderData)!;\n    expect(renderData.node).toEqual(renderData.node);\n    const parentDomNode = window.document.createElement('div');\n    parentDomNode.appendChild(renderData.node);\n    expect(renderData.node.parentElement).toEqual(parentDomNode);\n    expect(typeof renderData.node.addEventListener).toEqual('function');\n    renderData.dispose();\n    expect(renderData.node.parentElement).toEqual(null);\n  });\n  it('hover node visible', () => {\n    const renderData = document.getNode('end_0')!.getData(FlowNodeRenderData)!;\n    renderData.toggleMouseEnter();\n    expect(renderData.hovered).toEqual(true);\n    expect(renderData.activated).toEqual(true);\n    expect(document.renderState.getNodeHovered()).toEqual(renderData.entity);\n    renderData.toggleMouseLeave();\n    expect(renderData.hovered).toEqual(false);\n    expect(renderData.activated).toEqual(false);\n    expect(document.renderState.getNodeHovered()).toEqual(undefined);\n  });\n  it('hover node hidden', () => {\n    const renderData = document\n      .getNode('$inlineBlocks$dynamicSplit_0')!\n      .getData(FlowNodeRenderData)!;\n    renderData.toggleMouseEnter();\n    expect(renderData.hovered).toEqual(false); // 是隐藏节点\n    expect(document.renderState.getNodeHovered()).toEqual(renderData.entity);\n    renderData.toggleMouseLeave();\n    expect(document.renderState.getNodeHovered()).toEqual(undefined);\n  });\n  it('hover node silent', () => {\n    const renderData = document.getNode('end_0')!.getData(FlowNodeRenderData)!;\n    renderData.toggleMouseEnter(true);\n    expect(renderData.hovered).toEqual(false);\n    expect(document.renderState.getNodeHovered()).toEqual(renderData.entity);\n    renderData.toggleMouseLeave(true);\n    expect(renderData.hovered).toEqual(false);\n    expect(document.renderState.getNodeHovered()).toEqual(undefined);\n  });\n  it('activated', () => {\n    const renderData = document.getNode('$blockIcon$dynamicSplit_0')!.getData(FlowNodeRenderData)!;\n    renderData.activated = true;\n    expect(renderData.activated).toEqual(true);\n    expect(renderData.entity.parent!.getData(FlowNodeRenderData)!.activated).toEqual(true);\n    expect(renderData.lineActivated).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/datas/flow-node-transition-data.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { baseMockAddBranch } from '../flow.mock';\nimport { createDocumentContainer } from '../flow-document-container.mock';\nimport { FlowDocument } from '../../src/flow-document';\nimport { FlowNodeTransitionData } from '../../src/datas';\n\ndescribe('flow-node-transition-data', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  const originSetTimeout = setTimeout;\n  beforeEach(() => {\n    vi.stubGlobal('setTimeout', (fn: any) => {\n      fn();\n      return 1;\n    });\n    container = createDocumentContainer();\n    document = container.get<FlowDocument>(FlowDocument);\n    document.fromJSON(baseMockAddBranch, true);\n    document.transformer.refresh();\n  });\n  afterEach(() => {\n    vi.stubGlobal('setTimeout', originSetTimeout);\n    document.dispose();\n  });\n\n  it('get lines and labels for all nodes', () => {\n    const allNodeTransitionInfo: any = {};\n\n    document.getAllNodes().forEach((_node) => {\n      const transitionData = _node.getData(FlowNodeTransitionData)!;\n      allNodeTransitionInfo[_node.id] = {\n        lines: transitionData.lines,\n        labels: transitionData.labels,\n      };\n    });\n\n    expect(allNodeTransitionInfo).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-document-container.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { decorate, injectable, Container, type interfaces } from 'inversify';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport {\n  type FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  FlowNodeTransitionData,\n} from '../src';\n\nexport class FlowDocumentMockRegister implements FlowDocumentContribution {\n  registerDocument(document: FlowDocument) {\n    document.registerNodeDatas(FlowNodeTransformData, FlowNodeRenderData, FlowNodeTransitionData);\n  }\n}\n\ndecorate(injectable(), FlowDocumentMockRegister);\n\nexport function createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(EntityManager).toSelf();\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-document-transformer.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { FlowDocument, FlowNodeTransformData } from '../src';\nimport { baseMockAddNode } from './flow.mock';\nimport { createDocumentContainer } from './flow-document-container.mock';\n\ninterface TransformTestData {\n  version: number;\n  localBoundsStr: string;\n  localID: number;\n  worldID: number;\n  boundsStr: string;\n}\n\nfunction getTransformData(document: FlowDocument): Record<string, TransformTestData> {\n  const data: Record<string, TransformTestData> = {};\n  document.traverse((node) => {\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n    data[node.id] = {\n      version: transform.version,\n      boundsStr: transform.bounds.toStyleStr(),\n      localBoundsStr: transform.localBounds.toStyleStr(),\n      localID: transform.transform.localID, // 用来判断是否有更新\n      worldID: transform.transform.worldID,\n    };\n  });\n  return data;\n}\n\ndescribe('flow-document-transformer', () => {\n  let container = createDocumentContainer();\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.get(FlowDocument).fromJSON(baseMockAddNode);\n  });\n  it('updateTransformsTree', () => {\n    const document = container.get<FlowDocument>(FlowDocument);\n    document.transformer.updateTransformsTree();\n    // root 会包含三个子节点：start_0, dynamicSplit_0, end_0\n    expect(document.root.getData<TransformData>(TransformData)!.children.length).toEqual(3);\n  });\n  it('transform version', () => {\n    const document = container.get<FlowDocument>(FlowDocument);\n    const preData = getTransformData(document);\n    document.transformer.refresh();\n    const postData = getTransformData(document);\n    expect(preData.root.version).toEqual(0);\n    expect(preData.start_0).toEqual({\n      version: 0,\n      boundsStr: 'left: 0px; top: 0px; width: 0px; height: 0px;',\n      localBoundsStr: 'left: 0px; top: 0px; width: 0px; height: 0px;',\n      localID: 0,\n      worldID: 0,\n    });\n    expect(postData.start_0).toEqual({\n      version: 2, // 更新了 size 和 position\n      boundsStr: 'left: -140px; top: 0px; width: 280px; height: 60px;',\n      localBoundsStr: 'left: -140px; top: 0px; width: 280px; height: 60px;',\n      localID: 2, // 更新了 size 和 position\n      worldID: 1,\n    });\n    expect(postData.root).toEqual({\n      version: 0,\n      boundsStr: 'left: -140px; top: 0px; width: 280px; height: 428px;',\n      localBoundsStr: 'left: -140px; top: 0px; width: 280px; height: 428px;',\n      localID: 0, // 只更新了 position\n      worldID: 0,\n    });\n  });\n  it('refresh', () => {\n    const document = container.get<FlowDocument>(FlowDocument);\n    document.transformer.refresh();\n    const preData = getTransformData(document);\n    document.transformer.refresh();\n    const nextData = getTransformData(document);\n    // 数据没有变化\n    expect(preData).toEqual(nextData);\n  });\n  it('transform with tree change', () => {\n    const document = container.get<FlowDocument>(FlowDocument);\n    document.transformer.refresh();\n    const preData = getTransformData(document);\n    expect(preData.dynamicSplit_0).toEqual({\n      version: 3,\n      boundsStr: 'left: -140px; top: 92px; width: 280px; height: 244px;',\n      localBoundsStr: 'left: -140px; top: 92px; width: 280px; height: 244px;',\n      localID: 3,\n      worldID: 1,\n    });\n    // expect(preData.$blockOrderIcon$block_1).toEqual({\n    //   version: 2,\n    //   boundsStr: 'left: -140px; top: 184px; width: 280px; height: 60px;',\n    //   localBoundsStr: 'left: -140px; top: 0px; width: 280px; height: 60px;',\n    //   localID: 2,\n    //   worldID: 1,\n    // })\n    document.addFromNode('start_0', { id: 'test', type: 'test' });\n    document.transformer.refresh();\n    const nextData = getTransformData(document);\n    expect(preData.start_0).toEqual(nextData.start_0);\n    expect(nextData.dynamicSplit_0).toEqual({\n      version: 4,\n      boundsStr: 'left: -140px; top: 184px; width: 280px; height: 244px;',\n      localBoundsStr: 'left: -140px; top: 184px; width: 280px; height: 244px;',\n      localID: 4,\n      worldID: 2,\n    });\n    // 子节点页跟着更新\n    // expect(nextData.$blockOrderIcon$block_1).toEqual({\n    //   version: 2, // 由于 local 未更新所以 version 没变\n    //   boundsStr: 'left: -140px; top: 276px; width: 280px; height: 60px;',\n    //   localBoundsStr: 'left: -140px; top: 0px; width: 280px; height: 60px;',\n    //   localID: 2, // local 相对位置未更新\n    //   worldID: 2, // world 绝对位置更新\n    // })\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-document.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { type FlowDocumentJSON, type FlowNodeJSON } from '../src/typings';\nimport { FlowDocument } from '../src';\nimport { baseMock, baseMockAddBranch, baseMockAddNode, baseMockNodeEnd } from './flow.mock';\nimport { createDocumentContainer } from './flow-document-container.mock';\n\ndescribe('flow-document', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.get(FlowDocument).fromJSON(baseMockAddNode);\n    document = container.get<FlowDocument>(FlowDocument);\n  });\n  it('fromJSON', () => {\n    document = container.get<FlowDocument>(FlowDocument);\n    expect(document.root.childrenLength).toEqual(3);\n    expect(document.root.children[0].parent).toEqual(document.root);\n    expect(document.getNode('$blockOrderIcon$block_0')!.originParent).toEqual(\n      document.getNode('dynamicSplit_0')\n    );\n    expect(document.getNode('$blockOrderIcon$block_0')!.parent).toEqual(\n      document.getNode('block_0')\n    );\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|-------- noop_0\n|-- end_0`);\n  });\n  it('fromJSON is equal toJSON', () => {\n    function normalizeNode(node: FlowNodeJSON): FlowNodeJSON {\n      return {\n        ...node,\n        type: node.type || 'block',\n        blocks: node.blocks?.map((b) => normalizeNode(b)),\n      };\n    }\n    function jsonEqual(from: FlowDocumentJSON, to: FlowDocumentJSON) {\n      const nodes = to.nodes.map((node) => normalizeNode(node));\n      expect(from.nodes).toEqual(nodes);\n    }\n    document.fromJSON(baseMock);\n    jsonEqual(document.toJSON(), baseMock);\n    document.fromJSON(baseMockAddNode);\n    jsonEqual(document.toJSON(), baseMockAddNode);\n    document.fromJSON(baseMockAddBranch);\n    jsonEqual(document.toJSON(), baseMockAddBranch);\n\n    // eslint-disable-next-line guard-for-in\n    // for (const key in dataList) {\n    //   const json = (dataList as any)[key]\n    //   document.fromJSON(json)\n    //   jsonEqual(document.toJSON(), json)\n    // }\n  });\n  it('fromJSON with cache', () => {\n    // Step1: 导入初始结构\n    document.fromJSON(baseMock);\n    const originNodes = document.getAllNodes().slice();\n    expect(document.size).toEqual(10);\n    // Step2: 同一个数据，结构未变化，数据未变化\n    document.fromJSON(baseMock);\n    expect(document.size).toEqual(10);\n    const nodes2 = document.getAllNodes().slice();\n    expect(originNodes).toEqual(nodes2);\n    // Step3: 添加一个节点，结构产生变化\n    document.fromJSON(baseMockAddNode);\n    expect(document.size).toEqual(11);\n    const nodes3 = document.getAllNodes().slice();\n    // 新添加的节点会放在最后边，前面所有的 instance 都不变\n    expect(nodes3.slice(0, -1)).toEqual(originNodes);\n    // Step4: 添加一个块\n    document.fromJSON(baseMockAddBranch);\n    expect(document.size).toEqual(13); // 会添加两个节点\n    const nodes4 = document.getAllNodes().slice();\n    expect(nodes4.slice(0, -3)).toEqual(originNodes);\n    // Step5: 跑最初始的数据，结构变化，节点被删除了两个\n    document.fromJSON(baseMock);\n    expect(document.size).toEqual(10);\n    const nodes5 = document.getAllNodes().slice();\n    expect(nodes5).toEqual(originNodes);\n  });\n  // it('remove node from JSON', () => {\n  //   document.fromJSON(dataList['split-nested']);\n  //   expect(document.size).toBeGreaterThan(40);\n  //   document.fromJSON(dataList.empty);\n  //   expect(document.size).toEqual(3);\n  // });\n  it('add base node', () => {\n    document.addFromNode('start_0', { id: 'new_noop', type: 'noop' });\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- new_noop\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|-------- noop_0\n|-- end_0`);\n    expect(document.size).toEqual(12);\n  });\n  it('add split node', () => {\n    document.addFromNode('dynamicSplit_0', {\n      id: 'new_split',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'b1' }, { id: 'b2' }],\n    });\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|-------- noop_0\n|-- new_split\n|---- $blockIcon$new_split\n|---- $inlineBlocks$new_split\n|------ b1\n|-------- $blockOrderIcon$b1\n|------ b2\n|-------- $blockOrderIcon$b2\n|-- end_0`);\n    expect(document.size).toEqual(18);\n    document.addFromNode('block_1', {\n      id: 'new_split2',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'nb1' }, { id: 'nb2' }],\n    });\n    expect(document.toString()).toMatchSnapshot();\n    expect(document.size).toEqual(25);\n  });\n  it('removeNode', () => {\n    document.removeNode('dynamicSplit_0');\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- end_0`);\n    expect(document.size).toEqual(3);\n  });\n  it('removeLeafBlock', () => {\n    document.removeNode('noop_0');\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|-- end_0`);\n  });\n  it('removeBlockFistChild', () => {\n    document = container.get<FlowDocument>(FlowDocument);\n    document.removeNode('$blockOrderIcon$block_1');\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- noop_0\n|-- end_0`);\n  });\n  it('removeBlockWithChild', () => {\n    document.removeNode('block_1');\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|-- end_0`);\n  });\n  it('flow node type changed', () => {\n    document.fromJSON({\n      nodes: [\n        {\n          id: 'start_0',\n          type: 'start',\n          blocks: [],\n        },\n        {\n          id: 'dynamicSplit_0',\n          type: 'noop', // Change node type to noop\n        },\n        {\n          id: 'end_0',\n          type: 'dynamicSplit', // Change node type to dynamicSplit\n          blocks: [{ id: 'block_0' }, { id: 'block_1' }],\n        },\n      ],\n    });\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|-- end_0\n|---- $blockIcon$end_0\n|---- $inlineBlocks$end_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- $blockOrderIcon$block_1`);\n    expect(document.size).toEqual(10);\n  });\n  /**\n   * 分支移动\n   */\n  it('flow node move', () => {\n    document.fromJSON({\n      nodes: [\n        {\n          id: 'start_0',\n          type: 'start',\n          blocks: [],\n        },\n        {\n          id: 'end_0',\n          type: 'end',\n          blocks: [],\n        },\n        {\n          id: 'dynamicSplit_0',\n          type: 'dynamicSplit',\n          blocks: [{ id: 'block_1' }, { id: 'block_0' }], // swap blocks order\n        },\n      ],\n    });\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- end_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_1\n|-------- $blockOrderIcon$block_1\n|------ block_0\n|-------- $blockOrderIcon$block_0`);\n  });\n  /**\n   * 节点结束判断\n   */\n  it('flow is node end', () => {\n    document.fromJSON(baseMockNodeEnd);\n\n    expect(document.getNode('dynamicSplit_0')?.isNodeEnd).toBeTruthy();\n    expect(document.getNode('block_0')?.isNodeEnd).toBeTruthy();\n    expect(document.getNode('noop_0')?.isNodeEnd).toBeTruthy();\n  });\n  it('node registry add cache', () => {\n    const registry1 = document.getNodeRegistry('start');\n    const registry2 = document.getNodeRegistry('start');\n    expect(registry1 === registry2).toBeTruthy();\n    expect(\n      document.getNode('block_0')!.getNodeRegistry() ===\n        document.getNode('block_1')!.getNodeRegistry()\n    ).toBeTruthy();\n    expect(\n      document.getNode('block_0')!.getNodeMeta() === document.getNode('block_1')!.getNodeMeta()\n    ).toBeTruthy();\n  });\n  it('document is disposed and call toJSON should throw error', () => {\n    document.dispose();\n    expect(() => document.toJSON()).toThrowError(/disposed/);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-node-entity.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { FlowDocument } from '../src/flow-document';\nimport { baseMockAddNode } from './flow.mock';\nimport { createDocumentContainer } from './flow-document-container.mock';\n\ninterface BlockData {\n  children: string[];\n  pre?: string;\n  next?: string;\n  depth: number;\n  childrenSize: number;\n}\n\nconst blockData: { [key: string]: BlockData } = {\n  root: {\n    children: ['start_0', 'dynamicSplit_0', 'end_0'],\n    pre: undefined,\n    next: undefined,\n    depth: 0,\n    childrenSize: 10,\n  },\n  start_0: {\n    children: [],\n    next: 'dynamicSplit_0',\n    pre: undefined,\n    depth: 1,\n    childrenSize: 0,\n  },\n  dynamicSplit_0: {\n    children: ['$blockIcon$dynamicSplit_0', '$inlineBlocks$dynamicSplit_0'],\n    next: 'end_0',\n    pre: 'start_0',\n    depth: 1,\n    childrenSize: 7,\n  },\n  $blockIcon$dynamicSplit_0: {\n    children: [],\n    next: '$inlineBlocks$dynamicSplit_0',\n    pre: undefined,\n    depth: 2,\n    childrenSize: 0,\n  },\n  $inlineBlocks$dynamicSplit_0: {\n    children: ['block_0', 'block_1'],\n    pre: '$blockIcon$dynamicSplit_0',\n    next: undefined,\n    depth: 2,\n    childrenSize: 5,\n  },\n  block_0: {\n    children: ['$blockOrderIcon$block_0'],\n    depth: 3,\n    pre: undefined,\n    next: 'block_1',\n    childrenSize: 1,\n  },\n  $blockOrderIcon$block_0: {\n    children: [],\n    depth: 4,\n    pre: undefined,\n    next: undefined,\n    childrenSize: 0,\n  },\n  block_1: {\n    children: ['$blockOrderIcon$block_1', 'noop_0'],\n    depth: 3,\n    pre: 'block_0',\n    next: undefined,\n    childrenSize: 2,\n  },\n  $blockOrderIcon$block_1: {\n    children: [],\n    next: 'noop_0',\n    depth: 4,\n    pre: undefined,\n    childrenSize: 0,\n  },\n  noop_0: {\n    children: [],\n    pre: '$blockOrderIcon$block_1',\n    next: undefined,\n    depth: 4,\n    childrenSize: 0,\n  },\n  end_0: {\n    children: [],\n    pre: 'dynamicSplit_0',\n    next: undefined,\n    depth: 1,\n    childrenSize: 0,\n  },\n};\ndescribe('flow-node-entity', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.get(FlowDocument).fromJSON(baseMockAddNode);\n    document = container.get<FlowDocument>(FlowDocument);\n  });\n  it('get children', () => {\n    const currentBlockData: { [key: string]: BlockData } = {};\n    document.traverse((node, depth) => {\n      currentBlockData[node.id] = {\n        children: node.children.map((b) => b.id),\n        depth,\n        next: node.next?.id,\n        pre: node.pre?.id,\n        childrenSize: node.allChildren.length,\n      };\n    });\n    expect(currentBlockData).toEqual(blockData);\n  });\n  it('flow node delete', () => {\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    const node = document.getNode('$blockOrderIcon$block_1')!;\n    node.dispose();\n    expect(document.toString()).toEqual(`root\n|-- start_0\n|-- dynamicSplit_0\n|---- $blockIcon$dynamicSplit_0\n|---- $inlineBlocks$dynamicSplit_0\n|------ block_0\n|-------- $blockOrderIcon$block_0\n|------ block_1\n|-------- noop_0\n|-- end_0`);\n    // transform 数据还在\n    expect(node.getData(TransformData)).toBeDefined();\n  });\n  it('getExtInfo and updateExtInfo', () => {\n    const node = document.getNode('start_0');\n    let changedTimes = 0;\n    node.onExtInfoChange((data) => {\n      changedTimes++;\n    });\n    expect(node.toJSON().data).toEqual(undefined);\n    expect(node.getExtInfo()).toEqual(undefined);\n    node.updateExtInfo({ title: 'start' });\n    expect(node.getExtInfo()).toEqual({ title: 'start' });\n    expect(changedTimes).toEqual(1);\n    node.updateExtInfo({ title: 'start' }); // same\n    expect(changedTimes).toEqual(1);\n    node.updateExtInfo({ content: 'content' });\n    expect(node.getExtInfo()).toEqual({ title: 'start', content: 'content' });\n    expect(changedTimes).toEqual(2);\n    expect(node.toJSON()).toEqual({\n      id: 'start_0',\n      type: 'start',\n      data: { title: 'start', content: 'content' }, // By default, extInfo will be present in data\n    });\n    node.updateExtInfo({ title: 'start2' }, true);\n    expect(node.getExtInfo()).toEqual({ title: 'start2' });\n    expect(changedTimes).toEqual(3);\n    expect(node.toJSON().data).toEqual({ title: 'start2' });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-node-registry.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { createDocumentContainer } from './flow-document-container.mock';\nimport { FlowDocument, FlowNodeRegistry } from '../src';\n\nfunction registerNode(doc: FlowDocument, newRegistry: FlowNodeRegistry): FlowNodeRegistry {\n  doc.registerFlowNodes(newRegistry);\n  return doc.getNodeRegistry(newRegistry.type);\n}\nconst mockRegistries: FlowNodeRegistry[] = [\n  {\n    type: 'dynamicSplit',\n    meta: {},\n    onCreate(node, json) {\n      return node.document.addInlineBlocks(node, json.blocks || []);\n    },\n    extendChildRegistries: [\n      {\n        type: 'blockIcon',\n        customKey: 'blockIcon_base',\n      },\n      {\n        type: 'inlineBlocks',\n        customKey: 'inlineBlocks_base',\n      },\n    ],\n    onAdd: () => {},\n  },\n  {\n    type: 'a',\n    extend: 'dynamicSplit',\n  },\n  {\n    type: 'b',\n    extend: 'a',\n    extendChildRegistries: [\n      {\n        type: 'blockIcon',\n        customKey: 'blockIcon_from_b',\n      },\n    ],\n  },\n  {\n    type: 'c',\n    extend: 'b',\n    extendChildRegistries: [\n      {\n        type: 'blockIcon',\n        customKey: 'blockIcon_from_c',\n      },\n      {\n        type: 'inlineBlocks',\n        customKey: 'inlineBlocks_from_c',\n      },\n    ],\n  },\n];\ndescribe('flow-node-registry', () => {\n  let doc: FlowDocument;\n  beforeEach(() => {\n    const container = createDocumentContainer();\n    doc = container.get<FlowDocument>(FlowDocument);\n    doc.registerFlowNodes(...mockRegistries);\n  });\n  it('extend check', () => {\n    expect(doc.getNodeRegistry('dynamicSplit').__extends__).toEqual(undefined);\n    expect(doc.getNodeRegistry('a').__extends__).toEqual(['dynamicSplit']);\n    expect(doc.getNodeRegistry('b').__extends__).toEqual(['a', 'dynamicSplit']);\n    expect(doc.getNodeRegistry('c').__extends__).toEqual(['b', 'a', 'dynamicSplit']);\n    expect(doc.isExtend('dynamicSplit', 'dynamicSplit')).toBeFalsy();\n    expect(doc.isExtend('a', 'b')).toBeFalsy();\n    expect(doc.isExtend('a', 'dynamicSplit')).toBeTruthy();\n    expect(doc.isExtend('b', 'dynamicSplit')).toBeTruthy();\n    expect(doc.isExtend('b', 'a')).toBeTruthy();\n    expect(doc.isExtend('c', 'dynamicSplit')).toBeTruthy();\n    expect(doc.isExtend('c', 'b')).toBeTruthy();\n    expect(doc.isExtend('c', 'a')).toBeTruthy();\n    expect(doc.isTypeOrExtendType('dynamicSplit', 'dynamicSplit')).toBeTruthy();\n    expect(doc.isTypeOrExtendType('c', 'a')).toBeTruthy();\n  });\n  it('base extend', () => {\n    expect(doc.getNodeRegistry('a').onAdd).toBeTypeOf('function');\n    doc.addNode({\n      id: 'a',\n      type: 'a',\n      parent: doc.root,\n    });\n    expect(doc.toString()).toEqual(`root\n|-- a\n|---- $blockIcon$a\n|---- $inlineBlocks$a`);\n    expect(doc.getNode('$blockIcon$a').getNodeRegistry().customKey).toBe('blockIcon_base');\n  });\n  it('extend nested', () => {\n    expect(doc.getNodeRegistry('b').onAdd).toBeTypeOf('function');\n    doc.addNode({\n      id: 'b',\n      type: 'b',\n      parent: doc.root,\n    });\n    doc.addNode({\n      id: 'c',\n      type: 'c',\n      parent: doc.root,\n    });\n    expect(doc.toString()).toEqual(`root\n|-- b\n|---- $blockIcon$b\n|---- $inlineBlocks$b\n|-- c\n|---- $blockIcon$c\n|---- $inlineBlocks$c`);\n    expect(doc.getNode('$blockIcon$b').getNodeRegistry().customKey).toBe('blockIcon_from_b');\n    expect(doc.getNode('$inlineBlocks$b').getNodeRegistry().customKey).toBe('inlineBlocks_base');\n    expect(doc.getNode('$blockIcon$c').getNodeRegistry().customKey).toBe('blockIcon_from_c');\n    expect(doc.getNode('$inlineBlocks$c').getNodeRegistry().customKey).toBe('inlineBlocks_from_c');\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-render-tree.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\n// import { FlowDocumentConfigEnum } from '../src/typings';\n// import { FlowDocumentConfigDefaultData } from '../src/flow-document-config';\nimport { FlowDocument } from '../src/flow-document';\nimport { baseMockNodeEnd2 } from './flow.mock';\nimport { createDocumentContainer } from './flow-document-container.mock';\n\ndescribe('flow-render-tree', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    document = container.get<FlowDocument>(FlowDocument);\n  });\n\n  /**\n   * 节点折叠\n   */\n  it('node collapsed', () => {\n    container.get(FlowDocument).fromJSON(baseMockNodeEnd2);\n    document = container.get<FlowDocument>(FlowDocument);\n\n    const { renderTree } = document;\n\n    const inlineBlocks1 = document.getNode('$inlineBlocks$dynamicSplitcxIBv')!;\n    const inlineBlocks2 = document.getNode('$inlineBlocks$split')!;\n    inlineBlocks1.collapsed = true;\n    renderTree.updateRenderStruct();\n\n    expect(inlineBlocks1.children.length).toEqual(0);\n    expect(inlineBlocks1.allCollapsedChildren.length).toEqual(5);\n    expect(document.renderTree.toString()).toEqual(`root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|-------- endbL5T2\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- dynamicSplitcxIBv\n|---------- $blockIcon$dynamicSplitcxIBv\n|---------- $inlineBlocks$dynamicSplitcxIBv\n|-------- endT3VLX\n|------ _sJEq\n|-------- $blockOrderIcon$_sJEq\n|-- staticSplitHLvrh\n|---- $blockIcon$staticSplitHLvrh\n|---- $inlineBlocks$staticSplitHLvrh\n|------ fPE-N\n|-------- $blockOrderIcon$fPE-N\n|------ ulpHV\n|-------- $blockOrderIcon$ulpHV\n|-- end_0`);\n    inlineBlocks2.collapsed = true;\n    renderTree.updateRenderStruct();\n\n    expect(inlineBlocks2.children.length).toEqual(0);\n    expect(inlineBlocks2.allCollapsedChildren.length).toEqual(16);\n    expect(document.renderTree.toString()).toEqual(`root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|-- staticSplitHLvrh\n|---- $blockIcon$staticSplitHLvrh\n|---- $inlineBlocks$staticSplitHLvrh\n|------ fPE-N\n|-------- $blockOrderIcon$fPE-N\n|------ ulpHV\n|-------- $blockOrderIcon$ulpHV\n|-- end_0`);\n  });\n\n  it('render tree stop modified tree', () => {\n    const { renderTree } = document;\n    expect(() => renderTree.remove()).toThrowError(/cannot use/);\n    expect(() => renderTree.addChild()).toThrowError(/cannot use/);\n    expect(() => renderTree.insertAfter()).toThrowError(/cannot use/);\n    expect(() => renderTree.removeParent()).toThrowError(/cannot use/);\n  });\n\n  // /**\n  //  * 处理结束节点偏移\n  //  * 结束节点产品改方案\n  //  */\n  // it.skip('refine end branch', () => {\n  //   container.bind(FlowDocumentConfigDefaultData).toConstantValue({\n  //     [FlowDocumentConfigEnum.END_NODES_REFINE_BRANCH]: true,\n  //   });\n  //   container.get(FlowDocument).fromJSON(dataList.end);\n  //   document = container.get<FlowDocument>(FlowDocument);\n  //\n  //   const inlineBlocks1 = document.getNode('$inlineBlocks$dynamicSplitcxIBv')!;\n  //   const inlineBlocks2 = document.getNode('$inlineBlocks$split')!;\n  //   inlineBlocks1.collapsed = false;\n  //   inlineBlocks2.collapsed = false;\n  //\n  //   const { renderTree } = document;\n  //   renderTree.updateRenderStruct();\n  //\n  //   expect(document.renderTree.toString()).toMatchSnapshot();\n  //\n  //   inlineBlocks2.collapsed = true;\n  //   renderTree.updateRenderStruct();\n  //\n  //   // 结束节点拽进去的节点都需要收起\n  //   expect(document.renderTree.toString()).toMatchSnapshot();\n  // });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow-virtual-tree.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { FlowVirtualTree } from '../src/flow-virtual-tree';\n\nimport NodeInfo = FlowVirtualTree.NodeInfo;\n\ninterface Node {\n  id: string;\n}\n\ndescribe('flow-virtual-tree', () => {\n  let tree: FlowVirtualTree<Node>;\n  beforeEach(() => {\n    tree = new FlowVirtualTree<Node>({\n      id: 'root',\n    });\n    addBase();\n  });\n  function addBase(): void {\n    const noop1 = tree.addChild(tree.root, { id: 'noop1' });\n    const noop2 = tree.addChild(noop1, { id: 'noop2' });\n    tree.addChild(tree.root, { id: 'noop1_n' });\n    tree.addChild(noop2, { id: 'noop3' });\n    tree.addChild(noop2, { id: 'noop4' });\n    tree.addChild(noop2, { id: 'noop5' });\n  }\n  function get(id: string): Node {\n    return tree.getById(id)!;\n  }\n\n  function getInfo(id: string): FlowVirtualTree.NodeInfo<Node> {\n    return tree.getInfo(get(id)!)!;\n  }\n\n  it('get infos', () => {\n    const infos: Record<string, NodeInfo<Node>> = {};\n    // tree.traverse()\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop4\n|------ noop5\n|-- noop1_n`);\n    expect(tree.size).toEqual(7);\n    tree.traverse((n) => {\n      infos[n.id] = {\n        parent: tree.getParent(n),\n        pre: tree.getPre(n),\n        next: tree.getNext(n),\n        children: tree.getChildren(n),\n      };\n    });\n    expect(infos).toEqual({\n      root: { children: [{ id: 'noop1' }, { id: 'noop1_n' }] },\n      noop1: { parent: { id: 'root' }, next: { id: 'noop1_n' }, children: [{ id: 'noop2' }] },\n      noop2: {\n        parent: { id: 'noop1' },\n        children: [{ id: 'noop3' }, { id: 'noop4' }, { id: 'noop5' }],\n      },\n      noop3: { parent: { id: 'noop2' }, next: { id: 'noop4' }, children: [] },\n      noop4: { parent: { id: 'noop2' }, pre: { id: 'noop3' }, next: { id: 'noop5' }, children: [] },\n      noop5: { parent: { id: 'noop2' }, pre: { id: 'noop4' }, children: [] },\n      noop1_n: { parent: { id: 'root' }, pre: { id: 'noop1' }, children: [] },\n    });\n  });\n\n  it('add child from node existed', () => {\n    tree.addChild(get('root'), get('noop4'));\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop5\n|-- noop1_n\n|-- noop4`);\n    expect(getInfo('noop4').pre).toEqual(get('noop1_n'));\n    expect(getInfo('noop4').parent).toEqual(get('root'));\n    expect(getInfo('noop3').next).toEqual(get('noop5'));\n    expect(getInfo('noop5').pre).toEqual(get('noop3'));\n  });\n\n  it('add child same', () => {\n    tree.addChild(get('root'), get('noop1'));\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop4\n|------ noop5\n|-- noop1_n`);\n  });\n\n  it('remove child', () => {\n    tree.remove(get('noop4')!);\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop5\n|-- noop1_n`);\n    expect(tree.size).toEqual(6);\n    expect(getInfo('noop3').next).toEqual(get('noop5'));\n    expect(getInfo('noop5').pre).toEqual(get('noop3'));\n    tree.remove(get('noop3')!);\n    expect(getInfo('noop5').pre).toBeUndefined();\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop5\n|-- noop1_n`);\n    expect(tree.size).toEqual(5);\n    tree.addChild(get('noop5'), { id: 'left' });\n    tree.remove(get('noop2')!);\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|-- noop1_n`);\n    expect(tree.size).toEqual(3);\n  });\n\n  it('clear', () => {\n    tree.clear();\n    expect(tree.size).toEqual(0);\n  });\n\n  it('clone', () => {\n    const clone = tree.clone();\n    expect(clone.toString()).toEqual(tree.toString());\n    expect(clone.size).toEqual(tree.size);\n  });\n\n  it('dispose', () => {\n    let times = 0;\n    tree.onTreeChange(() => {\n      times += 1;\n    });\n    tree.addChild(tree.root, { id: 'noop1' });\n    tree.dispose();\n    tree.addChild(tree.root, { id: 'noop2' });\n    expect(tree.toString()).toEqual(`root\n|-- noop2`);\n    expect(times).toEqual(1);\n  });\n\n  it('insertAfter', () => {\n    tree.insertAfter(get('noop2'), { id: 'noop2_next' });\n    expect(getInfo('noop2').next).toEqual(get('noop2_next'));\n    expect(getInfo('noop2_next').pre).toEqual(get('noop2'));\n    expect(getInfo('noop2_next').parent).toEqual(get('noop1'));\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop4\n|------ noop5\n|---- noop2_next\n|-- noop1_n`);\n    tree.insertAfter(get('noop3'), { id: 'noop3_next' });\n    expect(tree.toString()).toEqual(`root\n|-- noop1\n|---- noop2\n|------ noop3\n|------ noop3_next\n|------ noop4\n|------ noop5\n|---- noop2_next\n|-- noop1_n`);\n    expect(getInfo('noop3').next).toEqual(get('noop3_next'));\n    expect(getInfo('noop3_next').pre).toEqual(get('noop3'));\n    expect(getInfo('noop3_next').next).toEqual(get('noop4'));\n    expect(getInfo('noop3_next').parent).toEqual(get('noop2'));\n    expect(getInfo('noop4').pre).toEqual(get('noop3_next'));\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/flow.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowDocumentJSON } from '../src';\n\nexport const baseMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'block_0' }, { id: 'block_1' }],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n    },\n  ],\n};\n\nexport const baseMockAddNode: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'block_0' }, { id: 'block_1', blocks: [{ id: 'noop_0', type: 'noop' }] }],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n    },\n  ],\n};\n\nexport const baseMockAddBranch: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      blocks: [\n        { id: 'block_0' },\n        { id: 'block_1', blocks: [{ id: 'noop_0', type: 'noop' }] },\n        { id: 'block_2' },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n    },\n  ],\n};\nexport const baseMockNodeEnd2: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'split',\n      type: 'dynamicSplit',\n      blocks: [\n        {\n          id: 'branch_0',\n          blocks: [\n            {\n              id: 'endbL5T2',\n              type: 'end',\n            },\n          ],\n        },\n        {\n          id: 'branch_1',\n          blocks: [\n            {\n              id: 'dynamicSplitcxIBv',\n              type: 'dynamicSplit',\n              blocks: [\n                {\n                  id: '8ZFL8',\n                  blocks: [\n                    {\n                      id: 'enddQN1D',\n                      type: 'end',\n                    },\n                  ],\n                },\n                {\n                  id: 'vo83H',\n                },\n              ],\n            },\n            {\n              id: 'endT3VLX',\n              type: 'end',\n            },\n          ],\n        },\n        {\n          id: '_sJEq',\n        },\n      ],\n    },\n    {\n      id: 'staticSplitHLvrh',\n      type: 'staticSplit',\n      blocks: [\n        {\n          id: 'fPE-N',\n        },\n        {\n          id: 'ulpHV',\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n    },\n  ],\n};\n\nexport const baseMockNodeEnd: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      blocks: [\n        { id: 'block_0', blocks: [{ id: 'noop_0', meta: { isNodeEnd: true }, type: 'end' }] },\n        { id: 'block_1', blocks: [{ id: 'noop_1', meta: { isNodeEnd: true }, type: 'end' }] },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/services/__snapshots__/flow-operation-base-service.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-operation-base-service > addBlock deleteBlock 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"test\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addBlock deleteBlock 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addBlock deleteBlock by index 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"test\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addBlock deleteBlock by index 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addChildNode deleteChildNode 1`] = `\n[\n  {\n    \"id\": \"test-node\",\n    \"type\": \"block\",\n  },\n]\n`;\n\nexports[`flow-operation-base-service > addChildNode deleteChildNode 2`] = `[]`;\n\nexports[`flow-operation-base-service > addFromNode deleteFromNode 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"id\": \"test\",\n      \"type\": \"noop\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addFromNode deleteFromNode 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addNodes deleteNodes 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"id\": \"test1\",\n      \"type\": \"noop\",\n    },\n    {\n      \"id\": \"test2\",\n      \"type\": \"noop\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > addNodes deleteNodes 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > dragNodes 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > dragNodes 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveBlock 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveBlock 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveChildNodes 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveChildNodes 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [\n            {\n              \"id\": \"noop_0\",\n              \"type\": \"noop\",\n            },\n          ],\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveNodes 1`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"id\": \"noop_0\",\n      \"type\": \"noop\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`flow-operation-base-service > moveNodes 2`] = `\n{\n  \"nodes\": [\n    {\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"id\": \"noop_0\",\n          \"type\": \"noop\",\n        },\n      ],\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/canvas-engine/document/__tests__/services/flow-operation-base-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { baseMockAddNode, baseMockAddBranch } from '../flow.mock';\nimport { createDocumentContainer } from '../flow-document-container.mock';\nimport { FlowOperationBaseService, FlowDocument, OperationType, OnNodeMoveEvent } from '../../src';\n\ndescribe('flow-operation-base-service', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let flowOperationService: FlowOperationBaseService;\n  beforeEach(() => {\n    container = createDocumentContainer();\n    document = container.get(FlowDocument);\n    document.fromJSON(baseMockAddNode);\n    flowOperationService = container.get<FlowOperationBaseService>(FlowOperationBaseService);\n  });\n\n  it('addFromNode deleteFromNode', () => {\n    const json = document.toJSON();\n    const value = {\n      fromId: 'start_0',\n      data: {\n        id: 'test',\n        type: 'noop',\n      },\n    };\n    flowOperationService.apply({\n      type: OperationType.addFromNode,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.deleteFromNode,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('addBlock deleteBlock', () => {\n    const json = document.toJSON();\n    const value = {\n      targetId: 'dynamicSplit_0',\n      blockData: {\n        id: 'test',\n      },\n    };\n    flowOperationService.apply({\n      type: OperationType.addBlock,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.deleteBlock,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('addBlock deleteBlock by index', () => {\n    const json = document.toJSON();\n    const value = {\n      targetId: 'dynamicSplit_0',\n      blockData: {\n        id: 'test',\n      },\n      index: 1,\n    };\n    flowOperationService.apply({\n      type: OperationType.addBlock,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.deleteBlock,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('moveNodes', () => {\n    const value = {\n      fromId: 'block_1',\n      toId: 'start_0',\n      nodeIds: ['noop_0'],\n    };\n    flowOperationService.apply({\n      type: OperationType.moveNodes,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.moveNodes,\n      value: {\n        ...value,\n        fromId: 'start_0',\n        toId: 'block_1',\n      },\n    });\n    expect(document.toJSON()).matchSnapshot();\n    // expect(document.toJSON()).toEqual(json);\n  });\n\n  it('moveBlock', () => {\n    document.fromJSON(baseMockAddBranch);\n    const json = document.toJSON();\n\n    flowOperationService.apply({\n      type: OperationType.moveBlock,\n      value: {\n        nodeId: 'block_0',\n        fromParentId: '$inlineBlocks$dynamicSplit_0',\n        fromIndex: 0,\n        toParentId: '$inlineBlocks$dynamicSplit_0',\n        toIndex: 1,\n      },\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.moveBlock,\n      value: {\n        nodeId: 'block_0',\n        fromParentId: '$inlineBlocks$dynamicSplit_0',\n        fromIndex: 1,\n        toParentId: '$inlineBlocks$dynamicSplit_0',\n        toIndex: 0,\n      },\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('dragNodes', () => {\n    document.fromJSON(baseMockAddBranch);\n\n    flowOperationService.dragNodes({\n      dropNode: document.getNode('block_1')!,\n      nodes: [document.getNode('block_0')!],\n    });\n    expect(document.toJSON()).matchSnapshot();\n\n    flowOperationService.dragNodes({\n      dropNode: document.getNode('block_1')!,\n      nodes: [document.getNode('block_2')!],\n    });\n    expect(document.toJSON()).matchSnapshot();\n  });\n\n  it('moveChildNodes', () => {\n    document.fromJSON(baseMockAddBranch);\n    const json = document.toJSON();\n\n    flowOperationService.apply({\n      type: OperationType.moveChildNodes,\n      value: {\n        nodeIds: ['block_0', 'block_1'],\n        fromParentId: '$inlineBlocks$dynamicSplit_0',\n        fromIndex: 0,\n        toParentId: '$inlineBlocks$dynamicSplit_0',\n        toIndex: 1,\n      },\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.moveChildNodes,\n      value: {\n        nodeIds: ['block_0', 'block_1'],\n        fromParentId: '$inlineBlocks$dynamicSplit_0',\n        fromIndex: 1,\n        toParentId: '$inlineBlocks$dynamicSplit_0',\n        toIndex: 0,\n      },\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('addNodes deleteNodes', () => {\n    const json = document.toJSON();\n    const value = {\n      fromId: 'start_0',\n      nodes: [\n        {\n          id: 'test1',\n          type: 'noop',\n        },\n        {\n          id: 'test2',\n          type: 'noop',\n        },\n      ],\n    };\n    flowOperationService.apply({\n      type: OperationType.addNodes,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.deleteNodes,\n      value,\n    });\n    expect(document.toJSON()).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('addChildNode deleteChildNode', () => {\n    const json = document.toJSON();\n    const value = {\n      parentId: 'noop_0',\n      data: {\n        id: 'test-node',\n      },\n      index: 0,\n    };\n    flowOperationService.apply({\n      type: OperationType.addChildNode,\n      value,\n    });\n    expect(document.getNode('noop_0')?.children.map((c) => c.toJSON())).matchSnapshot();\n    flowOperationService.apply({\n      type: OperationType.deleteChildNode,\n      value,\n    });\n    expect(document.getNode('noop_0')?.children.map((c) => c.toJSON())).matchSnapshot();\n    expect(document.toJSON()).toEqual(json);\n  });\n\n  it('addNode', () => {\n    const fn = vi.fn();\n    flowOperationService.onNodeAdd(fn);\n    const child = flowOperationService.addNode(\n      {\n        id: 'test',\n        type: 'test',\n      },\n      {\n        parent: 'root',\n        index: 1,\n        hidden: true,\n      }\n    );\n    expect(child.id).toEqual('test');\n    expect(child.pre?.id).toEqual('start_0');\n    expect(child.next?.id).toEqual('dynamicSplit_0');\n    expect(child.hidden).toEqual(true);\n    expect(fn).toBeCalledTimes(1);\n  });\n\n  it('moveNode should fire event', () => {\n    let event: OnNodeMoveEvent;\n\n    flowOperationService.onNodeMove((e) => {\n      event = e;\n      expect(event.node.id === 'noop_0');\n      expect(event.fromIndex === 1);\n      expect(event.toParent.id === 'block_0');\n      expect(event.toIndex === 1);\n    });\n    flowOperationService.moveNode('noop_0', { parent: 'block_0' });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/document\",\n  \"version\": \"0.1.8\",\n  \"description\": \"automation flow engine\",\n  \"keywords\": [\n    \"flow\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"test:update\": \"vitest run --update\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/datas/flow-node-render-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Compare, Disposable, domUtils, Emitter } from '@flowgram.ai/utils';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport { FlowNodeBaseType } from '../typings';\nimport type { FlowNodeEntity } from '../entities';\nimport { FlowNodeTransformData } from './index';\n\nexport interface FlowNodeRenderSchema {\n  addable: boolean; // 是否可添加节点\n  expandable: boolean; // 是否可展开\n  collapsed?: boolean; // 复合节点是否收起\n  expanded: boolean;\n  activated: boolean; // 是否高亮节点\n  hovered: boolean; // 是否悬浮在节点上\n  dragging: boolean; // 是否正在拖拽\n  stackIndex: number; // 渲染层级\n  extInfo?: Record<string, any>; // 扩展渲染状态字段\n}\n\n/**\n * 节点渲染状态相关数据\n */\nexport class FlowNodeRenderData extends EntityData<FlowNodeRenderSchema> {\n  static type = 'FlowNodeRenderData';\n\n  declare entity: FlowNodeEntity;\n\n  private _node?: HTMLDivElement;\n\n  protected onExtInfoChangeEmitter = new Emitter<{ newInfo: any; oldInfo: any }>();\n\n  readonly onExtInfoChange = this.onExtInfoChangeEmitter.event;\n\n  get key(): string {\n    return this.entity.id;\n  }\n\n  getDefaultData(): FlowNodeRenderSchema {\n    const { addable, expandable, defaultExpanded } = this.entity.getNodeMeta();\n    return {\n      addable,\n      expandable,\n      expanded: defaultExpanded || false,\n      activated: false,\n      hovered: false,\n      dragging: false,\n      stackIndex: 0,\n    };\n  }\n\n  updateExtInfo(info: Record<string, any>, fullUpdate?: boolean) {\n    const oldInfo = this.data.extInfo;\n    const newInfo = fullUpdate ? info : { ...oldInfo, ...info };\n    if (Compare.isChanged(oldInfo, newInfo)) {\n      this.update({\n        extInfo: newInfo,\n      });\n      this.onExtInfoChangeEmitter.fire({ oldInfo, newInfo });\n    }\n  }\n\n  getExtInfo(): Record<string, any> | undefined {\n    return this.data.extInfo;\n  }\n\n  constructor(entity: FlowNodeEntity) {\n    super(entity);\n    this.toDispose.push(\n      Disposable.create(() => {\n        if (this._node) this._node.remove();\n      })\n    );\n  }\n\n  get addable(): boolean {\n    return this.data.addable;\n  }\n\n  get expandable(): boolean {\n    return this.data.expandable;\n  }\n\n  get draggable(): boolean {\n    const { draggable } = this.entity.getNodeMeta();\n\n    if (typeof draggable === 'function') {\n      return draggable(this.entity);\n    }\n\n    return draggable;\n  }\n\n  get expanded(): boolean {\n    return this.data.expanded;\n  }\n\n  set expanded(expanded: boolean) {\n    if (this.expandable && this.data.expanded !== expanded) {\n      this.data.expanded = expanded;\n      this.fireChange();\n    }\n  }\n\n  toggleExpand() {\n    this.expanded = !this.expanded;\n  }\n\n  mouseLeaveTimeout?: ReturnType<typeof setTimeout>;\n\n  toggleMouseEnter(silent = false) {\n    this.entity.document.renderState.setNodeHovered(this.entity);\n    if (silent) return;\n    const transform = this.entity.getData(FlowNodeTransformData)!;\n    if (transform.renderState.hidden) {\n      return;\n    }\n    if (this.mouseLeaveTimeout) {\n      clearTimeout(this.mouseLeaveTimeout);\n      this.mouseLeaveTimeout = undefined;\n    }\n\n    transform.renderState.hovered = true;\n\n    if (this.entity.isFirst && this.entity.parent?.id !== 'root') {\n      // 分支中第一个节点 hover，parent activated 设置为 true\n      transform.parent!.renderState.activated = true;\n    } else {\n      transform.renderState.activated = true;\n    }\n  }\n\n  toggleMouseLeave(silent = false) {\n    this.entity.document.renderState.setNodeHovered(undefined);\n    if (silent) return;\n    const transform = this.entity.getData(FlowNodeTransformData)!;\n    this.mouseLeaveTimeout = setTimeout(() => {\n      transform.renderState.hovered = false;\n\n      if (this.entity.isFirst && this.entity.parent?.id !== 'root') {\n        transform.parent!.renderState.activated = false;\n      }\n      transform.renderState.activated = false;\n    }, 200);\n  }\n\n  get hidden(): boolean {\n    return this.entity.hidden;\n  }\n\n  set hovered(hovered: boolean) {\n    this.data.hovered = hovered;\n    this.fireChange();\n  }\n\n  get hovered() {\n    return this.data.hovered;\n  }\n\n  get dragging(): boolean {\n    return this.data.dragging;\n  }\n\n  set dragging(dragging: boolean) {\n    if (this.data.dragging !== dragging) {\n      this.data.dragging = dragging;\n      this.fireChange();\n    }\n  }\n\n  set activated(activated: boolean) {\n    if (this.entity.flowNodeType === FlowNodeBaseType.BLOCK_ICON && this.entity.parent) {\n      this.entity.parent.getData<FlowNodeRenderData>(FlowNodeRenderData)!.activated = activated;\n      return;\n    }\n    if (this.data.activated !== activated) {\n      this.data.activated = activated;\n      this.fireChange();\n    }\n  }\n\n  get activated() {\n    const { entity } = this;\n    if (entity.parent && entity.parent.getData<FlowNodeRenderData>(FlowNodeRenderData)!.activated) {\n      return true;\n    }\n    return this.data.activated;\n  }\n\n  get stackIndex(): number {\n    return this.data.stackIndex;\n  }\n\n  set stackIndex(index: number) {\n    this.data.stackIndex = index;\n  }\n\n  get lineActivated() {\n    const { activated } = this;\n    if (!activated) return false;\n    // 只有 parent 高亮的情况才高亮下面的线条，否则只高亮 node\n    // inlineBlock 仅看自身\n    // 圈选情况下个节点被高量，则也跟着高量\n    return Boolean(\n      this.entity.parent?.getData(FlowNodeRenderData)?.activated ||\n        this.entity.isInlineBlock ||\n        this.entity.next?.getData(FlowNodeRenderData)!.activated\n    );\n  }\n\n  get node(): HTMLDivElement {\n    if (this._node) return this._node;\n    this._node = domUtils.createDivWithClass('gedit-flow-activity-node');\n    this._node.dataset.testid = 'sdk.workflow.canvas.node';\n    this._node.dataset.nodeId = this.entity.id;\n    return this._node;\n  }\n\n  dispose() {\n    super.dispose();\n    this.onExtInfoChangeEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/datas/flow-node-transform-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable, type IPoint, Rectangle } from '@flowgram.ai/utils';\nimport {\n  Bounds,\n  EntityData,\n  PositionSchema,\n  type SizeSchema,\n  TransformData,\n} from '@flowgram.ai/core';\n\nimport type { FlowNodeEntity } from '../entities';\nimport { FlowNodeRenderData } from './flow-node-render-data';\n\nexport interface FlowNodeTransformSchema {\n  size: SizeSchema; // 当前节点大小\n}\n\nexport class FlowNodeTransformData extends EntityData<FlowNodeTransformSchema> {\n  static type = 'FlowNodeTransformData';\n\n  // 这里加 declare 原因：覆盖了 EntityData 默认的 EntityType，如果不声明会报 TS2612 错误\n  declare entity: FlowNodeEntity;\n\n  transform: TransformData;\n\n  renderState: FlowNodeRenderData;\n\n  localDirty = true;\n\n  get origin() {\n    return this.transform.origin;\n  }\n\n  get key(): string {\n    return this.entity.id;\n  }\n\n  getDefaultData(): FlowNodeTransformSchema {\n    const { size, hidden } = this.entity.getNodeMeta();\n    // 更新默认 size 大小\n    return {\n      size: !hidden ? { ...size } : { width: 0, height: 0 },\n    };\n  }\n\n  constructor(entity: FlowNodeEntity) {\n    super(entity);\n    const { origin } = this.entity.getNodeMeta();\n    this.transform = this.entity.addData<TransformData>(TransformData);\n    this.transform.changeLocked = true;\n    this.transform.update({ origin: { ...origin } });\n    this.transform.changeLocked = false;\n    this.renderState = this.entity.addData<FlowNodeRenderData>(FlowNodeRenderData);\n    this.bindChange(this.transform);\n    // 删除节点要让下一个节点或者父节点变成 dirty\n    this.toDispose.push(\n      Disposable.create(() => {\n        const { next, parent } = this;\n        if (next) next.localDirty = true;\n        if (parent) parent.localDirty = true;\n      })\n    );\n    // this.bindChange(this.renderState)\n  }\n\n  /**\n   * 获取节点是否展开\n   */\n  get collapsed(): boolean {\n    return this.entity.collapsed;\n  }\n\n  set collapsed(collapsed: boolean) {\n    this.entity.collapsed = collapsed;\n    this.localDirty = true;\n\n    // 第一个子节点也设置为 dirty\n    if (this.firstChild) this.firstChild.localDirty = true;\n\n    this.fireChange();\n  }\n\n  /**\n   * 获取节点的大小\n   */\n  get size(): SizeSchema {\n    return this.entity.memoGlobal<SizeSchema>('size', () => {\n      if (this.isContainer) return this.transform.localSize;\n      return this.data.size;\n    });\n  }\n\n  get position(): PositionSchema {\n    const { position } = this.transform;\n    return {\n      x: position.x,\n      y: position.y,\n    };\n  }\n\n  set position(position: PositionSchema) {\n    this.transform.update({\n      position,\n    });\n  }\n\n  set size(size: SizeSchema) {\n    const { width, height } = this.data.size;\n    // Container size 由子节点决定\n    if (this.isContainer) return;\n    if (size.width !== width || size.height !== height) {\n      this._data.size = { ...size };\n      this.localDirty = true;\n      this.fireChange();\n    }\n  }\n\n  get inputPoint() {\n    return this.entity.memoGlobal<IPoint>('inputPoint', () => {\n      const { getInputPoint } = this.entity.getNodeRegistry();\n      return getInputPoint\n        ? getInputPoint(this, this.entity.document.layout)\n        : this.defaultInputPoint;\n    });\n  }\n\n  get defaultInputPoint() {\n    return this.entity.memoGlobal<IPoint>('defaultInputPoint', () =>\n      this.entity.document.layout.getDefaultInputPoint(this.entity)\n    );\n  }\n\n  get defaultOutputPoint() {\n    return this.entity.memoGlobal<IPoint>('defaultOutputPoint', () =>\n      this.entity.document.layout.getDefaultOutputPoint(this.entity)\n    );\n  }\n\n  get outputPoint() {\n    return this.entity.memoGlobal<IPoint>('outputPoint', () => {\n      const { getOutputPoint } = this.entity.getNodeRegistry();\n      return getOutputPoint\n        ? getOutputPoint(this, this.entity.document.layout)\n        : this.defaultOutputPoint;\n    });\n  }\n\n  /**\n   * 原点的最左偏移\n   */\n  get originDeltaX(): number {\n    return this.entity.memoLocal<number>('originDeltaX', () => {\n      const { children } = this;\n      const { getOriginDeltaX } = this.entity.getNodeRegistry();\n      if (getOriginDeltaX) return getOriginDeltaX(this, this.entity.document.layout);\n      // 没有子节点，则采用自身的原点偏移\n      if (children.length === 0) {\n        return -this.size.width * this.origin.x;\n      }\n      // 采用子节点的最左偏移量来计算\n      if (children.length === 1) return children[0].originDeltaX;\n      // 这里代表水平偏移，则采用第一个节点加上自身偏移\n      if (this.entity.isInlineBlocks && children.length > 1) {\n        return children[0].originDeltaX + this.transform.position.x;\n      }\n      return children.reduce((res: undefined | number, child) => {\n        const deltaX = child.originDeltaX;\n        return res === undefined || deltaX < res ? deltaX : (res as number);\n      }, undefined) as number;\n    });\n  }\n\n  /**\n   * 原点 y 轴偏移\n   */\n  get originDeltaY(): number {\n    return this.entity.memoLocal<number>('originDeltaY', () => {\n      const { children } = this;\n      const { getOriginDeltaY } = this.entity.getNodeRegistry();\n      if (getOriginDeltaY) return getOriginDeltaY(this, this.entity.document.layout);\n      // 没有子节点，则采用自身的原点偏移\n      if (children.length === 0) {\n        return -this.size.height * this.origin.y;\n      }\n      // 采用子节点的最上偏移量来计算\n      if (children.length === 1) return children[0].originDeltaY;\n      // 这里代表水平偏移，则采用第一个节点加上自身偏移\n      if (this.entity.isInlineBlocks && children.length > 1) {\n        return children[0].originDeltaY + this.transform.position.y;\n      }\n      return children.reduce((res: undefined | number, child) => {\n        const deltaY = child.originDeltaY;\n        return res === undefined || deltaY < res ? deltaY : (res as number);\n      }, undefined) as number;\n    });\n  }\n\n  /**\n   * 绝对坐标 bbox, 不包含自身的 spacing(marginBottom), 但是包含 inlineSpacing 和 子节点的 spacing\n   */\n  get bounds(): Rectangle {\n    return this.entity.memoGlobal<Rectangle>('bounds', () => {\n      const { transform } = this;\n\n      if (this.isContainer) {\n        const childrenRects = transform.children.map(\n          (c) => c.entity.getData<FlowNodeTransformData>(FlowNodeTransformData)!.boundsWithPadding\n        );\n        // Container 要加上 inlineSpacing\n        return Rectangle.enlarge(childrenRects).withPadding(this.padding);\n      }\n      return transform.bounds; // 单个节点取默认的 bounds\n    });\n  }\n\n  get boundsWithPadding(): Rectangle {\n    return this.entity.memoGlobal<Rectangle>('boundsWithPadding', () => {\n      const { transform } = this;\n\n      if (this.isContainer) {\n        const childrenRects = transform.children.map(\n          (c) => c.entity.getData<FlowNodeTransformData>(FlowNodeTransformData)!.boundsWithPadding\n        );\n        return Rectangle.enlarge(childrenRects).withPadding(this.padding);\n      }\n      return transform.bounds.clone().withPadding(this.padding);\n    });\n  }\n\n  get isContainer(): boolean {\n    return this.transform.isContainer;\n  }\n\n  /**\n   * 相对坐标 bbox, 这里的 localBounds 会加入 padding 一起算\n   */\n  get localBounds(): Rectangle {\n    return this.entity.memoLocal<Rectangle>('localBounds', () => {\n      const { transform } = this;\n\n      if (this.isContainer) {\n        const childrenRects = transform.children.map(\n          (c) => c.entity.getData<FlowNodeTransformData>(FlowNodeTransformData)!.localBounds\n        );\n        const childrenBounds = Rectangle.enlarge(childrenRects).withPadding(this.padding);\n        return Bounds.applyMatrix(childrenBounds, transform.localTransform);\n      }\n\n      return transform.localBounds.clone().withPadding(this.padding);\n    });\n  }\n\n  get padding() {\n    return this.entity.document.layout.getPadding(this.entity);\n  }\n\n  setParentTransform(transform?: FlowNodeTransformData): void {\n    // 拖拽父元素变化需要重新布局\n    if (this.transform.parent !== transform?.transform) {\n      this.localDirty = true;\n    }\n    this.transform.setParent(transform?.transform);\n  }\n\n  get spacing(): number {\n    const { spacing } = this.entity.getNodeMeta();\n    return typeof spacing === 'function' ? spacing(this) : spacing;\n  }\n\n  get inlineSpacingPre(): number {\n    const { inlineSpacingPre } = this.entity.getNodeMeta();\n    return typeof inlineSpacingPre === 'function' ? inlineSpacingPre(this) : inlineSpacingPre;\n  }\n\n  get inlineSpacingAfter(): number {\n    const { inlineSpacingAfter } = this.entity.getNodeMeta();\n    return typeof inlineSpacingAfter === 'function' ? inlineSpacingAfter(this) : inlineSpacingAfter;\n  }\n\n  get minInlineBlockSpacing(): number {\n    const { minInlineBlockSpacing } = this.entity.getNodeMeta();\n    return typeof minInlineBlockSpacing === 'function'\n      ? minInlineBlockSpacing(this)\n      : minInlineBlockSpacing;\n  }\n\n  get children(): FlowNodeTransformData[] {\n    return this.entity.children.map(\n      (child) => child.getData<FlowNodeTransformData>(FlowNodeTransformData)!\n    );\n  }\n\n  /**\n   * 上一个节点的 transform 数据\n   */\n  get pre(): FlowNodeTransformData | undefined {\n    return this.entity.pre?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  get originParent(): FlowNodeTransformData | undefined {\n    return this.entity.originParent?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  get isFirst(): boolean {\n    return this.entity.isFirst;\n  }\n\n  get isLast(): boolean {\n    return this.entity.isLast;\n  }\n\n  get lastChild(): FlowNodeTransformData | undefined {\n    return this.entity.lastChild?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  get firstChild(): FlowNodeTransformData | undefined {\n    return this.entity.firstChild?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  /**\n   * 下一个节点的 transform 数据\n   */\n  get next(): FlowNodeTransformData | undefined {\n    return this.entity.next?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  /**\n   * parent 节点的 transform 数据\n   */\n  get parent(): FlowNodeTransformData | undefined {\n    return this.entity.parent?.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/datas/flow-node-transition-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport {\n  FlowNodeBaseType,\n  type FlowTransitionLabel,\n  FlowTransitionLabelEnum,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n} from '../typings';\nimport { type FlowNodeEntity } from '../entities';\nimport { FlowNodeTransformData } from './flow-node-transform-data';\nimport { FlowNodeRenderData } from './flow-node-render-data';\n\nexport interface FlowNodeTransitionSchema {}\n\n// 画线到下一个节点\nexport const drawLineToNext = (transition: FlowNodeTransitionData) => {\n  const { transform } = transition;\n\n  // 默认绘制一根连接到下一个节点的线条\n  const currentOutput = transform.outputPoint;\n  if (transform.next) {\n    return [\n      {\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: currentOutput,\n        to: transform.next.inputPoint,\n      },\n    ];\n  }\n\n  return [];\n};\n\n// 画线到父节点的结尾\nexport const drawLineToBottom = (transition: FlowNodeTransitionData) => {\n  const { transform } = transition;\n\n  const currentOutput = transform.outputPoint;\n  const isParentRoot = transform.parent?.entity.flowNodeType === FlowNodeBaseType.ROOT;\n  const parentOutput = transform.parent?.outputPoint;\n  if (\n    !isParentRoot &&\n    !transform.next &&\n    parentOutput &&\n    !new Point().copyFrom(currentOutput).equals(parentOutput) &&\n    !transition.isNodeEnd\n  ) {\n    return [\n      {\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: currentOutput,\n        to: parentOutput,\n      },\n    ];\n  }\n\n  return [];\n};\n\nexport class FlowNodeTransitionData extends EntityData<FlowNodeTransitionSchema> {\n  static type = 'FlowNodeTransitionData';\n\n  declare entity: FlowNodeEntity;\n\n  // 当前节点的 transform\n  declare transform: FlowNodeTransformData;\n\n  declare renderData: FlowNodeRenderData;\n\n  getDefaultData(): FlowNodeTransitionSchema {\n    return {};\n  }\n\n  formatLines(lines: FlowTransitionLine[]) {\n    if (this.entity.document.options?.formatNodeLines) {\n      return this.entity.document.options?.formatNodeLines?.(this.entity, lines);\n    }\n    return lines;\n  }\n\n  formatLabels(labels: FlowTransitionLabel[]) {\n    if (this.entity.document.options.formatNodeLabels) {\n      return this.entity.document.options?.formatNodeLabels?.(this.entity, labels);\n    }\n    return labels;\n  }\n\n  get lines(): FlowTransitionLine[] {\n    return this.entity.memoGlobal<FlowTransitionLine[]>('lines', () => {\n      const { getChildLines } = this.entity.parent?.getNodeRegistry() || {};\n\n      if (getChildLines) {\n        return this.formatLines(getChildLines(this, this.entity.document.layout));\n      }\n\n      const { getLines } = this.entity.getNodeRegistry();\n\n      if (getLines) {\n        return this.formatLines(getLines(this, this.entity.document.layout));\n      }\n\n      // 横向布局不画线\n      if (this.transform.entity.isInlineBlock) {\n        return [];\n      }\n\n      return this.formatLines([...drawLineToNext(this), ...drawLineToBottom(this)]);\n    });\n  }\n\n  get labels() {\n    return this.entity.memoGlobal<FlowTransitionLabel[]>('labels', () => {\n      const { getChildLabels } = this.entity.parent?.getNodeRegistry() || {};\n\n      if (getChildLabels) {\n        return this.formatLabels(getChildLabels(this, this.entity.document.layout));\n      }\n\n      const { getLabels } = this.entity.getNodeRegistry();\n\n      if (getLabels) {\n        return this.formatLabels(getLabels(this, this.entity.document.layout));\n      }\n\n      // 横向布局不画加号\n      if (this.transform.entity.isInlineBlock) {\n        return [];\n      }\n\n      // 默认在中间点添加一个加号\n      const currentOutput = this.transform.outputPoint;\n      if (this.transform.next) {\n        return this.formatLabels([\n          {\n            offset: Point.getMiddlePoint(currentOutput, this.transform.next.inputPoint),\n            type: FlowTransitionLabelEnum.ADDER_LABEL,\n          },\n        ]);\n      }\n\n      const parentOutput = this.transform.parent?.outputPoint;\n      if (\n        parentOutput &&\n        !new Point().copyFrom(currentOutput).equals(parentOutput) &&\n        !this.isNodeEnd\n      ) {\n        return this.formatLabels([\n          {\n            offset: parentOutput,\n            type: FlowTransitionLabelEnum.ADDER_LABEL,\n          },\n        ]);\n      }\n\n      return [];\n    });\n  }\n\n  constructor(entity: FlowNodeEntity) {\n    super(entity);\n    this.transform = this.entity.addData<FlowNodeTransformData>(FlowNodeTransformData);\n    this.renderData = this.entity.addData<FlowNodeRenderData>(FlowNodeRenderData);\n\n    this.bindChange(this.transform);\n    this.bindChange(this.renderData);\n  }\n\n  get collapsed() {\n    return this.entity.collapsed;\n  }\n\n  get isNodeEnd(): boolean {\n    return this.entity.isNodeEnd;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/datas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-node-transform-data';\nexport * from './flow-node-transition-data';\nexport * from './flow-node-render-data';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/entities/flow-document-transformer-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter } from '@flowgram.ai/utils';\nimport { ConfigEntity, type EntityOpts } from '@flowgram.ai/core';\n\nimport type { FlowDocument } from '../flow-document';\nimport { FlowNodeTransformData } from '../datas';\n\ninterface FlowDocumentTransformerEntityConfig extends EntityOpts {\n  document: FlowDocument;\n}\n\n/**\n * 用于通知所有 layer 更新\n */\nexport class FlowDocumentTransformerEntity extends ConfigEntity<\n  {\n    loading: boolean;\n    treeVersion: number;\n  },\n  FlowDocumentTransformerEntityConfig\n> {\n  static type = 'FlowDocumentTransformerEntity';\n\n  protected onRefreshEmitter = new Emitter<void>();\n\n  protected lastTransformVersion = -1;\n\n  protected lastTreeVersion = -1;\n\n  document: FlowDocument;\n\n  readonly onRefresh = this.onRefreshEmitter.event;\n\n  constructor(conf: FlowDocumentTransformerEntityConfig) {\n    super(conf);\n    this.document = conf.document;\n    this.toDispose.push(\n      this.document.originTree.onTreeChange(() => {\n        this.config.treeVersion += 1;\n        this.fireChange();\n      })\n    );\n    this.toDispose.push(this.onRefreshEmitter);\n  }\n\n  getDefaultConfig(): { loading: boolean; treeVersion: number } {\n    return {\n      loading: true,\n      treeVersion: 0,\n    };\n  }\n\n  get loading(): boolean {\n    return this.config.loading;\n  }\n\n  set loading(loading) {\n    if (this.config.loading !== loading) {\n      this.config.loading = loading;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * 更新矩阵结构 (这个只有在树结构变化时候才会触发，如：添加节点、删除节点、改变位置节点)\n   */\n  updateTransformsTree(): void {\n    // 更新 node 结构树\n    this.document.renderTree.traverse((node, depth, index) => {\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n      // 收起时清空子节点\n      if (transform.collapsed) {\n        transform.transform.clearChildren();\n      }\n      if (node.parent) {\n        transform.setParentTransform(node.parent!.getData(FlowNodeTransformData));\n      }\n      // 更新 index\n      node.index = index;\n    });\n  }\n\n  clear(): void {\n    this.lastTreeVersion = -1;\n    this.lastTransformVersion = -1;\n  }\n\n  isTreeDirty(): boolean {\n    const transformVersion = this.entityManager.getEntityDataVersion(FlowNodeTransformData);\n    const isTreeVersionChanged = this.lastTreeVersion !== this.config.treeVersion;\n    const isTransformVersionChanged = this.lastTransformVersion !== transformVersion;\n    return isTreeVersionChanged || isTransformVersionChanged;\n  }\n\n  /**\n   * 刷新节点的相对偏移\n   */\n  refresh(): void {\n    const transformVersion = this.entityManager.getEntityDataVersion(FlowNodeTransformData);\n\n    const isTreeVersionChanged = this.lastTreeVersion !== this.config.treeVersion;\n    const isTransformVersionChanged = this.lastTransformVersion !== transformVersion;\n\n    this.entityManager.changeEntityLocked = true;\n    if (isTreeVersionChanged) {\n      this.document.renderTree.updateRenderStruct(); // 重新调整 renderTree 结构\n      this.updateTransformsTree(); // 更新树结构\n      this.lastTreeVersion = this.config.treeVersion;\n    }\n\n    if (isTreeVersionChanged || isTransformVersionChanged) {\n      // 位置计算不需要重新触发 Layer 刷新\n      this.document.layout.update(); // 更新布局\n      this.lastTransformVersion = this.entityManager.getEntityDataVersion(FlowNodeTransformData);\n      this.lastTreeVersion = this.config.treeVersion;\n      this.onRefreshEmitter.fire();\n    }\n\n    this.entityManager.changeEntityLocked = false;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/entities/flow-node-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Event, type Rectangle } from '@flowgram.ai/utils';\nimport { Entity, type EntityOpts } from '@flowgram.ai/core';\n\nimport {\n  FlowLayoutDefault,\n  FlowNodeJSON,\n  FlowNodeMeta,\n  FlowNodeRegistry,\n  FlowNodeType,\n} from '../typings';\nimport type { FlowDocument } from '../flow-document';\nimport { FlowNodeRenderData, FlowNodeTransformData } from '../datas';\n\nexport interface FlowNodeEntityConfig extends EntityOpts {\n  document: FlowDocument;\n  flowNodeType: FlowNodeType;\n  originParent?: FlowNodeEntity;\n  meta?: FlowNodeMeta;\n}\n\nexport interface FlowNodeInitData {\n  originParent?: FlowNodeEntity;\n  parent?: FlowNodeEntity;\n  hidden?: boolean;\n  meta?: FlowNodeMeta;\n  index?: number;\n}\n\nexport class FlowNodeEntity extends Entity<FlowNodeEntityConfig> {\n  private _memoLocalCache = new Map<string, any>();\n\n  private _memoGlobalCache = new Map<string, any>();\n\n  static type = 'FlowNodeEntity';\n\n  private _registerCache?: FlowNodeRegistry;\n\n  private _metaCache?: Required<FlowNodeMeta>;\n\n  metaFromJSON?: FlowNodeMeta;\n\n  /**\n   * 真实的父节点，条件块在内部会创建一些空的块节点，这些块需要关联它真实的父亲节点\n   */\n  originParent?: FlowNodeEntity;\n\n  flowNodeType: FlowNodeType = 'unknown'; // 流程类型\n\n  /**\n   * 是否隐藏\n   */\n  private _hidden = false;\n\n  index = -1;\n\n  /**\n   * 文档引用\n   */\n  document: FlowDocument;\n\n  constructor(conf: FlowNodeEntityConfig) {\n    super(conf);\n    this.document = conf.document;\n    this.flowNodeType = conf.flowNodeType;\n    this.originParent = conf.originParent;\n    this.metaFromJSON = conf.meta;\n    this.onDispose(() => {\n      this.document.originTree\n        .getChildren(this)\n        .slice()\n        .forEach((child) => {\n          child.dispose();\n        });\n      this.document.originTree.remove(this, false);\n      this.originParent = undefined;\n    });\n  }\n\n  initData(initConf: FlowNodeInitData): void {\n    if (initConf.originParent !== this.originParent) {\n      this.originParent = initConf.originParent;\n      this._registerCache = undefined;\n    }\n    if (initConf.parent) {\n      initConf.parent.addChild(this, initConf.index);\n    }\n    // TODO 这个 meta 不会触发 data 数据更新\n    if (initConf.meta !== this.metaFromJSON) {\n      this._metaCache = undefined;\n      this.metaFromJSON = initConf.meta;\n    }\n    this._hidden = !!(this.getNodeMeta().hidden || initConf.hidden);\n  }\n\n  get isStart(): boolean {\n    return this.getNodeMeta().isStart;\n  }\n\n  get isFirst(): boolean {\n    return !this.pre;\n  }\n\n  get isLast(): boolean {\n    return !this.next;\n  }\n\n  /**\n   * 子节点采用水平布局\n   */\n  get isInlineBlocks(): boolean {\n    const originIsInlineBlocks = this.getNodeMeta().isInlineBlocks;\n    return typeof originIsInlineBlocks === 'function'\n      ? originIsInlineBlocks(this)\n      : originIsInlineBlocks;\n  }\n\n  /**\n   * 水平节点\n   */\n  get isInlineBlock(): boolean {\n    const parent = this.document.renderTree.getParent(this);\n    return !!(parent && parent.isInlineBlocks);\n  }\n\n  /**\n   * 节点结束标记\n   * - 当前节点是结束节点\n   * - 当前节点最后一个节点包含结束标记\n   * - 当前节点为 inlineBlock，每一个 block 包含结束标记\n   *\n   * 由子元素确定，因此使用 memoLocal\n   */\n  get isNodeEnd(): boolean {\n    return this.memoLocal<boolean>('isNodeEnd', () => {\n      if (this.getNodeMeta().isNodeEnd) {\n        return true;\n      }\n\n      if (this.isInlineBlocks && this.collapsedChildren.length) {\n        return this.collapsedChildren.every((child) => child.isNodeEnd);\n      }\n\n      if (this.lastCollapsedChild) {\n        return this.lastCollapsedChild.isNodeEnd;\n      }\n\n      return false;\n    });\n  }\n\n  /**\n   * 添加 子节点\n   *\n   * @param child 插入节点\n   */\n  addChild(child: FlowNodeEntity, index?: number) {\n    if (child.parent === this) return;\n    this.document.originTree.addChild(this, child, index);\n  }\n\n  get hasChild(): boolean {\n    return this.children.length > 0;\n  }\n\n  get pre(): FlowNodeEntity | undefined {\n    return this.document.renderTree.getPre(this);\n  }\n\n  get next(): FlowNodeEntity | undefined {\n    return this.document.renderTree.getNext(this);\n  }\n\n  get parent(): FlowNodeEntity | undefined {\n    return this.document.renderTree.getParent(this);\n  }\n\n  getNodeRegistry<M extends FlowNodeRegistry = FlowNodeRegistry & { meta: FlowNodeMeta }>(): M {\n    if (this._registerCache) return this._registerCache as M;\n    this._registerCache = this.document.getNodeRegistry(this.flowNodeType, this.originParent);\n    return this._registerCache as M;\n  }\n\n  /**\n   * @deprecated\n   * use getNodeRegistry instead\n   */\n  getNodeRegister<M extends FlowNodeRegistry = FlowNodeRegistry>(): M {\n    return this.getNodeRegistry<M>();\n  }\n\n  getNodeMeta<M extends FlowNodeMeta = FlowNodeMeta>(): M & Required<FlowNodeMeta> {\n    if (this._metaCache) return this._metaCache as M & Required<FlowNodeMeta>;\n    if (this.metaFromJSON) {\n      this._metaCache = {\n        ...this.getNodeRegistry().meta,\n        ...this.metaFromJSON,\n      } as M & Required<FlowNodeMeta>;\n    } else {\n      this._metaCache = this.getNodeRegistry().meta as M & Required<FlowNodeMeta>;\n    }\n    return this._metaCache as M & Required<FlowNodeMeta>;\n  }\n\n  /**\n   * 获取所有子节点，包含 child 及其所有兄弟节点\n   */\n  get allChildren(): FlowNodeEntity[] {\n    const children: FlowNodeEntity[] = [];\n    for (const child of this.children) {\n      children.push(child);\n      children.push(...child.allChildren);\n    }\n    return children;\n  }\n\n  /**\n   * 获取所有收起的子节点，包含 child 及其所有兄弟节点\n   */\n  get allCollapsedChildren(): FlowNodeEntity[] {\n    const children: FlowNodeEntity[] = [];\n    for (const child of this.collapsedChildren) {\n      children.push(child);\n      children.push(...child.allCollapsedChildren);\n    }\n    return children;\n  }\n\n  /**\n   *\n   * Get child blocks\n   *\n   * use `blocks` instead\n   * @deprecated\n   */\n\n  get collapsedChildren(): FlowNodeEntity[] {\n    return this.document.renderTree.getCollapsedChildren(this);\n  }\n\n  /**\n   * Get child blocks\n   */\n  get blocks(): FlowNodeEntity[] {\n    return this.collapsedChildren;\n  }\n\n  /**\n   * Get last block\n   */\n  get lastBlock(): FlowNodeEntity | undefined {\n    return this.lastCollapsedChild;\n  }\n\n  /**\n   * use `lastBlock` instead\n   */\n  get lastCollapsedChild(): FlowNodeEntity | undefined {\n    const { collapsedChildren } = this;\n    return collapsedChildren[collapsedChildren.length - 1];\n  }\n\n  /**\n   * 获取子节点，如果子节点收起来，则会返回 空数组\n   */\n  get children(): FlowNodeEntity[] {\n    return this.document.renderTree.getChildren(this);\n  }\n\n  get lastChild(): FlowNodeEntity | undefined {\n    const { children } = this;\n    return children[children.length - 1];\n  }\n\n  get firstChild(): FlowNodeEntity | undefined {\n    return this.children[0];\n  }\n\n  memoLocal<T>(key: string, fn: () => T): T {\n    if (this._memoLocalCache.has(key)) {\n      return this._memoLocalCache.get(key) as T;\n    }\n    const data = fn();\n    this._memoLocalCache.set(key, data);\n    return data as T;\n  }\n\n  memoGlobal<T>(key: string, fn: () => T): T {\n    if (this._memoGlobalCache.has(key)) {\n      return this._memoGlobalCache.get(key) as T;\n    }\n    const data = fn();\n    this._memoGlobalCache.set(key, data);\n    return data as T;\n  }\n\n  clearMemoGlobal() {\n    this._memoGlobalCache.clear();\n  }\n\n  clearMemoLocal() {\n    this._memoLocalCache.clear();\n  }\n\n  get childrenLength() {\n    return this.children.length;\n  }\n\n  get collapsed(): boolean {\n    if (this.document.renderTree.isCollapsed(this)) return true;\n    return !!this.parent?.collapsed;\n  }\n\n  set collapsed(collapsed) {\n    this.document.renderTree.setCollapsed(this, collapsed);\n    this.clearMemoGlobal();\n    this.clearMemoLocal();\n  }\n\n  get hidden(): boolean {\n    return this._hidden;\n  }\n\n  // 展开该节点\n  openInsideCollapsed() {\n    this.document.renderTree.openNodeInsideCollapsed(this);\n  }\n\n  /**\n   * 可以重载\n   */\n  getJSONData(): any {\n    return this.getExtInfo();\n  }\n\n  /**\n   * 生成 JSON\n   * @param newId\n   */\n  toJSON(): FlowNodeJSON {\n    return this.document.toNodeJSON(this);\n  }\n\n  get isVertical(): boolean {\n    return this.document.layout.name === FlowLayoutDefault.VERTICAL_FIXED_LAYOUT;\n  }\n\n  /**\n   * 修改节点扩展信息\n   * @param info\n   */\n  updateExtInfo<T extends Record<string, any> = Record<string, any>>(\n    extInfo: T,\n    fullUpdate?: boolean\n  ): void {\n    this.getData(FlowNodeRenderData).updateExtInfo(extInfo, fullUpdate);\n  }\n\n  /**\n   * 获取节点扩展信息\n   */\n  getExtInfo<T extends Record<string, any> = Record<string, any>>(): T {\n    return this.getData<FlowNodeRenderData>(FlowNodeRenderData).getExtInfo() as T;\n  }\n\n  get onExtInfoChange(): Event<{ newInfo: any; oldInfo: any }> {\n    return this.renderData.onExtInfoChange;\n  }\n\n  /**\n   * 获取渲染数据\n   */\n  get renderData(): FlowNodeRenderData {\n    return this.getData(FlowNodeRenderData);\n  }\n\n  /**\n   * 获取位置大小数据\n   */\n  get transform(): FlowNodeTransformData {\n    return this.getData(FlowNodeTransformData);\n  }\n\n  /**\n   * 获取节点的位置及大小矩形\n   */\n  get bounds(): Rectangle {\n    return this.transform.bounds;\n  }\n\n  /**\n   * Check node extend type\n   */\n  isExtend(parentType: FlowNodeType): boolean {\n    return this.document.isExtend(this.flowNodeType, parentType);\n  }\n\n  /**\n   * Check node type\n   * @param parentType\n   */\n  isTypeOrExtendType(parentType: FlowNodeType): boolean {\n    return this.document.isTypeOrExtendType(this.flowNodeType, parentType);\n  }\n}\n\nexport namespace FlowNodeEntity {\n  export function is(obj: Entity): obj is FlowNodeEntity {\n    return obj instanceof FlowNodeEntity;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/entities/flow-renderer-state-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { debounce } from 'lodash-es';\nimport { type Disposable } from '@flowgram.ai/utils';\nimport { ConfigEntity, type EntityOpts } from '@flowgram.ai/core';\n\nimport { LABEL_SIDE_TYPE } from '../typings';\nimport { type FlowNodeEntity } from './flow-node-entity';\n\ninterface FlowRendererStateEntityConfig extends EntityOpts {}\n\ninterface FlowRendererState {\n  nodeHoveredId?: string;\n  nodeDroppingId?: string;\n  nodeDragStartId?: string;\n  nodeDragIds?: string[]; // 框选批量拖拽\n  nodeDragIdsWithChildren?: string[]; // 批量拖拽（含子节点）\n  dragLabelSide?: LABEL_SIDE_TYPE;\n  dragging?: boolean;\n  isBranch?: boolean;\n}\n/**\n * 渲染相关的全局状态管理\n */\nexport class FlowRendererStateEntity extends ConfigEntity<\n  FlowRendererState,\n  FlowRendererStateEntityConfig\n> {\n  static type = 'FlowRendererStateEntity';\n\n  getDefaultConfig() {\n    return {};\n  }\n\n  constructor(conf: FlowRendererStateEntityConfig) {\n    super(conf);\n  }\n\n  getNodeHovered(): FlowNodeEntity | undefined {\n    return this.config.nodeHoveredId\n      ? this.entityManager.getEntityById(this.config.nodeHoveredId)\n      : undefined;\n  }\n\n  setNodeHovered(node: FlowNodeEntity | undefined): void {\n    this.updateConfig({\n      nodeHoveredId: node?.id,\n    });\n  }\n\n  get dragging() {\n    return this.config.dragging;\n  }\n\n  setDragging(dragging: boolean) {\n    this.updateConfig({\n      dragging,\n    });\n  }\n\n  get isBranch() {\n    return this.config.isBranch;\n  }\n\n  setIsBranch(isBranch: boolean) {\n    this.updateConfig({\n      isBranch,\n    });\n  }\n\n  getDragLabelSide(): LABEL_SIDE_TYPE | undefined {\n    return this.config.dragLabelSide;\n  }\n\n  setDragLabelSide(dragLabelSide?: LABEL_SIDE_TYPE): void {\n    this.updateConfig({\n      dragLabelSide,\n    });\n  }\n\n  getNodeDroppingId(): string | undefined {\n    return this.config.nodeDroppingId;\n  }\n\n  setNodeDroppingId(nodeDroppingId?: string): void {\n    this.updateConfig({\n      nodeDroppingId,\n    });\n  }\n\n  getDragStartEntity(): FlowNodeEntity | undefined {\n    const { nodeDragStartId } = this.config;\n    return this.entityManager.getEntityById(nodeDragStartId!);\n  }\n\n  setDragStartEntity(node?: FlowNodeEntity): void {\n    this.updateConfig({\n      nodeDragStartId: node?.id,\n    });\n  }\n\n  // 拖拽多个节点时\n  getDragEntities(): FlowNodeEntity[] {\n    const { nodeDragIds } = this.config;\n    return (nodeDragIds || []).map((_id) => this.entityManager.getEntityById(_id)!);\n  }\n\n  // 设置拖拽的节点\n  setDragEntities(nodes: FlowNodeEntity[]): void {\n    this.updateConfig({\n      nodeDragIds: nodes.map((_node) => _node.id),\n      nodeDragIdsWithChildren: nodes\n        .map((_node) => [_node.id, ..._node.allCollapsedChildren.map((_n) => _n.id)])\n        .flat(),\n    });\n  }\n\n  onNodeHoveredChange(\n    fn: (hoveredNode: FlowNodeEntity | undefined) => void,\n    debounceTime = 100 // 延迟执行避免频繁 hover\n  ): Disposable {\n    return this.onConfigChanged(debounce(() => fn(this.getNodeHovered()), debounceTime));\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/entities/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-node-entity';\nexport * from './flow-document-transformer-entity';\nexport * from './flow-renderer-state-entity';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-document-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, optional } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nexport const FlowDocumentConfigDefaultData = Symbol('FlowDocumentConfigDefaultData');\n\n/**\n * 用于文档扩展配置\n */\n@injectable()\nexport class FlowDocumentConfig {\n  private onDataChangeEmitter = new Emitter<string>();\n\n  readonly onChange = this.onDataChangeEmitter.event;\n\n  constructor(\n    @inject(FlowDocumentConfigDefaultData)\n    @optional()\n    private _data: Record<string, any> = {},\n  ) {}\n\n  get(key: string): any {\n    return this._data[key];\n  }\n\n  set(key: string, value: any): void {\n    if (this.get(key) !== value) {\n      this._data[key] = value;\n      this.onDataChangeEmitter.fire(key);\n    }\n  }\n\n  registerConfigs(config: Record<string, any>) {\n    Object.keys(config).forEach(key => {\n      this.set(key, config[key]);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-document-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { FlowOperationBaseService } from './typings/flow-operation';\nimport { FlowDragService } from './services/flow-drag-service';\nimport { FlowGroupService, FlowOperationBaseServiceImpl } from './services';\nimport { HorizontalFixedLayout, VerticalFixedLayout } from './layout';\nimport { FlowDocumentContribution } from './flow-document-contribution';\nimport { FlowDocumentConfig } from './flow-document-config';\nimport { FlowDocument, FlowDocumentProvider } from './flow-document';\n\nexport const FlowDocumentContainerModule = new ContainerModule((bind) => {\n  bind(FlowDocument).toSelf().inSingletonScope();\n  bind(FlowDocumentProvider)\n    .toDynamicValue((ctx) => () => ctx.container.get(FlowDocument))\n    .inSingletonScope();\n  bind(FlowDocumentConfig).toSelf().inSingletonScope();\n  bind(VerticalFixedLayout).toSelf().inSingletonScope();\n  bind(HorizontalFixedLayout).toSelf().inSingletonScope();\n  bind(FlowDragService).toSelf().inSingletonScope();\n  bind(FlowOperationBaseService).to(FlowOperationBaseServiceImpl).inSingletonScope();\n  bind(FlowGroupService).toSelf().inSingletonScope();\n  bind(FlowDocumentContribution).toDynamicValue((ctx) => ({\n    registerDocument: (document: FlowDocument) => {\n      document.registerLayout(ctx.container.get(VerticalFixedLayout));\n      document.registerLayout(ctx.container.get(HorizontalFixedLayout));\n    },\n  }));\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-document-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowDocument } from './flow-document';\n\nexport const FlowDocumentContribution = Symbol('FlowDocumentContribution');\n\nexport interface FlowDocumentContribution<T extends FlowDocument = FlowDocument> {\n  /**\n   * 注册\n   * @param document\n   */\n  registerDocument?(document: T): void;\n\n  /**\n   * 加载数据\n   * @param document\n   */\n  loadDocument?(document: T): Promise<void>;\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-document-options.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type FlowNodeJSON,\n  DefaultSpacingKey,\n  FlowTransitionLine,\n  FlowTransitionLabel,\n  FlowNodeRegistry,\n  FlowNodeType,\n} from './typings';\nimport { FlowNodeEntity } from './entities';\n\nexport const FlowDocumentOptions = Symbol('FlowDocumentOptions');\n\n/**\n * 流程画布配置\n */\nexport interface FlowDocumentOptions {\n  /**\n   * 布局，默认 垂直布局\n   */\n  defaultLayout?: string;\n  /**\n   * 所有节点的默认展开状态\n   */\n  allNodesDefaultExpanded?: boolean;\n  toNodeJSON?(node: FlowNodeEntity): FlowNodeJSON;\n  fromNodeJSON?(node: FlowNodeEntity, json: FlowNodeJSON, isFirstCreate: boolean): void;\n  constants?: Record<string, any>;\n  formatNodeLines?: (node: FlowNodeEntity, lines: FlowTransitionLine[]) => FlowTransitionLine[];\n  formatNodeLabels?: (node: FlowNodeEntity, lines: FlowTransitionLabel[]) => FlowTransitionLabel[];\n  preNodeCreate?: (node: FlowNodeEntity) => void;\n  /**\n   * 获取默认的节点配置\n   */\n  getNodeDefaultRegistry?: (type: FlowNodeType) => FlowNodeRegistry;\n}\n\nexport const FlowDocumentOptionsDefault: FlowDocumentOptions = {\n  allNodesDefaultExpanded: false,\n};\n\n/**\n * 支持外部 constants 自定义的 key 枚举\n */\nexport const ConstantKeys = {\n  ...DefaultSpacingKey,\n  /**\n   * loop 底部留白\n   */\n  INLINE_SPACING_BOTTOM: 'INLINE_SPACING_BOTTOM',\n  /**\n   * inlineBlocks 的 inlineTop\n   * loop 循环线条上边距\n   */\n  INLINE_BLOCKS_INLINE_SPACING_TOP: 'INLINE_BLOCKS_INLINE_SPACING_TOP',\n  /**\n   * inlineBlocks 的 inlineBottom\n   * loop 循环线条的下边距\n   *\n   */\n  INLINE_BLOCKS_INLINE_SPACING_BOTTOM: 'INLINE_BLOCKS_INLINE_SPACING_BOTTOM',\n  /***\n   * 线条、label 默认颜色\n   */\n  BASE_COLOR: 'BASE_COLOR',\n  /***\n   * 线条、label 激活后的颜色\n   */\n  BASE_ACTIVATED_COLOR: 'BASE_ACTIVATED_COLOR',\n  /**\n   * Branch bottom margin\n   * 分支下边距\n   */\n  INLINE_BLOCKS_PADDING_TOP: 'INLINE_BLOCKS_PADDING_TOP',\n};\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-document.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { omit } from 'lodash-es';\nimport { inject, injectable, multiInject, optional, postConstruct } from 'inversify';\nimport { type Disposable, Emitter } from '@flowgram.ai/utils';\nimport { type EntityData, type EntityDataRegistry, EntityManager } from '@flowgram.ai/core';\n\nimport {\n  AddNodeData,\n  DEFAULT_FLOW_NODE_META,\n  type FlowDocumentJSON,\n  FlowLayout,\n  FlowLayoutDefault,\n  FlowNodeBaseType,\n  type FlowNodeJSON,\n  FlowNodeRegistry,\n  FlowNodeType,\n} from './typings';\nimport { FlowVirtualTree } from './flow-virtual-tree';\nimport { FlowRenderTree } from './flow-render-tree';\nimport {\n  ConstantKeys,\n  FlowDocumentOptions,\n  FlowDocumentOptionsDefault,\n} from './flow-document-options';\nimport { FlowDocumentContribution } from './flow-document-contribution';\nimport { FlowDocumentConfig } from './flow-document-config';\nimport { FlowDocumentTransformerEntity, FlowNodeEntity, FlowRendererStateEntity } from './entities';\n\nexport type FlowDocumentProvider = () => FlowDocument;\nexport const FlowDocumentProvider = Symbol('FlowDocumentProvider');\n/**\n * 流程整个文档数据\n */\n@injectable()\nexport class FlowDocument<T = FlowDocumentJSON> implements Disposable {\n  @inject(EntityManager) protected entityManager: EntityManager;\n\n  @inject(FlowDocumentConfig) readonly config: FlowDocumentConfig;\n\n  /**\n   * 流程画布配置项\n   */\n  @inject(FlowDocumentOptions) @optional() public options: FlowDocumentOptions;\n\n  @multiInject(FlowDocumentContribution)\n  @optional()\n  protected contributions: FlowDocumentContribution[] = [];\n\n  protected registers = new Map<FlowNodeType, FlowNodeRegistry>();\n\n  private nodeRegistryCache = new Map<string, any>();\n\n  protected nodeDataRegistries: EntityDataRegistry[] = [];\n\n  protected layouts: FlowLayout[] = [];\n\n  protected currentLayoutKey: string = '';\n\n  protected onNodeUpdateEmitter = new Emitter<{\n    node: FlowNodeEntity;\n    /**\n     * use 'json' instead\n     * @deprecated\n     */\n    data: FlowNodeJSON;\n    json: FlowNodeJSON;\n  }>();\n\n  protected onNodeCreateEmitter = new Emitter<{\n    node: FlowNodeEntity;\n    /**\n     * use 'json' instead\n     * @deprecated\n     */\n    data: FlowNodeJSON;\n    json: FlowNodeJSON;\n  }>();\n\n  protected onNodeDisposeEmitter = new Emitter<{\n    node: FlowNodeEntity;\n  }>();\n\n  protected onLayoutChangeEmitter = new Emitter<FlowLayout>();\n\n  readonly onNodeUpdate = this.onNodeUpdateEmitter.event;\n\n  readonly onNodeCreate = this.onNodeCreateEmitter.event;\n\n  readonly onNodeDispose = this.onNodeDisposeEmitter.event;\n\n  readonly onLayoutChange = this.onLayoutChangeEmitter.event;\n\n  private _disposed = false;\n\n  root: FlowNodeEntity;\n\n  /**\n   * 原始的 tree 结构\n   */\n  originTree: FlowVirtualTree<FlowNodeEntity>;\n\n  transformer: FlowDocumentTransformerEntity;\n\n  /**\n   * 渲染相关的全局轧辊台\n   */\n  renderState: FlowRendererStateEntity;\n\n  /**\n   * 渲染后的 tree 结构\n   */\n  renderTree: FlowRenderTree<FlowNodeEntity>;\n\n  /**\n   *\n   */\n  get disposed(): boolean {\n    return this._disposed;\n  }\n\n  @postConstruct()\n  init(): void {\n    if (!this.options) this.options = FlowDocumentOptionsDefault;\n    this.currentLayoutKey = this.options.defaultLayout || FlowLayoutDefault.VERTICAL_FIXED_LAYOUT;\n    this.contributions.forEach((contrib) => contrib.registerDocument?.(this));\n    this.root = this.addNode({ id: 'root', type: FlowNodeBaseType.ROOT });\n    this.originTree = new FlowVirtualTree<FlowNodeEntity>(this.root);\n    this.transformer = this.entityManager.createEntity<FlowDocumentTransformerEntity>(\n      FlowDocumentTransformerEntity,\n      { document: this }\n    );\n    this.renderState =\n      this.entityManager.createEntity<FlowRendererStateEntity>(FlowRendererStateEntity);\n    this.renderTree = new FlowRenderTree<FlowNodeEntity>(this.root, this.originTree, this);\n    // 布局第一次加载时候触发一次\n    this.layout.reload?.();\n  }\n\n  /**\n   * 从数据初始化 O(n)\n   * @param json\n   */\n  /**\n   * 加载数据，可以被重载\n   * @param json 文档数据更新\n   * @param fireRender 是否要触发渲染，默认 true\n   */\n  fromJSON(json: FlowDocumentJSON | any, fireRender = true): void {\n    if (this._disposed) return;\n    // 清空 tree 数据 重新计算\n    this.originTree.clear();\n    this.renderTree.clear();\n    // 暂停触发画布更新\n    this.entityManager.changeEntityLocked = true;\n    // 添加前的节点\n    const oldNodes = this.entityManager.getEntities<FlowNodeEntity>(FlowNodeEntity);\n    // 添加后的节点\n    const newNodes: FlowNodeEntity[] = [this.root];\n    this.addBlocksAsChildren(this.root, json.nodes || [], newNodes);\n    // 删除无效的节点\n    oldNodes.forEach((node) => {\n      if (!newNodes.includes(node)) {\n        node.dispose();\n      }\n    });\n    this.entityManager.changeEntityLocked = false;\n    this.transformer.loading = false;\n    if (fireRender) this.fireRender();\n  }\n\n  get layout(): FlowLayout {\n    const layout = this.layouts.find((layout) => layout.name == this.currentLayoutKey);\n    if (!layout) {\n      throw new Error(`Unknown flow layout: ${this.currentLayoutKey}`);\n    }\n    return layout;\n  }\n\n  async load(): Promise<void> {\n    await Promise.all(this.contributions.map((c) => c.loadDocument?.(this)));\n  }\n\n  get loading(): boolean {\n    return this.transformer.loading;\n  }\n\n  /**\n   * 触发 render\n   */\n  fireRender(): void {\n    if (this.transformer.isTreeDirty()) {\n      this.entityManager.fireEntityChanged(FlowNodeEntity.type);\n      this.entityManager.fireEntityChanged(FlowDocumentTransformerEntity.type);\n    }\n  }\n\n  /**\n   * 从指定节点的下一个节点新增\n   * @param fromNode\n   * @param json\n   */\n  addFromNode(fromNode: FlowNodeEntity | string, json: FlowNodeJSON): FlowNodeEntity {\n    const node = typeof fromNode === 'string' ? this.getNode(fromNode)! : fromNode;\n    this.entityManager.changeEntityLocked = true;\n    const { parent } = node;\n    const result = this.addNode({\n      ...json,\n      parent,\n      // originParent,\n    });\n    this.originTree.insertAfter(node, result);\n    this.entityManager.changeEntityLocked = false;\n    this.entityManager.fireEntityChanged(FlowNodeEntity.type);\n    return result;\n  }\n\n  removeNode(node: FlowNodeEntity | string) {\n    if (typeof node === 'string') {\n      this.getNode(node)?.dispose();\n    } else {\n      node.dispose();\n    }\n  }\n\n  /**\n   * 添加节点，如果节点已经存在则不会重复创建\n   * @param data\n   * @param addedNodes\n   */\n  addNode(data: AddNodeData, addedNodes?: FlowNodeEntity[]): FlowNodeEntity {\n    const { id, type = 'block', originParent, parent, meta, hidden, index } = data;\n    let node = this.getNode(id);\n    let isNew = false;\n    const register = this.getNodeRegistry(type, data.originParent);\n    // node 类型变化则全部删除重新来\n    if (node && node.flowNodeType !== data.type) {\n      node.dispose();\n      node = undefined;\n    }\n    if (!node) {\n      const { dataRegistries } = register;\n      node = this.entityManager.createEntity<FlowNodeEntity>(FlowNodeEntity, {\n        id,\n        document: this,\n        flowNodeType: type,\n        originParent,\n        meta,\n      });\n      this.options.preNodeCreate?.(node);\n      const datas = dataRegistries\n        ? this.nodeDataRegistries.concat(...dataRegistries)\n        : this.nodeDataRegistries;\n      node.addInitializeData(datas);\n      node.onDispose(() => this.onNodeDisposeEmitter.fire({ node: node! }));\n      this.options.fromNodeJSON?.(node, data, true);\n      isNew = true;\n    } else {\n      this.options.fromNodeJSON?.(node, data, false);\n    }\n    // 初始化数据重制\n    node.initData({\n      originParent,\n      parent,\n      meta,\n      hidden,\n      index,\n    });\n    // 开始节点加到 root 里边\n    if (node.isStart) {\n      this.root.addChild(node);\n    }\n    addedNodes?.push(node);\n    // 自定义创建逻辑\n    if (register.onCreate) {\n      const extendNodes = register.onCreate(node, data);\n      if (extendNodes && addedNodes) {\n        addedNodes.push(...extendNodes);\n      }\n    } else if (data.blocks && data.blocks.length > 0) {\n      // 兼容老的写法\n      if (!data.blocks[0].type) {\n        this.addInlineBlocks(node, data.blocks, addedNodes);\n      } else {\n        this.addBlocksAsChildren(node, data.blocks as FlowNodeJSON[], addedNodes);\n      }\n    }\n\n    if (isNew) {\n      this.onNodeCreateEmitter.fire({\n        node,\n        data,\n        json: data,\n      });\n    } else {\n      this.onNodeUpdateEmitter.fire({ node, data, json: data });\n    }\n\n    return node;\n  }\n\n  addBlocksAsChildren(\n    parent: FlowNodeEntity,\n    blocks: FlowNodeJSON[],\n    addedNodes?: FlowNodeEntity[]\n  ): void {\n    for (const block of blocks) {\n      this.addNode(\n        {\n          ...block,\n          parent,\n        },\n        addedNodes\n      );\n    }\n  }\n\n  /**\n   * block 格式：\n   * node:  (最原始的 id)\n   *  blockIcon\n   *  inlineBlocks\n   *    block\n   *      blockOrderIcon\n   *    block\n   *      blockOrderIcon\n   * @param node\n   * @param blocks\n   * @param addedNodes\n   */\n  addInlineBlocks(\n    node: FlowNodeEntity,\n    blocks: FlowNodeJSON[],\n    addedNodes: FlowNodeEntity[] = []\n  ): FlowNodeEntity[] {\n    // 块列表开始节点，用来展示块的按钮\n    const blockIconNode = this.addNode({\n      id: `$blockIcon$${node.id}`,\n      type: FlowNodeBaseType.BLOCK_ICON,\n      originParent: node,\n      parent: node,\n    });\n    addedNodes.push(blockIconNode);\n    // 水平布局\n    const inlineBlocksNode = this.addNode({\n      id: `$inlineBlocks$${node.id}`,\n      type: FlowNodeBaseType.INLINE_BLOCKS,\n      originParent: node,\n      parent: node,\n    });\n    addedNodes.push(inlineBlocksNode);\n    blocks.forEach((blockData) => {\n      this.addBlock(node, blockData, addedNodes);\n    });\n    return addedNodes;\n  }\n\n  /**\n   * 添加单个 block\n   * @param target\n   * @param blockData\n   * @param addedNodes\n   * @param parent 默认去找 $inlineBlocks$\n   */\n  addBlock(\n    target: FlowNodeEntity | string,\n    blockData: FlowNodeJSON,\n    addedNodes?: FlowNodeEntity[],\n    parent?: FlowNodeEntity,\n    index?: number\n  ): FlowNodeEntity {\n    const node: FlowNodeEntity = typeof target === 'string' ? this.getNode(target)! : target;\n    const { onBlockChildCreate } = node.getNodeRegistry();\n    if (onBlockChildCreate) {\n      return onBlockChildCreate(node, blockData, addedNodes);\n    }\n    parent = parent || this.getNode(`$inlineBlocks$${node.id}`);\n    // 块节点会生成一个空的 Block 节点用来切割 Block\n    const block = this.addNode({\n      ...omit(blockData, 'blocks'),\n      type: blockData.type || FlowNodeBaseType.BLOCK,\n      originParent: node,\n      parent,\n      index,\n    });\n\n    if (blockData.meta?.defaultCollapsed) {\n      block.collapsed = true;\n    }\n\n    // 块开始节点\n    const blockOrderIcon = this.addNode({\n      id: `$blockOrderIcon$${blockData.id}`,\n      type: FlowNodeBaseType.BLOCK_ORDER_ICON,\n      originParent: node,\n      meta: blockData.meta,\n      data: blockData.data,\n      parent: block,\n    });\n    addedNodes?.push(block, blockOrderIcon);\n    if (blockData.blocks) {\n      this.addBlocksAsChildren(block, blockData.blocks as FlowNodeJSON[], addedNodes);\n    }\n    return block;\n  }\n\n  /**\n   * 根据 id 获取节点\n   * @param id\n   */\n  getNode(id: string): FlowNodeEntity | undefined {\n    if (!id) return undefined;\n    return this.entityManager.getEntityById<FlowNodeEntity>(id);\n  }\n\n  /**\n   * 注册节点\n   * @param registries\n   */\n  registerFlowNodes<T extends FlowNodeRegistry<any>>(...registries: T[]): void {\n    registries.forEach((newRegistry) => {\n      if (!newRegistry) {\n        throw new Error('[FlowDocument] registerFlowNodes parameters get undefined registry.');\n      }\n      const preRegistry = this.registers.get(newRegistry.type);\n      this.registers.set(newRegistry.type, {\n        ...preRegistry,\n        ...newRegistry,\n        meta: {\n          ...preRegistry?.meta,\n          ...newRegistry?.meta,\n        },\n        extendChildRegistries: FlowNodeRegistry.mergeChildRegistries(\n          preRegistry?.extendChildRegistries,\n          newRegistry?.extendChildRegistries\n        ),\n      });\n    });\n  }\n\n  /**\n   * Check node extend\n   * @param currentType\n   * @param extendType\n   */\n  isExtend(currentType: FlowNodeType, extendType: FlowNodeType): boolean {\n    return (this.getNodeRegistry(currentType).__extends__ || []).includes(extendType);\n  }\n\n  /**\n   * Check node type\n   * @param currentType\n   * @param extendType\n   */\n  isTypeOrExtendType(currentType: FlowNodeType, extendType: FlowNodeType): boolean {\n    return currentType === extendType || this.isExtend(currentType, extendType);\n  }\n\n  /**\n   * 导出数据，可以重载\n   */\n  toJSON(): T | any {\n    if (this.disposed) {\n      throw new Error(\n        'The FlowDocument has been disposed and it is no longer possible to call toJSON.'\n      );\n    }\n    return {\n      nodes: this.root.toJSON().blocks,\n    };\n  }\n\n  /**\n   * @deprecated\n   * use `getNodeRegistry` instead\n   */\n  getNodeRegister<T extends FlowNodeRegistry = FlowNodeRegistry>(\n    type: FlowNodeType,\n    originParent?: FlowNodeEntity\n  ): T {\n    return this.getNodeRegistry<T>(type, originParent);\n  }\n\n  getNodeRegistry<T extends FlowNodeRegistry = FlowNodeRegistry>(\n    type: FlowNodeType,\n    originParent?: FlowNodeEntity\n  ): T {\n    const typeKey = `${type}_${originParent?.flowNodeType || ''}`;\n    if (this.nodeRegistryCache.has(typeKey)) {\n      return this.nodeRegistryCache.get(typeKey) as T;\n    }\n    const customDefaultRegistry = this.options.getNodeDefaultRegistry?.(type);\n    let register = this.registers.get(type) || { type };\n    const extendRegisters: FlowNodeRegistry[] = [];\n    const extendKey = register.extend;\n    // 继承重载\n    if (register.extend && this.registers.has(register.extend)) {\n      register = FlowNodeRegistry.merge(\n        this.getNodeRegistry(register.extend),\n        register,\n        register.type\n      );\n    }\n    // 父节点覆盖\n    if (originParent) {\n      const extendRegister = this.getNodeRegistry(\n        originParent.flowNodeType\n      ).extendChildRegistries?.find((r) => r.type === type);\n      if (extendRegister) {\n        if (extendRegister.extend && this.registers.has(extendRegister.extend)) {\n          extendRegisters.push(this.registers.get(extendRegister.extend)!);\n        }\n        extendRegisters.push(extendRegister);\n      }\n    }\n    register = FlowNodeRegistry.extend(register, extendRegisters);\n    const defaultNodeMeta = DEFAULT_FLOW_NODE_META(type, this);\n    defaultNodeMeta.spacing =\n      this.options?.constants?.[ConstantKeys.NODE_SPACING] || defaultNodeMeta.spacing;\n\n    const res = {\n      ...customDefaultRegistry,\n      ...register,\n      meta: {\n        ...defaultNodeMeta,\n        ...customDefaultRegistry?.meta,\n        ...register.meta,\n      },\n    } as T;\n    // Save the \"extend\" attribute\n    if (extendKey) {\n      res.extend = extendKey;\n    }\n    this.nodeRegistryCache.set(typeKey, res);\n    return res;\n  }\n\n  /**\n   * 节点注入数据\n   * @param nodeDatas\n   */\n  registerNodeDatas(...nodeDatas: EntityDataRegistry[]): void {\n    this.nodeDataRegistries.push(...nodeDatas);\n  }\n\n  /**\n   * traverse all nodes, O(n)\n   *   R\n   *   |\n   *   +---1\n   *   |   |\n   *   |   +---1.1\n   *   |   |\n   *   |   +---1.2\n   *   |   |\n   *   |   +---1.3\n   *   |   |    |\n   *   |   |    +---1.3.1\n   *   |   |    |\n   *   |   |    +---1.3.2\n   *   |   |\n   *   |   +---1.4\n   *   |\n   *   +---2\n   *       |\n   *       +---2.1\n   *\n   *  sort: [1, 1.1, 1.2, 1.3, 1.3.1, 1.3.2, 1.4, 2, 2.1]\n   * @param fn\n   * @param node\n   * @param depth\n   * @return isBreak\n   */\n  traverse(\n    fn: (node: FlowNodeEntity, depth: number, index: number) => boolean | void,\n    node = this.root,\n    depth = 0\n  ): boolean | void {\n    return this.originTree.traverse(fn, node, depth);\n  }\n\n  get size(): number {\n    return this.getAllNodes().length;\n  }\n\n  hasNode(nodeId: string): boolean {\n    return !!this.entityManager.getEntityById(nodeId);\n  }\n\n  getAllNodes(): FlowNodeEntity[] {\n    return this.entityManager.getEntities(FlowNodeEntity);\n  }\n\n  toString(showType?: boolean): string {\n    return this.originTree.toString(showType);\n  }\n\n  /**\n   * 返回需要渲染的数据\n   */\n  getRenderDatas<T extends EntityData>(\n    dataRegistry: EntityDataRegistry<T>,\n    containHiddenNodes = true\n  ): T[] {\n    const result: T[] = [];\n    this.renderTree.traverse((node) => {\n      if (!containHiddenNodes && node.hidden) return;\n      result.push(node.getData<T>(dataRegistry)!);\n    });\n    return result;\n  }\n\n  toNodeJSON(node: FlowNodeEntity): FlowNodeJSON {\n    if (this.options.toNodeJSON) {\n      return this.options.toNodeJSON(node);\n    }\n    const nodesMap: Record<string, FlowNodeJSON> = {};\n    let startNodeJSON: FlowNodeJSON;\n    this.traverse((node) => {\n      const isSystemNode = node.id.startsWith('$');\n      if (isSystemNode) return;\n      const nodeJSONData = node.getJSONData();\n      const nodeJSON: FlowNodeJSON = {\n        id: node.id,\n        type: node.flowNodeType,\n      };\n      if (nodeJSONData !== undefined) {\n        nodeJSON.data = nodeJSONData;\n      }\n      if (!startNodeJSON) startNodeJSON = nodeJSON;\n      let { parent } = node;\n      if (parent && parent.id.startsWith('$')) {\n        parent = parent.originParent;\n      }\n      const parentJSON = parent ? nodesMap[parent.id] : undefined;\n      if (parentJSON) {\n        if (!parentJSON.blocks) {\n          parentJSON.blocks = [];\n        }\n        parentJSON.blocks.push(nodeJSON);\n      }\n      nodesMap[node.id] = nodeJSON;\n    }, node);\n    return startNodeJSON!;\n  }\n\n  /**\n   * 移动节点\n   * @param param0\n   * @returns\n   */\n  moveNodes({\n    dropNodeId,\n    sortNodeIds,\n    inside = false,\n  }: {\n    dropNodeId: string;\n    sortNodeIds: string[];\n    inside?: boolean;\n  }) {\n    const dropEntity = this.getNode(dropNodeId);\n    if (!dropEntity) {\n      return;\n    }\n\n    const sortNodes = sortNodeIds.map((id) => this.getNode(id)!);\n\n    // 按照顺序一个个移动到目标节点下\n    this.entityManager.changeEntityLocked = true;\n    for (const node of sortNodes.reverse()) {\n      if (inside) {\n        this.originTree.addChild(dropEntity, node, 0);\n      } else {\n        this.originTree.insertAfter(dropEntity, node);\n      }\n    }\n\n    this.entityManager.changeEntityLocked = false;\n    this.fireRender();\n  }\n\n  /**\n   * 移动子节点\n   * @param param0\n   * @returns\n   */\n  moveChildNodes({\n    toParentId,\n    toIndex,\n    nodeIds,\n  }: {\n    toParentId: string;\n    nodeIds: string[];\n    toIndex: number;\n  }) {\n    if (nodeIds.length === 0) {\n      return;\n    }\n\n    const toParent = this.getNode(toParentId);\n    if (!toParent) {\n      return;\n    }\n\n    this.entityManager.changeEntityLocked = true;\n\n    this.originTree.moveChilds(\n      toParent,\n      nodeIds.map((nodeId) => this.getNode(nodeId) as FlowNodeEntity),\n      toIndex\n    );\n\n    this.entityManager.changeEntityLocked = false;\n    this.fireRender();\n  }\n\n  /**\n   * 注册布局\n   * @param layout\n   */\n  registerLayout(layout: FlowLayout) {\n    this.layouts.push(layout);\n  }\n\n  /**\n   * 更新布局\n   * @param layoutKey\n   */\n  setLayout(layoutKey: string) {\n    if (this.currentLayoutKey === layoutKey) return;\n    const layout = this.layouts.find((layout) => layout.name === layoutKey);\n    if (!layout) return;\n    this.currentLayoutKey = layoutKey;\n    this.transformer.clear();\n    layout.reload?.();\n    this.fireRender();\n    this.onLayoutChangeEmitter.fire(this.layout);\n  }\n\n  /**\n   * 切换垂直或水平布局\n   */\n  toggleFixedLayout() {\n    this.setLayout(\n      this.layout.name === FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT\n        ? FlowLayoutDefault.VERTICAL_FIXED_LAYOUT\n        : FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT\n    );\n  }\n\n  dispose() {\n    if (this._disposed) return;\n    this.registers.clear();\n    this.nodeRegistryCache.clear();\n    this.originTree.dispose();\n    this.renderTree.dispose();\n    this.onNodeUpdateEmitter.dispose();\n    this.onNodeCreateEmitter.dispose();\n    this.onNodeDisposeEmitter.dispose();\n    this.onLayoutChangeEmitter.dispose();\n    this._disposed = true;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-render-tree.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentConfigEnum, FlowNodeBaseType, FlowNodeSplitType } from './typings';\nimport { FlowVirtualTree } from './flow-virtual-tree';\nimport type { FlowDocument } from './flow-document';\nimport type { FlowNodeEntity } from './entities';\n\n/**\n * Render Tree 会只读模式，不具备操作 tree 结构元素\n */\nexport class FlowRenderTree<T extends FlowNodeEntity> extends FlowVirtualTree<T> {\n  protected originTree: FlowVirtualTree<T>;\n\n  protected document: FlowDocument;\n\n  /**\n   * 折叠的节点\n   * @protected\n   */\n  protected nodesCollapsed = new Set<T>();\n\n  constructor(readonly root: T, originTree: FlowVirtualTree<T>, document: FlowDocument) {\n    super(root);\n    this.originTree = originTree;\n    this.onTreeChange = this.originTree.onTreeChange;\n    this.document = document;\n  }\n\n  isCollapsed(node: T): boolean {\n    return this.nodesCollapsed.has(node);\n  }\n\n  get collapsedNodeList(): T[] {\n    return Array.from(this.nodesCollapsed);\n  }\n\n  /**\n   * 折叠元素\n   * @param node\n   * @param collapsed\n   */\n  setCollapsed(node: T, collapsed: boolean): void {\n    if (collapsed) {\n      this.nodesCollapsed.add(node);\n    } else {\n      this.nodesCollapsed.delete(node);\n    }\n    this.originTree.fireTreeChange();\n  }\n\n  /**\n   *\n   */\n  openNodeInsideCollapsed(node: T) {\n    // 找到所有的originTree上的parent\n    let curr: T | undefined = this.originTree.getInfo(node)?.parent;\n\n    while (curr) {\n      if (this.nodesCollapsed.has(curr)) {\n        this.nodesCollapsed.delete(curr);\n      }\n      const { parent } = this.originTree.getInfo(curr) || {};\n      curr = parent;\n    }\n    this.originTree.fireTreeChange();\n  }\n\n  /**\n   * 更新结束节点等位置信息，分支里如果全是结束节点则要做相应的偏移\n   */\n  updateRenderStruct(): void {\n    this.map = this.originTree.cloneMap();\n\n    // 结束节点位置更新逻辑开关\n    if (this.document.config.get(FlowDocumentConfigEnum.END_NODES_REFINE_BRANCH)) {\n      this.refineBranch(this.root);\n    }\n\n    // 节点展开收起\n    this.hideCollapsed();\n  }\n\n  /**\n   * 隐藏收起节点\n   */\n  protected hideCollapsed() {\n    this.nodesCollapsed.forEach(collapsedNode => {\n      const collapsedNodeInfo = this.getInfo(collapsedNode);\n      if (!collapsedNodeInfo) {\n        // 自动回收节点数据\n        this.nodesCollapsed.delete(collapsedNode);\n        return;\n      }\n\n      const iconChild = collapsedNodeInfo.children.find(\n        _child =>\n          _child.flowNodeType === FlowNodeBaseType.BLOCK_ICON ||\n          _child.flowNodeType === FlowNodeBaseType.BLOCK_ORDER_ICON,\n      );\n\n      // ⚠️注意：BLOCK_ICON 和 BLOCK_ORDER_ICON 作为一个 block 的标识节点，收起时需要保留\n      if (iconChild) {\n        const iconInfo = this.getInfo(iconChild);\n        iconInfo.next = undefined;\n        iconInfo.pre = undefined;\n        collapsedNodeInfo.children = [iconChild];\n\n        return;\n      }\n\n      // 收起节点children置为空\n      collapsedNodeInfo.children = [];\n    });\n  }\n\n  // 节点是否为结束节点\n  isNodeEnd(node: T): boolean {\n    if (node.getNodeMeta().isNodeEnd) {\n      return true;\n    }\n    const { children } = this.getInfo(node);\n\n    if (children.length > 0 && node.isInlineBlocks) {\n      return children.every(child => this.isNodeEnd(child));\n    }\n\n    if (node.isInlineBlock) {\n      return this.isNodeEnd(children[children.length - 1]);\n    }\n\n    return false;\n  }\n\n  /**\n   * 优化精简分支线\n   * - 结束节点拉直分支线\n   */\n  protected refineBranch(block: T) {\n    let curr: T | undefined = this.getInfo(block).children[0];\n\n    while (curr) {\n      if (\n        curr.flowNodeType === FlowNodeSplitType.DYNAMIC_SPLIT ||\n        curr.flowNodeType === FlowNodeSplitType.STATIC_SPLIT\n      ) {\n        const { next, children: branchChildren } = this.getInfo(curr);\n        const { children } = this.getInfo(branchChildren[1]);\n\n        const passBlocks = (children || []).filter(child => !this.isNodeEnd(child));\n\n        const shouldDragAllNextNodes = passBlocks.length === 1;\n        if (shouldDragAllNextNodes && next) {\n          this.dragNextNodesToBlock(passBlocks[0], next);\n        }\n\n        // 对每一个分支再进行refineBranch\n        children?.forEach(child => {\n          this.refineBranch(child);\n        });\n\n        if (shouldDragAllNextNodes) {\n          break;\n        }\n      }\n\n      curr = curr.next as T;\n    }\n  }\n\n  // 结束节点拽分支，将后续节点拽到对应分支内\n  protected dragNextNodesToBlock(toBlock: T, next: T) {\n    const toBlockInfo = this.getInfo(toBlock);\n    const nextInfo = this.getInfo(next);\n    const toBlockLastChild = toBlockInfo.children[toBlock.children.length - 1];\n\n    if (nextInfo.parent) {\n      const nextParentInfo = this.getInfo(nextInfo.parent);\n\n      // 1. next的节点和之前的节点断连\n      if (nextInfo.pre) {\n        this.getInfo(nextInfo.pre).next = undefined;\n      }\n\n      // 2. next连接到before上\n      if (toBlockLastChild) {\n        const lastChildInfo = this.getInfo(toBlockLastChild);\n        lastChildInfo.next = next;\n        nextInfo.pre = toBlockLastChild;\n      }\n\n      // 3. 获取所有后续节点, 将所有后续节点重新设置parent\n      const nextNodeIndex = nextParentInfo.children.indexOf(next);\n      const allNextNodes = nextParentInfo.children.slice(nextNodeIndex);\n      nextParentInfo.children = nextParentInfo.children.slice(0, nextNodeIndex);\n      for (const node of allNextNodes) {\n        const nodeInfo = this.getInfo(node);\n        toBlockInfo.children.push(node);\n        nodeInfo.parent = toBlock;\n      }\n    }\n  }\n\n  getInfo(node: T): FlowVirtualTree.NodeInfo<T> {\n    const info = this.map.get(node) || this.originTree.getInfo(node);\n    return info;\n  }\n\n  // 或者originTree节点的信息\n  getOriginInfo(node: T): FlowVirtualTree.NodeInfo<T> {\n    return this.originTree.getInfo(node);\n  }\n\n  // 获取收起的隐藏节点\n  getCollapsedChildren(node: T): T[] {\n    return this.getOriginInfo(node).children || [];\n  }\n\n  remove(): void {\n    throw new Error('Render Tree cannot use remove node');\n  }\n\n  addChild(): T {\n    throw new Error('Render tree cannot use add child');\n  }\n\n  insertAfter(): void {\n    throw new Error('Render tree cannot use insert after');\n  }\n\n  removeParent(): void {\n    throw new Error('Render tree cannot use remove parent');\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/flow-virtual-tree.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Disposable, Emitter } from '@flowgram.ai/utils';\n\nimport { type FlowNodeType } from './typings';\n\n/**\n * 存储节点的 tree 结构信息\n * 策略是 \"重修改轻查询\"，即修改时候做的事情更多，查询都通过指针来操作\n */\nexport class FlowVirtualTree<T extends { id: string; flowNodeType?: FlowNodeType }>\n  implements Disposable\n{\n  protected onTreeChangeEmitter = new Emitter<void>();\n\n  /**\n   * tree 结构变化时候触发\n   */\n  onTreeChange = this.onTreeChangeEmitter.event;\n\n  protected map: Map<T, FlowVirtualTree.NodeInfo<T>> = new Map();\n\n  constructor(readonly root: T) {}\n\n  dispose() {\n    this.map.clear();\n    this.onTreeChangeEmitter.dispose();\n  }\n\n  getInfo(node: T): FlowVirtualTree.NodeInfo<T> {\n    let res: FlowVirtualTree.NodeInfo<T> | undefined = this.map.get(node);\n    if (!res) {\n      res = { children: [] };\n      this.map.set(node, res);\n    }\n    return res;\n  }\n\n  clear(): void {\n    this.map.clear();\n  }\n\n  cloneMap(): Map<T, FlowVirtualTree.NodeInfo<T>> {\n    const newMap: Map<T, FlowVirtualTree.NodeInfo<T>> = new Map();\n    for (const [key, value] of this.map) {\n      newMap.set(key, {\n        ...value,\n        children: value.children.slice(),\n      });\n    }\n\n    return newMap;\n  }\n\n  clone(): FlowVirtualTree<T> {\n    const newTree = new FlowVirtualTree<T>(this.root);\n    newTree.map = this.cloneMap();\n    return newTree;\n  }\n\n  remove(node: T, withChildren = true): void {\n    this.removeParent(node);\n    if (withChildren) {\n      this._removeChildren(node);\n    }\n    this.map.delete(node);\n    this.fireTreeChange();\n  }\n\n  addChild(parent: T, child: T, index?: number): T {\n    const parentInfo = this.getInfo(parent);\n    const childInfo = this.getInfo(child);\n    if (childInfo.parent) {\n      if (childInfo.parent === parent) return child;\n      if (childInfo.parent !== parent) {\n        this.removeParent(child);\n      }\n    }\n\n    const len = parentInfo.children.length;\n    const idx = typeof index === 'undefined' ? len - 1 : index - 1;\n    const lastChild = parentInfo.children[idx];\n    const nextChild = parentInfo.children[idx + 1];\n    if (lastChild) this.getInfo(lastChild).next = child;\n    if (nextChild) this.getInfo(nextChild).pre = child;\n    childInfo.pre = lastChild;\n    childInfo.next = nextChild;\n    parentInfo.children.splice(idx + 1, 0, child);\n    childInfo.parent = parent;\n    this.fireTreeChange();\n    return child;\n  }\n\n  moveChilds(parent: T, childs: T[], index?: number): T[] {\n    const parentInfo = this.getInfo(parent);\n    const len = parentInfo.children.length;\n    let childIndex: number = index ?? len;\n\n    childs.forEach((child) => {\n      const childInfo = this.getInfo(child);\n      if (childInfo.parent) {\n        this.removeParent(child);\n      }\n    });\n\n    childs.forEach((child) => {\n      const childInfo = this.getInfo(child);\n      let lastChild: T | undefined = parentInfo.children[childIndex - 1];\n      let nextChild: T | undefined = parentInfo.children[childIndex];\n\n      if (lastChild) this.getInfo(lastChild).next = child;\n      if (nextChild) this.getInfo(nextChild).pre = child;\n      childInfo.pre = lastChild;\n      childInfo.next = nextChild;\n      parentInfo.children.splice(childIndex, 0, child);\n      childInfo.parent = parent;\n      childIndex++;\n    });\n\n    this.fireTreeChange();\n    return childs;\n  }\n\n  getById(id: string): T | undefined {\n    for (const node of this.map.keys()) {\n      if (node.id === id) return node;\n    }\n  }\n\n  /**\n   * 插入节点到后边\n   * @param before\n   * @param after\n   */\n  insertAfter(before: T, after: T) {\n    const beforeInfo = this.getInfo(before);\n    const afterInfo = this.getInfo(after);\n    this.removeParent(after);\n    if (beforeInfo.parent) {\n      const parentInfo = this.getInfo(beforeInfo.parent);\n      parentInfo.children.splice(parentInfo.children.indexOf(before) + 1, 0, after);\n      const { next } = beforeInfo;\n      if (next) {\n        this.getInfo(next).pre = after;\n      }\n      afterInfo.next = next;\n      beforeInfo.next = after;\n      afterInfo.pre = before;\n      afterInfo.parent = beforeInfo.parent;\n    }\n    this.fireTreeChange();\n  }\n\n  removeParent(node: T): void {\n    const info = this.getInfo(node);\n    if (!info.parent) return;\n    const parentInfo = this.getInfo(info.parent);\n    const index = parentInfo.children.indexOf(node);\n    parentInfo.children.splice(index, 1);\n    const { pre, next } = info;\n    if (pre) this.getInfo(pre).next = next;\n    if (next) this.getInfo(next).pre = pre;\n    this.fireTreeChange();\n  }\n\n  private _removeChildren(node: T): void {\n    // 删除子节点\n    const children = this.getChildren(node);\n    if (children.length > 0) {\n      children.forEach((child) => {\n        this._removeChildren(child);\n        this.map.delete(child);\n      });\n    }\n  }\n\n  getParent(node: T): T | undefined {\n    return this.getInfo(node).parent;\n  }\n\n  getPre(node: T): T | undefined {\n    return this.getInfo(node).pre;\n  }\n\n  getNext(node: T): T | undefined {\n    return this.getInfo(node).next;\n  }\n\n  getChildren(node: T): T[] {\n    return this.getInfo(node).children;\n  }\n\n  traverse(\n    fn: (node: T, depth: number, index: number) => boolean | void,\n    node = this.root,\n    depth = 0,\n    index = 0\n  ): boolean | void {\n    const breaked = fn(node, depth, index);\n    if (breaked) return true;\n    const info = this.getInfo(node);\n    const shouldBreak = info.children.find((child, i) => this.traverse(fn, child, depth + 1, i));\n    if (shouldBreak) return true;\n  }\n\n  /**\n   * 通知文档树结构更新\n   */\n  fireTreeChange(): void {\n    this.onTreeChangeEmitter.fire();\n  }\n\n  get size(): number {\n    return this.map.size;\n  }\n\n  toString(showType?: boolean): string {\n    const ret: string[] = [];\n    this.traverse((node, depth) => {\n      if (depth === 0) {\n        ret.push(node.id);\n      } else {\n        ret.push(\n          `|${new Array(depth).fill('--').join('')} ${\n            showType ? `${node.flowNodeType}(${node.id})` : node.id\n          }`\n        );\n      }\n    });\n    return `${ret.join('\\n')}`;\n  }\n}\n\nexport namespace FlowVirtualTree {\n  export interface NodeInfo<T> {\n    parent?: T;\n    next?: T;\n    pre?: T;\n    children: T[];\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './typings';\nexport * from './entities';\nexport * from './datas';\nexport * from './flow-document';\nexport * from './flow-virtual-tree';\nexport * from './flow-document-contribution';\nexport * from './flow-document-container-module';\nexport * from './flow-document-config';\nexport * from './flow-document-options';\nexport * from './services';\nexport * from './utils';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/layout/horizontal-fixed-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject, multiInject, optional } from 'inversify';\nimport { IPoint, OriginSchema, PaddingSchema, ScrollSchema, SizeSchema } from '@flowgram.ai/utils';\n\nimport { type FlowLayout, FlowLayoutDefault, FlowLayoutContribution } from '../typings';\nimport { type FlowDocument, FlowDocumentProvider } from '../flow-document';\nimport { FlowNodeEntity } from '../entities';\nimport { FlowNodeTransformData } from '../datas';\n\n// 开始节点距离上边 36 像素\nconst DEFAULT_SCROLL = -36;\n\n/**\n * 用于描述节点的结构特征\n */\ninterface FlowNodeTransformStructData {\n  childrenLength: number;\n  index: number;\n}\n/**\n * 用于描述节点的结构特征\n */\ninterface FlowNodeTransformStructData {\n  childrenLength: number;\n  index: number;\n}\n\nfunction isStructDataEqual(\n  struct1: FlowNodeTransformStructData,\n  struct2: FlowNodeTransformStructData\n): boolean {\n  return struct1.childrenLength === struct2.childrenLength && struct1.index === struct2.index;\n}\n\n@injectable()\nexport class HorizontalFixedLayout implements FlowLayout {\n  name = FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT;\n\n  protected structDataMap = new WeakMap<FlowNodeEntity, FlowNodeTransformStructData>();\n\n  @inject(FlowDocumentProvider) protected documentProvider: FlowDocumentProvider;\n\n  @multiInject(FlowLayoutContribution)\n  @optional()\n  contribs?: FlowLayoutContribution[];\n\n  get document(): FlowDocument {\n    return this.documentProvider();\n  }\n\n  reload() {\n    this.structDataMap = new WeakMap();\n  }\n\n  /**\n   * 更新布局\n   */\n  update(): void {\n    this.updateLocalTransform(this.document.root);\n  }\n\n  /**\n   * 更新节点的偏移\n   * @param node\n   * @param forceChange\n   */\n  updateLocalTransform(node: FlowNodeEntity, forceChange = false): boolean {\n    const { children, parent, isInlineBlock } = node;\n\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    const { getDelta, getOrigin } = node.getNodeRegistry();\n    const lastStructData = this.structDataMap.get(node) || {\n      childrenLength: 0,\n      index: -1,\n    };\n    // 重新计算都要清空 bounds 缓存，因为 bounds 依赖所有\n    node.clearMemoGlobal();\n    let localDirty = transform.localDirty || forceChange;\n    const newStructData: FlowNodeTransformStructData = {\n      index: node.index,\n      childrenLength: node.children.length,\n    };\n    // index 变化也要重新计算\n    if (!isStructDataEqual(lastStructData, newStructData)) {\n      localDirty = true;\n      this.structDataMap.set(node, newStructData);\n    }\n    // Step1: 计算子节点\n    let siblingDirty = false;\n    if (children.length > 0) {\n      for (const child of children) {\n        const childDirty = this.updateLocalTransform(child, siblingDirty);\n        // 子节点变更则父节点跟着变更\n        if (childDirty) {\n          siblingDirty = true;\n          localDirty = true;\n        }\n      }\n    }\n    // 如果没有变更则不执行\n    if (!localDirty) return false;\n    // Step2: 计算节点的 position 偏移量\n    node.clearMemoLocal();\n    transform.transform.update({\n      origin: getOrigin ? getOrigin(transform, this) : this.getDefaultNodeOrigin(),\n    });\n    const preTransform = transform.pre;\n    const delta = getDelta?.(transform, this) || { x: 0, y: 0 };\n    const inlineSpacingPre =\n      isInlineBlock && transform.parent?.inlineSpacingPre ? transform.parent?.inlineSpacingPre : 0;\n    const fromParentDelta = parent?.getNodeRegistry().getChildDelta?.(transform, this) || {\n      x: 0,\n      y: 0,\n    };\n    delta.x += fromParentDelta.x;\n    delta.y += fromParentDelta.y;\n\n    // Step3：根据上一个节点的相对偏移算当前偏移\n    const position = { x: delta.x, y: delta.y };\n    // 水平布局\n    if (isInlineBlock) {\n      position.x += inlineSpacingPre;\n    } else {\n      position.x += preTransform?.localBounds.right || 0;\n      position.x += preTransform?.spacing || 0;\n    }\n\n    transform.transform.update({\n      size: transform.data.size,\n      position,\n    });\n\n    // 布局结束后可执行额外逻辑\n    this.onAfterUpdateLocalTransform(transform);\n\n    transform.localDirty = false;\n\n    return true;\n  }\n\n  onAfterUpdateLocalTransform(transform: FlowNodeTransformData) {\n    // 执行 register 上的 onAfterUpdateLocalTransform\n    const { onAfterUpdateLocalTransform } = transform.entity.getNodeRegistry();\n    onAfterUpdateLocalTransform?.(transform, this);\n\n    // 执行 contribution 上的 onAfterUpdateLocalTransform\n    this.contribs?.forEach((_contrib) => {\n      _contrib?.onAfterUpdateLocalTransform?.(transform, this);\n    });\n  }\n\n  getNodeTransform(node: FlowNodeEntity): FlowNodeTransformData {\n    return node.getData(FlowNodeTransformData)!;\n  }\n\n  getPadding(node: FlowNodeEntity): PaddingSchema {\n    const { inlineSpacingPre, inlineSpacingAfter, padding } = node.getNodeMeta();\n    const transform = this.getNodeTransform(node);\n    if (padding) {\n      return typeof padding === 'function' ? padding(transform) : padding;\n    }\n\n    const paddingPre =\n      typeof inlineSpacingPre === 'function' ? inlineSpacingPre(transform) : inlineSpacingPre;\n    const paddingAfter =\n      typeof inlineSpacingAfter === 'function' ? inlineSpacingAfter(transform) : inlineSpacingAfter;\n\n    return {\n      left: paddingPre,\n      top: 0,\n      right: paddingAfter,\n      bottom: 0,\n    };\n  }\n\n  getInitScroll(contentSize: SizeSchema): ScrollSchema {\n    return {\n      scrollX: DEFAULT_SCROLL,\n      scrollY: -contentSize.height / 2,\n    };\n  }\n\n  getDefaultInputPoint(node: FlowNodeEntity): IPoint {\n    return this.getNodeTransform(node).bounds.leftCenter;\n  }\n\n  getDefaultOutputPoint(node: FlowNodeEntity): IPoint {\n    return this.getNodeTransform(node).bounds.rightCenter;\n  }\n\n  getDefaultNodeOrigin(): OriginSchema {\n    return { x: 0, y: 0.5 };\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/layout/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './vertical-fixed-layout';\nexport * from './horizontal-fixed-layout';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/layout/vertical-fixed-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject, multiInject, optional } from 'inversify';\nimport { IPoint, OriginSchema, PaddingSchema, ScrollSchema, SizeSchema } from '@flowgram.ai/utils';\n\nimport { type FlowLayout, FlowLayoutDefault, FlowLayoutContribution } from '../typings';\nimport { type FlowDocument, FlowDocumentProvider } from '../flow-document';\nimport { FlowNodeEntity } from '../entities';\nimport { FlowNodeTransformData } from '../datas';\n\n// 开始节点距离上边 36 像素\nconst DEFAULT_SCROLL = -36;\n\n/**\n * 用于描述节点的结构特征\n */\ninterface FlowNodeTransformStructData {\n  childrenLength: number;\n  index: number;\n}\n/**\n * 用于描述节点的结构特征\n */\ninterface FlowNodeTransformStructData {\n  childrenLength: number;\n  index: number;\n}\n\nfunction isStructDataEqual(\n  struct1: FlowNodeTransformStructData,\n  struct2: FlowNodeTransformStructData\n): boolean {\n  return struct1.childrenLength === struct2.childrenLength && struct1.index === struct2.index;\n}\n\n@injectable()\nexport class VerticalFixedLayout implements FlowLayout {\n  name = FlowLayoutDefault.VERTICAL_FIXED_LAYOUT;\n\n  protected structDataMap = new WeakMap<FlowNodeEntity, FlowNodeTransformStructData>();\n\n  @inject(FlowDocumentProvider) protected documentProvider: FlowDocumentProvider;\n\n  @multiInject(FlowLayoutContribution)\n  @optional()\n  contribs?: FlowLayoutContribution[];\n\n  get document(): FlowDocument {\n    return this.documentProvider();\n  }\n\n  reload() {\n    this.structDataMap = new WeakMap();\n  }\n\n  /**\n   * 更新布局\n   */\n  update(): void {\n    this.updateLocalTransform(this.document.root);\n  }\n\n  /**\n   * 更新节点的偏移\n   * @param node\n   * @param forceChange\n   */\n  updateLocalTransform(node: FlowNodeEntity, forceChange = false): boolean {\n    const { children, parent, isInlineBlock } = node;\n\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    const { getDelta, getOrigin } = node.getNodeRegistry();\n    const lastStructData = this.structDataMap.get(node) || {\n      childrenLength: 0,\n      index: -1,\n    };\n    // 重新计算都要清空 bounds 缓存，因为 bounds 依赖所有\n    node.clearMemoGlobal();\n    let localDirty = transform.localDirty || forceChange;\n    const newStructData: FlowNodeTransformStructData = {\n      index: node.index,\n      childrenLength: node.children.length,\n    };\n    // index 变化也要重新计算\n    if (!isStructDataEqual(lastStructData, newStructData)) {\n      localDirty = true;\n      this.structDataMap.set(node, newStructData);\n    }\n    // Step1: 计算子节点\n    let siblingDirty = false;\n    if (children.length > 0) {\n      for (const child of children) {\n        const childDirty = this.updateLocalTransform(child, siblingDirty);\n        // 子节点变更则父节点跟着变更\n        if (childDirty) {\n          siblingDirty = true;\n          localDirty = true;\n        }\n      }\n    }\n    // 如果没有变更则不执行\n    if (!localDirty) return false;\n    // Step2: 计算节点的 position 偏移量\n    node.clearMemoLocal();\n    transform.transform.update({\n      origin: getOrigin ? getOrigin(transform, this) : this.getDefaultNodeOrigin(),\n    });\n    const preTransform = transform.pre;\n    const delta = getDelta?.(transform, this) || { x: 0, y: 0 };\n    const inlineSpacingPre =\n      isInlineBlock && transform.parent?.inlineSpacingPre ? transform.parent?.inlineSpacingPre : 0;\n    const fromParentDelta = parent?.getNodeRegistry().getChildDelta?.(transform, this) || {\n      x: 0,\n      y: 0,\n    };\n    delta.x += fromParentDelta.x;\n    delta.y += fromParentDelta.y;\n\n    // Step3：根据上一个节点的相对偏移算当前偏移\n    const position = { x: delta.x, y: delta.y };\n    // 垂直布局\n    if (isInlineBlock) {\n      position.y += inlineSpacingPre;\n    } else {\n      position.y += preTransform?.localBounds.bottom || 0;\n      position.y += preTransform?.spacing || 0;\n    }\n\n    transform.transform.update({\n      size: transform.data.size,\n      position,\n    });\n\n    // 布局结束后可执行额外逻辑\n    this.onAfterUpdateLocalTransform(transform);\n\n    transform.localDirty = false;\n\n    return true;\n  }\n\n  onAfterUpdateLocalTransform(transform: FlowNodeTransformData) {\n    // 执行 register 上的 onAfterUpdateLocalTransform\n    const { onAfterUpdateLocalTransform } = transform.entity.getNodeRegistry();\n    onAfterUpdateLocalTransform?.(transform, this);\n\n    // 执行 contribution 上的 onAfterUpdateLocalTransform\n    this.contribs?.forEach((_contrib) => {\n      _contrib?.onAfterUpdateLocalTransform?.(transform, this);\n    });\n  }\n\n  getNodeTransform(node: FlowNodeEntity): FlowNodeTransformData {\n    return node.getData(FlowNodeTransformData)!;\n  }\n\n  getPadding(node: FlowNodeEntity): PaddingSchema {\n    const { inlineSpacingPre, inlineSpacingAfter, padding } = node.getNodeMeta();\n    const transform = this.getNodeTransform(node);\n    if (padding) {\n      return typeof padding === 'function' ? padding(transform) : padding;\n    }\n\n    const paddingPre =\n      typeof inlineSpacingPre === 'function' ? inlineSpacingPre(transform) : inlineSpacingPre;\n    const paddingAfter =\n      typeof inlineSpacingAfter === 'function' ? inlineSpacingAfter(transform) : inlineSpacingAfter;\n\n    return {\n      left: 0,\n      top: paddingPre,\n      right: 0,\n      bottom: paddingAfter,\n    };\n  }\n\n  getInitScroll(contentSize: SizeSchema): ScrollSchema {\n    return {\n      scrollX: -contentSize.width / 2,\n      scrollY: DEFAULT_SCROLL,\n    };\n  }\n\n  getDefaultInputPoint(node: FlowNodeEntity): IPoint {\n    return this.getNodeTransform(node).bounds.topCenter;\n  }\n\n  getDefaultOutputPoint(node: FlowNodeEntity): IPoint {\n    return this.getNodeTransform(node).bounds.bottomCenter;\n  }\n\n  getDefaultNodeOrigin(): OriginSchema {\n    return { x: 0.5, y: 0 };\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-drag-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { FlowGroupUtils } from './flow-group-service/flow-group-utils';\nimport {\n  FlowNodeBaseType,\n  FlowNodeJSON,\n  FlowOperationBaseService,\n  LABEL_SIDE_TYPE,\n} from '../typings';\nimport { FlowDocument } from '../flow-document';\nimport { FlowNodeEntity, FlowRendererStateEntity } from '../entities';\n\n/**\n * 拖拽相关操作\n * 外部实现抽象类\n */\n@injectable()\nexport class FlowDragService {\n  @inject(FlowDocument)\n  protected document: FlowDocument;\n\n  @inject(FlowOperationBaseService)\n  protected operationService: FlowOperationBaseService;\n\n  @inject(EntityManager)\n  protected entityManager: EntityManager;\n\n  protected onDropEmitter = new Emitter<{\n    dropNode: FlowNodeEntity;\n    dragNodes: FlowNodeEntity[];\n    dragJSON?: FlowNodeJSON;\n  }>();\n\n  readonly onDrop = this.onDropEmitter.event;\n\n  get renderState(): FlowRendererStateEntity {\n    return this.document.renderState;\n  }\n\n  // 拖拽所有节点中的首个节点\n  get dragStartNode(): FlowNodeEntity {\n    return this.renderState.getDragStartEntity()!;\n  }\n\n  // 拖拽的所有节点\n  get dragNodes(): FlowNodeEntity[] {\n    return this.renderState.getDragEntities();\n  }\n\n  // 放置的区域\n  get dropNodeId(): string | undefined {\n    return this.renderState.getNodeDroppingId();\n  }\n\n  // 是否在拖拽分支\n  get isDragBranch(): boolean {\n    return this.renderState.isBranch || this.dragStartNode?.isInlineBlock;\n  }\n\n  // 拖拽的所有节点及其自节点\n  get nodeDragIdsWithChildren(): string[] {\n    return this.renderState.config.nodeDragIdsWithChildren || [];\n  }\n\n  get dragging(): boolean {\n    return !!this.renderState.dragging;\n  }\n\n  get labelSide(): LABEL_SIDE_TYPE | undefined {\n    return this.renderState.config.dragLabelSide;\n  }\n\n  /**\n   * 放置到目标分支\n   */\n  dropBranch(): void {\n    this.dropNode();\n  }\n\n  /**\n   * 移动并且创建节点\n   * Move and create node\n   */\n  async dropCreateNode(\n    json: FlowNodeJSON,\n    onCreateNode?: (json: FlowNodeJSON, dropEntity: FlowNodeEntity) => Promise<FlowNodeEntity>\n  ) {\n    const dropEntity = this.document.getNode(this.dropNodeId!);\n\n    if (!dropEntity) {\n      return;\n    }\n\n    if (json) {\n      const dragNodes = await onCreateNode?.(json, dropEntity);\n      this.onDropEmitter.fire({\n        dropNode: dropEntity,\n        dragNodes: dragNodes ? [dragNodes] : [],\n        dragJSON: json,\n      });\n    }\n  }\n\n  /**\n   * 移动到目标节点\n   */\n  dropNode(): void {\n    const dropEntity = this.document.getNode(this.dropNodeId!);\n    if (!dropEntity) {\n      return;\n    }\n\n    const sortNodes: FlowNodeEntity[] = [];\n    let curr: FlowNodeEntity | undefined = this.dragStartNode;\n    while (curr && this.dragNodes.includes(curr)) {\n      sortNodes.push(curr);\n      curr = curr.next;\n    }\n\n    this.operationService.dragNodes({\n      dropNode: dropEntity,\n      nodes: sortNodes,\n    });\n\n    if (sortNodes.length > 0) {\n      this.onDropEmitter.fire({\n        dropNode: dropEntity,\n        dragNodes: sortNodes,\n      });\n    }\n  }\n\n  /**\n   * 拖拽是否可以释放在该节点后面\n   */\n  isDroppableNode(node: FlowNodeEntity) {\n    // 没有拖拽节点时，所有节点都不可 drop\n    if (!this.dragging || this.isDragBranch) {\n      return false;\n    }\n\n    // 当前节点 & 下一个节点是否在拖拽区域\n    if (\n      this.nodeDragIdsWithChildren.includes(node.id) ||\n      (node.next && this.nodeDragIdsWithChildren.includes(node.next.id))\n    ) {\n      return false;\n    }\n\n    if (node.isInlineBlocks || node.isInlineBlock) {\n      return false;\n    }\n\n    // 分组节点不能嵌套\n    const hasGroupNode = this.dragNodes.some(\n      (node) => node.flowNodeType === FlowNodeBaseType.GROUP\n    );\n    if (hasGroupNode) {\n      const group = FlowGroupUtils.getNodeRecursionGroupController(node);\n      if (group) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  /**\n   * 拖拽分支是否可以释放在该分支\n   * @param node 拖拽的分支节点\n   * @param side 分支的前面还是后面\n   */\n  isDroppableBranch(node: FlowNodeEntity, side: LABEL_SIDE_TYPE = LABEL_SIDE_TYPE.NORMAL_BRANCH) {\n    // 外部添加拖拽标识，默认所有分支均可添加\n    if (this.renderState.isBranch) {\n      return true;\n    }\n    // 拖拽的是分支\n    if (this.isDragBranch) {\n      if (\n        // 拖拽到分支\n        !node.isInlineBlock ||\n        // 只能在同一分支条件下\n        node.parent !== this.dragStartNode.parent ||\n        // 自己不能拖拽给自己\n        node === this.dragStartNode\n      ) {\n        return false;\n      }\n\n      // 分支的下一个节点不是当前要拖拽的节点\n      if (side === LABEL_SIDE_TYPE.NORMAL_BRANCH && node.next !== this.dragStartNode) {\n        return true;\n      }\n\n      // 分支的前一个节点不是当前要拖拽的节点\n      if (side === LABEL_SIDE_TYPE.PRE_BRANCH && node.pre !== this.dragStartNode) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-group-service/flow-group-controller.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\n\nimport { FlowNodeEntity } from '../../entities';\nimport { FlowNodeRenderData, FlowNodeTransformData } from '../../datas';\nimport { FlowGroupUtils } from './flow-group-utils';\n\n/** 分组控制器 */\nexport class FlowGroupController {\n  private constructor(public readonly groupNode: FlowNodeEntity) {}\n\n  public get nodes(): FlowNodeEntity[] {\n    return this.groupNode.collapsedChildren || [];\n  }\n\n  public get collapsed(): boolean {\n    const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    return groupTransformData.collapsed;\n  }\n\n  public collapse(): void {\n    this.collapsed = true;\n  }\n\n  public expand(): void {\n    this.collapsed = false;\n  }\n\n  /** 获取分组外围的最大边框 */\n  public get bounds(): Rectangle {\n    const groupNodeBounds =\n      this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData).bounds;\n    return groupNodeBounds;\n  }\n\n  /** 是否是开始节点 */\n  public isStartNode(node?: FlowNodeEntity): boolean {\n    if (!node) {\n      return false;\n    }\n    const nodes = this.nodes;\n    if (!nodes[0]) {\n      return false;\n    }\n    return node.id === nodes[0].id;\n  }\n\n  /** 是否是结束节点 */\n  public isEndNode(node?: FlowNodeEntity): boolean {\n    if (!node) {\n      return false;\n    }\n    const nodes = this.nodes;\n    if (!nodes[nodes.length - 1]) {\n      return false;\n    }\n    return node.id === nodes[nodes.length - 1].id;\n  }\n\n  public set note(note: string) {\n    this.groupNode.getNodeMeta().note = note;\n  }\n\n  public get note(): string {\n    return this.groupNode.getNodeMeta().note || '';\n  }\n\n  public set noteHeight(height: number) {\n    this.groupNode.getNodeMeta().noteHeight = height;\n  }\n\n  public get noteHeight(): number {\n    return this.groupNode.getNodeMeta().noteHeight || 0;\n  }\n\n  public get positionConfig(): Record<string, number> {\n    return this.groupNode.getNodeMeta().positionConfig;\n  }\n\n  private set collapsed(collapsed: boolean) {\n    const groupTransformData = this.groupNode.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    groupTransformData.collapsed = collapsed;\n    groupTransformData.localDirty = true;\n    if (groupTransformData.parent) groupTransformData.parent.localDirty = true;\n    if (groupTransformData.parent?.firstChild)\n      groupTransformData.parent.firstChild.localDirty = true;\n  }\n\n  public set hovered(hovered: boolean) {\n    const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);\n    if (hovered) {\n      groupRenderData.toggleMouseEnter();\n    } else {\n      groupRenderData.toggleMouseLeave();\n    }\n    if (groupRenderData.hovered === hovered) {\n      return;\n    }\n    groupRenderData.hovered = hovered;\n  }\n\n  public get hovered(): boolean {\n    const groupRenderData = this.groupNode.getData<FlowNodeRenderData>(FlowNodeRenderData);\n    return groupRenderData.hovered;\n  }\n\n  public static create(groupNode?: FlowNodeEntity): FlowGroupController | undefined {\n    if (!groupNode) {\n      return;\n    }\n    if (!FlowGroupUtils.isGroupNode(groupNode)) {\n      return;\n    }\n    return new FlowGroupController(groupNode);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-group-service/flow-group-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { inject, injectable } from 'inversify';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { FlowNodeBaseType, FlowOperationBaseService, OperationType } from '../../typings';\nimport { FlowNodeEntity } from '../../entities';\nimport { FlowGroupUtils } from './flow-group-utils';\nimport { FlowGroupController } from './flow-group-controller';\n\n@injectable()\n/** 分组服务 */\nexport class FlowGroupService {\n  @inject(EntityManager) public readonly entityManager: EntityManager;\n\n  @inject(FlowOperationBaseService)\n  public readonly operationService: FlowOperationBaseService;\n\n  /** 创建分组节点 */\n  public createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {\n    if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {\n      return;\n    }\n    if (!FlowGroupUtils.validate(nodes)) {\n      return;\n    }\n    const sortedNodes: FlowNodeEntity[] = nodes.sort((a, b) => a.index - b.index);\n    const fromNode = sortedNodes[0];\n    const groupId = `group_${nanoid(5)}`;\n    this.operationService.apply({\n      type: OperationType.createGroup,\n      value: {\n        targetId: fromNode.id,\n        groupId,\n        nodeIds: nodes.map((node) => node.id),\n      },\n    });\n    const groupNode = this.entityManager.getEntityById<FlowNodeEntity>(groupId);\n    if (!groupNode) {\n      return;\n    }\n    const group = this.groupController(groupNode);\n    if (!group) {\n      return;\n    }\n    group.expand();\n    return groupNode;\n  }\n\n  /** 删除分组 */\n  public deleteGroup(groupNode: FlowNodeEntity): void {\n    const json = groupNode.toJSON();\n    if (!groupNode.pre || !json) {\n      return;\n    }\n    this.operationService.apply({\n      type: OperationType.deleteNodes,\n      value: {\n        fromId: groupNode.pre.id,\n        nodes: [json],\n      },\n    });\n  }\n\n  /** 取消分组 */\n  public ungroup(groupNode: FlowNodeEntity): void {\n    const group = this.groupController(groupNode);\n    if (!group) {\n      return;\n    }\n    const nodes = group.nodes;\n    if (!groupNode.pre) {\n      return;\n    }\n    group.collapse();\n    this.operationService.apply({\n      type: OperationType.ungroup,\n      value: {\n        groupId: groupNode.id,\n        targetId: groupNode.pre.id,\n        nodeIds: nodes.map((node) => node.id),\n      },\n    });\n  }\n\n  /** 返回所有分组节点 */\n  public getAllGroups(): FlowGroupController[] {\n    const allNodes = this.entityManager.getEntities<FlowNodeEntity>(FlowNodeEntity);\n    const groupNodes = allNodes.filter((node) => node.flowNodeType === FlowNodeBaseType.GROUP);\n    return groupNodes\n      .map((node) => this.groupController(node))\n      .filter(Boolean) as FlowGroupController[];\n  }\n\n  /** 获取分组控制器*/\n  public groupController(group: FlowNodeEntity): FlowGroupController | undefined {\n    return FlowGroupController.create(group);\n  }\n\n  public static validate(nodes: FlowNodeEntity[]): boolean {\n    return FlowGroupUtils.validate(nodes);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-group-service/flow-group-utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '../../typings';\nimport { FlowNodeEntity } from '../../entities';\nimport { FlowGroupController } from './flow-group-controller';\n\nexport namespace FlowGroupUtils {\n  /** 找到节点所有上级 */\n  const findNodeParents = (node: FlowNodeEntity): FlowNodeEntity[] => {\n    const parents = [];\n    let parent = node.parent;\n    while (parent) {\n      parents.push(parent);\n      parent = parent.parent;\n    }\n    return parents;\n  };\n\n  /** 节点是否处于分组中 */\n  const isNodeInGroup = (node: FlowNodeEntity): boolean => {\n    // 处于分组中\n    if (node?.parent?.flowNodeType === FlowNodeBaseType.GROUP) {\n      return true;\n    }\n    return false;\n  };\n\n  /** 判断节点能否组成分组 */\n  export const validate = (nodes: FlowNodeEntity[]): boolean => {\n    if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {\n      // 参数不合法\n      return false;\n    }\n\n    // 判断是否有分组节点\n    const isGroupRelatedNode = nodes.some((node) => isGroupNode(node));\n    if (isGroupRelatedNode) return false;\n\n    // 判断是否有节点已经处于分组中\n    const hasGroup = nodes.some((node) => node && isNodeInGroup(node));\n    if (hasGroup) return false;\n\n    // 判断是否来自同一个父亲\n    const parent = nodes[0].parent;\n    const isSameParent = nodes.every((node) => node.parent === parent);\n    if (!isSameParent) return false;\n\n    // 判断节点索引是否连续\n    const indexes = nodes.map((node) => node.index).sort((a, b) => a - b);\n    const isIndexContinuous = indexes.every((index, i, arr) => {\n      if (i === 0) {\n        return true;\n      }\n      return index === arr[i - 1] + 1;\n    });\n    if (!isIndexContinuous) return false;\n\n    // 判断节点父亲是否已经在分组中\n    const parents = findNodeParents(nodes[0]);\n    const parentsInGroup = parents.some((parent) => isNodeInGroup(parent));\n    if (parentsInGroup) return false;\n\n    // 参数正确\n    return true;\n  };\n\n  /** 获取节点分组控制 */\n  export const getNodeGroupController = (\n    node?: FlowNodeEntity\n  ): FlowGroupController | undefined => {\n    if (!node) {\n      return;\n    }\n    if (!isNodeInGroup(node)) {\n      return;\n    }\n    const groupNode = node?.parent;\n    return FlowGroupController.create(groupNode);\n  };\n\n  /** 向上递归查找分组递归控制 */\n  export const getNodeRecursionGroupController = (\n    node?: FlowNodeEntity\n  ): FlowGroupController | undefined => {\n    if (!node) {\n      return;\n    }\n    const group = getNodeGroupController(node);\n    if (group) {\n      return group;\n    }\n    if (node.parent) {\n      return getNodeRecursionGroupController(node.parent);\n    }\n    return;\n  };\n\n  /** 是否分组节点 */\n  export const isGroupNode = (group: FlowNodeEntity): boolean =>\n    group.flowNodeType === FlowNodeBaseType.GROUP;\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-group-service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowGroupController } from './flow-group-controller';\nexport { FlowGroupService } from './flow-group-service';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/flow-operation-base-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, postConstruct } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport {\n  FlowOperation,\n  FlowOperationBaseService,\n  MoveChildNodesOperationValue,\n  OperationType,\n} from '../typings/flow-operation';\nimport {\n  AddBlockConfig,\n  AddNodeConfig,\n  AddNodeData,\n  FlowNodeBaseType,\n  FlowNodeEntityOrId,\n  FlowNodeJSON,\n  MoveNodeConfig,\n  OnNodeAddEvent,\n  OnNodeMoveEvent,\n} from '../typings';\nimport { FlowDocument } from '../flow-document';\nimport { FlowNodeEntity } from '../entities';\n\n/**\n * 操作服务\n */\n@injectable()\nexport class FlowOperationBaseServiceImpl implements FlowOperationBaseService {\n  @inject(EntityManager)\n  protected entityManager: EntityManager;\n\n  @inject(FlowDocument)\n  protected document: FlowDocument;\n\n  protected onNodeAddEmitter = new Emitter<OnNodeAddEvent>();\n\n  readonly onNodeAdd = this.onNodeAddEmitter.event;\n\n  protected toDispose = new DisposableCollection();\n\n  private onNodeMoveEmitter = new Emitter<OnNodeMoveEvent>();\n\n  readonly onNodeMove = this.onNodeMoveEmitter.event;\n\n  @postConstruct()\n  protected init() {\n    this.toDispose.pushAll([this.onNodeAddEmitter, this.onNodeMoveEmitter]);\n  }\n\n  addNode(nodeJSON: FlowNodeJSON, config: AddNodeConfig = {}): FlowNodeEntity {\n    const { parent, index, hidden } = config;\n    let parentEntity;\n\n    if (parent) {\n      parentEntity = this.toNodeEntity(parent);\n    }\n\n    let register;\n    if (parentEntity) {\n      register = parentEntity.getNodeRegistry();\n    }\n\n    const addJSON = {\n      ...nodeJSON,\n      type: nodeJSON.type || FlowNodeBaseType.BLOCK,\n    };\n\n    const addNodeData: AddNodeData = {\n      ...addJSON,\n      parent: parentEntity,\n      index,\n      hidden,\n    };\n\n    let added;\n    if (parentEntity && register?.addChild) {\n      added = register.addChild(parentEntity, addJSON, {\n        index,\n        hidden,\n      });\n    } else {\n      added = this.document.addNode(addNodeData);\n    }\n\n    this.onNodeAddEmitter.fire({\n      node: added,\n      data: addNodeData,\n    });\n\n    return added;\n  }\n\n  addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity {\n    return this.document.addFromNode(fromNode, nodeJSON);\n  }\n\n  deleteNode(node: FlowNodeEntityOrId): void {\n    this.document.removeNode(node);\n  }\n\n  deleteNodes(nodes: FlowNodeEntityOrId[]): void {\n    (nodes || []).forEach((node) => {\n      this.deleteNode(node);\n    });\n  }\n\n  addBlock(\n    target: FlowNodeEntityOrId,\n    blockJSON: FlowNodeJSON,\n    config: AddBlockConfig = {}\n  ): FlowNodeEntity {\n    const { parent, index } = config;\n    return this.document.addBlock(target, blockJSON, undefined, parent, index);\n  }\n\n  moveNode(node: FlowNodeEntityOrId, config: MoveNodeConfig = {}) {\n    const { parent: newParent, index } = config;\n    const entity = this.toNodeEntity(node);\n    const parent = entity?.parent;\n\n    if (!parent) {\n      return;\n    }\n\n    const newParentEntity: FlowNodeEntity | undefined = newParent\n      ? this.toNodeEntity(newParent)\n      : parent;\n\n    if (!newParentEntity) {\n      console.warn('no new parent found', newParent);\n      return;\n    }\n\n    let toIndex = typeof index === 'undefined' ? newParentEntity.collapsedChildren.length : index;\n\n    return this.doMoveNode(entity, newParentEntity, toIndex);\n  }\n\n  /**\n   * 拖拽节点\n   * @param param0\n   * @returns\n   */\n  dragNodes({ dropNode, nodes }: { dropNode: FlowNodeEntity; nodes: FlowNodeEntity[] }) {\n    if (nodes.length === 0) {\n      return;\n    }\n\n    const startNode = nodes[0];\n    const fromParent = startNode.parent;\n    const toParent = dropNode.parent;\n\n    if (!fromParent || !toParent) {\n      return;\n    }\n\n    const fromIndex = fromParent.children.findIndex((child) => child === startNode);\n    const dropIndex = toParent.children.findIndex((child) => child === dropNode);\n\n    let toIndex = dropIndex + 1;\n    // 同父级节点移动，处理脏路径\n    if (fromParent === toParent && fromIndex < dropIndex) {\n      toIndex = toIndex - nodes.length;\n    }\n\n    const value: MoveChildNodesOperationValue = {\n      nodeIds: nodes.map((node) => node.id),\n      fromParentId: fromParent.id,\n      toParentId: toParent.id,\n      fromIndex,\n      toIndex,\n    };\n\n    return this.apply({\n      type: OperationType.moveChildNodes,\n      value,\n    });\n  }\n\n  /**\n   * 执行操作\n   * @param operation 可序列化的操作\n   * @returns 操作返回\n   */\n  apply(operation: FlowOperation): any {\n    const document = this.document;\n    switch (operation.type) {\n      case OperationType.addFromNode:\n        return document.addFromNode(operation.value.fromId, operation.value.data);\n      case OperationType.deleteFromNode:\n        return document.getNode(operation.value?.data?.id)?.dispose();\n      case OperationType.addBlock: {\n        let parent;\n\n        if (operation.value.parentId) {\n          parent = document.getNode(operation.value.parentId);\n        }\n        return document.addBlock(\n          operation.value.targetId,\n          operation.value.blockData,\n          undefined,\n          parent,\n          operation.value.index\n        );\n      }\n      case OperationType.deleteBlock: {\n        const entity = document.getNode(operation.value?.blockData.id);\n        return entity?.dispose();\n      }\n      case OperationType.createGroup: {\n        const groupNode = document.addFromNode(operation.value.targetId, {\n          id: operation.value.groupId,\n          type: FlowNodeBaseType.GROUP,\n        });\n        document.moveNodes({\n          dropNodeId: operation.value.groupId,\n          sortNodeIds: operation.value.nodeIds,\n          inside: true,\n        });\n        return groupNode;\n      }\n      case OperationType.ungroup: {\n        document.moveNodes({\n          dropNodeId: operation.value.groupId,\n          sortNodeIds: operation.value.nodeIds,\n        });\n        return document.getNode(operation.value.groupId)?.dispose();\n      }\n      case OperationType.moveNodes: {\n        return document.moveNodes({\n          dropNodeId: operation.value.toId,\n          sortNodeIds: operation.value.nodeIds,\n        });\n      }\n      case OperationType.moveBlock: {\n        return document.moveChildNodes({\n          ...operation.value,\n          nodeIds: [operation.value.nodeId],\n        });\n      }\n      case OperationType.addNodes: {\n        let fromId = operation.value.fromId;\n        (operation.value.nodes || []).forEach((node) => {\n          const added = document.addFromNode(fromId, node);\n          fromId = added.id;\n        });\n        break;\n      }\n      case OperationType.deleteNodes: {\n        (operation.value.nodes || []).forEach((node) => {\n          const entity = document.getNode(node.id);\n          entity?.dispose();\n        });\n        break;\n      }\n      case OperationType.addChildNode: {\n        return document.addNode({\n          ...operation.value.data,\n          parent: operation.value.parentId ? document.getNode(operation.value.parentId) : undefined,\n          originParent: operation.value.originParentId\n            ? document.getNode(operation.value.originParentId)\n            : undefined,\n          index: operation.value.index,\n          hidden: operation.value.hidden,\n        });\n      }\n      case OperationType.deleteChildNode:\n        return document.getNode(operation.value.data.id)?.dispose();\n      case OperationType.moveChildNodes:\n        return document.moveChildNodes(operation.value);\n      default:\n        throw new Error(`unknown operation type`);\n    }\n  }\n\n  /**\n   * 事务执行\n   * @param transaction\n   */\n  transact(transaction: () => void) {\n    transaction();\n  }\n\n  dispose() {\n    this.toDispose.dispose();\n  }\n\n  protected toId(node: FlowNodeEntityOrId): string {\n    return typeof node === 'string' ? node : node.id;\n  }\n\n  protected toNodeEntity(node: FlowNodeEntityOrId): FlowNodeEntity | undefined {\n    return typeof node === 'string' ? this.document.getNode(node) : node;\n  }\n\n  protected getNodeIndex(node: FlowNodeEntityOrId): number {\n    const entity = this.toNodeEntity(node);\n    const parent = entity?.parent;\n\n    if (!parent) {\n      return -1;\n    }\n\n    return parent.children.findIndex((child) => child === entity);\n  }\n\n  protected doMoveNode(node: FlowNodeEntity, newParent: FlowNodeEntity, index: number) {\n    if (!node.parent) {\n      throw new Error('root node cannot move');\n    }\n\n    const event: OnNodeMoveEvent = {\n      node,\n      fromParent: node.parent,\n      toParent: newParent,\n      fromIndex: this.getNodeIndex(node),\n      toIndex: index,\n    };\n\n    this.document.moveChildNodes({\n      nodeIds: [this.toId(node)],\n      toParentId: this.toId(newParent),\n      toIndex: index,\n    });\n    this.onNodeMoveEmitter.fire(event);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowDragService } from './flow-drag-service';\nexport { FlowOperationBaseServiceImpl } from './flow-operation-base-service';\nexport { FlowGroupService, FlowGroupController } from './flow-group-service';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow-group.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface FlowGroupJSON {\n  nodeIDs: string[];\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, PaddingSchema, ScrollSchema, SizeSchema } from '@flowgram.ai/utils';\n\nimport { type FlowNodeEntity } from '../entities';\nimport { type FlowNodeTransformData } from '../datas';\n\nexport const FlowLayout = Symbol('FlowLayout');\nexport const FlowLayoutContribution = Symbol('FlowLayoutContribution');\n\nexport enum FlowLayoutDefault {\n  VERTICAL_FIXED_LAYOUT = 'vertical-fixed-layout', // 垂直固定布局\n  HORIZONTAL_FIXED_LAYOUT = 'horizontal-fixed-layout', // 水平固定布局\n}\n\nexport namespace FlowLayoutDefault {\n  export function isVertical(layout: FlowLayout): boolean {\n    return layout.name === FlowLayoutDefault.VERTICAL_FIXED_LAYOUT;\n  }\n}\n\nexport interface FlowLayoutContribution {\n  onAfterUpdateLocalTransform?: (transform: FlowNodeTransformData, layout: FlowLayout) => void;\n}\n\n/**\n * 流程布局算法\n */\nexport interface FlowLayout {\n  /**\n   * 布局名字\n   */\n  name: string;\n  /**\n   * 布局切换时候触发\n   */\n  reload?(): void;\n  /**\n   * 更新布局\n   */\n  update(): void;\n\n  /**\n   * 获取节点的 padding 数据\n   * @param node\n   */\n  getPadding(node: FlowNodeEntity): PaddingSchema;\n\n  /**\n   * 获取默认滚动 目前用在 scroll-limit-layer\n   * @param contentSize\n   */\n  getInitScroll(contentSize: SizeSchema): ScrollSchema;\n\n  /**\n   * 获取默认输入点\n   */\n  getDefaultInputPoint(node: FlowNodeEntity): IPoint;\n\n  /**\n   * 获取默认输出点\n   */\n  getDefaultOutputPoint(node: FlowNodeEntity): IPoint;\n\n  /**\n   * 获取默认远点\n   */\n  getDefaultNodeOrigin(): IPoint;\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow-node-register.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { PaddingSchema } from '@flowgram.ai/utils';\nimport { PositionSchema } from '@flowgram.ai/utils';\nimport { type EntityDataRegistry, type OriginSchema, type SizeSchema } from '@flowgram.ai/core';\n\nimport { FlowDocument } from '../flow-document';\nimport { type FlowNodeEntity } from '../entities';\nimport { type FlowNodeTransformData, type FlowNodeTransitionData } from '../datas';\nimport { type FlowTransitionLabel, type FlowTransitionLine } from './flow-transition';\nimport { FlowLayout, FlowLayoutDefault } from './flow-layout';\nimport {\n  FLOW_DEFAULT_HIDDEN_TYPES,\n  FlowNodeBaseType,\n  type FlowNodeJSON,\n  FlowNodeType,\n} from './flow';\n\n/**\n * 节点渲染相关配置信息，可扩展\n */\nexport interface FlowNodeMeta {\n  isStart?: boolean; // 是否为开始节点\n  addable?: boolean; // 是否可添加节点\n  expandable?: boolean; // 是否可展开\n  draggable?: boolean | ((node: FlowNodeEntity) => boolean); // 是否可拖拽\n  selectable?: boolean | ((node: FlowNodeEntity, mousePos?: PositionSchema) => boolean); // 是否可被框选\n  deleteDisable?: boolean; // 是否禁用删除\n  copyDisable?: boolean; // 禁用复制\n  addDisable?: boolean; // 禁止添加\n  hidden?: boolean; // 是否隐藏\n  // maxSize?: SizeSchema // 默认展开后的大小\n  size?: SizeSchema; // 默认大小\n  autoResizeDisable?: boolean; // 禁用监听节点变化自动调整大小\n  /**\n   * @deprecated 使用 NodeRegister.getOrigin 代替\n   */\n  origin?: OriginSchema; // 原点配置，默认 垂直 { x: 0.5, y: 0 } 水平 { x: 0, y: 0.5 }\n  defaultExpanded?: boolean; // 默认是否展开\n  defaultCollapsed?: boolean; // 复合节点默认是否收起\n  spacing?: number | ((transform: FlowNodeTransformData) => number); // 兄弟节点间，等价于 marginBottom\n  padding?: PaddingSchema | ((transform: FlowNodeTransformData) => PaddingSchema); // 节点设置了 padding，则不需要 inlineSpacingPre 和 inlineSpacingAfter\n  inlineSpacingPre?: number | ((transform: FlowNodeTransformData) => number); // 父子节点间，等价于 paddingTop 或者 paddingLeft\n  inlineSpacingAfter?: number | ((transform: FlowNodeTransformData) => number); // 父子节点间，等价于 paddingBottom 或者 paddingRight\n  renderKey?: string; // 节点的渲染组件，可以绑定 react 组件\n  isInlineBlocks?: boolean | ((node: FlowNodeEntity) => boolean); // 采用水平布局\n  minInlineBlockSpacing?: number | ((node: FlowNodeTransformData) => number); // 最小的 inlineBlock 的间距\n  isNodeEnd?: boolean; // 是否标识节点结束\n  [key: string]: any;\n}\n\n/**\n * spacing default key 值\n */\nexport const DefaultSpacingKey = {\n  /**\n   * 普通节点间距。垂直 / 水平\n   */\n  NODE_SPACING: 'SPACING',\n  /**\n   * 分支节点间距\n   */\n  BRANCH_SPACING: 'BRANCH_SPACING',\n  /**\n   * 圆弧线条拐角 radius\n   */\n  ROUNDED_LINE_RADIUS: 'ROUNDED_LINE_RADIUS',\n  /**\n   * 圆弧线条 x radius\n   */\n  ROUNDED_LINE_X_RADIUS: 'ROUNDED_LINE_X_RADIUS',\n  /**\n   * 圆弧线条 y radius\n   */\n  ROUNDED_LINE_Y_RADIUS: 'ROUNDED_LINE_Y_RADIUS',\n  /**\n   * dynamicSplit block list 下部留白间距，因为有两个拐弯，所以翻一倍\n   */\n  INLINE_BLOCKS_PADDING_BOTTOM: 'INLINE_BLOCKS_PADDING_BOTTOM',\n  /**\n   * 复合节点距离上个节点的距离\n   * 条件分支菱形下边和分支的距离\n   */\n  COLLAPSED_SPACING: 'COLLAPSED_SPACING',\n  /**\n   * width of hover area\n   */\n  HOVER_AREA_WIDTH: 'HOVER_AREA_WIDTH',\n};\n\n/**\n * 默认一些间隔参数\n */\nexport const DEFAULT_SPACING = {\n  NULL: 0,\n  [DefaultSpacingKey.NODE_SPACING]: 32, // 普通节点间距。垂直 / 水平\n  [DefaultSpacingKey.BRANCH_SPACING]: 20, // 分支节点间距\n  /**\n   * @deprecated use 'BRANCH_SPACING' instead\n   */\n  MARGIN_RIGHT: 20, // 分支节点右边间距\n  INLINE_BLOCK_PADDING_BOTTOM: 16, // block 底部留白\n  INLINE_BLOCKS_PADDING_TOP: 30, // block list 上部留白间距\n  // JS 浮点数有误差，1046.6 -1006.6 = 39.9999999，会导致 间距/20 < 2 导致布局计算问题，因此需要额外增加 0.1 像素\n  [DefaultSpacingKey.INLINE_BLOCKS_PADDING_BOTTOM]: 40.1, // block lit 下部留白间距，因为有两个拐弯，所以翻一倍\n  MIN_INLINE_BLOCK_SPACING: 200, // 分支间最小边距 (垂直布局)\n  MIN_INLINE_BLOCK_SPACING_HORIZONTAL: 80, // 分支间最小边距 (水平布局)\n  [DefaultSpacingKey.COLLAPSED_SPACING]: 12, // 复合节点距离上个节点的距离\n  [DefaultSpacingKey.ROUNDED_LINE_RADIUS]: 16, // 圆弧线条拐角 radius\n  [DefaultSpacingKey.ROUNDED_LINE_X_RADIUS]: 16, // 圆弧线条 x radius\n  [DefaultSpacingKey.ROUNDED_LINE_Y_RADIUS]: 20, // 圆弧线条 y radius\n  [DefaultSpacingKey.HOVER_AREA_WIDTH]: 20, // width of hover area\n};\n\n/**\n * 拖拽种类枚举\n * 1. 节点拖拽\n * 2. 分支拖拽\n */\nexport enum DRAGGING_TYPE {\n  NODE = 'node',\n  BRANCH = 'branch',\n}\n\n/**\n * 拖拽分支 Adder、Line 类型\n */\nexport enum LABEL_SIDE_TYPE {\n  // 前缀分支\n  PRE_BRANCH = 'pre_branch',\n  NORMAL_BRANCH = 'normal_branch',\n}\n\n/**\n * 默认节点大小\n */\nexport const DEFAULT_SIZE = {\n  width: 280,\n  height: 60,\n};\n\n/**\n * 默认 meta 配置\n */\nexport const DEFAULT_FLOW_NODE_META: (\n  nodeType: FlowNodeType,\n  document: FlowDocument\n) => FlowNodeMeta = (nodeType: FlowNodeType, document: FlowDocument) => {\n  const hidden = FLOW_DEFAULT_HIDDEN_TYPES.includes(nodeType);\n  return {\n    isStart: nodeType === 'start',\n    hidden,\n    defaultExpanded: document.options.allNodesDefaultExpanded,\n    size: DEFAULT_SIZE,\n    origin: document.layout.getDefaultNodeOrigin(),\n    isInlineBlocks: nodeType === FlowNodeBaseType.INLINE_BLOCKS,\n    // miniSize: { width: 200, height: 40 },\n    spacing: DEFAULT_SPACING.SPACING,\n    inlineSpacingPre: 0,\n    inlineSpacingAfter: 0,\n    expandable: true,\n    draggable: true,\n    selectable: true,\n    renderKey: '',\n    minInlineBlockSpacing: () => {\n      const isVertical = FlowLayoutDefault.isVertical(document.layout);\n      return isVertical\n        ? DEFAULT_SPACING.MIN_INLINE_BLOCK_SPACING\n        : DEFAULT_SPACING.MIN_INLINE_BLOCK_SPACING_HORIZONTAL;\n    },\n  } as FlowNodeMeta;\n};\n\n/**\n * 节点注册\n */\nexport interface FlowNodeRegistry<M extends FlowNodeMeta = FlowNodeMeta> {\n  /**\n   * 从另外一个注册扩展\n   */\n  extend?: string;\n  /**\n   * 节点类型\n   */\n  type: FlowNodeType;\n  /**\n   * 节点注册的数据，可以理解为 ECS 里的 Component, 这里可以配置自定义数据\n   */\n  dataRegistries?: EntityDataRegistry[];\n  /**\n   * 节点画布相关初始化配置信息，会覆盖 DEFAULT_FLOW_NODE_META\n   */\n  meta?: Partial<M>;\n  /**\n   * 自定义创建节点，可以自定义节点的树形结构\n   * 返回新加入的节点，这样才能统计缓存\n   *\n   * @action 使用该方法，在创建时候将会忽略 json blocks 数据，而是交给适用节点自己处理 json 逻辑\n   */\n  onCreate?: (node: FlowNodeEntity, json: FlowNodeJSON) => FlowNodeEntity[] | void;\n  /**\n   * 添加子 block，一般用于分支的动态添加\n   */\n  onBlockChildCreate?: (\n    node: FlowNodeEntity,\n    json: FlowNodeJSON,\n    addedNodes?: FlowNodeEntity[] // 新创建的节点都要存在这里\n  ) => FlowNodeEntity;\n  /**\n   * 创建线条\n   */\n  getLines?: (transition: FlowNodeTransitionData, layout: FlowLayout) => FlowTransitionLine[];\n  /**\n   * 创建 label\n   */\n  getLabels?: (transition: FlowNodeTransitionData, layout: FlowLayout) => FlowTransitionLabel[];\n\n  /**\n   * 调整子节点的线条，优先级高于子节点本身的 getLines\n   */\n  getChildLines?: (transition: FlowNodeTransitionData, layout: FlowLayout) => FlowTransitionLine[];\n\n  /**\n   * 调整子节点的 Labels，优先级高于子节点本身的 getLabels\n   */\n  getChildLabels?: (\n    transition: FlowNodeTransitionData,\n    layout: FlowLayout\n  ) => FlowTransitionLabel[];\n\n  /**\n   * 自定义输入节点\n   */\n  getInputPoint?: (transform: FlowNodeTransformData, layout: FlowLayout) => IPoint;\n  /**\n   * 自定义输出节点\n   */\n  getOutputPoint?: (transform: FlowNodeTransformData, layoutKey: FlowLayout) => IPoint;\n  /**\n   *  获取当前节点 Position 偏移量，偏移量计算只能使用已经计算完的数据，如上一个节点或者子节点，不然会造成 o(n^2) 复杂度\n   *\n   *  1. 切记不要用当前节点的 localBounds(相对位置 bbox)，因为 delta 计算发生在 localBounds 计算之前\n   *  2. 切记不要用 bounds(绝对位置 bbox, 会触发所有父节点绝对位置计算), bounds 只能在最终 render 时候使用\n   *  3. 可以用 pre 节点 和 子节点的 localBounds 或者 size 数据，因为子节点是先算的\n   *  4. 可以用当前节点的 size (所有子节点的最大 bbox), 这是已经确定下来的\n   */\n  getDelta?: (transform: FlowNodeTransformData, layout: FlowLayout) => IPoint | undefined;\n\n  /**\n   * 动态获取原点，会覆盖 meta.origin\n   */\n  getOrigin?(transform: FlowNodeTransformData, layout: FlowLayout): IPoint;\n  /**\n   * 原点 X 偏移\n   * @param transform\n   */\n  getOriginDeltaX?: (transform: FlowNodeTransformData, layout: FlowLayout) => number;\n  /**\n   * 原点 Y 偏移\n   * @param transform\n   */\n  getOriginDeltaY?: (transform: FlowNodeTransformData, layout: FlowLayout) => number;\n  /**\n   * 通过 parent 计算当前节点的偏移，规则同 getDelta\n   */\n  getChildDelta?: (childBlock: FlowNodeTransformData, layout: FlowLayout) => IPoint | undefined;\n  /**\n   * 在当前节点布局完成后调用，可以对布局做更精细的调整\n   */\n  onAfterUpdateLocalTransform?: (transform: FlowNodeTransformData, layout: FlowLayout) => void;\n\n  /**\n   * 子节点的 registry 覆盖，这里通过 originParent 来查找\n   */\n  extendChildRegistries?: FlowNodeRegistry[];\n\n  /**\n   * @deprecated\n   * 自定义子节点添加逻辑\n   * @param node 节点\n   * @param json 添加的节点 JSON\n   * @param options 其它配置\n   * @returns\n   */\n  addChild?: (\n    node: FlowNodeEntity,\n    json: FlowNodeJSON,\n    options?: {\n      hidden?: boolean;\n      index?: number;\n    }\n  ) => FlowNodeEntity;\n\n  /**\n   * 内部用于继承逻辑判断，不要使用\n   */\n  __extends__?: FlowNodeType[];\n  /**\n   * 扩展注册器\n   */\n  [key: string]: any;\n}\n\nexport namespace FlowNodeRegistry {\n  export function mergeChildRegistries(\n    r1: FlowNodeRegistry[] = [],\n    r2: FlowNodeRegistry[] = []\n  ): FlowNodeRegistry[] {\n    if (r1.length === 0 || r2.length === 0) {\n      return [...r1, ...r2];\n    }\n    const r1Filter = r1.map((r1Current) => {\n      const r2Current = r2.find((n) => n.type === r1Current.type);\n      if (r2Current) {\n        return merge(r1Current, r2Current, r1Current.type);\n      }\n      return r1Current;\n    });\n    const r2Filter = r2.filter((n) => !r1.some((r) => r.type === n.type));\n    return [...r1Filter, ...r2Filter];\n  }\n  export function merge(\n    registry1: FlowNodeRegistry,\n    registry2: FlowNodeRegistry,\n    finalType: FlowNodeType\n  ): FlowNodeRegistry {\n    const extendKeys = registry1.__extends__ ? registry1.__extends__.slice() : [];\n    if (registry1.type !== registry2.type) {\n      extendKeys.unshift(registry1.type);\n    }\n    return {\n      ...registry1,\n      ...registry2,\n      extendChildRegistries: mergeChildRegistries(\n        registry1.extendChildRegistries,\n        registry2.extendChildRegistries\n      ),\n      meta: { ...registry1.meta, ...registry2.meta },\n      extend: undefined,\n      type: finalType,\n      __extends__: extendKeys,\n    };\n  }\n\n  export function extend(\n    registry: FlowNodeRegistry,\n    extendRegistries: FlowNodeRegistry[]\n  ): FlowNodeRegistry {\n    if (!extendRegistries.length) return registry;\n    return extendRegistries.reduce((res, ext) => merge(res, ext, registry.type), registry);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow-operation.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Event } from '@flowgram.ai/utils';\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { type FlowNodeEntity } from '../entities/flow-node-entity';\nimport { AddNodeData, FlowNodeJSON } from './flow';\n\nexport enum OperationType {\n  addFromNode = 'addFromNode',\n  deleteFromNode = 'deleteFromNode',\n  addBlock = 'addBlock',\n  deleteBlock = 'deleteBlock',\n  createGroup = 'createGroup',\n  ungroup = 'ungroup',\n  moveNodes = 'moveNodes',\n  moveBlock = 'moveBlock',\n  moveChildNodes = 'moveChildNodes',\n  addNodes = 'addNodes',\n  deleteNodes = 'deleteNodes',\n  addChildNode = 'addChildNode',\n  deleteChildNode = 'deleteChildNode',\n  addNode = 'addNode',\n  deleteNode = 'deleteNode',\n}\n\nexport interface AddOrDeleteFromNodeOperationValue {\n  fromId: string;\n  data: FlowNodeJSON;\n}\n\nexport interface AddOrDeleteNodeOperationValue {\n  fromId: string;\n  data: FlowNodeJSON;\n}\n\nexport interface AddFromNodeOperation {\n  type: OperationType.addFromNode;\n  value: AddOrDeleteFromNodeOperationValue;\n}\n\nexport interface DeleteFromNodeOperation {\n  type: OperationType.deleteFromNode;\n  value: AddOrDeleteFromNodeOperationValue;\n}\n\nexport interface AddOrDeleteBlockValue {\n  targetId: string;\n  index?: number;\n  blockData: FlowNodeJSON;\n  parentId?: string;\n}\n\nexport interface createOrUngroupValue {\n  targetId: string;\n  groupId: string;\n  nodeIds: string[];\n}\n\nexport interface AddBlockOperation {\n  type: OperationType.addBlock;\n  value: AddOrDeleteBlockValue;\n}\n\nexport interface DeleteBlockOperation {\n  type: OperationType.deleteBlock;\n  value: AddOrDeleteBlockValue;\n}\n\nexport interface CreateGroupOperation {\n  type: OperationType.createGroup;\n  value: createOrUngroupValue;\n}\n\nexport interface UngroupOperation {\n  type: OperationType.ungroup;\n  value: createOrUngroupValue;\n}\n\nexport interface MoveNodesOperationValue {\n  fromId: string;\n  toId: string;\n  nodeIds: string[];\n}\n\nexport interface MoveNodesOperation {\n  type: OperationType.moveNodes;\n  value: MoveNodesOperationValue;\n}\n\nexport interface AddOrDeleteNodesOperationValue {\n  fromId: string;\n  nodes: FlowNodeJSON[];\n}\n\nexport interface AddNodesOperation {\n  type: OperationType.addNodes;\n  value: AddOrDeleteNodesOperationValue;\n}\n\nexport interface DeleteNodesOperation {\n  type: OperationType.deleteNodes;\n  value: AddOrDeleteNodesOperationValue;\n}\n\nexport interface MoveChildNodesOperationValue {\n  nodeIds: string[];\n  fromParentId: string;\n  fromIndex: number;\n  toParentId: string;\n  toIndex: number;\n}\n\nexport type MoveBlockOperationValue = {\n  nodeId: string;\n  fromParentId: string;\n  fromIndex: number;\n  toParentId: string;\n  toIndex: number;\n};\n\nexport interface MoveBlockOperation {\n  type: OperationType.moveBlock;\n  value: MoveBlockOperationValue;\n}\n\nexport interface MoveChildNodesOperation {\n  type: OperationType.moveChildNodes;\n  value: MoveChildNodesOperationValue;\n}\n\nexport interface AddChildNodeOperation {\n  type: OperationType.addChildNode;\n  value: AddOrDeleteChildNodeValue;\n}\n\nexport interface DeleteChildNodeOperation {\n  type: OperationType.deleteChildNode;\n  value: AddOrDeleteChildNodeValue;\n}\n\nexport interface AddOrDeleteChildNodeValue {\n  data: FlowNodeJSON;\n  parentId?: string;\n  index?: number;\n  originParentId?: string;\n  hidden?: boolean;\n}\n\nexport interface AddNodeOperation {\n  type: OperationType.addNode;\n  value: AddOrDeleteNodeValue;\n}\n\nexport interface DeleteNodeOperation {\n  type: OperationType.deleteNode;\n  value: AddOrDeleteNodeValue;\n}\n\nexport interface AddOrDeleteNodeValue {\n  data: FlowNodeJSON;\n  parentId?: string;\n  index?: number;\n  hidden?: boolean;\n}\n\nexport type FlowOperation =\n  | AddFromNodeOperation\n  | DeleteFromNodeOperation\n  | AddBlockOperation\n  | DeleteBlockOperation\n  | CreateGroupOperation\n  | UngroupOperation\n  | MoveNodesOperation\n  | AddNodesOperation\n  | DeleteNodesOperation\n  | MoveBlockOperation\n  | AddChildNodeOperation\n  | DeleteChildNodeOperation\n  | MoveChildNodesOperation\n  | AddNodeOperation\n  | DeleteNodeOperation;\n\nexport type FlowNodeEntityOrId = string | FlowNodeEntity;\n\n// 添加节点时的配置\nexport type AddNodeConfig = {\n  // 父节点\n  parent?: FlowNodeEntityOrId;\n  // 是否隐藏\n  hidden?: boolean;\n  // 插入位置\n  index?: number;\n};\n\n/**\n * 添加block时的配置\n */\nexport interface AddBlockConfig {\n  // 父节点，默认去找 $inlineBlocks$\n  parent?: FlowNodeEntity;\n  // 插入位置，默认最后\n  index?: number;\n}\n\n/**\n * 移动节点时的配置\n */\nexport interface MoveNodeConfig {\n  // 目标父节点,如果不传,默认使用移动节点的父节点\n  parent?: FlowNodeEntityOrId;\n  // 目标位置, 默认移动到最后\n  index?: number;\n}\n\n/**\n * 节点添加事件\n */\nexport interface OnNodeAddEvent {\n  node: FlowNodeEntity;\n  data: AddNodeData;\n}\n\n/**\n * 节点移动事件\n */\nexport interface OnNodeMoveEvent {\n  node: FlowNodeEntity;\n  fromParent: FlowNodeEntity;\n  fromIndex: number;\n  toParent: FlowNodeEntity;\n  toIndex: number;\n}\n\nexport interface FlowOperationBaseService extends Disposable {\n  /**\n   * 执行操作\n   * @param operation 可序列化的操作\n   * @returns 操作返回\n   */\n  apply(operation: FlowOperation): any;\n\n  /**\n   * 添加节点，如果节点已经存在则不会重复创建\n   * @param nodeJSON 节点数据\n   * @param config 配置\n   * @returns 成功添加的节点\n   */\n  addNode(nodeJSON: FlowNodeJSON, config?: AddNodeConfig): FlowNodeEntity;\n\n  /**\n   * 基于某一个起始节点往后面添加\n   * @param fromNode 起始节点\n   * @param nodeJSON 添加的节点JSON\n   */\n  addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity;\n\n  /**\n   * 删除节点\n   * @param node 节点\n   * @returns\n   */\n  deleteNode(node: FlowNodeEntityOrId): void;\n\n  /**\n   * 批量删除节点\n   * @param nodes\n   */\n  deleteNodes(nodes: FlowNodeEntityOrId[]): void;\n\n  /**\n   * 添加块（分支）\n   * @param target 目标\n   * @param blockJSON 块数据\n   * @param config 配置\n   * @returns\n   */\n  addBlock(\n    target: FlowNodeEntityOrId,\n    blockJSON: FlowNodeJSON,\n    config?: AddBlockConfig\n  ): FlowNodeEntity;\n\n  /**\n   * 移动节点\n   * @param node 被移动的节点\n   * @param config 移动节点配置\n   */\n  moveNode(node: FlowNodeEntityOrId, config?: MoveNodeConfig): void;\n\n  /**\n   * 拖拽节点\n   * @param param0\n   * @returns\n   */\n  dragNodes({ dropNode, nodes }: { dropNode: FlowNodeEntity; nodes: FlowNodeEntity[] }): void;\n\n  /**\n   * 添加节点的回调\n   */\n  onNodeAdd: Event<OnNodeAddEvent>;\n\n  /**\n   * 节点移动的回调\n   */\n  onNodeMove: Event<OnNodeMoveEvent>;\n}\n\nexport const FlowOperationBaseService = Symbol('FlowOperationBaseService');\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow-transition.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint } from '@flowgram.ai/utils';\n\nimport { type FlowNodeEntity } from '../entities';\nimport { type LABEL_SIDE_TYPE } from './flow-node-register';\n\n// 内置几种线条\nexport enum FlowTransitionLineEnum {\n  STRAIGHT_LINE, // 直线\n  DIVERGE_LINE, // 分支线 (一种 ROUNDED_LINE)\n  MERGE_LINE, // 汇聚线 (一种 ROUNDED_LINE)\n  ROUNDED_LINE, // 自定义圆角转弯线\n  CUSTOM_LINE, // 自定义，用于处理循环节点等自定义线条\n  DRAGGING_LINE, // 分支拖拽场景渲染的线条\n}\n\nexport interface Vertex extends IPoint {\n  radiusX?: number;\n  radiusY?: number;\n  // 圆弧出入点位置移动\n  moveX?: number;\n  moveY?: number;\n  /**\n   * Strategy for handling arc curvature when space is insufficient, defaults to compress\n   */\n  radiusOverflow?: 'compress' | 'truncate';\n}\n\nexport interface FlowTransitionLine {\n  type: FlowTransitionLineEnum;\n  from: IPoint;\n  to: IPoint;\n  vertices?: Vertex[]; // 自定义圆角转弯线需要转弯的拐点\n  arrow?: boolean; // 是否有箭头\n  renderKey?: string; // 只有自定义线条需要\n  isHorizontal?: boolean; // 是否为水平布局\n  isDraggingLine?: boolean; // 是否是拖拽线条\n  activated?: boolean; // 是否激活态\n  side?: LABEL_SIDE_TYPE; // 区分是否分支前缀线条\n  style?: React.CSSProperties;\n  lineId?: string;\n}\n\n// 内置几种标签\nexport enum FlowTransitionLabelEnum {\n  ADDER_LABEL, // 添加按钮\n  TEXT_LABEL, // 文本标签\n  COLLAPSE_LABEL, // 复合节点收起的展开标签\n  COLLAPSE_ADDER_LABEL, // 复合节点收起 + 加号复合标签\n  CUSTOM_LABEL, // 自定义，用于处理循环节点等自定义标签\n  BRANCH_DRAGGING_LABEL, // 分支拖拽场景下的 label\n}\n\nexport interface FlowTransitionLabel {\n  type: FlowTransitionLabelEnum;\n  // type === 'CUSTOM_LABEL'需要配置的数据\n  renderKey?: string;\n  offset: IPoint; // 位置\n  width?: number; // 宽度\n  rotate?: string; // 循环, 如 '60deg'\n  /**\n   * Anchor point for positioning, relative to the label's bounding box\n   * 重心偏移量，相对于标签边界框\n   *\n   * Format: [x, y] / 格式：[x, y]\n   * Default Value: [0.5, 0.5] indicates center / 默认值：[0.5, 0.5] 表示居中\n   */\n  origin?: [number, number];\n  props?: Record<string, any>;\n  labelId?: string;\n}\n\nexport interface AdderProps {\n  node: FlowNodeEntity; // 实际挂载 label 的节点\n  from: FlowNodeEntity; // 边起始点在哪个节点\n  to: FlowNodeEntity; // 边终点在哪个节点\n  renderTo: FlowNodeEntity; // 实际渲染（renderTree）边终点在哪个节点\n  [key: string]: any;\n}\n\nexport interface CollapseProps {\n  node: FlowNodeEntity; // 实际挂载 label 的节点\n  collapseNode: FlowNodeEntity; // 要展开收起的节点，默认为 node\n  activateNode?: FlowNodeEntity; // 设置获取 label 激活状态的节点\n  forceVisible?: boolean; // 是否强制显示\n  [key: string]: any;\n}\n\nexport interface CustomLabelProps {\n  node: FlowNodeEntity; // 实际挂载 label 的节点\n  [key: string]: any;\n}\n\nexport interface CollapseAdderProps extends AdderProps, CollapseProps {\n  [key: string]: any;\n}\n\nexport interface DragNodeProps {\n  node: FlowNodeEntity; // 实际挂载 label 的节点\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/flow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '../entities';\nimport { type FlowNodeMeta } from './flow-node-register';\n\nexport type FlowNodeType = string | number;\n\n/**\n * Flow node json data\n */\nexport interface FlowNodeJSON {\n  id: string;\n  type?: FlowNodeBaseType | FlowNodeSplitType | FlowNodeType; // 如果缺省 则 为 block\n  data?: Record<string, any>; // 节点额外扩展的内容\n  meta?: FlowNodeMeta;\n  blocks?: FlowNodeJSON[]; // 子节点\n}\n\nexport type FlowDocumentJSON = {\n  nodes: FlowNodeJSON[];\n};\n\nexport enum FlowNodeBaseType {\n  START = 'start', // 开始节点\n  DEFAULT = 'default', // 默认节点类型\n  ROOT = 'root', // 根节点\n  EMPTY = 'empty', // 空节点，宽和高为 0\n  INLINE_BLOCKS = 'inlineBlocks', // 所有块合并为 InlineBlocks\n  BLOCK_ICON = 'blockIcon', // 图标节点，如条件分支的头部的 菱形图标\n  BLOCK = 'block', // 块节点\n  BLOCK_ORDER_ICON = 'blockOrderIcon', // 带顺序的图标节点，一般为 block 第一个分支节点\n  GROUP = 'group', // 分组节点\n  END = 'end', // 结束节点\n  BREAK = 'break', // 分支结束\n  CONDITION = 'condition', // 可以连接多条线的条件判断节点，目前只支持横向布局\n  SUB_CANVAS = 'subCanvas', // 自由布局子画布\n  MULTI_INPUTS = 'multiInputs', // 多输入\n  MULTI_OUTPUTS = 'multiOutputs', // 多输出\n  INPUT = 'input', // 输入节点\n  OUTPUT = 'output', // 输出节点\n  SLOT = 'slot', // 插槽节点\n  SLOT_BLOCK = 'slotBlock', // 插槽子节点\n}\n\nexport enum FlowNodeSplitType {\n  SIMPLE_SPLIT = 'simpleSplit', // 无 BlockOrderIcon\n  DYNAMIC_SPLIT = 'dynamicSplit', // 动态分支\n  STATIC_SPLIT = 'staticSplit', // 静态分支\n}\n\nexport enum FlowDocumentConfigEnum {\n  // 结束节点拖拽分支逻辑\n  END_NODES_REFINE_BRANCH = 'END_NODES_REFINE_BRANCH',\n}\n\nexport const FLOW_DEFAULT_HIDDEN_TYPES: FlowNodeType[] = [\n  FlowNodeBaseType.ROOT,\n  FlowNodeBaseType.INLINE_BLOCKS,\n  FlowNodeBaseType.BLOCK,\n];\n\nexport type AddNodeData = FlowNodeJSON & {\n  originParent?: FlowNodeEntity;\n  parent?: FlowNodeEntity;\n  hidden?: boolean;\n  index?: number;\n};\n"
  },
  {
    "path": "packages/canvas-engine/document/src/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow';\nexport * from './flow-layout';\nexport * from './flow-transition';\nexport * from './flow-node-register';\nexport * from './flow-operation';\nexport * from './flow-group';\n"
  },
  {
    "path": "packages/canvas-engine/document/src/utils/get-default-spacing.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DEFAULT_SPACING } from '../typings';\nimport { FlowDocumentOptions } from '../flow-document-options';\nimport { FlowNodeEntity } from '../entities/flow-node-entity';\n\n/**\n *\n * @param node 节点 entity\n * @param key 从 DocumentOptions 里获取 constants 的 key\n * @param defaultSpacing 默认从 DEFAULT_SPACING 获取 spacing，也可以外部传入默认值\n * @returns\n */\nexport const getDefaultSpacing = (node: FlowNodeEntity, key: string, defaultSpacing?: number) => {\n  const flowDocumentOptions = node.getService<FlowDocumentOptions>(FlowDocumentOptions);\n  const spacing = flowDocumentOptions?.constants?.[key] || defaultSpacing || DEFAULT_SPACING[key];\n  return spacing;\n};\n"
  },
  {
    "path": "packages/canvas-engine/document/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { getDefaultSpacing } from './get-default-spacing';\n"
  },
  {
    "path": "packages/canvas-engine/document/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"],\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"**/__tests__/**\", \"**/__mocks__/**\"]\n}\n"
  },
  {
    "path": "packages/canvas-engine/document/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/canvas-engine/document/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/__tests__/__snapshots__/flow-activities.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-activities > block addChild 1`] = `\n\"root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|-------- test\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- noop_0\n|-- end_0\"\n`;\n\nexports[`flow-activities > create dynamic split 1`] = `\n\"root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- noop_0\n|-- end_0\"\n`;\n\nexports[`flow-activities > create dynamic split 2`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 92px; width: 580px; height: 350.1px;\",\n  \"localBoundsStr\": \"left: -290px; top: 92px; width: 580px; height: 350.1px;\",\n}\n`;\n\nexports[`flow-activities > create dynamic split 3`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 196px; width: 580px; height: 246.1px;\",\n  \"localBoundsStr\": \"left: -290px; top: 104px; width: 580px; height: 246.1px;\",\n}\n`;\n\nexports[`flow-activities > create dynamic split expanded 1`] = `\n\"root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- noop_0\n|-------- split2\n|---------- $blockIcon$split2\n|---------- $inlineBlocks$split2\n|------------ s2_branch_0\n|-------------- $blockOrderIcon$s2_branch_0\n|------------ s2_branch_1\n|-------------- $blockOrderIcon$s2_branch_1\n|-- end_0\"\n`;\n\nexports[`flow-activities > create loop empty 1`] = `\n\"root\n|-- start_0\n|-- loop_0\n|---- $blockIcon$loop_0\n|---- $inlineBlocks$loop_0\n|------ $loopLeftEmpty$loop_0\n|------ $block$loop_0\n|-------- $loopRightEmpty$loop_0\n|-- end_0\"\n`;\n\nexports[`flow-activities > create tryCatch 1`] = `\n\"root\n|-- start\n|-- tryCatch\n|---- $tryCatchIcon$tryCatch\n|---- $mainInlineBlocks$tryCatch\n|------ try_branch_0\n|-------- $trySlot$try_branch_0\n|-------- try_noop_0\n|------ $catchInlineBlocks$tryCatch\n|-------- catch_branch_1\n|---------- $blockOrderIcon$catch_branch_1\n|---------- catch_noop_0\n|-------- catch_branch_2\n|---------- $blockOrderIcon$catch_branch_2\n|-------- catch_branch_3\n|---------- $blockOrderIcon$catch_branch_3\n|-- end\"\n`;\n\nexports[`flow-activities > empty split addChild 1`] = `\n\"root\n|-- start_0\n|-- empty-split\n|---- $blockIcon$empty-split\n|---- $inlineBlocks$empty-split\n|------ branch\n|-------- $blockOrderIcon$branch\n|-- end_0\"\n`;\n\nexports[`flow-activities > extend block addChild 1`] = `\n\"root\n|-- start_0\n|-- test-extend\n|---- $blockIcon$test-extend\n|---- $inlineBlocks$test-extend\n|------ test-extend-block\n|-------- test\n|-------- $blockOrderIcon$test-extend-block\n|-- empty-split\n|---- $blockIcon$empty-split\n|---- $inlineBlocks$empty-split\n|-- end_0\"\n`;\n\nexports[`flow-activities > insert a dynamic split node 1`] = `\n\"root\n|-- start_0\n|-- split2\n|---- $blockIcon$split2\n|---- $inlineBlocks$split2\n|------ b1\n|-------- $blockOrderIcon$b1\n|------ b2\n|-------- $blockOrderIcon$b2\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- noop_0\n|-- end_0\"\n`;\n\nexports[`flow-activities > insert a dynamic split node 2`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 358.1px; width: 580px; height: 350.1px;\",\n  \"localBoundsStr\": \"left: -290px; top: 358.1px; width: 580px; height: 350.1px;\",\n}\n`;\n\nexports[`flow-activities > insert a dynamic split node 3`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 462.1px; width: 580px; height: 246.1px;\",\n  \"localBoundsStr\": \"left: -290px; top: 104px; width: 580px; height: 246.1px;\",\n}\n`;\n\nexports[`flow-activities > insert a dynamic split node 4`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 92px; width: 580px; height: 250.10000000000002px;\",\n  \"localBoundsStr\": \"left: -290px; top: 92px; width: 580px; height: 250.1px;\",\n}\n`;\n\nexports[`flow-activities > insert a dynamic split node 5`] = `\n{\n  \"boundsStr\": \"left: -290px; top: 196px; width: 580px; height: 146.1px;\",\n  \"localBoundsStr\": \"left: -290px; top: 104px; width: 580px; height: 146.1px;\",\n}\n`;\n\nexports[`flow-activities > loop addChild 1`] = `\n\"root\n|-- start_0\n|-- loop_0\n|---- $blockIcon$loop_0\n|---- $inlineBlocks$loop_0\n|------ $loopLeftEmpty$loop_0\n|------ $block$loop_0\n|-------- $loopRightEmpty$loop_0\n|-------- test1\n|-------- test2\n|-- end_0\"\n`;\n\nexports[`flow-activities > split addChild 1`] = `\n\"root\n|-- start_0\n|-- split\n|---- $blockIcon$split\n|---- $inlineBlocks$split\n|------ branch_0\n|-------- $blockOrderIcon$branch_0\n|------ branch_2\n|-------- $blockOrderIcon$branch_2\n|------ branch_1\n|-------- $blockOrderIcon$branch_1\n|-------- noop_0\n|-- end_0\"\n`;\n\nexports[`flow-activities > tryCatch add branch 1`] = `\n\"root\n|-- start\n|-- tryCatch\n|---- $tryCatchIcon$tryCatch\n|---- $mainInlineBlocks$tryCatch\n|------ try_branch_0\n|-------- $trySlot$try_branch_0\n|-------- try_noop_0\n|------ $catchInlineBlocks$tryCatch\n|-------- catch_branch_1\n|---------- $blockOrderIcon$catch_branch_1\n|---------- catch_noop_0\n|-------- catch_branch_2\n|---------- $blockOrderIcon$catch_branch_2\n|-------- catch_branch_3\n|---------- $blockOrderIcon$catch_branch_3\n|-------- xxxx\n|---------- $blockOrderIcon$xxxx\n|-- end\"\n`;\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/__tests__/flow-activities.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container, type interfaces } from 'inversify';\nimport {\n  FlowDocumentContainerModule,\n  FlowNodeRegistry,\n  type FlowDocumentJSON,\n} from '@flowgram.ai/document';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { FixedLayoutContainerModule } from '../src/fixed-layout-container-module';\n\nexport function createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.load(FixedLayoutContainerModule);\n  container.bind(EntityManager).toSelf();\n  return container;\n}\n\nexport const dynamicSplitMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'split',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'branch_0' }, { id: 'branch_1', blocks: [{ id: 'noop_0', type: 'noop' }] }],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n\nexport const dynamicSplitEmptyMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'empty-split',\n      type: 'dynamicSplit',\n      blocks: [],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n\nexport const dynamicSplitExpandedMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'split',\n      type: 'dynamicSplit',\n      blocks: [\n        {\n          id: 'branch_0',\n        },\n        {\n          id: 'branch_1',\n          blocks: [\n            { id: 'noop_0', type: 'noop' },\n            {\n              id: 'split2',\n              type: 'dynamicSplit',\n              blocks: [\n                { id: 's2_branch_0', meta: { defaultExpanded: true } },\n                { id: 's2_branch_1' },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\nexport const tryCatchMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'tryCatch',\n      type: 'tryCatch',\n      blocks: [\n        { id: 'try_branch_0', blocks: [{ id: 'try_noop_0', type: 'noop' }] },\n        { id: 'catch_branch_1', blocks: [{ id: 'catch_noop_0', type: 'noop' }] },\n        { id: 'catch_branch_2' },\n        { id: 'catch_branch_3' },\n      ],\n    },\n    {\n      id: 'end',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\nexport const loopEmpty: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      blocks: [],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n\nexport const extendChildNodeMock: FlowNodeRegistry = {\n  type: 'test-extend',\n  extend: 'dynamicSplit',\n  extendChildRegistries: [\n    {\n      type: 'block',\n      addChild(node, json, options = {}) {\n        const { index } = options;\n        const document = node.document;\n        return document.addNode({\n          ...json,\n          ...options,\n          parent: node,\n          index,\n        });\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/__tests__/flow-activities.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport {\n  DEFAULT_SPACING,\n  FlowDocument,\n  type FlowDocumentJSON,\n  FlowNodeTransformData,\n  FlowOperationBaseService,\n} from '@flowgram.ai/document';\n\nimport {\n  createDocumentContainer,\n  dynamicSplitEmptyMock,\n  dynamicSplitExpandedMock,\n  dynamicSplitMock,\n  extendChildNodeMock,\n  loopEmpty,\n  tryCatchMock,\n} from './flow-activities.mock';\n\ninterface TransformTestData {\n  // version: number\n  localBoundsStr: string;\n  // localID: number\n  // worldID: number\n  boundsStr: string;\n}\n\nfunction getTransformData(document: FlowDocument): Record<string, TransformTestData> {\n  const data: Record<string, TransformTestData> = {};\n  document.traverse((node) => {\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n    data[node.id] = {\n      // version: transform.version,\n      boundsStr: transform.bounds.toStyleStr(),\n      localBoundsStr: transform.localBounds.toStyleStr(),\n      // localID: transform.transform.localID, // 用来判断是否有更新\n      // worldID: transform.transform.worldID,\n    };\n  });\n  return data;\n}\n\ndescribe('flow-activities', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let operationService: FlowOperationBaseService;\n\n  /**\n   * 计算两次数据保证不变\n   * @param data\n   */\n  function refreshTwice(data: FlowDocumentJSON): Record<string, TransformTestData> {\n    document.fromJSON(data);\n    document.transformer.refresh();\n    const transformData = getTransformData(document);\n    document.fromJSON(data);\n    // 重新计算不会更新数据\n    document.transformer.refresh();\n    expect(transformData).toEqual(getTransformData(document));\n    return transformData;\n  }\n  function get(key: string): FlowNodeTransformData {\n    return document.getNode(key)!.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n  }\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    document = container.get<FlowDocument>(FlowDocument);\n    operationService = container.get<FlowOperationBaseService>(FlowOperationBaseService);\n  });\n  it('create dynamic split', () => {\n    const preData = refreshTwice(dynamicSplitMock);\n    expect(document.toString()).toMatchSnapshot();\n    expect(preData.split).toMatchSnapshot();\n    expect(preData.$inlineBlocks$split).toMatchSnapshot();\n  });\n  it('insert a dynamic split node', () => {\n    const preData = refreshTwice(dynamicSplitMock);\n    // 添加一个新条件分支\n    document.addFromNode('start_0', {\n      id: 'split2',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'b1' }, { id: 'b2' }],\n    });\n    document.transformer.refresh();\n    expect(document.toString()).toMatchSnapshot();\n    expect(document.getNode('start_0')!.next!.id).toEqual('split2');\n    expect(document.getNode('split2')!.next!.id).toEqual('split');\n    const nextData = getTransformData(document);\n    expect(preData.start_0).toEqual(nextData.start_0);\n    expect(nextData.split).toMatchSnapshot();\n    expect(nextData.$inlineBlocks$split).toMatchSnapshot();\n    expect(nextData.split2).toMatchSnapshot();\n    expect(nextData.$inlineBlocks$split2).toMatchSnapshot();\n  });\n  it('create dynamic split expanded', () => {\n    refreshTwice(dynamicSplitExpandedMock);\n    expect(document.toString()).toMatchSnapshot();\n    const trans1 = get('branch_0');\n    const trans2 = get('s2_branch_0');\n    // 这两个节点不能相交，左右间隔相差 20\n    expect(trans1.bounds.right + DEFAULT_SPACING.MARGIN_RIGHT).toEqual(trans2.bounds.left);\n    // 两条分支是左右对称关系，不受子节点影响\n  });\n  /**\n   * 分支居中对齐\n   */\n  it('dynamic split branch middle origin', () => {\n    refreshTwice(dynamicSplitExpandedMock);\n    let iconCenter = get('$blockIcon$split').bounds.center.x;\n    let branch0Center = get('$blockOrderIcon$branch_0').bounds.center.x;\n    let branch1Center = get('$blockOrderIcon$branch_1').bounds.center.x;\n    // icon 在两个分支之间\n    expect(iconCenter - branch0Center).toEqual(branch1Center - iconCenter);\n    iconCenter = get('$blockIcon$split2').bounds.center.x;\n    branch0Center = get('$blockOrderIcon$s2_branch_0').bounds.center.x;\n    branch1Center = get('$blockOrderIcon$s2_branch_1').bounds.center.x;\n    expect(iconCenter - branch0Center).toEqual(branch1Center - iconCenter);\n  });\n  it('create tryCatch', () => {\n    refreshTwice(tryCatchMock);\n    expect(document.toString()).toMatchSnapshot();\n  });\n  it('tryCatch add branch', () => {\n    refreshTwice(tryCatchMock);\n    document.addBlock('tryCatch', { id: 'xxxx' });\n    expect(document.toString()).toMatchSnapshot();\n  });\n  it('create loop empty', () => {\n    document.fromJSON(loopEmpty);\n    expect(document.toString()).toMatchSnapshot();\n    refreshTwice(loopEmpty);\n  });\n  it('loop addChild', () => {\n    document.fromJSON(loopEmpty);\n    operationService.addNode(\n      {\n        id: 'test1',\n        type: 'test',\n      },\n      {\n        parent: 'loop_0',\n        index: 0,\n      }\n    );\n    operationService.addNode(\n      {\n        id: 'test2',\n        type: 'test',\n      },\n      {\n        parent: 'loop_0',\n      }\n    );\n    expect(document.toString()).toMatchSnapshot();\n  });\n  it('split addChild', () => {\n    document.fromJSON(dynamicSplitMock);\n    const node = operationService.addNode(\n      {\n        id: 'branch_2',\n        type: 'block',\n      },\n      {\n        parent: 'split',\n        index: 1,\n        hidden: true,\n      }\n    );\n    expect(document.toString()).toMatchSnapshot();\n    expect(node.hidden).toBeTruthy();\n  });\n  it('empty split addChild', () => {\n    document.fromJSON(dynamicSplitEmptyMock);\n    operationService.addNode(\n      {\n        id: 'branch',\n        type: 'block',\n      },\n      {\n        parent: 'empty-split',\n      }\n    );\n    expect(document.toString()).toMatchSnapshot();\n  });\n  it('block addChild', () => {\n    document.fromJSON(dynamicSplitMock);\n    operationService.addNode(\n      {\n        id: 'test',\n        type: 'test',\n      },\n      {\n        parent: 'branch_0',\n        index: 0,\n      }\n    );\n    expect(document.toString()).toMatchSnapshot();\n  });\n  it('extend block addChild', () => {\n    document.registerFlowNodes(extendChildNodeMock);\n    document.fromJSON(dynamicSplitEmptyMock);\n    operationService.addNode(\n      {\n        id: 'test-extend',\n        type: 'test-extend',\n      },\n      {\n        parent: 'root',\n        index: 1,\n      }\n    );\n    operationService.addNode(\n      {\n        id: 'test-extend-block',\n        type: 'block',\n      },\n      {\n        parent: 'test-extend',\n        index: 0,\n      }\n    );\n    operationService.addNode(\n      {\n        id: 'test',\n        type: 'test',\n      },\n      {\n        parent: 'test-extend-block',\n        index: 0,\n      }\n    );\n    expect(document.toString()).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/fixed-layout-core\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/block-icon.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { DefaultSpacingKey, getDefaultSpacing } from '@flowgram.ai/document';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n} from '@flowgram.ai/document';\n\n/**\n * 图标占位节点，如条件分支的菱形图标\n */\nexport const BlockIconRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.BLOCK_ICON,\n  meta: {\n    spacing: 20, // 占位节点下边偏小\n    // 条件分支 icon 默认的高度比较高，在流程里下边有文字\n    size: { width: 250, height: 84 },\n  },\n  /**\n   * 是一个占位节点，后续要加上 label 展开收起的图标\n   */\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n\n    // 为节点，画一个加号即可\n    if (transition.entity.parent!.collapsedChildren.length <= 1) {\n      return [];\n    }\n\n    const collapsedSpacing = getDefaultSpacing(\n      transition.entity,\n      DefaultSpacingKey.COLLAPSED_SPACING\n    );\n\n    return [\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_LABEL,\n        offset: Point.move(\n          currentTransform.outputPoint,\n          isVertical ? { y: collapsedSpacing } : { x: collapsedSpacing }\n        ),\n        props: {\n          collapseNode: transition.entity.parent,\n          activateNode: transition.entity,\n        },\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/block-order-icon.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Point } from '@flowgram.ai/utils';\nimport { DefaultSpacingKey, getDefaultSpacing } from '@flowgram.ai/document';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n} from '@flowgram.ai/document';\n\n/**\n * 带顺序的图标节点，一般为 block 第一个分支节点\n * - 只有一个分支时，不需要展开收起\n */\nexport const BlockOrderIconRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.BLOCK_ORDER_ICON,\n  meta: {\n    spacing: 40,\n  },\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n\n    const currentOutput = currentTransform.outputPoint;\n    const nextTransform = currentTransform.next;\n    const parentTransform = currentTransform.parent;\n\n    // 为空分支，画一个加号即可\n    if (transition.entity.parent!.collapsedChildren.length <= 1) {\n      const parentOutput = parentTransform?.outputPoint as IPoint;\n      return [\n        {\n          offset: parentOutput,\n          type: FlowTransitionLabelEnum.ADDER_LABEL,\n        },\n      ];\n    }\n\n    const collapsedSpacing = getDefaultSpacing(\n      transition.entity,\n      DefaultSpacingKey.COLLAPSED_SPACING\n    );\n\n    return [\n      {\n        offset: nextTransform\n          ? Point.getMiddlePoint(currentOutput, nextTransform.inputPoint)\n          : Point.move(\n              currentOutput,\n              isVertical ? { y: collapsedSpacing } : { x: collapsedSpacing }\n            ),\n        // 收起展开复合按钮\n        type: FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,\n        props: {\n          activateNode: transition.entity,\n          collapseNode: transition.entity.parent,\n        },\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_SPACING,\n  FlowLayoutDefault,\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n  LABEL_SIDE_TYPE,\n} from '@flowgram.ai/document';\n\n/**\n * block, block 的输入输出点由子节点决定\n */\nexport const BlockRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.BLOCK,\n  meta: {\n    spacing: DEFAULT_SPACING.NULL,\n    inlineSpacingAfter: DEFAULT_SPACING.INLINE_BLOCK_PADDING_BOTTOM,\n    hidden: true,\n  },\n  getLines(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n    const lines: FlowTransitionLine[] = [\n      {\n        type: FlowTransitionLineEnum.DIVERGE_LINE,\n        from: currentTransform.parent!.inputPoint,\n        to: currentTransform.inputPoint,\n        side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n      },\n    ];\n\n    const hasBranchDraggingAdder =\n      currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;\n\n    // 分支拖拽场景线条 push\n    // 当有其余分支的时候，绘制一条两个分支之间的线条\n    if (hasBranchDraggingAdder) {\n      if (isVertical) {\n        const currentOffsetRightX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.right\n          : currentTransform.bounds.right;\n        const nextOffsetLeftX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next?.firstChild.bounds?.left\n            : currentTransform.next?.bounds?.left) || 0;\n        const currentInputPointY = currentTransform.inputPoint.y;\n        if (currentTransform?.next) {\n          lines.push({\n            type: FlowTransitionLineEnum.DRAGGING_LINE,\n            from: currentTransform.parent!.inputPoint,\n            to: {\n              x: (currentOffsetRightX + nextOffsetLeftX) / 2,\n              y: currentInputPointY,\n            },\n            side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n          });\n        }\n      } else {\n        const currentOffsetBottomX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.bottom\n          : currentTransform.bounds.bottom;\n        const nextOffsetTopX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next?.firstChild.bounds?.top\n            : currentTransform.next?.bounds?.top) || 0;\n        const currentInputPointX = currentTransform.inputPoint.x;\n        if (currentTransform?.next) {\n          lines.push({\n            type: FlowTransitionLineEnum.DRAGGING_LINE,\n            from: currentTransform.parent!.inputPoint,\n            to: {\n              x: currentInputPointX,\n              y: (currentOffsetBottomX + nextOffsetTopX) / 2,\n            },\n            side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n          });\n        }\n      }\n    }\n\n    // 最后一个节点是 end 节点，不绘制 mergeLine\n    if (!transition.isNodeEnd) {\n      lines.push({\n        type: FlowTransitionLineEnum.MERGE_LINE,\n        from: currentTransform.outputPoint,\n        to: currentTransform.parent!.outputPoint,\n        side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n      });\n    }\n\n    return lines;\n  },\n  getInputPoint(trans) {\n    const child = trans.firstChild;\n    return child ? child.inputPoint : trans.defaultInputPoint;\n  },\n  getOutputPoint(trans, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    const child = trans.lastChild;\n    if (isVertical) {\n      return {\n        x: child ? child.outputPoint.x : trans.bounds.bottomCenter.x,\n        y: trans.bounds.bottom,\n      };\n    }\n    return {\n      x: trans.bounds.right,\n      y: child ? child.outputPoint.y : trans.bounds.rightCenter.y,\n    };\n  },\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n\n    const draggingLabel = [];\n\n    const hasBranchDraggingAdder =\n      currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;\n\n    // 获取两个分支节点中间点作为拖拽标签插入位置\n    if (hasBranchDraggingAdder) {\n      if (isVertical) {\n        const currentOffsetRightX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.right\n          : currentTransform.bounds.right;\n        const nextOffsetLeftX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next.firstChild.bounds?.left\n            : currentTransform.next?.bounds?.left) || 0;\n        const currentInputPointY = currentTransform.inputPoint.y;\n        if (currentTransform?.next) {\n          draggingLabel.push({\n            offset: {\n              x: (currentOffsetRightX + nextOffsetLeftX) / 2,\n              y: currentInputPointY,\n            },\n            type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n            width: nextOffsetLeftX - currentOffsetRightX,\n            props: {\n              side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n            },\n          });\n        }\n      } else {\n        const currentOffsetBottomX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.bottom\n          : currentTransform.bounds.bottom;\n        const nextOffsetTopX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next.firstChild.bounds?.top\n            : currentTransform.next?.bounds?.top) || 0;\n        const currentInputPointX = currentTransform.inputPoint.x;\n        if (currentTransform?.next) {\n          draggingLabel.push({\n            offset: {\n              x: currentInputPointX,\n              y: (currentOffsetBottomX + nextOffsetTopX) / 2,\n            },\n            type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n            width: nextOffsetTopX - currentOffsetBottomX,\n            props: {\n              side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n            },\n          });\n        }\n      }\n    }\n\n    return [...draggingLabel];\n  },\n\n  /**\n   * @depreacted\n   */\n  addChild(node, json, options = {}) {\n    const { index } = options;\n    const document = node.document;\n    return document.addNode({\n      ...json,\n      ...options,\n      parent: node,\n      index: typeof index === 'number' ? index + 1 : undefined,\n    });\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/break.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\n/**\n * Break 节点, 用于分支断开\n */\nexport const BreakRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.BREAK,\n  extend: FlowNodeBaseType.END,\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/dynamic-split.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowLayoutDefault,\n  type FlowNodeRegistry,\n  FlowNodeSplitType,\n  FlowTransitionLabelEnum,\n  ConstantKeys,\n  getDefaultSpacing,\n} from '@flowgram.ai/document';\n\n/**\n * 可以动态添加分支的分支节点\n * dynamicSplit:  (最原始的 id)\n *  blockIcon\n *  inlineBlocks\n *    block1\n *      blockOrderIcon\n *    block2\n *      blockOrderIcon\n */\nexport const DynamicSplitRegistry: FlowNodeRegistry = {\n  type: FlowNodeSplitType.DYNAMIC_SPLIT,\n  meta: {\n    hidden: true,\n    inlineSpacingAfter: (node) =>\n      node.collapsed && node.entity.collapsedChildren.length > 1 ? 21 : 0,\n    // 判断是否有分支节点\n    spacing: (node) => {\n      const spacing = getDefaultSpacing(node.entity, ConstantKeys.NODE_SPACING);\n      return node.children.length === 1 ? spacing : spacing / 2;\n    },\n  },\n  getLabels(transition) {\n    if (transition.isNodeEnd) {\n      return [];\n    }\n\n    return [\n      {\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n    ];\n  },\n  onCreate(node, json) {\n    return node.document.addInlineBlocks(node, json.blocks || []);\n  },\n  getInputPoint(transform) {\n    // block icon\n    return transform.firstChild?.inputPoint || transform.defaultInputPoint;\n  },\n  getOutputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    // 没有分支节点\n    const noInlineBlocks = transform.children.length === 1;\n    const lastChildOutput = transform.lastChild?.outputPoint;\n    const spacing = getDefaultSpacing(transform.entity, ConstantKeys.NODE_SPACING);\n\n    if (isVertical) {\n      return {\n        x: lastChildOutput ? lastChildOutput.x : transform.bounds.center.x,\n        y: transform.bounds.bottom + (noInlineBlocks ? spacing / 2 : 0),\n      };\n    }\n\n    return {\n      x: transform.bounds.right + (noInlineBlocks ? spacing / 2 : 0),\n      y: lastChildOutput ? lastChildOutput.y : transform.bounds.center.y,\n    };\n  },\n\n  /**\n   * @depreacted\n   */\n  addChild(node, json, options = {}) {\n    const { index } = options;\n    const document = node.document;\n    const parentId = `$inlineBlocks$${node.id}`;\n    let parent = document.getNode(parentId);\n    if (!parent) {\n      parent = document.addNode({\n        id: parentId,\n        type: 'inlineBlocks',\n        originParent: node,\n        parent: node,\n      });\n    }\n    return document.addBlock(node, json, undefined, undefined, index);\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/empty.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n  ConstantKeys,\n  getDefaultSpacing,\n} from '@flowgram.ai/document';\n\n/**\n * 占位节点，宽高为 0, 该节点下边同样有 \"添加 label\"\n */\nexport const EmptyRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.EMPTY,\n  meta: {\n    spacing: (node) => {\n      const spacing = getDefaultSpacing(node.entity, ConstantKeys.NODE_SPACING);\n      return spacing / 2;\n    },\n    size: { width: 0, height: 0 },\n    hidden: true,\n  },\n  getLabels(transition) {\n    return [\n      {\n        offset: transition.transform.bounds,\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/end.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\n/**\n * 结束节点\n */\nexport const EndRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.END,\n  meta: {\n    draggable: false,\n    isNodeEnd: true,\n    selectable: false,\n    copyDisable: true,\n  },\n  // 结束节点没有出边和 label\n  getLines() {\n    return [];\n  },\n  getLabels() {\n    return [];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './dynamic-split';\nexport * from './static-split';\nexport * from './block';\nexport * from './inline-blocks';\nexport * from './block-icon';\nexport * from './block-order-icon';\nexport * from './start';\nexport * from './try-catch';\nexport * from './loop';\nexport * from './root';\nexport * from './empty';\nexport * from './end';\nexport * from './simple-split';\nexport * from './break';\nexport * from './input';\nexport * from './output';\nexport * from './multi-outputs';\nexport * from './multi-inputs';\nexport * from './slot';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/inline-blocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { FlowRendererKey } from '@flowgram.ai/renderer';\nimport {\n  DEFAULT_SPACING,\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  type FlowNodeTransitionData,\n  FlowTransitionLabelEnum,\n  FlowLayoutDefault,\n  ConstantKeys,\n  getDefaultSpacing,\n} from '@flowgram.ai/document';\n\n/**\n * 水平 Block 的偏移\n */\nexport const InlineBlocksRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.INLINE_BLOCKS,\n  meta: {\n    hidden: true,\n    spacing: (node) => getDefaultSpacing(node.entity, ConstantKeys.NODE_SPACING),\n    isInlineBlocks: true,\n    inlineSpacingPre: (transform) => {\n      if (transform.entity.blocks.length === 0) {\n        return 0;\n      }\n      return (\n        getDefaultSpacing(transform.entity, ConstantKeys.INLINE_BLOCKS_PADDING_TOP) ||\n        DEFAULT_SPACING.INLINE_BLOCKS_PADDING_TOP\n      );\n    },\n    inlineSpacingAfter: (transform) => {\n      if (transform.entity.blocks.length === 0) {\n        return 0;\n      }\n      return getDefaultSpacing(transform.entity, ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM);\n    },\n  },\n  /**\n   * 控制子分支的间距\n   * @param child\n   */\n  getChildDelta(child, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    const preTransform = child.entity.pre?.getData(FlowNodeTransformData);\n    if (preTransform) {\n      const { localBounds: preBounds } = preTransform;\n      if (isVertical) {\n        const leftSpacing = preTransform.size.width + preTransform.originDeltaX;\n\n        // 如果小于最小宽度，偏移最小宽度的距离\n        const delta = Math.max(\n          child.parent!.minInlineBlockSpacing - leftSpacing,\n          getDefaultSpacing(child.entity, ConstantKeys.BRANCH_SPACING) - child.originDeltaX\n        );\n\n        return {\n          // 这里需要加上原点的偏移量，并加上水平间距\n          x: preBounds.right + delta,\n          y: 0,\n        };\n      } else {\n        const bottomSpacing = preTransform.size.height + preTransform.originDeltaY;\n\n        // 如果小于最小高度，偏移最小高度的距离\n        const delta = Math.max(\n          child.parent!.minInlineBlockSpacing - bottomSpacing,\n          getDefaultSpacing(child.entity, ConstantKeys.BRANCH_SPACING) - child.originDeltaY\n        );\n\n        return {\n          x: 0,\n          // 这里需要加上原点的偏移量，并加上垂直间距\n          y: preBounds.bottom + delta,\n        };\n      }\n    }\n    return {\n      x: 0,\n      y: 0,\n    };\n  },\n  /**\n   * 控制条件分支居中布局\n   * @param trans\n   */\n  getDelta(trans, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    const { pre, collapsed } = trans;\n\n    if (collapsed) {\n      return { x: 0, y: 0 };\n    }\n\n    if (isVertical) {\n      const preCenter = pre!.localBounds.center.x;\n      const firstBlockX = trans.firstChild?.transform.position.x || 0;\n      const lastBlockX = trans.lastChild?.transform.position.x || 0;\n      const currentCenter = (lastBlockX - firstBlockX) / 2;\n\n      return {\n        x: preCenter - currentCenter,\n        y: 0,\n      };\n    }\n    const preCenter = pre!.localBounds.center.y;\n    const firstBlockY = trans.firstChild?.transform.position.y || 0;\n    const lastBlockY = trans.lastChild?.transform.position.y || 0;\n    const currentCenter = (lastBlockY - firstBlockY) / 2;\n\n    return {\n      x: 0,\n      y: preCenter - currentCenter,\n    };\n  },\n  getLabels(transition) {\n    return getBranchAdderLabel(transition);\n  },\n  getLines() {\n    return [];\n  },\n  // 和前序节点对齐\n  getInputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    if (isVertical) {\n      return {\n        x: transform.pre?.outputPoint.x || 0,\n        y: transform.bounds.top,\n      };\n    }\n    return {\n      x: transform.bounds.left,\n      y: transform.pre?.outputPoint.y || 0,\n    };\n  },\n  getOutputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    // 收缩时，出点为入点\n    if (transform.collapsed) {\n      return transform.inputPoint;\n    }\n\n    if (isVertical) {\n      return {\n        x: transform.pre?.outputPoint.x || 0,\n        y: transform.bounds.bottom,\n      };\n    }\n    return {\n      x: transform.bounds.right,\n      y: transform.pre?.outputPoint.y || 0,\n    };\n  },\n};\n\n/**\n * inlineBlocks 获取 BranchAdder 和展开 label\n */\nexport function getBranchAdderLabel(transition: FlowNodeTransitionData) {\n  const { isVertical } = transition.entity;\n  const currentTransform = transition.transform;\n\n  // 收起时展示收起 label\n  if (currentTransform.collapsed) {\n    return [\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_LABEL,\n        offset: Point.move(currentTransform.inputPoint, isVertical ? { y: 10 } : { x: 10 }),\n        props: {\n          activateNode: transition.entity.pre,\n        },\n      },\n    ];\n  }\n\n  return [\n    {\n      type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n      renderKey: FlowRendererKey.BRANCH_ADDER,\n      offset: Point.move(currentTransform.inputPoint, isVertical ? { y: 10 } : { x: 10 }),\n      props: {\n        // 激活状态\n        activated: transition.entity.getData(FlowNodeRenderData)!.activated,\n        transform: currentTransform,\n        // 传给外部使用的 node 信息\n        node: currentTransform.originParent?.entity,\n      },\n    },\n  ];\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/input.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n  LABEL_SIDE_TYPE,\n  FlowTransitionLabelEnum,\n} from '@flowgram.ai/document';\n\n/**\n * 输入节点\n */\nexport const InputRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.INPUT,\n  extend: FlowNodeBaseType.BLOCK,\n  meta: {\n    hidden: false,\n  },\n  getLines(transition, layout) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n    const lines: FlowTransitionLine[] = [];\n\n    const hasBranchDraggingAdder =\n      currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;\n\n    // 分支拖拽场景线条 push\n    // 当有其余分支的时候，绘制一条两个分支之间的线条\n    if (hasBranchDraggingAdder) {\n      if (isVertical) {\n        const currentOffsetRightX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.right\n          : currentTransform.bounds.right;\n        const nextOffsetLeftX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next?.firstChild.bounds?.left\n            : currentTransform.next?.bounds?.left) || 0;\n        const currentInputPointY = currentTransform.outputPoint.y;\n        if (currentTransform?.next) {\n          lines.push({\n            type: FlowTransitionLineEnum.MERGE_LINE,\n            isDraggingLine: true,\n            from: {\n              x: (currentOffsetRightX + nextOffsetLeftX) / 2,\n              y: currentInputPointY,\n            },\n            to: currentTransform.parent!.outputPoint,\n            side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n          });\n        }\n      } else {\n        const currentOffsetBottomX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.bottom\n          : currentTransform.bounds.bottom;\n        const nextOffsetTopX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next?.firstChild.bounds?.top\n            : currentTransform.next?.bounds?.top) || 0;\n        const currentInputPointX = currentTransform.outputPoint.x;\n        if (currentTransform?.next) {\n          lines.push({\n            type: FlowTransitionLineEnum.MERGE_LINE,\n            isDraggingLine: true,\n            from: {\n              x: currentInputPointX,\n              y: (currentOffsetBottomX + nextOffsetTopX) / 2,\n            },\n            to: currentTransform.parent!.outputPoint,\n            side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n          });\n        }\n      }\n    }\n\n    // 最后一个节点是 end 节点，不绘制 mergeLine\n    if (!transition.isNodeEnd) {\n      lines.push({\n        type: FlowTransitionLineEnum.MERGE_LINE,\n        from: currentTransform.outputPoint,\n        to: currentTransform.parent!.outputPoint,\n        side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n      });\n    }\n\n    return lines;\n  },\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n\n    const draggingLabel = [];\n\n    const hasBranchDraggingAdder =\n      currentTransform && currentTransform.entity.isInlineBlock && transition.renderData.draggable;\n\n    // 获取两个分支节点中间点作为拖拽标签插入位置\n    if (hasBranchDraggingAdder) {\n      if (isVertical) {\n        const currentOffsetRightX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.right\n          : currentTransform.bounds.right;\n        const nextOffsetLeftX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next.firstChild.bounds?.left\n            : currentTransform.next?.bounds?.left) || 0;\n        const currentInputPointY = currentTransform.outputPoint.y;\n        if (currentTransform?.next) {\n          draggingLabel.push({\n            offset: {\n              x: (currentOffsetRightX + nextOffsetLeftX) / 2,\n              y: currentInputPointY,\n            },\n            type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n            width: nextOffsetLeftX - currentOffsetRightX,\n            props: {\n              side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n            },\n          });\n        }\n      } else {\n        const currentOffsetBottomX = currentTransform.firstChild\n          ? currentTransform.firstChild.bounds.bottom\n          : currentTransform.bounds.bottom;\n        const nextOffsetTopX =\n          (currentTransform.next?.firstChild\n            ? currentTransform.next.firstChild.bounds?.top\n            : currentTransform.next?.bounds?.top) || 0;\n        const currentInputPointX = currentTransform.outputPoint.x;\n        if (currentTransform?.next) {\n          draggingLabel.push({\n            offset: {\n              x: currentInputPointX,\n              y: (currentOffsetBottomX + nextOffsetTopX) / 2,\n            },\n            type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n            width: nextOffsetTopX - currentOffsetBottomX,\n            props: {\n              side: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n            },\n          });\n        }\n      }\n    }\n\n    return [...draggingLabel];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ConstantKeys } from '@flowgram.ai/document';\n\nexport enum LoopTypeEnum {\n  LOOP_LEFT_EMPTY_BLOCK = 'loopLeftEmptyBlock',\n  LOOP_RIGHT_EMPTY_BLOCK = 'loopRightEmptyBlock',\n  LOOP_EMPTY_BRANCH = 'loopEmptyBranch',\n}\n\nexport const LoopSpacings = {\n  SPACING: 16, // 距离下面节点距离\n  COLLAPSE_INLINE_SPACING_BOTTOM: 60, // 距离下面节点距离\n  [ConstantKeys.INLINE_SPACING_BOTTOM]: 48, // 下边空白\n  MIN_INLINE_BLOCK_SPACING: 280, // 最小循环圈宽度\n  HORIZONTAL_MIN_INLINE_BLOCK_SPACING: 180, // 水平布局下的最小循环圈高度\n  LEFT_EMPTY_BLOCK_WIDTH: 80, // 左边空分支宽度\n  EMPTY_BRANCH_SPACING: 20, // 左边空分支宽度\n  LOOP_BLOCK_ICON_SPACING: 13, // inlineBlocks 的 inlineBottom\n  [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_BOTTOM]: 23, // inlineBlocks 的 inlineBottom\n  INLINE_BLOCKS_INLINE_SPACING_TOP: 30, // inlineBlocks 的 inlineTop\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './constants';\nexport * from './loop-left-empty-block';\nexport * from './loop-right-empty-block';\nexport * from './loop-empty-branch';\nexport * from './loop-inline-blocks';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/loop-empty-branch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeRegistry, FlowTransitionLabelEnum } from '@flowgram.ai/document';\n\nimport { LoopSpacings, LoopTypeEnum } from './constants';\n\nexport const LoopEmptyBranchRegistry: FlowNodeRegistry = {\n  type: LoopTypeEnum.LOOP_EMPTY_BRANCH,\n  meta: {\n    inlineSpacingAfter: 0,\n    spacing: LoopSpacings.EMPTY_BRANCH_SPACING,\n    size: {\n      width: 100,\n      height: 0,\n    },\n  },\n  getLabels(transition) {\n    const { isVertical } = transition.entity;\n    const currentTransform = transition.transform;\n    if (isVertical) {\n      return [\n        {\n          type: FlowTransitionLabelEnum.ADDER_LABEL,\n          offset: {\n            x: currentTransform.inputPoint.x,\n            y: currentTransform.bounds.center.y + 8, // 右边空节点\n          },\n        },\n      ];\n    }\n    return [\n      {\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n        offset: {\n          x: currentTransform.bounds.center.x + 8,\n          y: currentTransform.inputPoint.y,\n        },\n      },\n    ];\n  },\n  onAfterUpdateLocalTransform(transform): void {\n    if (transform.entity.isVertical) {\n      transform.data.size = {\n        width: 100,\n        height: 0,\n      };\n    } else {\n      transform.data.size = {\n        width: 0,\n        height: 100,\n      };\n    }\n    transform.transform.update({\n      size: transform.data.size,\n    });\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/loop-inline-blocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { FlowTextKey } from '@flowgram.ai/renderer';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  type FlowTransitionLabel,\n  FlowTransitionLabelEnum,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n  getDefaultSpacing,\n  ConstantKeys,\n} from '@flowgram.ai/document';\n\nimport { LoopSpacings } from './constants';\n\nexport const LoopInlineBlocksNodeRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.INLINE_BLOCKS,\n  meta: {\n    inlineSpacingPre: (node) => {\n      const inlineBlocksInlineSpacingTop = getDefaultSpacing(\n        node.entity,\n        ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_TOP,\n        LoopSpacings.INLINE_BLOCKS_INLINE_SPACING_TOP\n      );\n      return inlineBlocksInlineSpacingTop;\n    },\n    inlineSpacingAfter: (node) => {\n      const inlineBlocksInlineSpacingBottom = getDefaultSpacing(\n        node.entity,\n        ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_BOTTOM,\n        LoopSpacings.INLINE_BLOCKS_INLINE_SPACING_BOTTOM\n      );\n      return inlineBlocksInlineSpacingBottom;\n    },\n    minInlineBlockSpacing: (node) =>\n      node.entity.isVertical\n        ? LoopSpacings.MIN_INLINE_BLOCK_SPACING\n        : LoopSpacings.HORIZONTAL_MIN_INLINE_BLOCK_SPACING,\n  },\n  getLines(transition) {\n    const currentTransform = transition.transform;\n    const parentTransform = currentTransform.parent!;\n    const { isVertical } = transition.entity;\n\n    const lines: FlowTransitionLine[] = [\n      // 循环结束线\n      {\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: currentTransform.outputPoint,\n        to: parentTransform.outputPoint,\n      },\n    ];\n\n    // 收起时展示\n    if (currentTransform.collapsed) {\n      return lines;\n    }\n\n    const [leftBlockTransform] = currentTransform.children;\n\n    return [\n      ...lines,\n      // 循环回撤线 - 1 分成两段可以被 viewport 识别出矩阵区域\n      {\n        type: FlowTransitionLineEnum.ROUNDED_LINE,\n        from: currentTransform.outputPoint,\n        to: leftBlockTransform.outputPoint,\n        vertices: [\n          isVertical\n            ? { x: leftBlockTransform.inputPoint.x, y: currentTransform.bounds.bottom }\n            : { x: currentTransform.bounds.right, y: leftBlockTransform.inputPoint.y },\n        ],\n      },\n      // 循环回撤线 - 2\n      {\n        type: FlowTransitionLineEnum.ROUNDED_LINE,\n        from: leftBlockTransform.outputPoint,\n        to: Point.move(\n          currentTransform.inputPoint,\n          isVertical ? { x: -12, y: 10 } : { x: 10, y: -12 }\n        ),\n        vertices: [\n          isVertical\n            ? { x: leftBlockTransform.inputPoint.x, y: currentTransform.bounds.top + 10 }\n            : { x: currentTransform.bounds.left + 10, y: leftBlockTransform.inputPoint.y },\n        ],\n        arrow: true,\n      },\n    ];\n  },\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n\n    const labels: FlowTransitionLabel[] = [];\n\n    // 收起时展示\n    if (currentTransform.collapsed) {\n      return labels;\n    }\n\n    const leftBlockTransform = currentTransform.children[0];\n    const rightBlockTransform = currentTransform.children[1];\n\n    if (transition.entity.originParent?.id.startsWith('while_')) {\n      // 满足条件时\n      labels.push({\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.LOOP_WHILE_TEXT,\n        rotate: isVertical ? '' : '-90deg',\n        offset: isVertical\n          ? {\n              x: (currentTransform.inputPoint.x + rightBlockTransform.inputPoint.x) / 2,\n              y: currentTransform.inputPoint.y + 10,\n            }\n          : {\n              x: currentTransform.inputPoint.x + 10,\n              y: (currentTransform.inputPoint.y + rightBlockTransform.inputPoint.y) / 2,\n            },\n      });\n    } else {\n      // 循环遍历\n      labels.push({\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.LOOP_TRAVERSE_TEXT,\n        // rotate: isVertical ? '' : '-90deg',\n        offset: isVertical\n          ? { x: leftBlockTransform.inputPoint.x, y: currentTransform.bounds.center.y + 5 }\n          : { x: currentTransform.bounds.center.x + 5, y: leftBlockTransform.inputPoint.y },\n      });\n    }\n\n    return labels;\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/loop-left-empty-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeRegistry } from '@flowgram.ai/document';\n\nimport { LoopSpacings, LoopTypeEnum } from './constants';\n\nexport const LoopLeftEmptyBlockRegistry: FlowNodeRegistry = {\n  type: LoopTypeEnum.LOOP_LEFT_EMPTY_BLOCK,\n  meta: {\n    inlineSpacingAfter: 0,\n    spacing: 0,\n    size: {\n      width: LoopSpacings.LEFT_EMPTY_BLOCK_WIDTH,\n      height: 0,\n    },\n  },\n  onAfterUpdateLocalTransform(transform): void {\n    // 根据布局要置换宽高数据\n    if (transform.entity.isVertical) {\n      transform.data.size = {\n        width: LoopSpacings.LEFT_EMPTY_BLOCK_WIDTH,\n        height: 0,\n      };\n    } else {\n      transform.data.size = {\n        width: 0,\n        height: LoopSpacings.LEFT_EMPTY_BLOCK_WIDTH,\n      };\n    }\n    transform.transform.update({\n      size: transform.data.size,\n    });\n  },\n  getLines() {\n    return [];\n  },\n  getLabels() {\n    return [];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop-extends/loop-right-empty-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeRegistry } from '@flowgram.ai/document';\n\nimport { BlockRegistry } from '../block';\nimport { LoopTypeEnum } from './constants';\n\nexport const LoopRightEmptyBlockRegistry: FlowNodeRegistry = {\n  ...BlockRegistry,\n  type: LoopTypeEnum.LOOP_RIGHT_EMPTY_BLOCK,\n  meta: {\n    ...BlockRegistry.meta,\n    inlineSpacingAfter: 0,\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/loop.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { FlowTextKey } from '@flowgram.ai/renderer';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeEntity,\n  type FlowNodeJSON,\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n  getDefaultSpacing,\n  ConstantKeys,\n} from '@flowgram.ai/document';\n\nimport {\n  LoopEmptyBranchRegistry,\n  LoopInlineBlocksNodeRegistry,\n  LoopLeftEmptyBlockRegistry,\n  LoopRightEmptyBlockRegistry,\n  LoopSpacings,\n  LoopTypeEnum,\n} from './loop-extends';\n\n/**\n * 循环节点\n */\nexport const LoopRegistry: FlowNodeRegistry = {\n  type: 'loop',\n  meta: {\n    hidden: true,\n    inlineSpacingAfter: (node) => {\n      if (node.collapsed) {\n        return LoopSpacings.COLLAPSE_INLINE_SPACING_BOTTOM;\n      }\n      const inlineSpacingBottom = getDefaultSpacing(\n        node.entity,\n        ConstantKeys.INLINE_SPACING_BOTTOM,\n        LoopSpacings.INLINE_SPACING_BOTTOM\n      );\n      return inlineSpacingBottom;\n    },\n    spacing: LoopSpacings.SPACING,\n  },\n  /**\n   * - loopNode\n   *  - loopBlockIcon\n   *  - loopInlineBlocks\n   *    - loopEmptyBlock 左侧占位区域\n   *    - loopBlock\n   *      - xxx\n   *      - xxx\n   * @param node\n   * @param json\n   */\n  onCreate(node: FlowNodeEntity, json: FlowNodeJSON) {\n    const { document } = node;\n    const loopBlocks = json.blocks || [];\n\n    const loopIconNode = document.addNode({\n      id: `$blockIcon$${node.id}`,\n      type: FlowNodeBaseType.BLOCK_ICON,\n      originParent: node,\n      parent: node,\n    });\n    const loopInlineBlocks = document.addNode({\n      id: `$inlineBlocks$${node.id}`,\n      hidden: true,\n      type: FlowNodeBaseType.INLINE_BLOCKS,\n      originParent: node,\n      parent: node,\n    });\n    const loopEmptyBlockNode = document.addNode({\n      id: `$loopLeftEmpty$${node.id}`,\n      hidden: true,\n      type: LoopTypeEnum.LOOP_LEFT_EMPTY_BLOCK,\n      originParent: node,\n      parent: loopInlineBlocks,\n    });\n\n    const loopBlockNode = document.addNode({\n      id: `$block$${node.id}`,\n      hidden: true,\n      type: FlowNodeBaseType.BLOCK, // : LoopTypeEnum.LOOP_RIGHT_EMPTY_BLOCK,\n      originParent: node,\n      parent: loopInlineBlocks,\n    });\n    const loopBranch = document.addNode({\n      id: `$loopRightEmpty$${node.id}`,\n      hidden: true,\n      type: LoopTypeEnum.LOOP_EMPTY_BRANCH,\n      originParent: node,\n      parent: loopBlockNode,\n    });\n    const otherNodes: FlowNodeEntity[] = [];\n    loopBlocks.forEach((b) =>\n      document.addNode(\n        {\n          ...b,\n          type: b.type!,\n          parent: loopBlockNode,\n        },\n        otherNodes\n      )\n    );\n\n    return [\n      loopIconNode,\n      loopEmptyBlockNode,\n      loopInlineBlocks,\n      loopBlockNode,\n      loopBranch,\n      ...otherNodes,\n    ];\n  },\n  getLabels(transition) {\n    const currentTransform = transition.transform;\n    const { isVertical } = transition.entity;\n    return [\n      // 循环结束\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.LOOP_END_TEXT,\n        // 循环 label 垂直样式展示，而非 rotate 旋转文案\n        props: isVertical\n          ? undefined\n          : {\n              style: {\n                maxWidth: '20px',\n                lineHeight: '12px',\n                whiteSpace: 'pre-wrap',\n              },\n            },\n        offset: Point.move(currentTransform.outputPoint, isVertical ? { y: -26 } : { x: -26 }),\n      },\n      {\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n        offset: currentTransform.outputPoint,\n      },\n    ];\n  },\n  // 和前序节点对齐\n  getInputPoint(transform) {\n    const { isVertical } = transform.entity;\n    if (isVertical) {\n      return {\n        x: transform.pre?.outputPoint.x || transform.firstChild?.outputPoint.x || 0,\n        y: transform.bounds.top,\n      };\n    }\n    return {\n      x: transform.bounds.left,\n      y: transform.pre?.outputPoint.y || transform.firstChild?.outputPoint.y || 0,\n    };\n  },\n  getOutputPoint(transform) {\n    const { isVertical } = transform.entity;\n    if (isVertical) {\n      return {\n        x: transform.pre?.outputPoint.x || transform.firstChild?.outputPoint.x || 0,\n        y: transform.bounds.bottom,\n      };\n    }\n    return {\n      x: transform.bounds.right,\n      y: transform.pre?.outputPoint.y || transform.firstChild?.outputPoint.y || 0,\n    };\n  },\n\n  extendChildRegistries: [\n    {\n      type: FlowNodeBaseType.BLOCK_ICON,\n      meta: {\n        spacing: LoopSpacings.LOOP_BLOCK_ICON_SPACING,\n      },\n    },\n    LoopLeftEmptyBlockRegistry,\n    LoopEmptyBranchRegistry,\n    LoopRightEmptyBlockRegistry,\n    LoopInlineBlocksNodeRegistry,\n  ],\n\n  /**\n   * @depreacted\n   */\n  addChild(node, json, options = {}) {\n    const { index } = options;\n    const document = node.document;\n    return document.addNode({\n      ...json,\n      ...options,\n      parent: document.getNode(`$block$${node.id}`),\n      index: typeof index === 'number' ? index + 1 : undefined,\n    });\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/multi-inputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Point } from '@flowgram.ai/utils';\nimport { FlowRendererKey } from '@flowgram.ai/renderer';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowNodeRenderData,\n  FlowTransitionLabelEnum,\n  FlowNodeSplitType,\n  getDefaultSpacing,\n  ConstantKeys,\n} from '@flowgram.ai/document';\n\n/**\n * 多输入节点, 只能作为 开始节点\n * - multiInputs:\n *   - inlineBlocks\n *     - input\n *     - input\n */\nexport const MultiInputsRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.MULTI_INPUTS,\n  extend: FlowNodeSplitType.SIMPLE_SPLIT,\n  extendChildRegistries: [\n    {\n      type: FlowNodeBaseType.BLOCK_ICON,\n      meta: {\n        hidden: true,\n        spacing: 0,\n      },\n      getLines() {\n        return [];\n      },\n      getLabels() {\n        return [];\n      },\n    },\n    {\n      type: FlowNodeBaseType.INLINE_BLOCKS,\n      meta: {\n        inlineSpacingPre: 0,\n      },\n      getLabels(transition) {\n        const isVertical = transition.entity.isVertical;\n        const currentTransform = transition.transform;\n        const spacing = getDefaultSpacing(\n          transition.entity,\n          ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM\n        );\n\n        if (currentTransform.collapsed || transition.entity.childrenLength === 0) {\n          return [\n            {\n              type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n              renderKey: FlowRendererKey.BRANCH_ADDER,\n              offset: Point.move(\n                currentTransform.outputPoint,\n                isVertical ? { y: spacing } : { x: spacing }\n              ),\n              props: {\n                // 激活状态\n                activated: transition.entity.getData(FlowNodeRenderData)!.activated,\n                transform: currentTransform,\n                // 传给外部使用的 node 信息\n                node: currentTransform.originParent?.entity,\n              },\n            },\n          ];\n        }\n\n        return [\n          {\n            type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n            renderKey: FlowRendererKey.BRANCH_ADDER,\n            offset: Point.move(\n              currentTransform.outputPoint,\n              isVertical ? { y: -spacing / 2 } : { x: -spacing / 2 }\n            ),\n            props: {\n              // 激活状态\n              activated: transition.entity.getData(FlowNodeRenderData)!.activated,\n              transform: currentTransform,\n              // 传给外部使用的 node 信息\n              node: currentTransform.originParent?.entity,\n            },\n          },\n        ];\n      },\n    },\n  ],\n  getLabels() {\n    return [];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/multi-outputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowLayoutDefault,\n  type FlowNodeRegistry,\n  FlowNodeSplitType,\n  FlowNodeBaseType,\n} from '@flowgram.ai/document';\n\nimport { DynamicSplitRegistry } from './dynamic-split';\nimport { BlockRegistry } from './block';\n\n/**\n * 多输出节点\n * - multiOutputs:\n *  - blockIcon\n *  - inlineBlocks\n *    - output or multiOutputs\n *    - output or multiOutputs\n */\nexport const MultiOuputsRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.MULTI_OUTPUTS,\n  extend: FlowNodeSplitType.SIMPLE_SPLIT,\n  meta: {\n    isNodeEnd: true,\n  },\n  getLines: (transition, layout) => {\n    // 嵌套在 mutliOutputs 下边\n    if (transition.entity.parent?.flowNodeType === FlowNodeBaseType.INLINE_BLOCKS) {\n      return BlockRegistry.getLines!(transition, layout);\n    }\n    return [];\n  },\n  getLabels: (transition, layout) => [\n    ...DynamicSplitRegistry.getLabels!(transition, layout),\n    ...BlockRegistry.getLabels!(transition, layout),\n  ],\n  getOutputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    const lastChildOutput = transform.lastChild?.outputPoint;\n\n    if (isVertical) {\n      return {\n        x: lastChildOutput ? lastChildOutput.x : transform.bounds.center.x,\n        y: transform.bounds.bottom,\n      };\n    }\n\n    return {\n      x: transform.bounds.right,\n      y: lastChildOutput ? lastChildOutput.y : transform.bounds.center.y,\n    };\n  },\n  extendChildRegistries: [\n    {\n      type: FlowNodeBaseType.BLOCK_ICON,\n      meta: {\n        // isNodeEnd: true\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/output.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\n/**\n * 输出节点, 一般作为 end 节点\n */\nexport const OuputRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.OUTPUT,\n  extend: FlowNodeBaseType.BLOCK,\n  meta: {\n    hidden: false,\n    isNodeEnd: true,\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/root.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DEFAULT_SPACING, FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\n/**\n * 根节点\n */\nexport const RootRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.ROOT,\n  meta: {\n    spacing: DEFAULT_SPACING.NULL,\n    hidden: true,\n  },\n  getInputPoint(transform) {\n    return transform.firstChild?.inputPoint || transform.bounds.topCenter;\n  },\n  getOutputPoint(transform) {\n    return transform.lastChild?.outputPoint || transform.bounds.bottomCenter;\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/simple-split.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type FlowNodeRegistry,\n  FlowNodeSplitType,\n  FlowNodeBaseType,\n  FlowNodeJSON,\n  FlowNodeEntity,\n} from '@flowgram.ai/document';\n\n/**\n * - simpleSplit:  (最原始的 id)\n *  blockIcon\n *  inlineBlocks\n *    node1\n *    node2\n */\nexport const SimpleSplitRegistry: FlowNodeRegistry = {\n  type: FlowNodeSplitType.SIMPLE_SPLIT,\n  extend: FlowNodeSplitType.DYNAMIC_SPLIT,\n  onBlockChildCreate(\n    originParent: FlowNodeEntity,\n    blockData: FlowNodeJSON,\n    addedNodes: FlowNodeEntity[] = [] // 新创建的节点都要存在这里\n  ) {\n    const { document } = originParent;\n    const parent = document.getNode(`$inlineBlocks$${originParent.id}`);\n    const realBlock = document.addNode(\n      {\n        ...blockData,\n        type: blockData.type || FlowNodeBaseType.BLOCK,\n        parent,\n      },\n      addedNodes\n    );\n    addedNodes.push(realBlock);\n    return realBlock;\n  },\n  // addChild(node, json, options = {}) {\n  //   const { index } = options;\n  //   const document = node.document;\n  //   return document.addBlock(node, json, undefined, undefined, index);\n  // }\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowRendererKey } from '@flowgram.ai/renderer';\n\nexport const RENDER_SLOT_ADDER_KEY: string = FlowRendererKey.SLOT_ADDER;\nexport const RENDER_SLOT_LABEL_KEY: string = FlowRendererKey.SLOT_LABEL;\nexport const RENDER_SLOT_COLLAPSE_KEY: string = FlowRendererKey.SLOT_COLLAPSE;\n\nexport const SlotSpacingKey = {\n  /**\n   * = Next Node - Slot END\n   */\n  SLOT_SPACING: 'SLOT_SPACING',\n\n  /**\n   * = Slot Start Line - Slot Icon Right\n   */\n  SLOT_START_DISTANCE: 'SLOT_START_DISTANCE',\n\n  /**\n   * = Slot Radius\n   */\n  SLOT_RADIUS: 'SLOT_RADIUS',\n\n  /**\n   * = Slot Port - Slot Start\n   */\n  SLOT_PORT_DISTANCE: 'SLOT_PORT_DISTANCE',\n\n  /**\n   * = Slot Label - Slot Start\n   */\n  SLOT_LABEL_DISTANCE: 'SLOT_LABEL_DISTANCE',\n\n  /**\n   * = Slot Block - Slot Port\n   */\n  SLOT_BLOCK_PORT_DISTANCE: 'SLOT_BLOCK_PORT_DISTANCE',\n\n  /**\n   * Vertical Layout: Slot Block - Slot Block\n   */\n  SLOT_BLOCK_VERTICAL_SPACING: 'SLOT_BLOCK_VERTICAL_SPACING',\n};\n\nexport const SLOT_START_DISTANCE = 16;\nexport const SLOT_PORT_DISTANCE = 100;\nexport const SLOT_LABEL_DISTANCE = 32;\nexport const SLOT_BLOCK_PORT_DISTANCE = 32.5;\nexport const SLOT_RADIUS = 16;\nexport const SLOT_SPACING = 32;\nexport const SLOT_BLOCK_VERTICAL_SPACING = 32.5;\nexport const SLOT_NODE_LAST_SPACING = 10;\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { SlotIconRegistry } from './slot-icon';\nexport { SlotInlineBlocksRegistry } from './slot-inline-blocks';\nexport { SlotBlockRegistry } from './slot-block';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { mean } from 'lodash-es';\nimport {\n  FlowNodeRegistry,\n  FlowNodeBaseType,\n  FlowTransitionLabelEnum,\n  FlowTransitionLineEnum,\n  getDefaultSpacing,\n  Vertex,\n} from '@flowgram.ai/document';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { getPortChildInput, getSlotChildLineStartPoint } from '../utils/transition';\nimport { SlotNodeType } from '../typings';\nimport {\n  RENDER_SLOT_ADDER_KEY,\n  SlotSpacingKey,\n  SLOT_PORT_DISTANCE,\n  SLOT_RADIUS,\n  SLOT_LABEL_DISTANCE,\n  RENDER_SLOT_LABEL_KEY,\n  SLOT_BLOCK_VERTICAL_SPACING,\n} from '../constants';\n\nexport const SlotBlockRegistry: FlowNodeRegistry = {\n  type: SlotNodeType.SlotBlock,\n  extend: FlowNodeBaseType.BLOCK,\n  meta: {\n    inlineSpacingAfter: 0,\n    inlineSpacingPre: 0,\n    spacing: (transform) => {\n      // 水平布局没有子节点情况\n      if (!transform.entity.isVertical && transform.size.width === 0) {\n        return 90;\n      }\n      return getDefaultSpacing(\n        transform.entity,\n        SlotSpacingKey.SLOT_BLOCK_VERTICAL_SPACING,\n        SLOT_BLOCK_VERTICAL_SPACING\n      );\n    },\n    isInlineBlocks: (node) => !node.isVertical,\n  },\n  getLines(transition) {\n    const icon = transition.transform.parent?.pre;\n    const start = getSlotChildLineStartPoint(icon);\n    const portPoint = transition.transform.inputPoint;\n\n    const radius = getDefaultSpacing(\n      transition.transform.entity,\n      SlotSpacingKey.SLOT_RADIUS,\n      SLOT_RADIUS\n    );\n\n    let startPortVertices: Vertex[] = [{ x: start.x, y: portPoint.y }];\n\n    /**\n     * When Radius is not enough, we should use truncate strategy\n     * 弧度不够时，采取截断策略\n     */\n    if (transition.entity.isVertical) {\n      const deltaY = Math.abs(portPoint.y - start.y);\n      let deltaX = radius;\n      let isTruncated = false;\n\n      if (deltaY < radius * 2) {\n        isTruncated = true;\n        if (deltaY < radius) {\n          // Calculate the x by circle equation\n          deltaX = Math.sqrt(radius ** 2 - (radius - deltaY) ** 2);\n        }\n      }\n\n      startPortVertices = [\n        {\n          x: start.x + deltaX,\n          y: start.y,\n          radiusX: radius,\n          radiusY: radius,\n          radiusOverflow: 'truncate',\n        },\n        {\n          x: start.x + deltaX,\n          y: portPoint.y,\n          ...(isTruncated ? { radiusX: 0, radiusY: 0 } : {}),\n        },\n      ];\n    }\n\n    /**\n     * When One Children, we should keep dash array align, so we draw one line directly to child nodes\n     * 只有一个子节点时，我们通常需要保证两条虚线是连贯的，因此我们直接合并，绘制一条线连到子节点\n     */\n    if (transition.transform.children.length === 1) {\n      return [\n        {\n          type: FlowTransitionLineEnum.ROUNDED_LINE,\n          from: start,\n          to: getPortChildInput(transition.transform.children[0]),\n          vertices: startPortVertices,\n          style: {\n            strokeDasharray: '5 5',\n          },\n          radius,\n        },\n      ];\n    }\n\n    return [\n      {\n        type: FlowTransitionLineEnum.ROUNDED_LINE,\n        from: start,\n        to: portPoint,\n        vertices: startPortVertices,\n        style: {\n          strokeDasharray: '5 5',\n        },\n        radius,\n      },\n      ...transition.transform.children.map((_child) => {\n        const childInput = getPortChildInput(_child);\n\n        return {\n          type: FlowTransitionLineEnum.ROUNDED_LINE,\n          radius,\n          from: portPoint,\n          to: childInput,\n          vertices: [{ x: portPoint.x, y: childInput.y }],\n          style: {\n            strokeDasharray: '5 5',\n          },\n        };\n      }),\n    ];\n  },\n  getLabels(transition) {\n    const icon = transition.transform.parent?.pre;\n    const start = getSlotChildLineStartPoint(icon);\n    const portPoint = transition.transform.inputPoint;\n\n    return [\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        renderKey: RENDER_SLOT_ADDER_KEY,\n        props: {\n          node: transition.entity,\n        },\n        offset: portPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        renderKey: RENDER_SLOT_LABEL_KEY,\n        props: {\n          node: transition.entity,\n        },\n        offset: {\n          x:\n            start.x +\n            getDefaultSpacing(\n              transition.entity,\n              SlotSpacingKey.SLOT_LABEL_DISTANCE,\n              SLOT_LABEL_DISTANCE\n            ),\n          y: portPoint.y,\n        },\n        origin: [0, 0.5],\n      },\n    ];\n  },\n  getInputPoint(transform) {\n    const icon = transform.parent?.pre;\n    const start = getSlotChildLineStartPoint(icon);\n\n    let inputY = transform.bounds.center.y;\n    if (transform.children.length) {\n      inputY = mean([\n        getPortChildInput(transform.firstChild).y,\n        getPortChildInput(transform.lastChild).y,\n      ]);\n    }\n\n    return {\n      x:\n        start.x +\n        getDefaultSpacing(transform.entity, SlotSpacingKey.SLOT_PORT_DISTANCE, SLOT_PORT_DISTANCE),\n      y: inputY,\n    };\n  },\n  getChildDelta(child, layout) {\n    const hasChild = !!child.firstChild;\n\n    if (child.entity.isVertical) {\n      // 绑定普通节点时进行重心纠偏\n      let deltaX = hasChild ? 0 : -child.originDeltaX;\n\n      return { x: deltaX, y: 0 };\n    }\n\n    let deltaY = hasChild ? 0 : -child.originDeltaY;\n\n    const preTransform = child.entity.pre?.getData(FlowNodeTransformData);\n    if (preTransform) {\n      const { localBounds: preBounds } = preTransform;\n\n      return {\n        x: 0,\n        y: preBounds.bottom + 30 + deltaY,\n      };\n    }\n\n    return { x: 0, y: deltaY };\n  },\n  getChildLabels() {\n    return [];\n  },\n  getChildLines() {\n    return [];\n  },\n  getDelta(transform) {\n    return {\n      x: 0,\n      y: 0,\n    };\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-icon.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry, FlowNodeBaseType } from '@flowgram.ai/document';\n\nimport { drawCollapseLabel, drawCollapseLine } from '../utils/transition';\nimport { canSlotDrilldown, insideSlot } from '../utils/node';\n\nexport const SlotIconRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.BLOCK_ICON,\n  meta: {\n    defaultExpanded: false,\n    spacing: 0,\n  },\n  getLines: (transition) => [\n    ...(canSlotDrilldown(transition.entity.parent!) ? drawCollapseLine(transition) : []),\n  ],\n  getLabels: (transition) => [\n    ...(canSlotDrilldown(transition.entity.parent!) ? drawCollapseLabel(transition) : []),\n  ],\n  getDelta: (transform) => {\n    // Slot 节点内部时，重新纠正重心调整产生的偏移\n    if (insideSlot(transform.entity.parent)) {\n      return transform.entity.isVertical\n        ? { x: -transform.originDeltaX, y: 0 }\n        : { x: 0, y: -transform.originDeltaY };\n    }\n    return { x: 0, y: 0 };\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/extends/slot-inline-blocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry, FlowNodeBaseType, getDefaultSpacing } from '@flowgram.ai/document';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { SlotNodeType } from '../typings';\nimport {\n  SlotSpacingKey,\n  SLOT_START_DISTANCE,\n  SLOT_PORT_DISTANCE,\n  SLOT_BLOCK_PORT_DISTANCE,\n} from '../constants';\n\nexport const SlotInlineBlocksRegistry: FlowNodeRegistry = {\n  type: SlotNodeType.SlotInlineBlocks,\n  extend: FlowNodeBaseType.BLOCK,\n  meta: {\n    spacing: 0,\n    inlineSpacingPre: 0,\n    inlineSpacingAfter: 0,\n    isInlineBlocks: (node) => !node.isVertical,\n  },\n  getLines() {\n    return [];\n  },\n  getLabels() {\n    return [];\n  },\n  getChildDelta(child, layout) {\n    if (child.entity.isVertical) {\n      return { x: 0, y: 0 };\n    }\n    const preTransform = child.entity.pre?.getData(FlowNodeTransformData);\n    if (preTransform) {\n      const { localBounds: preBounds } = preTransform;\n\n      return {\n        x: 0,\n        y: preBounds.bottom + 30,\n      };\n    }\n\n    return { x: 0, y: 0 };\n  },\n  /**\n   * 控制条件分支居右布局\n   */\n  getDelta(transform) {\n    if (!transform.children.length) {\n      return { x: 0, y: 0 };\n    }\n\n    const icon = transform.pre;\n    if (!icon) {\n      return { x: 0, y: 0 };\n    }\n\n    const startDistance = getDefaultSpacing(\n      transform.entity,\n      SlotSpacingKey.SLOT_START_DISTANCE,\n      SLOT_START_DISTANCE\n    );\n\n    const portDistance = getDefaultSpacing(\n      transform.entity,\n      SlotSpacingKey.SLOT_PORT_DISTANCE,\n      SLOT_PORT_DISTANCE\n    );\n\n    const portBlockDistance = getDefaultSpacing(\n      transform.entity,\n      SlotSpacingKey.SLOT_BLOCK_PORT_DISTANCE,\n      SLOT_BLOCK_PORT_DISTANCE\n    );\n\n    if (!transform.entity.isVertical) {\n      const noChildren = transform?.children?.every?.((_port) => !_port.children.length);\n      /**\n       * 如果没有 children 的时候，不需要有右侧的间距，避免水平布局的时候 Slot 右侧空间过大。\n       */\n      if (noChildren) {\n        return {\n          x: portDistance - icon.localBounds.width / 2,\n          y: icon.localBounds.bottom + startDistance,\n        };\n      }\n      return {\n        x: portDistance + portBlockDistance - icon.localBounds.width / 2,\n        y: icon.localBounds.bottom + startDistance,\n      };\n    }\n\n    const slotInlineBlockDelta = startDistance + portDistance + portBlockDistance;\n    return {\n      x: icon.localBounds.right + slotInlineBlockDelta,\n      y: -icon.localBounds.height,\n    };\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { SlotRegistry } from './slot';\nexport { SlotBlockRegistry } from './extends';\nexport { SlotSpacingKey } from './constants';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/slot.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRegistry, FlowNodeBaseType, getDefaultSpacing } from '@flowgram.ai/document';\n\nimport {\n  drawStraightAdder,\n  drawStraightLine,\n  getInputPoint,\n  getOutputPoint,\n} from './utils/transition';\nimport { insideSlot } from './utils/node';\nimport { getAllPortsMiddle } from './utils/layout';\nimport { createSlotFromJSON } from './utils/create';\nimport { SlotInlineBlocksRegistry, SlotIconRegistry } from './extends';\nimport {\n  SLOT_NODE_LAST_SPACING,\n  SLOT_SPACING,\n  SLOT_START_DISTANCE,\n  SlotSpacingKey,\n} from './constants';\n\nexport const SlotRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.SLOT,\n  extend: 'block',\n  meta: {\n    // Slot 节点内部暂时不允许拖拽\n    draggable: (node) => !insideSlot(node),\n    hidden: true,\n    spacing: (node) => getDefaultSpacing(node.entity, SlotSpacingKey.SLOT_SPACING, SLOT_SPACING),\n    padding: (node) => ({\n      left: 0,\n      right: node.collapsed\n        ? getDefaultSpacing(node.entity, SlotSpacingKey.SLOT_START_DISTANCE, SLOT_START_DISTANCE)\n        : 0,\n      bottom: !insideSlot(node.entity) && node.isLast ? SLOT_NODE_LAST_SPACING : 0,\n      top: 0,\n    }),\n    copyDisable: false,\n    defaultExpanded: false,\n  },\n  /**\n   * 业务通常需要重载方法\n   */\n  onCreate: createSlotFromJSON,\n  getLines: (transition) => [\n    ...(!insideSlot(transition.entity) ? drawStraightLine(transition) : []),\n  ],\n  getLabels: (transition) => [\n    ...(!insideSlot(transition.entity) ? drawStraightAdder(transition) : []),\n  ],\n  getInputPoint,\n  getOutputPoint,\n  onAfterUpdateLocalTransform(transform) {\n    const { isVertical } = transform.entity;\n\n    if (!isVertical) {\n      return;\n    }\n\n    const icon = transform.firstChild;\n    const inlineBlocks = transform.lastChild;\n\n    if (!icon || !inlineBlocks) {\n      return;\n    }\n\n    const iconSize = icon.localBounds.height;\n    const inlineBlocksSize = inlineBlocks.localBounds.height;\n\n    if (transform.collapsed || !inlineBlocks) {\n      return;\n    }\n\n    // 所有 Ports 的中间点\n    const portsMiddle = getAllPortsMiddle(inlineBlocks);\n\n    icon.entity.clearMemoLocal();\n    inlineBlocks.entity.clearMemoLocal();\n\n    if (iconSize / 2 + portsMiddle > inlineBlocksSize || !inlineBlocks.children.length) {\n      icon.transform.update({\n        position: { x: icon.transform.position.x, y: 0 },\n      });\n      inlineBlocks.transform.update({\n        position: {\n          x: inlineBlocks.transform.position.x,\n          y: Math.max(iconSize / 2 - inlineBlocksSize / 2, 0),\n        },\n      });\n\n      return;\n    }\n\n    inlineBlocks.transform.update({\n      position: { x: inlineBlocks.transform.position.x, y: 0 },\n    });\n    icon?.transform.update({\n      position: {\n        x: icon.transform.position.x,\n        y: Math.max(portsMiddle - iconSize / 2, 0), // 所有 port 的中间点\n      },\n    });\n  },\n  extendChildRegistries: [SlotIconRegistry, SlotInlineBlocksRegistry],\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/typings.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\nexport enum SlotNodeType {\n  Slot = FlowNodeBaseType.SLOT,\n  SlotBlock = FlowNodeBaseType.SLOT_BLOCK,\n  SlotInlineBlocks = 'slotInlineBlocks',\n  SlotBlockInlineBlocks = 'slotBlockInlineBlocks',\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/create.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\nimport { FlowNodeJSON } from '@flowgram.ai/document';\n\nimport { SlotNodeType } from '../typings';\n\n// Slot 样例数据\n// const mock = {\n//   type: 'slot',\n//   id: 'reactor_parent',\n//   blocks: [\n//     {\n//       id: 'port_LnSdK',\n//       blocks: [{ type: 'Slot', id: 'reactor_child' }],\n//     },\n//     {\n//       id: 'port_60X7U',\n//     },\n//     {\n//       id: 'port_JWhcm',\n//     },\n//     {\n//       id: 'port_scHWa',\n//     },\n//   ],\n// };\n\n/**\n * 创建 Slot 子节点\n * - Slot\n *  - SlotBlockIcon\n *  - SlotInlineBlocks\n *    - SlotBlock 1\n *        - SlotBlockIcon 1\n *          - ChildSlot 1\n *          - ChildSlot 2\n *    - SlotBlock 2\n *\n * 范例数据：\n * {\n *  type: 'Slot',\n *  id: 'reactor_parent',\n *  blocks: [\n *    {\n *      id: 'port_LnSdK',\n *      blocks: [{ type: 'Slot', id: 'reactor_child' }],\n *    },\n *    {\n *      id: 'port_60X7U',\n *    }\n *  ],\n * }\n *\n */\nexport const createSlotFromJSON = (node: FlowNodeEntity, json: FlowNodeJSON): FlowNodeEntity[] => {\n  const { document } = node;\n\n  const addedNodes: FlowNodeEntity[] = [];\n\n  // 块列表开始节点，用来展示块的按钮\n  const blockIconNode = document.addNode({\n    id: `$slotIcon$${node.id}`,\n    type: FlowNodeBaseType.BLOCK_ICON,\n    originParent: node,\n    parent: node,\n  });\n  const inlineBlocksNode = document.addNode({\n    id: `$slotInlineBlocks$${node.id}`,\n    type: SlotNodeType.SlotInlineBlocks,\n    originParent: node,\n    parent: node,\n  });\n  addedNodes.push(blockIconNode);\n  addedNodes.push(inlineBlocksNode);\n\n  const portJSONList = json.blocks || [];\n\n  portJSONList.forEach((_portJSON) => {\n    const port = document.addNode({\n      type: SlotNodeType.SlotBlock,\n      ..._portJSON,\n      originParent: node,\n      parent: inlineBlocksNode,\n    });\n    addedNodes.push(port);\n\n    (_portJSON.blocks || []).forEach((_portChild) => {\n      document.addNode(\n        {\n          type: SlotNodeType.Slot,\n          ..._portChild,\n          parent: port,\n        },\n        addedNodes\n      );\n    });\n  });\n\n  return addedNodes;\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { mean } from 'lodash-es';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nexport const getDisplayFirstChildTop = (transform: FlowNodeTransformData): number => {\n  if (transform.firstChild) {\n    return transform.localBounds.top + getDisplayFirstChildTop(transform.firstChild);\n  }\n\n  return transform.localBounds.center.y;\n};\n\n/**\n * 获取单个 Port 的中间点\n * @param inlineBlocks\n * @returns\n */\nexport const getPortMiddle = (_port: FlowNodeTransformData) => {\n  if (!_port.children.length) {\n    return _port.localBounds.top;\n  }\n\n  const portChildInputs = [_port.firstChild!, _port.lastChild!].map((_portChild) =>\n    getDisplayFirstChildTop(_portChild)\n  );\n\n  return _port.localBounds.top + mean(portChildInputs);\n};\n\n/**\n * 获取所有 Port 的中间点\n * @param inlineBlocks\n * @returns\n */\nexport const getAllPortsMiddle = (inlineBlocks: FlowNodeTransformData) => {\n  if (!inlineBlocks.children.length) {\n    return inlineBlocks.localBounds.height / 2;\n  }\n\n  const portInputs = [inlineBlocks.firstChild!, inlineBlocks.lastChild!].map((_port) =>\n    getPortMiddle(_port)\n  );\n\n  return mean(portInputs);\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { SlotNodeType } from '../typings';\n\n/**\n * Slot 节点是否可下钻，看 inlineBlocks 是否有子节点\n * @param Slot Slot 节点\n */\nexport const canSlotDrilldown = (Slot: FlowNodeEntity): boolean =>\n  !!Slot?.lastCollapsedChild?.blocks.length;\n\n/**\n * 是否是 Slot 内部\n * @param entity\n * @returns\n */\nexport const insideSlot = (entity?: FlowNodeEntity): boolean =>\n  !!entity?.parent?.isTypeOrExtendType(SlotNodeType.SlotBlock);\n\n/**\n * 获取在页面上实际渲染的第一个 Child 节点\n * @param node\n */\nexport const getDisplayFirstChildTransform = (\n  transform: FlowNodeTransformData\n): FlowNodeTransformData => {\n  if (transform.firstChild) {\n    return getDisplayFirstChildTransform(transform.firstChild);\n  }\n\n  return transform;\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/slot/utils/transition.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Point } from '@flowgram.ai/utils';\nimport {\n  type FlowNodeTransitionData,\n  FlowTransitionLineEnum,\n  FlowTransitionLabelEnum,\n  type FlowNodeTransformData,\n  type FlowTransitionLine,\n  type FlowTransitionLabel,\n  getDefaultSpacing,\n} from '@flowgram.ai/document';\n\nimport { RENDER_SLOT_COLLAPSE_KEY, SlotSpacingKey, SLOT_START_DISTANCE } from '../constants';\nimport { getDisplayFirstChildTransform } from './node';\n\n/**\n * 画 Slot 节点虚线起点\n * @param iconTransform blockIcon 的 transform\n * @returns\n */\nexport const getSlotChildLineStartPoint = (iconTransform?: FlowNodeTransformData): IPoint => {\n  if (!iconTransform) {\n    return { x: 0, y: 0 };\n  }\n\n  const startDistance = getDefaultSpacing(\n    iconTransform.entity,\n    SlotSpacingKey.SLOT_START_DISTANCE,\n    SLOT_START_DISTANCE\n  );\n\n  if (!iconTransform.entity.isVertical) {\n    return {\n      x: iconTransform?.bounds.center.x,\n      y: iconTransform?.bounds.bottom + startDistance,\n    };\n  }\n  return {\n    x: iconTransform?.bounds.right + startDistance,\n    y: iconTransform?.bounds.center.y,\n  };\n};\n\nexport const getOutputPoint = (transform: FlowNodeTransformData): IPoint => {\n  const icon = transform.firstChild;\n\n  if (!icon) {\n    return { x: 0, y: 0 };\n  }\n\n  if (!transform.entity.isVertical) {\n    return {\n      x: transform.bounds.right,\n      y: icon.outputPoint.y,\n    };\n  }\n\n  return {\n    x: icon.outputPoint.x,\n    y: transform.bounds.bottom,\n  };\n};\n\nexport const getInputPoint = (transform: FlowNodeTransformData): IPoint => {\n  const icon = transform.firstChild;\n\n  if (!icon) {\n    return { x: 0, y: 0 };\n  }\n\n  if (!transform.entity.isVertical) {\n    return {\n      x: transform.bounds.left,\n      y: icon.outputPoint.y,\n    };\n  }\n\n  return icon.inputPoint;\n};\n\n/**\n * 获取实连线终点\n * @param transition\n * @returns\n */\nexport const getTransitionToPoint = (transition: FlowNodeTransitionData): IPoint => {\n  let toPoint = transition.transform.next?.inputPoint;\n  const icon = transition.transform.firstChild;\n\n  const parent = transition.transform.parent;\n\n  if (!icon || !parent) {\n    return { x: 0, y: 0 };\n  }\n\n  if (!transition.transform.next) {\n    if (!transition.entity.isVertical) {\n      toPoint = {\n        x: parent.outputPoint.x,\n        y: icon.outputPoint.y,\n      };\n    } else {\n      toPoint = {\n        x: icon.outputPoint.x,\n        y: parent.outputPoint.y,\n      };\n    }\n  }\n\n  return toPoint || { x: 0, y: 0 };\n};\n\n/**\n * 画实现线\n * @param transition Slot 节点的 transition\n * @returns\n */\nexport const drawStraightLine = (transition: FlowNodeTransitionData): FlowTransitionLine[] => {\n  const icon = transition.transform.firstChild;\n  const toPoint = getTransitionToPoint(transition);\n\n  if (!icon) {\n    return [];\n  }\n\n  return [\n    {\n      type: FlowTransitionLineEnum.STRAIGHT_LINE,\n      from: icon.outputPoint,\n      to: toPoint,\n    },\n    {\n      type: FlowTransitionLineEnum.STRAIGHT_LINE,\n      from: icon.inputPoint,\n      to: transition.transform.inputPoint,\n    },\n  ];\n};\n\nexport const drawCollapseLabel = (transition: FlowNodeTransitionData): FlowTransitionLabel[] => {\n  const icon = transition.transform;\n\n  return [\n    {\n      type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n      renderKey: RENDER_SLOT_COLLAPSE_KEY,\n      offset: getSlotChildLineStartPoint(icon),\n      props: {\n        node: transition.entity.parent,\n      },\n    },\n  ];\n};\n\nexport const drawCollapseLine = (transition: FlowNodeTransitionData): FlowTransitionLine[] => {\n  const startDistance = getDefaultSpacing(\n    transition.transform.entity,\n    SlotSpacingKey.SLOT_START_DISTANCE,\n    SLOT_START_DISTANCE\n  );\n\n  return [\n    {\n      type: FlowTransitionLineEnum.STRAIGHT_LINE,\n      from: getSlotChildLineStartPoint(transition.transform),\n      to: Point.move(\n        getSlotChildLineStartPoint(transition.transform),\n        transition.entity.isVertical ? { x: -startDistance, y: 0 } : { x: 0, y: -startDistance }\n      ),\n      style: {\n        strokeDasharray: '5 5',\n      },\n    },\n  ];\n};\n\n/**\n * 画实线上的叫号\n * @param transition\n * @returns\n */\nexport const drawStraightAdder = (transition: FlowNodeTransitionData): FlowTransitionLabel[] => {\n  const toPoint = getTransitionToPoint(transition);\n  const fromPoint = transition.transform.firstChild!.outputPoint;\n\n  const hoverProps = transition.entity.isVertical\n    ? {\n        hoverHeight: toPoint.y - fromPoint.y,\n        hoverWidth: transition.transform.firstChild?.bounds.width,\n      }\n    : {\n        hoverHeight: transition.transform.firstChild?.bounds.height,\n        hoverWidth: toPoint.x - fromPoint.x,\n      };\n\n  return [\n    {\n      offset: Point.getMiddlePoint(fromPoint, toPoint),\n      type: FlowTransitionLabelEnum.ADDER_LABEL,\n      props: hoverProps,\n    },\n  ];\n};\n\n/**\n * 获取端口的子节点线条输入点\n * @param _child\n * @returns\n */\nexport const getPortChildInput = (_child?: FlowNodeTransformData) => {\n  if (!_child) {\n    return { x: 0, y: 0 };\n  }\n\n  const firstChild = getDisplayFirstChildTransform(_child);\n\n  return { x: _child.bounds.left, y: firstChild.bounds.center.y };\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/start.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\n/**\n * 开始节点\n */\nexport const StartRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.START,\n  meta: {\n    isStart: true,\n    draggable: false,\n    selectable: false, // 触发器等开始节点不能被框选\n    deleteDisable: true, // 禁止删除\n    copyDisable: true, // 禁止copy\n    addDisable: true, // 禁止添加\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/static-split.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType, type FlowNodeRegistry, FlowNodeSplitType } from '@flowgram.ai/document';\n\n/**\n * 不能动态添加分支的分支节点\n * staticSplit:  (最原始的 id)\n *  blockIcon\n *  inlineBlocks\n *    block1\n *      blockOrderIcon\n *    block2\n *      blockOrderIcon\n */\nexport const StaticSplitRegistry: FlowNodeRegistry = {\n  extend: FlowNodeSplitType.DYNAMIC_SPLIT,\n  type: FlowNodeSplitType.STATIC_SPLIT,\n  extendChildRegistries: [\n    {\n      type: FlowNodeBaseType.INLINE_BLOCKS,\n      getLabels() {\n        return [];\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/catch-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DEFAULT_SPACING,\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowTransitionLineEnum,\n} from '@flowgram.ai/document';\n\nimport { TryCatchSpacings, TryCatchTypeEnum } from './constants';\n\n/**\n * catch 分支\n */\nexport const CatchBlockRegistry: FlowNodeRegistry = {\n  extend: FlowNodeBaseType.BLOCK,\n  type: TryCatchTypeEnum.CATCH_BLOCK,\n  meta: {\n    hidden: true,\n    spacing: DEFAULT_SPACING.NULL,\n  },\n  getLines(transition) {\n    const { transform } = transition;\n    const { isVertical } = transition.entity;\n    const parentPoint = transform.parent!;\n    const { inputPoint, outputPoint } = transform;\n    let parentInputPoint;\n    if (isVertical) {\n      parentInputPoint = {\n        x: parentPoint.inputPoint.x,\n        y: parentPoint.inputPoint.y - TryCatchSpacings.CATCH_INLINE_SPACING,\n      };\n    } else {\n      parentInputPoint = {\n        x: parentPoint.inputPoint.x - TryCatchSpacings.CATCH_INLINE_SPACING,\n        y: parentPoint.inputPoint.y,\n      };\n    }\n\n    const lines = [\n      {\n        type: FlowTransitionLineEnum.DIVERGE_LINE,\n        from: parentInputPoint,\n        to: inputPoint,\n      },\n    ];\n\n    // 最后一个节点是 end 节点，不绘制 mergeLine\n    if (!transition.isNodeEnd) {\n      let mergePoint;\n      if (isVertical) {\n        mergePoint = {\n          x: parentPoint.outputPoint.x,\n          y: parentPoint.bounds.bottom,\n        };\n      } else {\n        mergePoint = {\n          x: parentPoint.bounds.right,\n          y: parentPoint.outputPoint.y,\n        };\n      }\n\n      lines.push({\n        type: FlowTransitionLineEnum.MERGE_LINE,\n        from: outputPoint,\n        to: mergePoint,\n      });\n    }\n\n    return lines;\n  },\n  getLabels() {\n    return [];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/catch-inline-blocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowRendererKey } from '@flowgram.ai/renderer';\nimport {\n  DEFAULT_SPACING,\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowNodeRenderData,\n  FlowTransitionLabelEnum,\n  FlowTransitionLineEnum,\n  FlowLayoutDefault,\n} from '@flowgram.ai/document';\n\nimport { TryCatchSpacings, TryCatchTypeEnum } from './constants';\n\n/**\n * catch 分支列表\n */\nexport const CatchInlineBlocksRegistry: FlowNodeRegistry = {\n  extend: FlowNodeBaseType.INLINE_BLOCKS,\n  type: TryCatchTypeEnum.CATCH_INLINE_BLOCKS,\n  meta: {\n    spacing: DEFAULT_SPACING.NULL,\n    inlineSpacingPre: DEFAULT_SPACING.NULL,\n    // inlineSpacingAfter: DEFAULT_SPACING.NULL,\n  },\n  getDelta() {\n    return undefined;\n  },\n  getLines(transition) {\n    const { transform } = transition;\n    const mainInlineBlocks = transform.parent!;\n\n    const lines = [\n      {\n        type: FlowTransitionLineEnum.DIVERGE_LINE,\n        from: mainInlineBlocks.pre!.outputPoint,\n        to: transform.inputPoint,\n      },\n    ];\n\n    if (!transform.entity.isNodeEnd) {\n      lines.push({\n        type: FlowTransitionLineEnum.MERGE_LINE,\n        from: transform.outputPoint,\n        to: mainInlineBlocks.outputPoint,\n      });\n    }\n\n    return lines;\n  },\n  getOriginDeltaX(transform) {\n    const { firstChild } = transform;\n    if (!firstChild) return 0;\n    return firstChild.originDeltaX;\n  },\n  getLabels(transition) {\n    const { inputPoint } = transition.transform;\n    const { isVertical } = transition.entity;\n    const currentTransform = transition.transform;\n\n    // 实际输入点\n    const actualInputPoint = {\n      x: isVertical ? inputPoint.x : inputPoint.x - TryCatchSpacings.CATCH_INLINE_SPACING,\n      y: isVertical ? inputPoint.y - TryCatchSpacings.CATCH_INLINE_SPACING : inputPoint.y,\n    };\n\n    if (currentTransform.collapsed) {\n      return [];\n    }\n\n    // branch adder 节点\n    return [\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        renderKey: FlowRendererKey.BRANCH_ADDER,\n        offset: actualInputPoint,\n        props: {\n          // 激活状态\n          activated: transition.entity.getData(FlowNodeRenderData)!.activated,\n          transform: currentTransform,\n          // 传给外部使用的 node 信息\n          node: currentTransform.originParent?.entity,\n        },\n      },\n    ];\n  },\n  getInputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    // 因为是左偏移，所以用第一条 catch 分支\n    const firstCatchBlock = transform.firstChild;\n    if (firstCatchBlock) {\n      return firstCatchBlock.inputPoint;\n    }\n    return isVertical ? transform.bounds.topCenter : transform.bounds.rightCenter;\n  },\n  getOutputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n\n    // 收缩时，出点为入点\n    if (transform.collapsed) {\n      return transform.inputPoint;\n    }\n\n    const firstCatchBlock = transform.firstChild;\n    if (firstCatchBlock) {\n      return isVertical\n        ? {\n            x: firstCatchBlock!.outputPoint?.x,\n            y: transform.bounds.bottom,\n          }\n        : {\n            x: transform.bounds.right,\n            y: firstCatchBlock!.outputPoint?.y,\n          };\n    }\n    return isVertical ? transform.bounds.bottomCenter : transform.bounds.rightCenter;\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * tryCatch 自定义类型\n */\nexport enum TryCatchTypeEnum {\n  MAIN_INLINE_BLOCKS = 'mainInlineBlocks',\n  CATCH_INLINE_BLOCKS = 'catchInlineBlocks',\n  TRY_BLOCK = 'tryBlock',\n  TRY_SLOT = 'trySlot',\n  CATCH_BLOCK = 'catchBlock',\n}\n\nexport enum TryCatchSpacings {\n  INLINE_SPACING_TOP = 54, // 上边空白\n  INLINE_SPACING_BOTTOM = 0, // 下边空白\n  TRY_START_LABEL_DELTA = -20, // try 开始标签偏移\n  TRY_END_LABEL_DELTA = -20, // try 结束标签偏移\n  CATCH_INLINE_SPACING = 20, //  Catch 分支的上边间隙\n  // lint-fix: 这里枚举重复了，但是看语义定义两个 key 是合理的。因此 disabled 处理\n  // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values\n  MAIN_INLINE_SPACING_TOP = 20,\n  MAIN_INLINE_SPACING_BOTTOM = 40, // main 分支下边留白，不然会遮挡 \"监控结束\"标签\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './constants';\nexport * from './catch-block';\nexport * from './catch-inline-blocks';\nexport * from './main-inline-blocks';\nexport * from './try-block';\nexport * from './try-slot';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/main-inline-blocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowRendererKey, FlowTextKey } from '@flowgram.ai/renderer';\nimport {\n  FlowNodeBaseType,\n  type FlowNodeRegistry,\n  FlowNodeTransformData,\n  FlowTransitionLabelEnum,\n  FlowTransitionLineEnum,\n  FlowLayoutDefault,\n  getDefaultSpacing,\n  ConstantKeys,\n} from '@flowgram.ai/document';\n\nimport { TryCatchSpacings, TryCatchTypeEnum } from './constants';\n\n/**\n * 主 BLOCK\n */\nexport const MainInlineBlocksRegistry: FlowNodeRegistry = {\n  extend: FlowNodeBaseType.INLINE_BLOCKS,\n  type: TryCatchTypeEnum.MAIN_INLINE_BLOCKS,\n  meta: {\n    inlineSpacingPre: TryCatchSpacings.MAIN_INLINE_SPACING_TOP,\n    inlineSpacingAfter: TryCatchSpacings.MAIN_INLINE_SPACING_BOTTOM,\n  },\n  getLines(transition) {\n    const { transform } = transition;\n    const tryBranch = transform.firstChild!;\n\n    const lines = [\n      {\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: tryBranch.outputPoint,\n        to: transform.originParent!.outputPoint,\n      },\n    ];\n\n    return lines;\n  },\n  getLabels(transition) {\n    const { transform } = transition;\n    const { isVertical } = transition.entity;\n    const catchInlineBlocks = transform.children[1]!;\n    const errLabelX = isVertical\n      ? (transform.parent!.outputPoint.x + catchInlineBlocks.inputPoint.x) / 2\n      : transform.inputPoint.x - TryCatchSpacings.INLINE_SPACING_TOP;\n    const errLabelY = isVertical\n      ? transform.inputPoint.y - TryCatchSpacings.INLINE_SPACING_TOP\n      : (transform.parent!.outputPoint.y + catchInlineBlocks.inputPoint.y) / 2;\n\n    const errorLabelX = errLabelX;\n    const errorLabelY = errLabelY;\n    return [\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.TRY_START_TEXT,\n        offset: {\n          x: isVertical\n            ? transform.inputPoint.x\n            : transform.inputPoint.x + TryCatchSpacings.TRY_START_LABEL_DELTA,\n          y: isVertical\n            ? transform.inputPoint.y + TryCatchSpacings.TRY_START_LABEL_DELTA\n            : transform.inputPoint.y,\n        },\n      },\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.TRY_END_TEXT,\n        offset: {\n          x: isVertical\n            ? transform.inputPoint.x\n            : transform.originParent!.outputPoint.x + TryCatchSpacings.TRY_END_LABEL_DELTA,\n          y: isVertical\n            ? transform.originParent!.outputPoint.y + TryCatchSpacings.TRY_END_LABEL_DELTA\n            : transform.inputPoint.y,\n        },\n      },\n      // 错误分支收起\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        renderKey: FlowRendererKey.TRY_CATCH_COLLAPSE,\n        offset: {\n          x: errorLabelX,\n          y: errorLabelY,\n        },\n        props: {\n          node: transform.lastChild?.entity,\n        },\n      },\n    ];\n  },\n  getInputPoint(transform) {\n    const tryBlock = transform.firstChild!;\n    return tryBlock.inputPoint;\n  },\n  getOutputPoint(transform, layout) {\n    const tryBlock = transform.firstChild!;\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    if (isVertical) {\n      return {\n        x: tryBlock.outputPoint.x,\n        y: transform.bounds.bottom + TryCatchSpacings.CATCH_INLINE_SPACING,\n      };\n    }\n    return {\n      x: transform.bounds.right + TryCatchSpacings.CATCH_INLINE_SPACING,\n      y: tryBlock.outputPoint.y,\n    };\n  },\n  getDelta() {\n    return undefined;\n  },\n  getChildDelta(child, layout) {\n    const preTransform = child.entity.pre?.getData(FlowNodeTransformData);\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    if (preTransform) {\n      const { localBounds: preBounds } = preTransform;\n      // try 分支和 catch 分支的最小间距不低于 minInlineBlockSpacing\n      let delta = 0;\n      if (isVertical) {\n        delta = Math.max(\n          child.parent!.minInlineBlockSpacing,\n          -child.originDeltaX + getDefaultSpacing(child.entity, ConstantKeys.BRANCH_SPACING)\n        );\n      } else {\n        delta = Math.max(\n          child.parent!.minInlineBlockSpacing,\n          -child.originDeltaY + getDefaultSpacing(child.entity, ConstantKeys.BRANCH_SPACING)\n        );\n      }\n\n      return {\n        // 分支只有两个所以这里可以写死间隔宽度\n        x: isVertical ? preBounds.right + delta : 0,\n        y: isVertical ? 0 : preBounds.bottom + delta,\n      };\n    }\n    return {\n      x: 0,\n      y: 0,\n    };\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/try-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DEFAULT_SPACING, FlowNodeBaseType, type FlowNodeRegistry } from '@flowgram.ai/document';\n\nimport { TryCatchTypeEnum } from './constants';\n\n/**\n * try 分支\n */\nexport const TryBlockRegistry: FlowNodeRegistry = {\n  extend: FlowNodeBaseType.BLOCK,\n  type: TryCatchTypeEnum.TRY_BLOCK,\n  meta: {\n    hidden: true,\n    spacing: DEFAULT_SPACING.NULL,\n  },\n  getLines() {\n    return [];\n  },\n  getLabels() {\n    return [];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch-extends/try-slot.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeRegistry, FlowTransitionLabelEnum } from '@flowgram.ai/document';\n\nimport { TryCatchTypeEnum } from './constants';\n\n/**\n * try 占位节点\n */\nexport const TrySlotRegistry: FlowNodeRegistry = {\n  type: TryCatchTypeEnum.TRY_SLOT,\n  meta: {\n    inlineSpacingAfter: 16,\n    spacing: 0,\n    size: {\n      width: 16,\n      height: 0,\n    },\n  },\n  onAfterUpdateLocalTransform(transform): void {\n    // 根据布局要置换宽高数据\n    if (transform.entity.isVertical) {\n      transform.data.size = {\n        width: 16,\n        height: 0,\n      };\n    } else {\n      transform.data.size = {\n        width: 0,\n        height: 16,\n      };\n    }\n    transform.transform.update({\n      size: transform.data.size,\n    });\n  },\n  getLabels(transition) {\n    return [\n      {\n        offset: transition.transform.bounds.center,\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/activities/try-catch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowLayoutDefault,\n  FlowNodeBaseType,\n  type FlowNodeEntity,\n  type FlowNodeJSON,\n  type FlowNodeRegistry,\n} from '@flowgram.ai/document';\n\nimport {\n  CatchBlockRegistry,\n  CatchInlineBlocksRegistry,\n  MainInlineBlocksRegistry,\n  TryBlockRegistry,\n  TryCatchSpacings,\n  TryCatchTypeEnum,\n  TrySlotRegistry,\n} from './try-catch-extends';\n\n/**\n * try catch 节点\n */\nexport const TryCatchRegistry: FlowNodeRegistry = {\n  type: 'tryCatch',\n  meta: {\n    hidden: true,\n    inlineSpacingAfter: TryCatchSpacings.INLINE_SPACING_BOTTOM,\n  },\n  /**\n   * 结构\n   * tryCatch\n   *    - tryCatchIcon\n   *    - mainInlineBlocks\n   *        - tryBlock // try 分支\n   *          - trySlot // 空节点用来占位，try 分支一开始没有节点\n   *        - catchInlineBlocks\n   *            - catchBlock // catch 分支 1\n   *              - blockOrderIcon\n   *              - node1\n   *            - catchBlock // catch 分支 2\n   *              - blockOrderIcon\n   *              - node 2\n   * @param node\n   * @param json\n   */\n  onCreate(node: FlowNodeEntity, json: FlowNodeJSON) {\n    const { document } = node;\n    const [tryBlock, ...catchBlocks] = json.blocks || [];\n    const addedNodes: FlowNodeEntity[] = [];\n    const tryCatchIconNode = document.addNode({\n      id: `$tryCatchIcon$${node.id}`,\n      type: FlowNodeBaseType.BLOCK_ICON,\n      originParent: node,\n      parent: node,\n    });\n    const mainBlockNode = document.addNode({\n      id: `$mainInlineBlocks$${node.id}`,\n      type: TryCatchTypeEnum.MAIN_INLINE_BLOCKS,\n      originParent: node,\n      parent: node,\n    });\n    const tryBlockNode = document.addNode({\n      id: tryBlock.id,\n      type: tryBlock.type || TryCatchTypeEnum.TRY_BLOCK,\n      originParent: node,\n      parent: mainBlockNode,\n      data: tryBlock.data,\n    });\n    const trySlotNode = document.addNode({\n      id: `$trySlot$${tryBlock.id}`,\n      hidden: true,\n      type: TryCatchTypeEnum.TRY_SLOT, // 占位节点\n      originParent: node,\n      parent: tryBlockNode,\n    });\n    const catchInlineBlocksNode = document.addNode({\n      id: `$catchInlineBlocks$${node.id}`,\n      type: TryCatchTypeEnum.CATCH_INLINE_BLOCKS,\n      originParent: node,\n      parent: mainBlockNode,\n    });\n    addedNodes.push(\n      tryCatchIconNode,\n      mainBlockNode,\n      tryBlockNode,\n      trySlotNode,\n      catchInlineBlocksNode\n    );\n    (tryBlock.blocks || []).forEach((blockData) => {\n      document.addNode(\n        {\n          ...blockData,\n          parent: tryBlockNode,\n        },\n        addedNodes\n      );\n    });\n    catchBlocks.forEach((blockData) => {\n      document.addBlock(node, blockData, addedNodes);\n    });\n    return addedNodes;\n  },\n  /**\n   * 添加 catch 分支\n   * @param node\n   * @param blockData\n   * @param addedNodes\n   */\n  onBlockChildCreate(node, blockData, addedNodes) {\n    const parent = node.document.getNode(`$catchInlineBlocks$${node.id}`);\n    const block = node.document.addNode({\n      id: blockData.id,\n      type: blockData.type || TryCatchTypeEnum.CATCH_BLOCK,\n      originParent: node,\n      parent,\n      data: blockData.data,\n    });\n    // 分支开始节点\n    const blockOrderIcon = node.document.addNode({\n      id: `$blockOrderIcon$${blockData.id}`,\n      type: FlowNodeBaseType.BLOCK_ORDER_ICON,\n      originParent: node,\n      parent: block,\n    });\n    if (blockData.blocks) {\n      node.document.addBlocksAsChildren(block, blockData.blocks || [], addedNodes);\n    }\n    addedNodes?.push(block, blockOrderIcon);\n    return block;\n  },\n  getInputPoint(transform) {\n    const tryCatchIcon = transform.firstChild!;\n    // 使用图标的 inputPoint\n    return tryCatchIcon.inputPoint;\n  },\n  getOutputPoint(transform, layout) {\n    const isVertical = FlowLayoutDefault.isVertical(layout);\n    const tryCatchIcon = transform.firstChild!;\n    if (isVertical) {\n      return {\n        x: tryCatchIcon.inputPoint.x,\n        y: transform.bounds.bottom,\n      };\n    }\n    return {\n      x: transform.bounds.right,\n      y: tryCatchIcon.inputPoint.y,\n    };\n  },\n  /**\n   * tryCatch 子节点配置\n   */\n  extendChildRegistries: [\n    /**\n     * icon 节点\n     */\n    {\n      type: FlowNodeBaseType.BLOCK_ICON,\n      meta: {\n        spacing: TryCatchSpacings.INLINE_SPACING_TOP,\n      },\n      getLabels() {\n        return [];\n      },\n    },\n    MainInlineBlocksRegistry,\n    CatchInlineBlocksRegistry,\n    TryBlockRegistry,\n    CatchBlockRegistry,\n    TrySlotRegistry,\n  ],\n};\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/fixed-layout-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\nimport { FlowRendererContribution } from '@flowgram.ai/renderer';\nimport { FlowDocumentContribution } from '@flowgram.ai/document';\nimport { PlaygroundContribution } from '@flowgram.ai/core';\nimport { bindContributions } from '@flowgram.ai/utils';\n\nimport { FlowRegisters } from './flow-registers';\n\nexport const FixedLayoutContainerModule = new ContainerModule(bind => {\n  bindContributions(bind, FlowRegisters, [\n    FlowDocumentContribution,\n    FlowRendererContribution,\n    PlaygroundContribution,\n  ]);\n});\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/flow-registers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport {\n  FlowLabelsLayer,\n  FlowLinesLayer,\n  FlowNodesContentLayer,\n  FlowNodesTransformLayer,\n  type FlowRendererContribution,\n  type FlowRendererRegistry,\n} from '@flowgram.ai/renderer';\nimport {\n  type FlowDocument,\n  type FlowDocumentContribution,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  FlowNodeTransitionData,\n} from '@flowgram.ai/document';\nimport { type PlaygroundContribution, PlaygroundLayer } from '@flowgram.ai/core';\n\nimport { EndRegistry } from './activities/end';\nimport {\n  BlockIconRegistry,\n  BlockOrderIconRegistry,\n  BlockRegistry,\n  DynamicSplitRegistry,\n  EmptyRegistry,\n  InlineBlocksRegistry,\n  LoopRegistry,\n  RootRegistry,\n  StartRegistry,\n  StaticSplitRegistry,\n  TryCatchRegistry,\n  SimpleSplitRegistry,\n  BreakRegistry,\n  MultiOuputsRegistry,\n  MultiInputsRegistry,\n  InputRegistry,\n  OuputRegistry,\n  SlotRegistry,\n  SlotBlockRegistry,\n} from './activities';\n\n@injectable()\nexport class FlowRegisters\n  implements FlowDocumentContribution, FlowRendererContribution, PlaygroundContribution\n{\n  /**\n   * 注册数据层\n   * @param document\n   */\n  registerDocument(document: FlowDocument) {\n    /**\n     * 注册节点 (ECS - Entity)\n     */\n    document.registerFlowNodes(\n      RootRegistry, // 根节点\n      StartRegistry, // 开始节点\n      DynamicSplitRegistry, // 动态分支（并行、排他）\n      StaticSplitRegistry, // 静态分支（审批）\n      SimpleSplitRegistry, // 简单分支 （不带 orderIcon 节点）\n      BlockRegistry, // 单条 block 注册\n      InlineBlocksRegistry, // 多个 block 组成的 block 列表\n      BlockIconRegistry, // icon 节点，如条件分支的菱形图标\n      BlockOrderIconRegistry, // 带顺序的图标节点，一般为 block 第一个分支节点\n      TryCatchRegistry, // TryCatch\n      EndRegistry, // 结束节点\n      LoopRegistry, // 循环节点\n      EmptyRegistry, // 占位节点\n      BreakRegistry, // 分支断开\n      MultiOuputsRegistry,\n      MultiInputsRegistry,\n      InputRegistry,\n      OuputRegistry,\n      SlotRegistry,\n      SlotBlockRegistry\n    );\n    /**\n     * 注册节点数据 (ECS - Component)\n     */\n    document.registerNodeDatas(\n      FlowNodeRenderData, // 渲染节点相关数据\n      FlowNodeTransitionData, // 线条绘制数据\n      FlowNodeTransformData // 坐标计算数据\n    );\n  }\n\n  /**\n   * 注册渲染层\n   * @param renderer\n   */\n  registerRenderer(renderer: FlowRendererRegistry) {\n    /**\n     * 注册 layer (ECS - System)\n     */\n    renderer.registerLayers(\n      FlowNodesTransformLayer, // 节点位置渲染\n      FlowNodesContentLayer, // 节点内容渲染\n      FlowLinesLayer, // 线条渲染\n      FlowLabelsLayer, // Label 渲染\n      PlaygroundLayer // 画布基础层，提供缩放、手势等能力\n    );\n  }\n\n  /**\n   * 画布开始渲染 (hook)\n   */\n  onReady(): void {}\n\n  /**\n   * 画布销毁 (hook)\n   */\n  onDispose(): void {}\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './fixed-layout-container-module';\nimport { SlotIconRegistry, SlotInlineBlocksRegistry } from './activities/slot/extends';\nimport {\n  BlockIconRegistry,\n  BlockOrderIconRegistry,\n  BlockRegistry,\n  DynamicSplitRegistry,\n  EmptyRegistry,\n  LoopRegistry,\n  StaticSplitRegistry,\n  TryCatchRegistry,\n  StartRegistry,\n  RootRegistry,\n  InlineBlocksRegistry,\n  EndRegistry,\n  SlotRegistry,\n  SlotBlockRegistry,\n} from './activities';\n\nexport const FixedLayoutRegistries = {\n  BlockIconRegistry,\n  BlockOrderIconRegistry,\n  BlockRegistry,\n  DynamicSplitRegistry,\n  EmptyRegistry,\n  LoopRegistry,\n  StaticSplitRegistry,\n  TryCatchRegistry,\n  StartRegistry,\n  RootRegistry,\n  InlineBlocksRegistry,\n  EndRegistry,\n  SlotRegistry,\n  SlotBlockRegistry,\n  SlotIconRegistry,\n  SlotInlineBlocksRegistry,\n};\n\n// Export constant\nexport { SlotSpacingKey } from './activities/slot/constants';\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../config/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/canvas-engine/fixed-layout-core/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/__snapshots__/workflow-lines-manager.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`workflow-lines-manager > test get all node inputs and outputs 1`] = `\n[\n  {\n    \"allInputs\": [],\n    \"allOutput\": [\n      {\n        \"data\": undefined,\n        \"id\": \"end_0\",\n        \"meta\": {\n          \"position\": {\n            \"x\": 800,\n            \"y\": 0,\n          },\n        },\n        \"type\": \"end\",\n      },\n    ],\n  },\n  {\n    \"allInputs\": [\n      {\n        \"data\": undefined,\n        \"id\": \"start_0\",\n        \"meta\": {\n          \"position\": {\n            \"x\": 0,\n            \"y\": 0,\n          },\n        },\n        \"type\": \"start\",\n      },\n    ],\n    \"allOutput\": [],\n  },\n]\n`;\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/hooks/use-current-dom-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { renderHook } from '@testing-library/react-hooks';\n\nimport { createDocument, createHookWrapper } from '../mocks';\nimport { useCurrentDomNode } from '../../src';\n\nit('use-current-dom-node', async () => {\n  const { container } = await createDocument();\n  const wrapper = createHookWrapper(container);\n  const { result } = renderHook(() => useCurrentDomNode(), {\n    wrapper,\n  });\n  expect(result.current.tagName).toEqual('DIV');\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/hooks/use-current-entity.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { renderHook } from '@testing-library/react-hooks';\n\nimport { createDocument, createHookWrapper } from '../mocks';\nimport { useCurrentEntity } from '../../src';\n\nit('use-current-entity', async () => {\n  const { container } = await createDocument();\n  const wrapper = createHookWrapper(container);\n  const { result } = renderHook(() => useCurrentEntity(), {\n    wrapper,\n  });\n  expect(result.current.id).toEqual('start_0');\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/hooks/use-node-render.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { renderHook } from '@testing-library/react-hooks';\nimport { render, screen, fireEvent, createEvent } from '@testing-library/react';\nimport { delay } from '@flowgram.ai/utils';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport { PositionData } from '@flowgram.ai/core';\n\nimport { createDocument, createHookWrapper } from '../mocks';\nimport {\n  useNodeRender,\n  WorkflowDocument,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '../../src';\n\nconst Button = ({ onClick, children }: any) => <button onClick={onClick}>{children}</button>;\n\ndescribe('use-node-render', () => {\n  let container: interfaces.Container;\n  let doc: WorkflowDocument;\n  let wrapper: any;\n  let node: WorkflowNodeEntity;\n  let domNode: HTMLDivElement;\n  beforeEach(async () => {\n    container = (await createDocument()).container;\n    doc = container.get(WorkflowDocument)!;\n    node = doc.getNode('start_0')!;\n    domNode = node.getData(FlowNodeRenderData).node!;\n    wrapper = createHookWrapper(container);\n  });\n  it('select node and listen change', async () => {\n    // 初始化\n    const { result } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result.current.selected).toEqual(false);\n    expect(result.current.activated).toEqual(false);\n    // 选中\n    result.current.selectNode(createEvent('click', domNode) as any);\n    const { result: result2 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result2.current.selected).toEqual(true);\n    expect(result2.current.activated).toEqual(true);\n    // 清除选中\n    container.get<WorkflowSelectService>(WorkflowSelectService).clear();\n    const { result: result3 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result3.current.selected).toEqual(false);\n    expect(result3.current.activated).toEqual(false);\n  });\n  it('toggle select', async () => {\n    const { result } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    result.current.selectNode(new MouseEvent('click', { shiftKey: true }) as any);\n    const { result: result2 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result2.current.selected).toEqual(true);\n    result.current.selectNode(new MouseEvent('click', { shiftKey: true }) as any);\n    const { result: result3 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result3.current.selected).toEqual(false);\n  });\n  it('delete node', async () => {\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    result.current.deleteNode();\n    expect(node.disposed).toEqual(true);\n  });\n  it('start drag', async () => {\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    render(<Button onClick={result.current.startDrag}>Click Me</Button>);\n    // start Drag\n    fireEvent.click(screen.getByText(/click me/i));\n    // start mousemove\n    fireEvent(\n      document,\n      new MouseEvent('mousemove', {\n        bubbles: true,\n        cancelable: true,\n        clientX: 100,\n        clientY: 100,\n      })\n    );\n    const { result: result2 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result2.current.selected).toEqual(true);\n    expect(node.getData(PositionData).toJSON()).toEqual({ x: 100, y: 100 });\n    result.current.selectNode(new MouseEvent('click', { shiftKey: true }) as any);\n    const { result: result3 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    // 拖拽时候无法再次触发选中事件\n    expect(result3.current.selected).toEqual(true);\n    fireEvent(\n      document,\n      new MouseEvent('mouseup', {\n        bubbles: true,\n        cancelable: true,\n        clientX: 100,\n        clientY: 100,\n      })\n    );\n    await delay(10);\n    // 拖拽结束可以取消选中\n    result.current.selectNode(new MouseEvent('click', { shiftKey: true }) as any);\n    expect(result.current.selected).toEqual(false);\n  });\n  it('start drag input', async () => {\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    render(<input role=\"input\" onClick={result.current.startDrag} />);\n    // start Drag\n    fireEvent.click(screen.getByRole('input'));\n    const { result: result2 } = renderHook(() => useNodeRender(), {\n      wrapper,\n    });\n    expect(result2.current.selected).toEqual(true);\n    fireEvent(\n      document,\n      new MouseEvent('mousemove', {\n        bubbles: true,\n        cancelable: true,\n        clientX: 100,\n        clientY: 100,\n      })\n    );\n    // input 无法拖拽\n    expect(node.getData(PositionData).toJSON()).toEqual({ x: 0, y: 0 });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/hooks/use-playground-readonly-state.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { renderHook } from '@testing-library/react-hooks';\nimport { PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport { createDocument, createHookWrapper } from '../mocks';\nimport { usePlaygroundReadonlyState } from '../../src';\n\ndescribe('use-workflow-document', () => {\n  it('base', async () => {\n    const { container } = await createDocument();\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => usePlaygroundReadonlyState(), {\n      wrapper,\n    });\n    expect(result.current).toEqual(false);\n    container.get<PlaygroundConfigEntity>(PlaygroundConfigEntity).readonly = true;\n    // 没有监听不会更新\n    expect(result.current).toEqual(false);\n  });\n  it('listen change', async () => {\n    const { container } = await createDocument();\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => usePlaygroundReadonlyState(true), {\n      wrapper,\n    });\n    expect(result.current).toEqual(false);\n    container.get<PlaygroundConfigEntity>(PlaygroundConfigEntity).readonly = true;\n    expect(result.current).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/hooks/use-workflow-document.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { renderHook } from '@testing-library/react-hooks';\n\nimport { createDocument, createHookWrapper } from '../mocks';\nimport { useWorkflowDocument } from '../../src';\n\ndescribe('use-workflow-document', () => {\n  it('base', async () => {\n    const { container, document } = await createDocument();\n    const wrapper = createHookWrapper(container);\n    const { result } = renderHook(() => useWorkflowDocument(), {\n      wrapper,\n    });\n    expect(result.current).toEqual(document);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/mocks/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { FlowDocumentContainerModule, FlowNodeBaseType } from '@flowgram.ai/document';\nimport {\n  PlaygroundMockTools,\n  PlaygroundReactProvider,\n  PlaygroundEntityContext,\n} from '@flowgram.ai/core';\n\nimport { WorkflowSimpleLineContribution } from '../simple-line';\nimport {\n  WorkflowDocument,\n  WorkflowDocumentContainerModule,\n  WorkflowJSON,\n  WorkflowLinesManager,\n} from '../../src';\n\n/**\n * 创建基本的 Container\n */\nexport function createWorkflowContainer(): interfaces.Container {\n  const container = PlaygroundMockTools.createContainer([\n    FlowDocumentContainerModule,\n    WorkflowDocumentContainerModule,\n  ]);\n  const linesManager = container.get(WorkflowLinesManager);\n  linesManager.registerContribution(WorkflowSimpleLineContribution);\n  linesManager.switchLineType(WorkflowSimpleLineContribution.type);\n  return container;\n}\n\nexport const baseJSON: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: undefined,\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: undefined,\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: undefined,\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n      data: { a: 33 },\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'if',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'else',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n\nexport const nestJSON: WorkflowJSON = {\n  nodes: [\n    ...baseJSON.nodes,\n    {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: { x: 1200, y: 0 },\n      },\n      data: undefined,\n      blocks: [\n        {\n          id: 'break_0',\n          type: 'break',\n          meta: {\n            position: { x: 0, y: 0 },\n          },\n          data: undefined,\n        },\n        {\n          id: 'variable_0',\n          type: 'variable',\n          meta: {\n            position: { x: 400, y: 0 },\n          },\n          data: undefined,\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'break_0',\n          targetNodeID: 'variable_0',\n          data: { a: 33 },\n        },\n      ],\n    },\n  ],\n  edges: [...baseJSON.edges],\n};\n\nexport function createDocument(data: WorkflowJSON = baseJSON) {\n  const container = createWorkflowContainer();\n  const document = container.get<WorkflowDocument>(WorkflowDocument);\n  document.fromJSON(data);\n  return {\n    document,\n    container,\n  };\n}\n\nexport function createHookWrapper(\n  container: interfaces.Container,\n  entityId: string = 'start_0'\n): any {\n  // eslint-disable-next-line react/display-name\n  return ({ children }: any) => (\n    <PlaygroundReactProvider playgroundContainer={container}>\n      <PlaygroundEntityContext.Provider value={container.get(WorkflowDocument).getNode(entityId)}>\n        {children}\n      </PlaygroundEntityContext.Provider>\n    </PlaygroundReactProvider>\n  );\n}\n\nexport function createSubCanvasNodes(document: WorkflowDocument) {\n  document.fromJSON({ nodes: [], edges: [] });\n  const loopNode = document.createWorkflowNode({\n    id: 'loop_0',\n    type: 'loop',\n    meta: {\n      position: { x: -100, y: 0 },\n      subCanvas: () => {\n        const parentNode = document.getNode('loop_0');\n        const canvasNode = document.getNode('subCanvas_0');\n        if (!parentNode || !canvasNode) {\n          return;\n        }\n        return {\n          isCanvas: false,\n          parentNode,\n          canvasNode,\n        };\n      },\n    },\n  });\n  const subCanvasNode = document.createWorkflowNode({\n    id: 'subCanvas_0',\n    type: FlowNodeBaseType.SUB_CANVAS,\n    meta: {\n      isContainer: true,\n      position: { x: 100, y: 0 },\n      subCanvas: () => ({\n        isCanvas: true,\n        parentNode: document.getNode('loop_0')!,\n        canvasNode: document.getNode('subCanvas_0')!,\n      }),\n    },\n  });\n  document.linesManager.createLine({\n    from: loopNode.id,\n    to: subCanvasNode.id,\n  });\n  const variableNode = document.createWorkflowNode(\n    {\n      id: 'variable_0',\n      type: 'variable',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n    },\n    false,\n    subCanvasNode.id\n  );\n  return {\n    loopNode,\n    subCanvasNode,\n    variableNode,\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/service/workflow-drag-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport { fireEvent, waitFor } from '@testing-library/react';\nimport { IPoint } from '@flowgram.ai/utils';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { PlaygroundConfigEntity, PositionData } from '@flowgram.ai/core';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { createWorkflowContainer, baseJSON, nestJSON } from '../mocks';\nimport {\n  WorkflowDragService,\n  WorkflowDocument,\n  WorkflowNodePortsData,\n  WorkflowLineEntity,\n  WorkflowSelectService,\n  WorkflowNodeEntity,\n  WorkflowLinesManager,\n  WorkflowPortEntity,\n} from '../../src';\n\nasync function fireMouseEvent(type: string, point: IPoint): Promise<void> {\n  fireEvent(\n    document,\n    new MouseEvent(type, {\n      bubbles: true,\n      cancelable: true,\n      clientX: point.x,\n      clientY: point.y,\n    })\n  );\n  await waitFor(() => {}, { timeout: 100 });\n}\n\ndescribe('workflow-drag-service', () => {\n  let dragService: WorkflowDragService;\n  let container: interfaces.Container;\n  let document: WorkflowDocument;\n  let startNode: WorkflowNodeEntity;\n  let endNode: WorkflowNodeEntity;\n  let startPorts: WorkflowNodePortsData;\n  let endPorts: WorkflowNodePortsData;\n  let conditionPorts: WorkflowNodePortsData;\n\n  async function drawingLine(\n    point: IPoint,\n    originLine?: WorkflowLineEntity\n  ): Promise<{\n    dragSuccess?: boolean; // 是否拖拽成功，不成功则为选择节点\n    newLine?: WorkflowLineEntity; // 新的线条\n  }> {\n    const promise = dragService.startDrawingLine(\n      startPorts.outputPorts[0]!,\n      { clientX: 0, clientY: 0 },\n      originLine\n    );\n    await fireMouseEvent('mousemove', point);\n    await fireMouseEvent('mouseup', point);\n    return promise;\n  }\n\n  async function drawingLineBetweenNodes(params: {\n    from?: WorkflowNodeEntity;\n    to?: WorkflowNodeEntity;\n    fromPort?: WorkflowPortEntity;\n    toPoint?: IPoint;\n    middlePoints?: IPoint[];\n  }): Promise<{\n    dragSuccess?: boolean; // 是否拖拽成功，不成功则为选择节点\n    newLine?: WorkflowLineEntity; // 新的线条\n  }> {\n    const { from, to, middlePoints } = params;\n    const fromPortsData = from?.ports!;\n    const toPortsData = to?.ports!;\n    const fromPort = fromPortsData?.outputPorts?.[0] ?? params.fromPort;\n    const fromPoint = fromPortsData?.getOutputPoint();\n    const toPoint = toPortsData?.getInputPoint() ?? params.toPoint;\n    if (!fromPort || !toPoint || !fromPoint) {\n      return {\n        dragSuccess: false,\n      };\n    }\n    const promise = dragService.startDrawingLine(fromPortsData.outputPorts[0]!, {\n      clientX: 0,\n      clientY: 0,\n    });\n    const middlePoint: IPoint = {\n      x: (fromPoint.x + toPoint.x) / 2,\n      y: (fromPoint.y + toPoint.y) / 2,\n    };\n    // 开始拖拽\n    await fireMouseEvent('mousemove', fromPoint);\n\n    // 经过中间节点\n    if (middlePoints?.length) {\n      for (const point of middlePoints) {\n        await fireMouseEvent('mousemove', point);\n      }\n    } else {\n      await fireMouseEvent('mousemove', middlePoint);\n    }\n\n    // 结束拖拽\n    await fireMouseEvent('mousemove', toPoint);\n    await fireMouseEvent('mouseup', toPoint);\n    return promise;\n  }\n\n  async function dragNodes(\n    nodes: WorkflowNodeEntity[],\n    point: IPoint,\n    mouseConfig?: any\n  ): Promise<boolean> {\n    container.get(WorkflowSelectService).selection = nodes;\n    const promise = dragService.startDragSelectedNodes({\n      clientX: 0,\n      clientY: 0,\n      ...mouseConfig,\n    });\n    await fireMouseEvent('mousemove', point);\n    await fireMouseEvent('mouseup', point);\n    return promise;\n  }\n\n  beforeEach(async () => {\n    container = createWorkflowContainer();\n    dragService = container.get(WorkflowDragService);\n    document = container.get(WorkflowDocument);\n    await document.fromJSON({\n      nodes: baseJSON.nodes,\n      edges: [],\n    });\n    startNode = document.getNode('start_0')!;\n    endNode = document.getNode('end_0')!;\n    startPorts = startNode.ports!;\n    endPorts = endNode.ports!;\n    conditionPorts = document.getNode('condition_0')!.ports!;\n  });\n  it('startDrawingLine', async () => {\n    // 连接到 end 节点\n    const drawToEnd = await drawingLine(endPorts.getInputPoint());\n    expect(drawToEnd.newLine!.id).toMatch('end_0');\n    expect(document.linesManager.getAllLines().length).toEqual(1);\n    // 连接到已有的线\n    const drawToSame = await drawingLine(endPorts.getInputPoint());\n    expect(drawToSame.newLine).toEqual(drawToEnd.newLine);\n    // 连接到未知节点\n    const drawUnknown = await drawingLine({ x: 9999, y: 9999 });\n    expect(drawUnknown.newLine).toEqual(undefined);\n    // 连接到输出点 (不能连接)\n    // 该 case 下等同于拖拽到节点连线，注释 case\n    // const drawToOutputPoint = await drawingLine(endPorts.getOutputPoint());\n    // expect(drawToOutputPoint.newLine).toEqual(undefined);\n  });\n  it('startDrawingLine when readonly', async () => {\n    container.get(PlaygroundConfigEntity).readonly = true;\n    const drawToEnd = await drawingLine(endPorts.getInputPoint());\n    expect(drawToEnd.dragSuccess).toEqual(false);\n    expect(drawToEnd.newLine).toEqual(undefined);\n  });\n  it('startDrawingLine with originLine', async () => {\n    const onDragLineEndCaller = vi.fn();\n    dragService.onDragLineEnd(async () => {\n      onDragLineEndCaller();\n    });\n    const startToEndLine = (await drawingLine(endPorts.getInputPoint())).newLine!;\n    // 鼠标没有偏移\n    const drawToZero = await drawingLine({ x: 0, y: 0 }, startToEndLine);\n    expect(drawToZero.dragSuccess).toEqual(false);\n    // 连到同一个点\n    const drawToEnd = await drawingLine(endPorts.getInputPoint(), startToEndLine);\n    expect(drawToEnd.dragSuccess).toEqual(true);\n    expect(drawToEnd.newLine).toEqual(undefined);\n    // 连到空白未知时候会把原来线条删除\n    const drawUnknown = await drawingLine({ x: 999, y: 999 }, startToEndLine);\n    expect(drawUnknown.dragSuccess).toEqual(true);\n    expect(drawUnknown.newLine).toEqual(undefined);\n    expect(startToEndLine.disposed).toEqual(true);\n    expect(document.linesManager.getAllLines().length).toEqual(0);\n    // 创建新的线条\n    const newStartToEndLine = (await drawingLine(endPorts.getInputPoint())).newLine!;\n    expect(document.linesManager.getAllLines().length).toEqual(1);\n    expect(newStartToEndLine.id).toEqual(newStartToEndLine.id);\n    expect(startToEndLine !== newStartToEndLine).toEqual(true);\n    // 将线条重连到另外的未知\n    const drawToOther = await drawingLine(conditionPorts.getInputPoint(), newStartToEndLine);\n    expect(drawToOther.dragSuccess).toEqual(true);\n    expect(drawToOther.newLine!.id).toMatch('condition_0');\n    expect(newStartToEndLine.disposed).toEqual(true);\n    expect(document.linesManager.getAllLines().length).toEqual(1);\n    expect(onDragLineEndCaller).toHaveBeenCalledTimes(6);\n  });\n  it('startDrawingLine inside sub canvas', async () => {\n    const linesManager = container.get(WorkflowLinesManager);\n    vi.spyOn(linesManager, 'canAddLine').mockImplementation(\n      (fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, silent?: boolean) => {\n        if (toPort?.node.flowNodeType === FlowNodeBaseType.SUB_CANVAS) {\n          return false;\n        }\n        return true;\n      }\n    );\n    await document.fromJSON({\n      nodes: [\n        {\n          id: 'sub_canvas_0',\n          type: FlowNodeBaseType.SUB_CANVAS,\n          meta: {\n            isContainer: true,\n            position: {\n              x: 0,\n              y: 0,\n            },\n            size: {\n              width: 1000,\n              height: 1000,\n            },\n          },\n          blocks: [\n            {\n              id: 'from_0',\n              type: 'from',\n              meta: {\n                position: {\n                  x: 100,\n                  y: 100,\n                },\n                size: {\n                  width: 50,\n                  height: 50,\n                },\n              },\n            },\n            {\n              id: 'to_0',\n              type: 'to',\n              meta: {\n                position: {\n                  x: 700,\n                  y: 700,\n                },\n                size: {\n                  width: 50,\n                  height: 50,\n                },\n              },\n            },\n          ],\n          edges: [],\n        },\n      ],\n      edges: [],\n    });\n    const fromNodeId = 'from_0';\n    const toNodeId = 'to_0';\n    const fromNode = document.getNode(fromNodeId)!;\n    const toNode = document.getNode(toNodeId)!;\n    expect(linesManager.getAllLines().length).toBe(0);\n    const { dragSuccess, newLine } = await drawingLineBetweenNodes({\n      from: fromNode,\n      to: toNode,\n    });\n    const line = newLine as WorkflowLineEntity;\n    expect(linesManager.getAllLines().length).toBe(1);\n    expect(line.inContainer).toBeTruthy();\n    expect(line.from?.id).toBe(fromNodeId);\n    expect(line.to?.id).toBe(toNodeId);\n    expect(dragSuccess).toBeTruthy();\n    expect(line.id).toBe(`${fromNodeId}_-${toNodeId}_`);\n  });\n  it('resetLine', async () => {\n    const selectService = container.get(WorkflowSelectService);\n    const startToEndLine = (await drawingLine(endPorts.getInputPoint())).newLine!;\n    // 点到同一个位置则选中线条\n    dragService.resetLine(startToEndLine, {\n      clientX: 0,\n      clientY: 0,\n    } as MouseEvent);\n    await fireMouseEvent('mousemove', { x: 0, y: 0 });\n    await fireMouseEvent('mouseup', { x: 0, y: 0 });\n    expect(startToEndLine.isDrawing).toBeFalsy();\n    expect(selectService.selection).toEqual([startToEndLine]);\n    selectService.selection = [];\n    // 点到不同位置\n    dragService.resetLine(startToEndLine, {\n      clientX: conditionPorts.getInputPoint().x,\n      clientY: conditionPorts.getInputPoint().y,\n    } as MouseEvent);\n    await fireMouseEvent('mousemove', { x: 0, y: 0 });\n    await fireMouseEvent('mouseup', { x: 0, y: 0 });\n    // 交互定义，如果本来已经连线，拖拽的目标点不可连线，则重置连线位置。\n    expect(startToEndLine.disposed).toEqual(false);\n    expect(selectService.selection).toEqual([]);\n  });\n  it('canResetLine', async () => {\n    document.options.canResetLine = () => false;\n    const startToEndLine = (await drawingLine(endPorts.getInputPoint())).newLine!;\n    const drawToOther = await drawingLine(conditionPorts.getInputPoint(), startToEndLine);\n    expect(startToEndLine.disposed).toEqual(false);\n    expect(drawToOther.newLine).toEqual(undefined);\n  });\n  it('canDeleteLine', async () => {\n    document.options.canDeleteLine = () => false;\n    const startToEndLine = (await drawingLine(endPorts.getInputPoint())).newLine!;\n    await drawingLine({ x: 999, y: 999 }, startToEndLine);\n    expect(startToEndLine.disposed).toEqual(false);\n    document.options.canDeleteLine = () => true;\n    await drawingLine({ x: 999, y: 999 }, startToEndLine);\n    expect(startToEndLine.disposed).toEqual(true);\n  });\n  it('startDragSelectedNodes empty', async () => {\n    const dragEmpty = await dragNodes([], { x: 0, y: 0 });\n    expect(dragEmpty).toEqual(false);\n  });\n  it('startDragSelectedNodes', async () => {\n    const dragResult = await dragNodes([startNode, endNode], {\n      x: 100,\n      y: 100,\n    });\n    expect(dragResult).toEqual(true);\n    expect(startNode.getData(PositionData).toJSON()).toEqual({\n      x: 100,\n      y: 100,\n    });\n    expect(endNode.getData(PositionData).toJSON()).toEqual({ x: 900, y: 100 });\n  });\n  it('startDragSelectedNodes with same parent', async () => {\n    await document.fromJSON({\n      nodes: nestJSON.nodes,\n      edges: [],\n    });\n    const loopNode = document.getNode('loop_0')!;\n    const breakNode = document.getNode('break_0')!;\n    const variableNode = document.getNode('variable_0')!;\n    const dragResult = await dragNodes([breakNode, variableNode], {\n      x: 100,\n      y: 100,\n    });\n    expect(dragResult).toEqual(true);\n    expect(breakNode.getData(PositionData).toJSON()).toEqual({\n      x: 140,\n      y: 0,\n    });\n    expect(variableNode.getData(PositionData).toJSON()).toEqual({\n      x: 540,\n      y: 0,\n    });\n    expect(loopNode.getData(PositionData).toJSON()).toEqual({\n      x: 1160,\n      y: 100,\n    });\n  });\n  it('startDragSelectedNodes with different parent', async () => {\n    await document.fromJSON({\n      nodes: nestJSON.nodes,\n      edges: [],\n    });\n    const breakNode = document.getNode('break_0')!;\n    const dragResult = await dragNodes([breakNode, startNode], {\n      x: 100,\n      y: 100,\n    });\n    expect(dragResult).toEqual(true);\n    expect(breakNode.getData(PositionData).toJSON()).toEqual({\n      x: 100,\n      y: 100,\n    });\n  });\n  it('startDragCard', async () => {\n    // 需要在 viewport 区域\n    document.playgroundConfig.updateConfig({\n      width: 1000,\n      height: 1000,\n    });\n    const domNode = global.document.createElement('div');\n    const promise = dragService.startDragCard(\n      'mockType',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {}\n    );\n    await fireMouseEvent('mousemove', { x: 100, y: 100 });\n    await fireMouseEvent('mouseup', { x: 100, y: 100 });\n    const result = await promise;\n    expect(result!.flowNodeType).toEqual('mockType');\n    expect(result!.getData(PositionData).toJSON()).toEqual({ x: 100, y: 100 });\n  });\n  it('startDragCard with cloneNode', async () => {\n    // 需要在 viewport 区域\n    document.playgroundConfig.updateConfig({\n      width: 1000,\n      height: 1000,\n    });\n    const domNode = global.document.createElement('div');\n    const promise = dragService.startDragCard(\n      'mockType',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {},\n      (e) => domNode.cloneNode(true) as HTMLDivElement\n    );\n    await fireMouseEvent('mousemove', { x: 100, y: 100 });\n    await fireMouseEvent('mouseup', { x: 100, y: 100 });\n    const result = await promise;\n    expect(result!.flowNodeType).toEqual('mockType');\n    expect(result!.getData(PositionData).toJSON()).toEqual({ x: 100, y: 100 });\n  });\n  it('startDragCard fail', async () => {\n    document.playgroundConfig.updateConfig({\n      width: 1000,\n      height: 1000,\n    });\n    const domNode = global.document.createElement('div');\n    const promise = dragService.startDragCard(\n      'mockType',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {}\n    );\n    await fireMouseEvent('mousemove', { x: -100, y: -100 });\n    await fireMouseEvent('mouseup', { x: -100, y: -100 });\n    const result = await promise;\n    expect(result).toEqual(undefined);\n  });\n  it('dropCard', async () => {\n    await document.fromJSON({\n      nodes: nestJSON.nodes,\n      edges: [],\n    });\n    // 需要在 viewport 区域\n    document.playgroundConfig.updateConfig({\n      width: 1000,\n      height: 1000,\n    });\n    const domNode = global.document.createElement('div');\n    const node = await dragService.dropCard(\n      'loop',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {}\n    );\n    expect(node!.flowNodeType).toEqual('loop');\n    expect(node!.getData(PositionData).toJSON()).toEqual({ x: 0, y: 0 });\n  });\n  it('dropCard to parent node', async () => {\n    await document.fromJSON({\n      nodes: nestJSON.nodes,\n      edges: [],\n    });\n    // 需要在 viewport 区域\n    document.playgroundConfig.updateConfig({\n      width: 1000,\n      height: 1000,\n    });\n    const domNode = global.document.createElement('div');\n    const node = await dragService.dropCard(\n      'break',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {},\n      document.getNode('loop_0')!\n    );\n    expect(node!.flowNodeType).toEqual('break');\n    expect(node!.getData(PositionData).toJSON()).toEqual({ x: -1200, y: 0 });\n  });\n  it('startDragCard and drop to container node', async () => {\n    await document.fromJSON({\n      nodes: [\n        {\n          id: 'loop_0',\n          type: 'loop',\n          meta: {\n            position: { x: 0, y: 500 },\n            size: { width: 100, height: 100 },\n            selectable: () => true,\n          },\n          data: undefined,\n        },\n        {\n          id: 'sub_canvas_0',\n          type: FlowNodeBaseType.SUB_CANVAS,\n          meta: {\n            isContainer: true,\n            position: { x: 0, y: -500 },\n            size: { width: 1000, height: 1000 },\n            selectable: true,\n          },\n          data: undefined,\n        },\n      ],\n      edges: [],\n    });\n    const loopNode = document.getNode('loop_0')!;\n    const subCanvas = document.getNode('sub_canvas_0')!;\n    subCanvas.originParent = loopNode;\n    const subCanvasTrans = subCanvas.getData(TransformData);\n    subCanvasTrans.update({\n      position: { x: 0, y: -500 },\n      size: { width: 1000, height: 1000 },\n    });\n    subCanvasTrans.fireChange();\n    // 需要在 viewport 区域\n    document.playgroundConfig.updateConfig({\n      width: 5000,\n      height: 5000,\n    });\n    const domNode = global.document.createElement('div');\n    const promise = dragService.startDragCard(\n      'variable',\n      { clientX: 0, clientY: 0, currentTarget: domNode } as any,\n      {}\n    );\n    await fireMouseEvent('mousemove', { x: 0, y: 0 });\n    expect((dragService as any)._droppableTransforms.length).toEqual(1);\n    expect((dragService as any)._dropNode?.id).toEqual('sub_canvas_0');\n    await fireMouseEvent('mousemove', { x: -2000, y: 0 });\n    expect((dragService as any)._dropNode?.id).toBeUndefined();\n    await fireMouseEvent('mousemove', { x: 10, y: 10 });\n    await fireMouseEvent('mousemove', { x: 0, y: 0 });\n    await fireMouseEvent('mouseup', { x: 0, y: 0 });\n    const node = (await promise) as WorkflowNodeEntity;\n    expect(node.parent?.id).toEqual('sub_canvas_0');\n    expect(node.flowNodeType).toEqual('variable');\n    expect(node.getData(PositionData).toJSON()).toEqual({ x: 0, y: 0 });\n  });\n  it('dispose', () => {\n    dragService.dispose();\n  });\n  it('adjustSubNodePosition failed', () => {\n    const pos = dragService.adjustSubNodePosition('variable', document.root);\n    expect(pos).toStrictEqual({ x: 0, y: 0 });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/service/workflow-hover-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi } from 'vitest';\nimport { interfaces } from 'inversify';\n\n// import { Playground, PositionData } from '@flowgram.ai/core'\nimport { createWorkflowContainer, baseJSON } from '../mocks';\nimport { WorkflowHoverService, WorkflowDocument } from '../../src';\n\ndescribe('workflow-hover-service', () => {\n  let hoverService: WorkflowHoverService;\n  let container: interfaces.Container;\n  let document: WorkflowDocument;\n  beforeEach(async () => {\n    container = createWorkflowContainer();\n    hoverService = container.get(WorkflowHoverService);\n    document = container.get(WorkflowDocument);\n    await document.fromJSON(baseJSON);\n  });\n  it('base hover', () => {\n    const fn = vi.fn();\n    hoverService.onHoveredChange(fn);\n    expect(hoverService.isSomeHovered()).toEqual(false);\n    expect(hoverService.hoveredKey).toEqual('');\n    expect(hoverService.isHovered('start_0')).toEqual(false);\n    expect(hoverService.hoveredNode).toEqual(undefined);\n    hoverService.updateHoveredKey('start_0');\n    expect(hoverService.isSomeHovered()).toEqual(true);\n    expect(hoverService.hoveredKey).toEqual('start_0');\n    expect(hoverService.isHovered('start_0')).toEqual(true);\n    expect(hoverService.hoveredNode).toEqual(document.getNode('start_0'));\n    expect(fn.mock.calls.length).toEqual(1);\n    // duplicate hover\n    hoverService.updateHoveredKey('start_0');\n    expect(fn.mock.calls.length).toEqual(1);\n    hoverService.clearHovered();\n    expect(hoverService.hoveredKey).toEqual('');\n    expect(hoverService.isHovered('start_0')).toEqual(false);\n    expect(hoverService.hoveredNode).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/service/workflow-select-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi } from 'vitest';\nimport { interfaces } from 'inversify';\nimport { Playground, PositionData } from '@flowgram.ai/core';\n\nimport { createWorkflowContainer, baseJSON } from '../mocks';\nimport { WorkflowSelectService, WorkflowDocument } from '../../src';\ndescribe('workflow-select-service', () => {\n  let selectService: WorkflowSelectService;\n  let container: interfaces.Container;\n  let document: WorkflowDocument;\n  beforeEach(async () => {\n    container = createWorkflowContainer();\n    selectService = container.get(WorkflowSelectService);\n    document = container.get(WorkflowDocument);\n    await document.fromJSON(baseJSON);\n  });\n  it('selectNode and clear', () => {\n    const fn = vi.fn();\n    expect(selectService.selection).toEqual([]);\n    expect(selectService.activatedNode).toEqual(undefined);\n    selectService.onSelectionChanged(fn);\n    const node = document.getNode('start_0')!;\n    selectService.selectNode(node);\n    expect(selectService.selection).toEqual([node]);\n    expect(selectService.selectedNodes).toEqual([node]);\n    expect(selectService.isSelected('start_0')).toEqual(true);\n    expect(selectService.isActivated('start_0')).toEqual(true);\n    expect(selectService.activatedNode).toEqual(node);\n    expect(fn.mock.calls.length).toEqual(1);\n    selectService.clear();\n    expect(selectService.isSelected('start_0')).toEqual(false);\n    expect(selectService.selection).toEqual([]);\n    expect(fn.mock.calls.length).toEqual(2);\n  });\n  it('set selection', () => {\n    const node = document.getNode('start_0')!;\n    selectService.selection = [node];\n    expect(selectService.selection).toEqual([node]);\n  });\n  it('set select', () => {\n    const node = document.getNode('start_0')!;\n    selectService.select(node);\n    expect(selectService.selection).toEqual([node]);\n  });\n  it('toggleSelect', () => {\n    selectService.toggleSelect(document.getNode('start_0')!);\n    expect(selectService.selectedNodes).toEqual([document.getNode('start_0')!]);\n    selectService.toggleSelect(document.getNode('start_0')!);\n    expect(selectService.selectedNodes).toEqual([]);\n  });\n  it('select and focus', () => {\n    const playground = container.get<Playground>(Playground);\n    global.document.body.appendChild(playground.node);\n    const node = document.getNode('start_0')!;\n    selectService.selectNodeAndFocus(node);\n    expect(selectService.selection).toEqual([node]);\n    expect(playground.focused).toEqual(true);\n  });\n  it('selectNodeAndScrollToView', async () => {\n    const node = document.getNode('start_0')!;\n    const playground = container.get<Playground>(Playground);\n    global.document.body.appendChild(playground.node);\n    node.updateData<PositionData>(PositionData, {\n      x: -999,\n      y: -999,\n    });\n    await selectService.selectNodeAndScrollToView(node);\n    expect(playground.config.scrollData.scrollX).toEqual(-999);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/simple-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Point, Rectangle } from '@flowgram.ai/utils';\n\nimport { getLineCenter } from '../src/utils/get-line-center';\nimport { LineCenterPoint, WorkflowLineRenderContribution } from '../src/typings';\nimport { POINT_RADIUS, WorkflowLineEntity } from '../src/entities';\n\nconst LINE_PADDING = 12;\n\nexport interface StraightData {\n  points: IPoint[];\n  path: string;\n  bbox: Rectangle;\n  center: LineCenterPoint;\n}\n\nexport class WorkflowSimpleLineContribution implements WorkflowLineRenderContribution {\n  public static type = 'SimpleLine';\n\n  public entity: WorkflowLineEntity;\n\n  constructor(entity: WorkflowLineEntity) {\n    this.entity = entity;\n  }\n\n  private data?: StraightData;\n\n  public get path(): string {\n    return this.data?.path ?? '';\n  }\n\n  public calcDistance(pos: IPoint): number {\n    if (!this.data) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    const [start, end] = this.data.points;\n    return Point.getDistance(pos, this.projectPointOnLine(pos, start, end));\n  }\n\n  public get bounds(): Rectangle {\n    if (!this.data) {\n      return new Rectangle();\n    }\n    return this.data.bbox;\n  }\n\n  get center() {\n    return this.data!.center;\n  }\n\n  public update(params: { fromPos: IPoint; toPos: IPoint }): void {\n    const { fromPos, toPos } = params;\n    const { vertical } = this.entity;\n\n    // 根据方向预先计算源点和目标点的偏移\n    const sourceOffset = {\n      x: vertical ? 0 : POINT_RADIUS,\n      y: vertical ? POINT_RADIUS : 0,\n    };\n    const targetOffset = {\n      x: vertical ? 0 : -POINT_RADIUS,\n      y: vertical ? -POINT_RADIUS : 0,\n    };\n\n    const points = [\n      {\n        x: fromPos.x + sourceOffset.x,\n        y: fromPos.y + sourceOffset.y,\n      },\n      {\n        x: toPos.x + targetOffset.x,\n        y: toPos.y + targetOffset.y,\n      },\n    ];\n\n    const bbox = Rectangle.createRectangleWithTwoPoints(points[0], points[1]);\n\n    // 调整所有点到 SVG 视口坐标系\n    const adjustedPoints = points.map((p) => ({\n      x: p.x - bbox.x + LINE_PADDING,\n      y: p.y - bbox.y + LINE_PADDING,\n    }));\n\n    // 生成直线路径\n    const path = `M ${adjustedPoints[0].x} ${adjustedPoints[0].y} L ${adjustedPoints[1].x} ${adjustedPoints[1].y}`;\n\n    this.data = {\n      points,\n      path,\n      bbox,\n      center: getLineCenter(fromPos, toPos, bbox, LINE_PADDING),\n    };\n  }\n\n  private projectPointOnLine(point: IPoint, lineStart: IPoint, lineEnd: IPoint): IPoint {\n    const dx = lineEnd.x - lineStart.x;\n    const dy = lineEnd.y - lineStart.y;\n\n    // 如果是垂直线\n    if (dx === 0) {\n      return { x: lineStart.x, y: point.y };\n    }\n    // 如果是水平线\n    if (dy === 0) {\n      return { x: point.x, y: lineStart.y };\n    }\n\n    const t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy);\n    const clampedT = Math.max(0, Math.min(1, t));\n\n    return {\n      x: lineStart.x + clampedT * dx,\n      y: lineStart.y + clampedT * dy,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/utils/location-config-to-point.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { expect } from 'vitest';\nimport { Rectangle } from '@flowgram.ai/utils';\n\nimport { locationConfigToPoint } from '../../src/utils/location-config-to-point';\n\ntest('locationConfigToPoint', () => {\n  const bounds = new Rectangle(10, 10, 100, 100);\n  expect(locationConfigToPoint(bounds, { left: 0, top: 0 })).toEqual(bounds.leftTop);\n  expect(locationConfigToPoint(bounds, { left: 0, bottom: 0 })).toEqual(bounds.leftBottom);\n  expect(locationConfigToPoint(bounds, { right: 0, bottom: 0 })).toEqual(bounds.rightBottom);\n  expect(locationConfigToPoint(bounds, { right: 0, top: 0 })).toEqual(bounds.rightTop);\n  expect(locationConfigToPoint(bounds, { left: 0, top: '0%' })).toEqual(bounds.leftTop);\n  expect(locationConfigToPoint(bounds, { right: 0, bottom: '0%' })).toEqual(bounds.rightBottom);\n  expect(locationConfigToPoint(bounds, { left: 0, top: '50%' })).toEqual(bounds.leftCenter);\n  expect(locationConfigToPoint(bounds, { right: 0, bottom: '50%' })).toEqual(bounds.rightCenter);\n  expect(locationConfigToPoint(bounds, { left: '50%', bottom: 0 })).toEqual(bounds.bottomCenter);\n  expect(locationConfigToPoint(bounds, { right: '50%', top: 0 })).toEqual(bounds.topCenter);\n  expect(locationConfigToPoint(bounds, { left: '50%', top: '50%' })).toEqual(bounds.center);\n  expect(locationConfigToPoint(bounds, { right: '50%', bottom: '50%' })).toEqual(bounds.center);\n  expect(locationConfigToPoint(bounds, { left: 11, top: 11 })).toEqual({ x: 21, y: 21 });\n  expect(locationConfigToPoint(bounds, { right: 11, bottom: 11 })).toEqual({\n    x: 10 + 100 - 11,\n    y: 10 + 100 - 11,\n  });\n  // with offset\n  expect(locationConfigToPoint(bounds, { left: 11, top: 11 }, { x: 100, y: 100 })).toEqual({\n    x: 121,\n    y: 121,\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/workflow-document.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi } from 'vitest';\nimport { interfaces } from 'inversify';\nimport { FlowNodeBaseType, FlowNodeRegistry, FlowNodeTransformData } from '@flowgram.ai/document';\nimport { PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport {\n  delay,\n  WorkflowContentChangeEvent,\n  WorkflowContentChangeType,\n  WorkflowDocument,\n  WorkflowJSON,\n  WorkflowLinesManager,\n  WorkflowNodeJSON,\n  WorkflowSubCanvas,\n} from '../src';\nimport { baseJSON, createSubCanvasNodes, createWorkflowContainer, nestJSON } from './mocks';\n\nlet container: interfaces.Container;\nlet document: WorkflowDocument;\nbeforeEach(() => {\n  container = createWorkflowContainer();\n  document = container.get(WorkflowDocument);\n});\n\ndescribe('workflow-document', () => {\n  it('load', async () => {\n    const fn = vi.fn();\n    document.onLoaded(fn);\n    await document.load();\n    expect(fn.mock.calls.length).toEqual(1);\n  });\n\n  it('base fromJSON and toJSON', () => {\n    document.fromJSON(baseJSON);\n    expect(document.toJSON()).toEqual(baseJSON);\n  });\n\n  it('nested fromJSON and toJSON', () => {\n    document.fromJSON(nestJSON);\n    expect(document.toJSON()).toEqual(nestJSON);\n  });\n\n  it('reload json', async () => {\n    document.fromJSON(baseJSON);\n    const newJSON = {\n      nodes: [\n        {\n          id: 'start_0',\n          type: 'start',\n          data: undefined,\n          meta: {\n            position: { x: 10, y: 10 },\n          },\n        },\n      ],\n      edges: [],\n    };\n    await document.reload(newJSON);\n    expect(document.toJSON()).toEqual(newJSON);\n  });\n\n  it('dispose', () => {\n    document.dispose();\n    expect((document as any).disposed).toEqual(true);\n    document.dispose();\n    expect((document as any).disposed).toEqual(true);\n  });\n\n  it('fitView', async () => {\n    const config = container.get<PlaygroundConfigEntity>(PlaygroundConfigEntity);\n    config.updateConfig({\n      width: 1000,\n      height: 800,\n    });\n    document.addNode({\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: -1000, y: -1000 },\n      },\n    });\n    await document.fitView(false);\n    expect(config.scrollData).toEqual({\n      scrollX: -500,\n      scrollY: -400,\n    });\n  });\n\n  it('getNodeDefaultPosition', () => {\n    const variableNodeType = 'variable';\n    const variableNodeRegister: FlowNodeRegistry = {\n      type: variableNodeType,\n      meta: {\n        position: { x: 10, y: 10 },\n        size: { width: 100, height: 100 },\n      },\n    };\n    document.registerFlowNodes<any>(variableNodeRegister);\n    expect(document.getNodeDefaultPosition(variableNodeType)).toEqual({\n      x: 0,\n      y: -50,\n    });\n  });\n\n  it('createWorkflowNodeByType', () => {\n    const node = document.createWorkflowNodeByType('start', {\n      x: 10,\n      y: 10,\n    });\n    const nodeTransData = node.getData(FlowNodeTransformData);\n    expect(nodeTransData.position).toEqual({ x: 10, y: 10 });\n    expect(node.flowNodeType).toEqual('start');\n  });\n  it('createWorkflowNodeByType with id', () => {\n    const node = document.createWorkflowNodeByType(\n      'start',\n      {\n        x: 10,\n        y: 10,\n      },\n      { id: 'start_0' }\n    );\n    expect(node.id).toEqual('start_0');\n    const nodeTransData = node.getData(FlowNodeTransformData);\n    expect(nodeTransData.position).toEqual({ x: 10, y: 10 });\n    expect(node.flowNodeType).toEqual('start');\n\n    let error: string = '';\n    try {\n      document.createWorkflowNodeByType(\n        'start',\n        {\n          x: 10,\n          y: 10,\n        },\n        { id: 'start_0' }\n      );\n    } catch (e: any) {\n      error = e.message;\n    }\n    expect(error).toMatch('duplicated');\n  });\n\n  it('getAllNodes', () => {\n    document.fromJSON(nestJSON);\n    const allNodeIds = document.getAllNodes().map((n) => n.id);\n    expect(allNodeIds).toEqual([\n      'start_0',\n      'condition_0',\n      'end_0',\n      'loop_0',\n      'break_0',\n      'variable_0',\n    ]);\n  });\n\n  it('getAssociatedNodes', () => {\n    const startNodeRegister: FlowNodeRegistry = {\n      type: 'start',\n      meta: {\n        isStart: true,\n      },\n    };\n    const endNodeRegister: FlowNodeRegistry = {\n      type: 'end',\n      meta: {\n        isNodeEnd: true,\n      },\n    };\n    document.registerFlowNodes<any>(startNodeRegister, endNodeRegister);\n    document.fromJSON({\n      nodes: [\n        ...baseJSON.nodes,\n        {\n          id: 'sun_canvas_0',\n          type: FlowNodeBaseType.SUB_CANVAS,\n          meta: {\n            isContainer: true,\n            position: { x: 10, y: 10 },\n          },\n          blocks: [\n            {\n              id: 'variable_0',\n              type: 'variable',\n              meta: {\n                position: { x: -10, y: 0 },\n              },\n            },\n            {\n              id: 'variable_1',\n              type: 'variable',\n              meta: {\n                position: { x: 10, y: 0 },\n              },\n            },\n          ],\n          edges: [],\n        },\n      ],\n      edges: [],\n    });\n    const endNode = document.getNode('end_0')!;\n    (endNode as any)._metaCache['isNodeEnd'] = true;\n    const associatedNodeIds = document.getAssociatedNodes().map((n) => n.id);\n    expect(associatedNodeIds).toEqual(['start_0', 'end_0', 'variable_0', 'variable_1']);\n  });\n\n  it('fireRender', () => {\n    expect(document.fireRender()).toBeUndefined();\n  });\n\n  it('fireContentChange', () => {\n    document.fromJSON(nestJSON);\n    const loopNode = document.getNode('loop_0')!;\n    const event: WorkflowContentChangeEvent = {\n      type: WorkflowContentChangeType.MOVE_NODE,\n      toJSON: () => document.toNodeJSON(loopNode),\n      entity: loopNode,\n    };\n    const fn = vi.fn();\n    document.onContentChange(fn);\n    document.fireContentChange(event);\n    expect(fn.mock.calls.length).toEqual(1);\n  });\n\n  it('toNodeJSON', () => {\n    const variableJSON = {\n      id: 'variable_0',\n      type: 'variable',\n      meta: { position: { x: 0, y: 0 } },\n    };\n    const variableNode = document.createWorkflowNode(variableJSON);\n    const variableToJSON = document.toNodeJSON(variableNode);\n    expect(variableToJSON).toEqual(variableJSON);\n  });\n\n  it('copyNode', () => {\n    document.fromJSON(nestJSON);\n    const loopNode = document.getNode('loop_0')!;\n    const copyFormat = (json: WorkflowNodeJSON) => ({\n      ...json,\n      meta: { ...json.meta, testFormat: true },\n    });\n    const newLoopNode = document.copyNode(loopNode, 'loop_1', copyFormat, {\n      x: -100,\n      y: -100,\n    });\n    const newLoopTransData = newLoopNode.getData(FlowNodeTransformData);\n    expect(newLoopNode.id).toEqual('loop_1');\n    expect(newLoopNode.flowNodeType).toEqual('loop');\n    expect(newLoopTransData.position).toEqual({ x: -100, y: -100 });\n    expect(newLoopNode.getNodeMeta().testFormat).toEqual(true);\n  });\n\n  it('copyNodeFromJSON', () => {\n    document.fromJSON(nestJSON);\n    const variableNode = document.copyNodeFromJSON(\n      'variable',\n      {\n        id: 'variable_0',\n        type: 'variable',\n        meta: { position: { x: 10, y: 10 } },\n      },\n      'variable_1',\n      { x: -50, y: -50 },\n      'loop_0'\n    );\n    const variableTransData = variableNode.getData(FlowNodeTransformData);\n    expect(variableTransData.position).toEqual({ x: -50, y: -50 });\n    expect(variableNode.id).toEqual('variable_1');\n    expect(variableNode.flowNodeType).toEqual('variable');\n    expect(variableNode.parent?.id).toEqual('loop_0');\n  });\n\n  it('copyNodeFromJSON with default position', () => {\n    document.fromJSON(nestJSON);\n    const variableNode = document.copyNodeFromJSON(\n      'variable',\n      {\n        id: 'variable_0',\n        type: 'variable',\n        meta: { position: { x: 0, y: 0 } },\n      },\n      'variable_1'\n    );\n    const variableTransData = variableNode.getData(FlowNodeTransformData);\n    expect(variableTransData.position).toEqual({ x: 30, y: 30 });\n    expect(variableNode.id).toEqual('variable_1');\n    expect(variableNode.flowNodeType).toEqual('variable');\n    expect(variableNode.parent?.id).toEqual('root');\n  });\n\n  it('canRemove', () => {\n    document.fromJSON(baseJSON);\n    const startNode = document.getNode('end_0')!;\n    (startNode as any)._metaCache['deleteDisable'] = true;\n    expect(document.canRemove(startNode)).toEqual(false);\n  });\n\n  it('fromJSON with empty json parameter', () => {\n    const expectedEmptyJSON: WorkflowJSON = {\n      nodes: [],\n      edges: [],\n    };\n    // no nodes or edges\n    document.fromJSON({});\n    expect(document.toJSON()).toEqual(expectedEmptyJSON);\n    // no edges\n    document.fromJSON({ nodes: [] });\n    expect(document.toJSON()).toEqual(expectedEmptyJSON);\n    // no nodes\n    document.fromJSON({ edges: [] });\n    expect(document.toJSON()).toEqual(expectedEmptyJSON);\n  });\n});\n\ndescribe('workflow-document createWorkflowNode', () => {\n  it('createWorkflowNode basic function', () => {\n    const node = document.createWorkflowNode({\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 10, y: 10 },\n      },\n    });\n    const nodeTransData = node.getData(FlowNodeTransformData);\n    expect(nodeTransData.position).toEqual({ x: 10, y: 10 });\n    expect(node.id).toEqual('start_0');\n    expect(node.flowNodeType).toEqual('start');\n  });\n  it('createWorkflowNode without position', () => {\n    const node = document.createWorkflowNode({\n      id: 'start_0',\n      type: 'start',\n      meta: {},\n    });\n    const nodeTrans = node.getData(FlowNodeTransformData);\n    expect(nodeTrans.position).toEqual({ x: 0, y: -30 });\n    expect(node.id).toEqual('start_0');\n    expect(node.flowNodeType).toEqual('start');\n  });\n  it('createWorkflowNode with form', () => {\n    const variableNodeType = 'variable';\n    const variableNodeRegister: FlowNodeRegistry = {\n      type: variableNodeType,\n      meta: {\n        position: { x: 10, y: 10 },\n      },\n      formMeta: {\n        root: {\n          name: 'root',\n          type: 'object',\n          children: [\n            {\n              name: 'nodeDescription',\n              type: 'form-void',\n              title: '',\n              abilities: [\n                {\n                  type: 'setter',\n                  options: {\n                    key: 'Text',\n                    text: '我是Variable节点',\n                  },\n                },\n              ],\n            },\n          ],\n        },\n      },\n    };\n    document.registerFlowNodes<any>(variableNodeRegister);\n    const node = document.createWorkflowNode({\n      id: 'variable_0',\n      type: variableNodeType,\n      meta: {\n        position: { x: 10, y: 10 },\n      },\n    });\n    const nodeTransData = node.getData(FlowNodeTransformData);\n    expect(nodeTransData.position).toEqual({ x: 10, y: 10 });\n    expect(node.id).toEqual('variable_0');\n    expect(node.flowNodeType).toEqual(variableNodeType);\n  });\n});\n\ndescribe('workflow-document with nestedJSON & subCanvas', () => {\n  it('subCanvas parentNode dispose', () => {\n    const { loopNode, subCanvasNode } = createSubCanvasNodes(document);\n    loopNode.dispose();\n    expect(loopNode.disposed).toEqual(true);\n    expect(subCanvasNode.disposed).toEqual(true);\n  });\n  it('subCanvas canvasNode dispose', () => {\n    const { loopNode, subCanvasNode } = createSubCanvasNodes(document);\n    subCanvasNode.dispose();\n    expect(loopNode.disposed).toEqual(true);\n    expect(subCanvasNode.disposed).toEqual(true);\n  });\n  it('createWorkflowNode with subCanvas', () => {\n    const variableSchema = {\n      id: 'variable_0',\n      type: 'variable',\n      meta: { position: { x: 0, y: 0 } },\n    };\n    const loopSchema = {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: { x: -100, y: 0 },\n        canvasPosition: { x: 100, y: 0 },\n      },\n      blocks: [variableSchema],\n    };\n    const { loopNode, subCanvasNode, variableNode } = createSubCanvasNodes(document);\n    expect(document.toNodeJSON(variableNode)).toEqual(variableSchema);\n    expect(document.toNodeJSON(loopNode)).toEqual(loopSchema);\n    expect(document.toNodeJSON(subCanvasNode)).toEqual(loopSchema);\n    expect(document.toJSON()).toEqual({\n      nodes: [loopSchema],\n      edges: [],\n    });\n  });\n  const subCanvasInlinePortSchema = {\n    nodes: [\n      {\n        id: 'loop_0',\n        type: 'loop',\n        meta: {\n          position: { x: -100, y: 0 },\n          canvasPosition: { x: 100, y: 0 },\n        },\n        blocks: [\n          {\n            id: 'variable_0',\n            type: 'variable',\n            meta: { position: { x: 0, y: 0 } },\n          },\n          {\n            id: 'variable_1',\n            type: 'variable',\n            meta: { position: { x: 0, y: 0 } },\n          },\n        ],\n        edges: [\n          { sourceNodeID: 'loop_0', targetNodeID: 'variable_0' },\n          { sourceNodeID: 'variable_0', targetNodeID: 'variable_1' },\n          { sourceNodeID: 'variable_0', targetNodeID: 'loop_0' },\n          { sourceNodeID: 'loop_0', targetNodeID: 'variable_1' },\n          { sourceNodeID: 'variable_1', targetNodeID: 'loop_0' },\n        ],\n      },\n    ],\n    edges: [],\n  };\n  it('toJSON with subCanvas inline port', () => {\n    const linesManager = container.get(WorkflowLinesManager);\n    const { subCanvasNode, variableNode: variableANode } = createSubCanvasNodes(document);\n    const variableBNode = document.createWorkflowNode(\n      {\n        id: 'variable_1',\n        type: 'variable',\n        meta: {\n          position: { x: 0, y: 0 },\n        },\n      },\n      false,\n      subCanvasNode.id\n    );\n    linesManager.createLine({\n      from: subCanvasNode.id,\n      to: variableANode.id,\n    });\n    linesManager.createLine({\n      from: subCanvasNode.id,\n      to: variableBNode.id,\n    });\n    linesManager.createLine({\n      from: variableANode.id,\n      to: variableBNode.id,\n    });\n    linesManager.createLine({\n      from: variableANode.id,\n      to: subCanvasNode.id,\n    });\n    linesManager.createLine({\n      from: variableBNode.id,\n      to: subCanvasNode.id,\n    });\n    const json = document.toJSON();\n    expect(json).toEqual(subCanvasInlinePortSchema);\n  });\n  it('fromJSON with subCanvas inline port', async () => {\n    const createCall = vi.fn();\n    const createCanvas = () => {\n      createCall();\n      document.createWorkflowNode({\n        id: 'subCanvas_0',\n        type: FlowNodeBaseType.SUB_CANVAS,\n        meta: {\n          isContainer: true,\n          position: { x: 100, y: 0 },\n          subCanvas: () => ({\n            isCanvas: true,\n            parentNode: document.getNode('loop_0')!,\n            canvasNode: document.getNode('subCanvas_0')!,\n          }),\n        },\n      });\n    };\n    const loopNodeRegister: FlowNodeRegistry = {\n      type: 'loop',\n      meta: {\n        subCanvas: () => {\n          const parentNode = document.getNode('loop_0');\n          const canvasNode = document.getNode('subCanvas_0');\n          if (!parentNode || !canvasNode) {\n            return;\n          }\n          return {\n            isCanvas: false,\n            parentNode,\n            canvasNode,\n          };\n        },\n      },\n      onCreate: (node, json) => {\n        createCanvas();\n      },\n    };\n    document.registerFlowNodes<any>(loopNodeRegister);\n    document.fromJSON(subCanvasInlinePortSchema);\n    await delay(10);\n    expect(createCall).toHaveBeenCalledTimes(1);\n    const loopNode = document.getNode('loop_0')!;\n    const subCanvas: WorkflowSubCanvas = loopNode?.getNodeMeta().subCanvas(loopNode);\n    expect(subCanvas).toBeDefined();\n    const canvasNode = subCanvas.canvasNode;\n    expect(canvasNode.id).toEqual('subCanvas_0');\n    expect(canvasNode.collapsedChildren.length).toEqual(2);\n    expect(document.toJSON()).toEqual(subCanvasInlinePortSchema);\n  });\n  it('document is disposed and call toJSON should throw error', () => {\n    document.dispose();\n    expect(() => document.toJSON()).toThrowError(/disposed/);\n  });\n  it('lineData change trigger onContentChange', () => {\n    document.fromJSON(baseJSON);\n    let contentChangeEvent: WorkflowContentChangeEvent;\n    document.onContentChange((e) => {\n      contentChangeEvent = e;\n    });\n    const line = document.linesManager.getLine({\n      from: 'start_0',\n      to: 'condition_0',\n    })!;\n    line.lineData = { b: 33 };\n    expect(document.toJSON().edges[0].data).toEqual({ b: 33 });\n    expect(contentChangeEvent!.type).toEqual(WorkflowContentChangeType.LINE_DATA_CHANGE);\n    expect(contentChangeEvent!.toJSON()).toEqual({\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n      data: { b: 33 },\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/__tests__/workflow-lines-manager.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\n\nimport {\n  WorkflowLinesManager,\n  WorkflowDocument,\n  WorkflowDocumentOptions,\n  WorkflowLineRenderData,\n  LineColors,\n} from '../src';\nimport { WorkflowSimpleLineContribution } from './simple-line';\nimport { createWorkflowContainer } from './mocks';\ndescribe('workflow-lines-manager', () => {\n  let linesManager: WorkflowLinesManager;\n  let container: interfaces.Container;\n  let document: WorkflowDocument;\n  beforeEach(() => {\n    container = createWorkflowContainer();\n    document = container.get(WorkflowDocument);\n    linesManager = container.get(WorkflowLinesManager);\n    linesManager.init(document);\n    document.createWorkflowNode({\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n    });\n    document.createWorkflowNode({\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n    });\n  });\n  it('base create and dispose', async () => {\n    expect(linesManager.toJSON()).toEqual([]);\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    })!;\n    const startNode = document.getNode('start_0')!.lines;\n    const endNode = document.getNode('end_0')!.lines;\n    expect(startNode.outputLines.length).toEqual(1);\n    expect(startNode.allLines.length).toEqual(1);\n    expect(endNode.inputLines.length).toEqual(1);\n    expect(endNode.allLines.length).toEqual(1);\n    expect(startNode.allOutputNodes.length).toEqual(1);\n    expect(endNode.allInputNodes.length).toEqual(1);\n    expect(line.id).toBe('start_0_-end_0_');\n    expect(linesManager.toJSON()).toEqual([{ sourceNodeID: 'start_0', targetNodeID: 'end_0' }]);\n    // line destroy\n    line.dispose();\n    expect(startNode.outputLines.length).toEqual(0);\n    expect(startNode.allLines.length).toEqual(0);\n    expect(endNode.inputLines.length).toEqual(0);\n    expect(endNode.allLines.length).toEqual(0);\n    expect(startNode.allOutputNodes.length).toEqual(0);\n    expect(endNode.allInputNodes.length).toEqual(0);\n    linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    })!;\n    expect(startNode.outputLines.length).toEqual(1);\n    // node destroy\n    endNode.entity.dispose();\n    expect(startNode.outputLines.length).toEqual(0);\n    expect(startNode.allLines.length).toEqual(0);\n  });\n  it('base create drawing line or hidden line', async () => {\n    const line = linesManager.createLine({\n      from: 'start_0',\n      drawingTo: { x: 0, y: 0, location: 'right' },\n    })!;\n    const startNode = document.getNode('start_0')!.lines;\n    expect(startNode.outputLines.length).toEqual(1);\n    expect(startNode.availableLines.length).toEqual(0);\n    line.dispose();\n    expect(startNode.outputLines.length).toEqual(0);\n    expect(startNode.availableLines.length).toEqual(0);\n    const line2 = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    })!;\n    line2.updateUIState({\n      highlightColor: line2.linesManager.lineColor.hidden,\n    });\n    expect(startNode.outputLines.length).toEqual(1);\n    expect(startNode.availableLines.length).toEqual(0);\n    line2.updateUIState({\n      highlightColor: '',\n    });\n    expect(startNode.outputLines.length).toEqual(1);\n    expect(startNode.availableLines.length).toEqual(1);\n  });\n\n  it('test base create line node', async () => {\n    expect(linesManager.toJSON()).toEqual([]);\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    })!;\n    const lineNode = line.node;\n    expect(lineNode.dataset.testid).toBe('sdk.workflow.canvas.line');\n    expect(lineNode.dataset.lineId).toBe('start_0_-end_0_');\n    expect(lineNode.dataset.fromNodeId).toBe('start_0');\n    expect(lineNode.dataset.fromPortId).toBe('port_output_start_0_');\n    expect(lineNode.dataset.toNodeId).toBe('end_0');\n    expect(lineNode.dataset.toPortId).toBe('port_input_end_0_');\n    expect(lineNode.dataset.hasError).toBe('false');\n  });\n\n  it('test base create line bezier', async () => {\n    expect(linesManager.toJSON()).toEqual([]);\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    })!;\n    const lineRenderData = line.getData(WorkflowLineRenderData);\n    expect(lineRenderData.position.from).toEqual({ x: 0, y: 0, location: 'right' });\n    expect(lineRenderData.position.to).toEqual({ x: 660, y: 30, location: 'left' });\n    expect(lineRenderData.path).toEqual('M 12 12 L 652 42');\n  });\n\n  it('test get all node inputs and outputs', async () => {\n    linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    });\n\n    const allNodeLineData = document.getAllNodes().map((_node) => _node.lines);\n\n    expect(\n      allNodeLineData.map((_line) => ({\n        allInputs: _line.allInputNodes,\n        allOutput: _line.allOutputNodes,\n      }))\n    ).toMatchSnapshot();\n  });\n  it('create without to node', () => {\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: '',\n      drawingTo: { x: 0, y: 0, location: 'left' },\n    });\n    expect(line!.isDrawing).toEqual(true);\n    expect(linesManager.toJSON()).toEqual([]);\n  });\n  it('create without from node', () => {\n    const line = linesManager.createLine({\n      from: '',\n      to: 'end_0',\n      drawingFrom: { x: 0, y: 0, location: 'right' },\n    });\n    expect(line!.isDrawing).toEqual(true);\n    expect(linesManager.toJSON()).toEqual([]);\n  });\n  it('create without from node and to node', () => {\n    const line = linesManager.createLine({\n      from: '',\n      to: '',\n    });\n    expect(line).toBeUndefined();\n    expect(linesManager.toJSON()).toEqual([]);\n  });\n\n  it('test document line options', () => {\n    const documentOptions = container.get<WorkflowDocumentOptions>(WorkflowDocumentOptions);\n    documentOptions.isErrorLine = () => true;\n    documentOptions.isReverseLine = () => true;\n    documentOptions.isHideArrowLine = () => true;\n    documentOptions.isFlowingLine = () => true;\n    documentOptions.isDisabledLine = () => true;\n    documentOptions.setLineClassName = () => 'custom-line-class';\n    documentOptions.setLineRenderType = () => WorkflowSimpleLineContribution.type;\n    documentOptions.lineColor = {\n      default: '#000',\n      error: '#000',\n    };\n\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    });\n\n    line?.fireRender();\n\n    expect(line?.reverse).toBeTruthy();\n    expect(line?.hideArrow).toBeTruthy();\n    expect(line?.flowing).toBeTruthy();\n    expect(line?.disabled).toBeTruthy();\n    expect(line?.hasError).toBeTruthy();\n    expect(line?.renderType).toBe(WorkflowSimpleLineContribution.type);\n    expect(line?.className).toBe('custom-line-class');\n    expect(line?.color).toBe('#000');\n  });\n\n  it('test set line state', () => {\n    const line = linesManager.createLine({\n      from: 'start_0',\n      to: 'end_0',\n    });\n\n    if (!line) {\n      expect.fail('line is not created');\n    }\n\n    expect(line.reverse).toBeFalsy();\n    line.processing = true;\n    expect(line.processing).toBeTruthy();\n\n    expect(line.hasError).toBeFalsy();\n    line.hasError = true;\n    line.fireRender();\n    expect(line.hasError).toBeTruthy();\n\n    try {\n      line.setToPort(line.toPort);\n      // 如果没有抛出错误，测试应该失败\n      expect.fail('Expected an error to be thrown');\n    } catch (e) {\n      expect((e as Error).message).toBe('[setToPort] only support drawing line.');\n    }\n  });\n\n  describe('flowing line support', () => {\n    it('should return flowing color when line is flowing', () => {\n      const documentOptions: WorkflowDocumentOptions = {\n        lineColor: {\n          flowing: '#ff0000', // 自定义流动颜色\n        },\n        isFlowingLine: () => true,\n      };\n\n      Object.assign(linesManager, { options: documentOptions });\n\n      const line = linesManager.createLine({\n        from: 'start_0',\n        to: 'end_0',\n      });\n\n      expect(line).toBeDefined();\n      expect(linesManager.isFlowingLine(line!)).toBe(true);\n      expect(linesManager.getLineColor(line!)).toBe('#ff0000');\n    });\n\n    it('should use default flowing color when no custom color provided', () => {\n      const documentOptions: WorkflowDocumentOptions = {\n        isFlowingLine: () => true,\n      };\n\n      Object.assign(linesManager, { options: documentOptions });\n\n      const line = linesManager.createLine({\n        from: 'start_0',\n        to: 'end_0',\n      });\n\n      expect(line).toBeDefined();\n      expect(linesManager.isFlowingLine(line!)).toBe(true);\n      expect(linesManager.getLineColor(line!)).toBe(LineColors.FLOWING);\n    });\n\n    it('should prioritize selected/hovered over flowing', () => {\n      const documentOptions: WorkflowDocumentOptions = {\n        lineColor: {\n          flowing: '#ff0000',\n          selected: '#00ff00',\n        },\n        isFlowingLine: () => true,\n      };\n\n      Object.assign(linesManager, { options: documentOptions });\n\n      const line = linesManager.createLine({\n        from: 'start_0',\n        to: 'end_0',\n      });\n\n      // 模拟选中状态\n      linesManager.selectService.select(line!);\n\n      expect(line).toBeDefined();\n      expect(linesManager.isFlowingLine(line!)).toBe(true);\n      // 选中状态应该优先于流动状态\n      expect(linesManager.getLineColor(line!)).toBe('#00ff00');\n    });\n    it('line data change', () => {\n      const line = linesManager.createLine({\n        from: 'start_0',\n        to: 'end_0',\n        data: { a: 1 },\n      })!;\n      expect(line.toJSON()).toEqual({\n        sourceNodeID: 'start_0',\n        targetNodeID: 'end_0',\n        data: { a: 1 },\n      });\n      line.lineData = { a: 2 };\n      expect(line.toJSON()).toEqual({\n        sourceNodeID: 'start_0',\n        targetNodeID: 'end_0',\n        data: { a: 2 },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-layout-core\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/esm/index.js\",\n      \"require\": \"./dist/index.js\"\n    },\n    \"./typings\": {\n      \"types\": \"./dist/typings/index.d.ts\",\n      \"import\": \"./dist/esm/typings/index.js\",\n      \"require\": \"./dist/typings/index.js\"\n    }\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"typesVersions\": {\n    \"*\": {\n      \"typings\": [\n        \"./dist/typings/index.d.ts\"\n      ]\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts src/typings --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/node\": \"workspace:*\",\n    \"@flowgram.ai/reactive\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum EditorCursorState {\n  GRAB = 'GRAB',\n  SELECT = 'SELECT',\n}\n\nexport enum InteractiveType {\n  /** 鼠标优先交互模式 */\n  MOUSE = 'MOUSE',\n\n  /** 触控板优先交互模式 */\n  PAD = 'PAD',\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entities/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './workflow-node-entity';\nexport * from './workflow-line-entity';\nexport * from './workflow-port-entity';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entities/workflow-line-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isEqual } from 'lodash-es';\nimport { domUtils, type IPoint, Rectangle, Emitter } from '@flowgram.ai/utils';\nimport { Entity, type EntityOpts } from '@flowgram.ai/core';\n\nimport { type WorkflowLinesManager } from '../workflow-lines-manager';\nimport { type WorkflowDocument } from '../workflow-document';\nimport { WORKFLOW_LINE_ENTITY } from '../utils/statics';\nimport {\n  LineRenderType,\n  type LinePosition,\n  LinePoint,\n  LineCenterPoint,\n} from '../typings/workflow-line';\nimport { type WorkflowEdgeJSON } from '../typings';\nimport { WorkflowLineRenderData } from '../entity-datas';\nimport { type WorkflowPortEntity } from './workflow-port-entity';\nimport { type WorkflowNodeEntity } from './workflow-node-entity';\n\nexport const LINE_HOVER_DISTANCE = 8; // 线条 hover 的最小检测距离\nexport const POINT_RADIUS = 10;\n\nexport interface WorkflowLinePortInfo {\n  from?: string; // 前置节点 id\n  to?: string; // 后置节点 id\n  fromPort?: string | number; // 连线的 port 位置\n  toPort?: string | number; // 连线的 port 位置\n  data?: any;\n}\n\nexport interface WorkflowLineEntityOpts extends EntityOpts, WorkflowLinePortInfo {\n  document: WorkflowDocument;\n  linesManager: WorkflowLinesManager;\n  drawingTo?: LinePoint;\n  drawingFrom?: LinePoint;\n}\n\nexport interface WorkflowLineInfo extends WorkflowLinePortInfo {\n  drawingTo?: LinePoint; // 正在画中的元素\n  drawingFrom?: LinePoint;\n}\n\nexport interface WorkflowLineUIState {\n  /**\n   * 是否出错\n   */\n  hasError: boolean;\n  /**\n   * 流动\n   */\n  flowing: boolean;\n  /**\n   * 禁用\n   */\n  disabled: boolean;\n  /**\n   * 箭头反转\n   */\n  reverse: boolean;\n  /**\n   * 隐藏箭头\n   */\n  hideArrow: boolean;\n  /**\n   * 线条宽度\n   * @default 2\n   */\n  strokeWidth?: number;\n  /**\n   * 选中后的线条宽度\n   * @default 3\n   */\n  strokeWidthSelected?: number;\n  /**\n   * 收缩\n   * @default 10\n   */\n  shrink: number;\n  /**\n   * @deprecated use `lockedColor` instead\n   */\n  highlightColor: string;\n  /**\n   * 曲率\n   * only for Bezier,\n   * @default 0.25\n   */\n  curvature: number;\n  /**\n   * Line locked color\n   */\n  lockedColor: string;\n  /**\n   * React className\n   */\n  className?: string;\n  /**\n   * React style\n   */\n  style?: React.CSSProperties;\n}\n\n/**\n * 线条\n */\nexport class WorkflowLineEntity extends Entity<WorkflowLineEntityOpts> {\n  static type = WORKFLOW_LINE_ENTITY;\n\n  /**\n   * 转成线条 id\n   * @param info\n   */\n  static portInfoToLineId(info: WorkflowLinePortInfo): string {\n    const { from, to, fromPort, toPort } = info;\n    return `${from}_${fromPort || ''}-${to || ''}_${toPort || ''}`;\n  }\n\n  private _onLineDataChangeEmitter = new Emitter<{ oldValue: any; newValue: any }>();\n\n  readonly document: WorkflowDocument;\n\n  readonly linesManager: WorkflowLinesManager;\n\n  readonly onLineDataChange = this._onLineDataChangeEmitter.event;\n\n  private _from?: WorkflowNodeEntity;\n\n  private _to?: WorkflowNodeEntity;\n\n  private _lineData: any;\n\n  private _uiState: WorkflowLineUIState = {\n    hasError: false,\n    flowing: false,\n    disabled: false,\n    hideArrow: false,\n    reverse: false,\n    shrink: 10,\n    curvature: 0.25,\n    highlightColor: '',\n    lockedColor: '',\n  };\n\n  /**\n   * 线条的 UI 状态\n   */\n  get uiState(): WorkflowLineUIState {\n    return this._uiState;\n  }\n\n  /**\n   * 更新线条的 ui 状态\n   * @param newState\n   */\n  updateUIState(newState: Partial<WorkflowLineUIState>): void {\n    let changed = false;\n    Object.keys(newState).forEach((key: string) => {\n      const value: any = newState[key as keyof WorkflowLineUIState] as any;\n      if (this._uiState[key as keyof WorkflowLineUIState] !== value) {\n        (this._uiState as any)[key as keyof WorkflowLineUIState] = value;\n        changed = true;\n      }\n    });\n    if (changed) {\n      this.fireChange();\n    }\n  }\n\n  /**\n   * 线条的扩展数据\n   */\n  get lineData(): any {\n    return this._lineData;\n  }\n\n  /**\n   * 更新线条扩展数据\n   * @param data\n   */\n  set lineData(newValue: any) {\n    const oldValue = this._lineData;\n    if (!isEqual(oldValue, newValue)) {\n      this._lineData = newValue;\n      this._onLineDataChangeEmitter.fire({ oldValue, newValue });\n      this.fireChange();\n    }\n  }\n\n  public stackIndex = 0;\n\n  /**\n   * 线条数据\n   */\n  info: WorkflowLineInfo = {\n    from: '',\n  };\n\n  readonly isDrawing: boolean;\n\n  /**\n   * 线条 Portal 挂载的 div\n   */\n  private _node?: HTMLDivElement;\n\n  constructor(opts: WorkflowLineEntityOpts) {\n    super(opts);\n    this.document = opts.document;\n    this.linesManager = opts.linesManager;\n    // 初始化\n    this.initInfo({\n      from: opts.from,\n      to: opts.to,\n      drawingTo: opts.drawingTo,\n      fromPort: opts.fromPort,\n      drawingFrom: opts.drawingFrom,\n      toPort: opts.toPort,\n      data: opts.data,\n    });\n    if (opts.drawingTo || opts.drawingFrom) {\n      this.isDrawing = true;\n    }\n    this.onEntityChange(() => {\n      this.fromPort?.validate();\n      this.toPort?.validate();\n    });\n    this.onDispose(() => {\n      this.fromPort?.validate();\n      this.toPort?.validate();\n    });\n    this.toDispose.push(this._onLineDataChangeEmitter);\n    // this.onDispose(() => {\n    // this._infoDispose.dispose();\n    // });\n  }\n\n  /**\n   * 获取线条的前置节点\n   */\n  get from(): WorkflowNodeEntity | undefined {\n    return this._from;\n  }\n\n  /**\n   * 获取线条的后置节点\n   */\n  get to(): WorkflowNodeEntity | undefined {\n    return this._to;\n  }\n\n  get isHidden(): boolean {\n    return this.highlightColor === this.linesManager.lineColor.hidden;\n  }\n\n  get inContainer(): boolean {\n    const nodeInContainer = (node?: WorkflowNodeEntity) =>\n      !!node?.parent && node.parent.flowNodeType !== 'root';\n    return nodeInContainer(this.from) || nodeInContainer(this.to);\n  }\n\n  /**\n   * 获取是否 testrun processing\n   * @deprecated  use `flowing` instead\n   */\n  get processing(): boolean {\n    return this._uiState.flowing;\n  }\n\n  /**\n   * 设置 testrun processing 状态\n   * @deprecated  use `flowing` instead\n   */\n  set processing(status: boolean) {\n    this.flowing = status;\n  }\n\n  // 获取连线是否为错误态\n  get hasError() {\n    return this.uiState.hasError;\n  }\n\n  // 设置连线的错误态\n  set hasError(hasError: boolean) {\n    this.updateUIState({\n      hasError,\n    });\n    if (this._node) {\n      this._node.dataset.hasError = this.hasError ? 'true' : 'false';\n    }\n  }\n\n  /**\n   * 设置线条的后置节点\n   */\n  setToPort(toPort?: WorkflowPortEntity) {\n    // 只有绘制中的线条才允许设置 port, 主要用于吸附到点\n    if (!this.isDrawing) {\n      throw new Error('[setToPort] only support drawing line.');\n    }\n    if (this.toPort === toPort) {\n      return;\n    }\n    const prePort = this.toPort;\n    if (\n      toPort &&\n      toPort.portType === 'input' &&\n      this.linesManager.canAddLine(this.fromPort!, toPort, true)\n    ) {\n      const { node, portID } = toPort;\n      this._to = node;\n      this.info.drawingTo = undefined;\n      this.info.to = node.id;\n      this.info.toPort = portID;\n    } else {\n      this._to = undefined;\n      this.info.to = undefined;\n      this.info.toPort = '';\n    }\n    /**\n     * 移动到端口又快速移出，需要更新 prePort 的状态\n     */\n    if (prePort) {\n      prePort.validate();\n    }\n    this.fireChange();\n  }\n\n  setFromPort(fromPort?: WorkflowPortEntity) {\n    // 只有绘制中的线条才允许设置 port, 主要用于吸附到点\n    if (!this.isDrawing) {\n      throw new Error('[setFromPort] only support drawing line.');\n    }\n    if (this.fromPort === fromPort) {\n      return;\n    }\n    const prePort = this.fromPort;\n    if (\n      fromPort &&\n      fromPort.portType === 'output' &&\n      this.linesManager.canAddLine(fromPort, this.toPort!, true)\n    ) {\n      const { node, portID } = fromPort;\n      this._from = node;\n      this.info.drawingFrom = undefined;\n      this.info.from = node.id;\n      this.info.fromPort = portID;\n    } else {\n      this._from = undefined;\n      this.info.from = undefined;\n      this.info.fromPort = '';\n    }\n    /**\n     * 移动到端口又快速移出，需要更新 prePort 的状态\n     */\n    if (prePort) {\n      prePort.validate();\n    }\n    this.fireChange();\n  }\n\n  /**\n   * 设置线条画线时的目标位置\n   */\n  set drawingTo(pos: LinePoint | undefined) {\n    const oldDrawingTo = this.info.drawingTo;\n    if (!pos) {\n      this.info.drawingTo = undefined;\n      this.fireChange();\n      return;\n    }\n    if (!oldDrawingTo || pos.x !== oldDrawingTo.x || pos.y !== oldDrawingTo.y) {\n      this.info.to = undefined;\n      this.info.drawingTo = pos;\n      this.fireChange();\n    }\n  }\n\n  set drawingFrom(pos: LinePoint | undefined) {\n    const oldDrawingFrom = this.info.drawingFrom;\n    if (!pos) {\n      this.info.drawingFrom = undefined;\n      this.fireChange();\n      return;\n    }\n    if (!oldDrawingFrom || pos.x !== oldDrawingFrom.x || pos.y !== oldDrawingFrom.y) {\n      this.info.from = undefined;\n      this.info.drawingFrom = pos;\n      this.fireChange();\n    }\n  }\n\n  get drawingFrom(): LinePoint | undefined {\n    return this.info.drawingFrom;\n  }\n\n  /**\n   * 获取线条正在画线的位置\n   */\n  get drawingTo(): LinePoint | undefined {\n    return this.info.drawingTo;\n  }\n\n  get highlightColor(): string {\n    return this.uiState.highlightColor || '';\n  }\n\n  set highlightColor(highlightColor) {\n    this.updateUIState({\n      highlightColor,\n    });\n  }\n\n  get lockedColor(): string {\n    return this.uiState.lockedColor;\n  }\n\n  set lockedColor(lockedColor: string) {\n    this.updateUIState({\n      lockedColor,\n    });\n  }\n\n  /**\n   * 获取线条的边框位置大小\n   */\n  get bounds(): Rectangle {\n    return this.getData(WorkflowLineRenderData).bounds;\n  }\n\n  get center(): LineCenterPoint {\n    return this.getData(WorkflowLineRenderData).center;\n  }\n\n  /**\n   * 获取点和线最接近的距离\n   */\n  getHoverDist(pos: IPoint): number {\n    return this.getData(WorkflowLineRenderData).calcDistance(pos);\n  }\n\n  get fromPort(): WorkflowPortEntity | undefined {\n    if (!this.from) {\n      return undefined;\n    }\n    return this.from.ports.getPortEntityByKey('output', this.info.fromPort);\n  }\n\n  get toPort(): WorkflowPortEntity | undefined {\n    if (!this.to) {\n      return undefined;\n    }\n    return this.to.ports.getPortEntityByKey('input', this.info.toPort);\n  }\n\n  /**\n   * 获取线条真实的输入输出节点坐标\n   */\n  get position(): LinePosition {\n    return this.getData(WorkflowLineRenderData).position;\n  }\n\n  /** 是否反转箭头 */\n  get reverse(): boolean {\n    return this.linesManager.isReverseLine(this, this.uiState.reverse);\n  }\n\n  /** 是否隐藏箭头 */\n  get hideArrow(): boolean {\n    return this.linesManager.isHideArrowLine(this, this.uiState.hideArrow);\n  }\n\n  /** 是否流动 */\n  get flowing(): boolean {\n    return this.linesManager.isFlowingLine(this, this.uiState.flowing);\n  }\n\n  set flowing(flowing: boolean) {\n    if (this._uiState.flowing !== flowing) {\n      this._uiState.flowing = flowing;\n      this.fireChange();\n    }\n  }\n\n  /** 是否禁用 */\n  get disabled(): boolean {\n    return this.linesManager.isDisabledLine(this, this.uiState.disabled);\n  }\n\n  /**\n   * @deprecated\n   */\n  get vertical(): boolean {\n    const fromLocation = this.fromPort?.location;\n    const toLocation = this.toPort?.location;\n    if (toLocation) {\n      return toLocation === 'top';\n    } else {\n      return fromLocation === 'bottom';\n    }\n  }\n\n  /** 获取线条渲染器类型 */\n  get renderType(): LineRenderType | undefined {\n    return this.linesManager.setLineRenderType(this);\n  }\n\n  /** 获取线条样式 */\n  get className(): string {\n    return [this.linesManager.setLineClassName(this), this._uiState.className]\n      .filter((s) => !!s)\n      .join(' ');\n  }\n\n  get color(): string | undefined {\n    return this.linesManager.getLineColor(this);\n  }\n\n  /**\n   * 初始化线条\n   * @param info 线条信息\n   */\n  protected initInfo(info: WorkflowLineInfo): void {\n    if (!isEqual(info, this.info)) {\n      this.info = info;\n      this._from = info.from ? this.document.getNode(info.from) : undefined;\n      this._to = info.to ? this.document.getNode(info.to) : undefined;\n      this._lineData = info.data;\n      this.fireChange();\n    }\n  }\n\n  // 校验连线是否为错误态\n  validate() {\n    this.validateSelf();\n  }\n\n  /**\n   * use `validate` instead\n   * @deprecated\n   */\n  protected validateSelf() {\n    const { fromPort, toPort } = this;\n\n    if (fromPort) {\n      this.hasError = this.linesManager.isErrorLine(fromPort, toPort, this.uiState.hasError);\n    }\n  }\n\n  is(line: WorkflowLineEntity | WorkflowLinePortInfo): boolean {\n    if (line instanceof WorkflowLineEntity) {\n      return this === line;\n    }\n    return WorkflowLineEntity.portInfoToLineId(line as WorkflowLinePortInfo) === this.id;\n  }\n\n  canRemove(newLineInfo?: Required<WorkflowLinePortInfo>): boolean {\n    return this.linesManager.canRemove(this, newLineInfo);\n  }\n\n  get node(): HTMLDivElement {\n    if (this._node) return this._node;\n    this._node = domUtils.createDivWithClass('gedit-flow-activity-line');\n    this._node.dataset.testid = 'sdk.workflow.canvas.line';\n    this._node.dataset.lineId = this.id;\n    this._node.dataset.fromNodeId = this.from?.id ?? '';\n    this._node.dataset.fromPortId = this.fromPort?.id ?? '';\n    this._node.dataset.toNodeId = this.to?.id ?? '';\n    this._node.dataset.toPortId = this.toPort?.id ?? '';\n    this._node.dataset.hasError = this.hasError ? 'true' : 'false';\n    return this._node;\n  }\n\n  toJSON(): WorkflowEdgeJSON {\n    const json: WorkflowEdgeJSON = {\n      sourceNodeID: this.info.from!,\n      targetNodeID: this.info.to!,\n      sourcePortID: this.info.fromPort,\n      targetPortID: this.info.toPort,\n    };\n    if (this._lineData !== undefined) {\n      json.data = this._lineData;\n    }\n    if (!json.sourcePortID) {\n      delete json.sourcePortID;\n    }\n    if (!json.targetPortID) {\n      delete json.targetPortID;\n    }\n    return json;\n  }\n\n  /** 触发线条渲染 */\n  fireRender(): void {\n    this.fireChange();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entities/workflow-node-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport type { WorkflowNodeLinesData, WorkflowNodePortsData } from '../entity-datas';\n\ndeclare module '@flowgram.ai/document' {\n  interface FlowNodeEntity {\n    lines: WorkflowNodeLinesData;\n    ports: WorkflowNodePortsData;\n  }\n}\nexport type WorkflowNodeEntity = FlowNodeEntity;\nexport const WorkflowNodeEntity = FlowNodeEntity;\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entities/workflow-port-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint, Rectangle, Emitter, Compare } from '@flowgram.ai/utils';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport {\n  Entity,\n  type EntityOpts,\n  PlaygroundConfigEntity,\n  TransformData,\n  type EntityRegistry,\n} from '@flowgram.ai/core';\n\nimport { type WorkflowDocument } from '../workflow-document';\nimport {\n  type WorkflowPortType,\n  getPortEntityId,\n  WORKFLOW_LINE_ENTITY,\n  domReactToBounds,\n} from '../utils/statics';\nimport { locationConfigToPoint } from '../utils/location-config-to-point';\nimport { type WorkflowNodeMeta, LinePointLocation, LinePoint } from '../typings';\nimport { type WorkflowNodeEntity } from './workflow-node-entity';\nimport { type WorkflowLineEntity } from './workflow-line-entity';\n\n// port 的宽度\nexport const PORT_SIZE = 24;\n\nexport interface WorkflowPort {\n  /**\n   * 没有代表 默认连接点，默认 input 类型 为最左边中心，output 类型为最右边中心\n   */\n  portID?: string | number;\n  /**\n   * 输入或者输出点\n   */\n  type: WorkflowPortType;\n  /**\n   * 端口位置\n   */\n  location?: LinePointLocation;\n  /**\n   * 端口位置配置\n   * @example\n   *  // bottom-center\n   *  {\n   *    left: '50%',\n   *    bottom: 0\n   *  }\n   *  // right-center\n   *  {\n   *    right: 0,\n   *    top: '50%'\n   *  }\n   */\n  locationConfig?: {\n    left?: string | number;\n    top?: string | number;\n    right?: string | number;\n    bottom?: string | number;\n  };\n  /**\n   * 相对于 location 的偏移\n   */\n  offset?: IPoint;\n  /**\n   * 端口热区大小\n   */\n  size?: { width: number; height: number };\n  /**\n   * 禁用端口\n   */\n  disabled?: boolean;\n  /**\n   * 将点位渲染到该父节点上\n   */\n  targetElement?: HTMLElement;\n}\n\nexport type WorkflowPorts = WorkflowPort[];\n\nexport interface WorkflowPortEntityOpts extends EntityOpts, WorkflowPort {\n  /**\n   * port 属于哪个节点\n   */\n  node: WorkflowNodeEntity;\n}\n\n/**\n * Port 抽象的 Entity\n */\nexport class WorkflowPortEntity extends Entity<WorkflowPortEntityOpts> {\n  static type = 'WorkflowPortEntity';\n\n  readonly node: WorkflowNodeEntity;\n\n  readonly portID: string | number = '';\n\n  readonly portType: WorkflowPortType;\n\n  private _disabled?: boolean;\n\n  private _hasError = false;\n\n  private _location?: LinePointLocation;\n\n  private _locationConfig?: WorkflowPort['locationConfig'];\n\n  private _size?: { width: number; height: number };\n\n  private _offset?: IPoint;\n\n  protected readonly _onErrorChangedEmitter = new Emitter<void>();\n\n  onErrorChanged = this._onErrorChangedEmitter.event;\n\n  targetElement?: HTMLElement;\n\n  static getPortEntityId(\n    node: WorkflowNodeEntity,\n    portType: WorkflowPortType,\n    portID: string | number = ''\n  ): string {\n    return getPortEntityId(node, portType, portID);\n  }\n\n  get position(): LinePointLocation | undefined {\n    return this._location;\n  }\n\n  constructor(opts: WorkflowPortEntityOpts) {\n    super(opts);\n    this.portID = opts.portID || '';\n    this.portType = opts.type;\n    this._disabled = opts.disabled;\n    this._offset = opts.offset;\n    this._locationConfig = opts.locationConfig;\n    this._location = opts.location;\n    this._size = opts.size;\n    this.node = opts.node;\n    this.updateTargetElement(opts.targetElement);\n    this.toDispose.push(this.node.getData(TransformData)!.onDataChange(() => this.fireChange()));\n    this.toDispose.push(this.node.onDispose(this.dispose.bind(this)));\n  }\n\n  // 获取连线是否为错误态\n  get hasError() {\n    return this._hasError;\n  }\n\n  // 设置连线的错误态，外部应使用 validate 进行更新\n  set hasError(hasError: boolean) {\n    if (hasError !== this._hasError) {\n      this._hasError = hasError;\n      this._onErrorChangedEmitter.fire();\n    }\n  }\n\n  validate() {\n    // 一个端口可能连接很多线，需要保证所有的连线都不包含错误\n    const anyLineHasError = this.allLines.some((line) => {\n      // 忽略已销毁和被隐藏的线\n      if (line.disposed || line.isHidden) {\n        return false;\n      }\n\n      return line.hasError;\n    });\n    // 如果没有连线错误，需校验端口自身错误\n    const isPortHasError = (this.node.document as WorkflowDocument).isErrorPort(this);\n    this.hasError = anyLineHasError || isPortHasError;\n  }\n\n  isErrorPort() {\n    return (this.node.document as WorkflowDocument).isErrorPort(this, this.hasError);\n  }\n\n  get location(): LinePointLocation {\n    if (this._location) {\n      return this._location;\n    }\n    if (this.portType === 'input') {\n      return 'left';\n    }\n    return 'right';\n  }\n\n  get point(): LinePoint {\n    const { targetElement, _locationConfig } = this;\n    const { bounds } = this.node.getData(FlowNodeTransformData)!;\n    const location = this.location;\n    if (targetElement) {\n      const pos = domReactToBounds(targetElement.getBoundingClientRect()).center;\n      const point = this.entityManager\n        .getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!\n        .getPosFromMouseEvent({\n          clientX: pos.x,\n          clientY: pos.y,\n        });\n      return {\n        x: point.x,\n        y: point.y,\n        location,\n      };\n    }\n    if (_locationConfig) {\n      return {\n        ...locationConfigToPoint(bounds, _locationConfig, this._offset),\n        location,\n      };\n    }\n    const offset = this._offset || { x: 0, y: 0 };\n    let point = { x: 0, y: 0 };\n    switch (location) {\n      case 'left':\n        point = bounds.leftCenter;\n        break;\n      case 'top':\n        point = bounds.topCenter;\n        break;\n      case 'right':\n        point = bounds.rightCenter;\n        break;\n      case 'bottom':\n        point = bounds.bottomCenter;\n        break;\n    }\n    return {\n      x: point.x + offset.x,\n      y: point.y + offset.y,\n      location,\n    };\n  }\n\n  /**\n   * 端口热区\n   */\n  get bounds(): Rectangle {\n    const { point } = this;\n    const size = this._size || { width: PORT_SIZE, height: PORT_SIZE };\n    return new Rectangle(\n      point.x - size.width / 2,\n      point.y - size.height / 2,\n      size.width,\n      size.height\n    );\n  }\n\n  isHovered(x: number, y: number): boolean {\n    return this.bounds.contains(x, y);\n  }\n\n  /**\n   * 相对节点左上角的位置\n   */\n  get relativePosition(): IPoint {\n    const { point } = this;\n    const { bounds } = this.node.getData(FlowNodeTransformData)!;\n    return {\n      x: point.x - bounds.x,\n      y: point.y - bounds.y,\n    };\n  }\n\n  updateTargetElement(el?: HTMLElement): void {\n    if (el !== this.targetElement) {\n      this.targetElement = el;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * 是否被禁用\n   */\n  get disabled(): boolean {\n    const document = this.node.document as WorkflowDocument;\n    if (typeof document.options.isDisabledPort === 'function') {\n      return document.options.isDisabledPort(this);\n    }\n    if (this._disabled) {\n      return true;\n    }\n    const meta = this.node.getNodeMeta<WorkflowNodeMeta>();\n    if (this.portType === 'input') {\n      return !!meta.inputDisable;\n    }\n    return !!meta.outputDisable;\n  }\n\n  /**\n   * 当前点位上连接的线条\n   * @deprecated use `availableLines` instead\n   */\n  get lines(): WorkflowLineEntity[] {\n    return this.allLines.filter((line) => !line.isDrawing);\n  }\n\n  /**\n   * 当前有效的线条，不包含正在画的线条和隐藏的线条（这个出现在线条重连会先把原来的线条隐藏）\n   */\n  get availableLines(): WorkflowLineEntity[] {\n    return this.allLines.filter((line) => !line.isDrawing && !line.isHidden);\n  }\n\n  /**\n   * 当前点位上连接的线条（包含 isDrawing === true 的线条）\n   */\n  get allLines() {\n    const lines: WorkflowLineEntity[] = [];\n    // TODO: 后续 sdk 支持 getEntitiesByType 单独根据 type 获取功能后修改\n    // eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n    const allLines = this.entityManager.getEntities<WorkflowLineEntity>({\n      type: WORKFLOW_LINE_ENTITY,\n    } as EntityRegistry);\n    allLines.forEach((line) => {\n      // 不包含 drawing 的线条\n      if (line.toPort === this || line.fromPort === this) {\n        lines.push(line);\n      }\n    });\n    return lines;\n  }\n\n  update(data: Exclude<WorkflowPort, 'portID' | 'type'>) {\n    let changed = false;\n    if (data.targetElement !== this.targetElement) {\n      this.targetElement = data.targetElement;\n      changed = true;\n    }\n    if (data.location !== this._location) {\n      this._location = data.location;\n      changed = true;\n    }\n    if (Compare.isChanged(data.offset, this._offset)) {\n      this._offset = data.offset;\n      changed = true;\n    }\n    if (Compare.isChanged(data.locationConfig, this._locationConfig)) {\n      this._locationConfig = data.locationConfig;\n      changed = true;\n    }\n    if (Compare.isChanged(data.size, this._size)) {\n      this._size = data.size;\n      changed = true;\n    }\n    if (data.disabled !== this._disabled) {\n      this._disabled = data.disabled;\n      changed = true;\n    }\n    if (changed) {\n      this.fireChange();\n    }\n  }\n\n  dispose(): void {\n    // 点位被删除，对应的线条也要删除\n    this.lines.forEach((l) => l.dispose());\n    super.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entity-datas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './workflow-node-ports-data';\nexport * from './workflow-node-lines-data';\nexport * from './workflow-line-render-data';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entity-datas/workflow-line-render-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport {\n  LineCenterPoint,\n  LinePosition,\n  LineRenderType,\n  WorkflowLineRenderContribution,\n  WorkflowLineRenderContributionFactory,\n} from '../typings';\nimport { WorkflowLineEntity } from '../entities';\n\nexport interface WorkflowLineRenderDataSchema {\n  version: string;\n  contributions: Map<LineRenderType, WorkflowLineRenderContribution>;\n  position: LinePosition;\n}\n\nexport class WorkflowLineRenderData extends EntityData<WorkflowLineRenderDataSchema> {\n  static type = 'WorkflowLineRenderData';\n\n  declare entity: WorkflowLineEntity;\n\n  constructor(entity: WorkflowLineEntity) {\n    super(entity);\n    this.syncContributions();\n  }\n\n  public getDefaultData(): WorkflowLineRenderDataSchema {\n    return {\n      version: '',\n      contributions: new Map(),\n      position: {\n        from: { x: 0, y: 0, location: 'right' },\n        to: { x: 0, y: 0, location: 'left' },\n      },\n    };\n  }\n\n  public get renderVersion(): string {\n    return this.data.version;\n  }\n\n  public get position(): LinePosition {\n    return this.data.position;\n  }\n\n  public get path(): string {\n    return this.currentLine?.path ?? '';\n  }\n\n  public calcDistance(pos: IPoint): number {\n    return this.currentLine?.calcDistance(pos) ?? Number.MAX_SAFE_INTEGER;\n  }\n\n  public get bounds(): Rectangle {\n    return this.currentLine?.bounds ?? new Rectangle();\n  }\n\n  /**\n   * 更新数据\n   * WARNING: 这个方法，必须在 requestAnimationFrame / useLayoutEffect 中调用，否则会引起浏览器强制重排\n   */\n  public update(): void {\n    this.syncContributions();\n    const oldVersion = this.data.version;\n    this.updatePosition();\n    const newVersion = this.data.version;\n    if (oldVersion === newVersion) {\n      return;\n    }\n    this.data.version = newVersion;\n    this.currentLine?.update({\n      fromPos: this.data.position.from,\n      toPos: this.data.position.to,\n    });\n  }\n\n  private get lineType(): LineRenderType {\n    return this.entity.renderType ?? this.entity.linesManager.lineType;\n  }\n\n  /**\n   * 获取 center 位置\n   */\n  get center(): LineCenterPoint {\n    return this.currentLine?.center || { x: 0, y: 0, labelX: 0, labelY: 0 };\n  }\n\n  /**\n   * 更新版本\n   * WARNING: 这个方法，必须在 requestAnimationFrame / useLayoutEffect 中调用，否则会引起浏览器强制重排\n   */\n  private updatePosition(): void {\n    this.data.position.from = this.entity.drawingFrom || this.entity.fromPort!.point;\n    this.data.position.to = this.entity.drawingTo || this.entity.toPort!.point;\n\n    this.data.version = [\n      this.lineType,\n      this.data.position.from.x,\n      this.data.position.from.y,\n      this.data.position.from.location,\n      this.data.position.to.x,\n      this.data.position.to.y,\n      this.data.position.to.location,\n      this.entity.uiState.shrink, // 这个会影响线条的变化\n      this.entity.uiState.curvature,\n    ].join('-');\n  }\n\n  private get currentLine(): WorkflowLineRenderContribution | undefined {\n    return this.data.contributions.get(this.lineType);\n  }\n\n  private syncContributions(): void {\n    if (this.entity.linesManager.contributionFactories.length === this.data.contributions.size) {\n      return;\n    }\n    this.entity.linesManager.contributionFactories.forEach((factory) => {\n      this.registerContribution(factory);\n    });\n  }\n\n  private registerContribution(contributionFactory: WorkflowLineRenderContributionFactory): void {\n    if (this.data.contributions.has(contributionFactory.type)) {\n      return;\n    }\n    const contribution = new contributionFactory(this.entity);\n    this.data.contributions.set(contributionFactory.type, contribution);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-lines-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable } from '@flowgram.ai/utils';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport { type WorkflowLineEntity, type WorkflowNodeEntity } from '../entities';\n\nexport interface WorkflowNodeLines {\n  inputLines: WorkflowLineEntity[];\n  outputLines: WorkflowLineEntity[];\n}\n\n/**\n * 节点的关联的线条\n */\nexport class WorkflowNodeLinesData extends EntityData<WorkflowNodeLines> {\n  static type = 'WorkflowNodeLinesData';\n\n  entity: WorkflowNodeEntity;\n\n  getDefaultData(): WorkflowNodeLines {\n    return {\n      inputLines: [],\n      outputLines: [],\n    };\n  }\n\n  constructor(entity: WorkflowNodeEntity) {\n    super(entity);\n    this.entity = entity;\n    this.entity.preDispose.push(\n      Disposable.create(() => {\n        this.inputLines.slice().forEach((line) => line.dispose());\n        this.outputLines.slice().forEach((line) => line.dispose());\n      })\n    );\n  }\n\n  /**\n   * 输入线条\n   */\n  get inputLines(): WorkflowLineEntity[] {\n    return this.data.inputLines;\n  }\n\n  /**\n   * 输出线条\n   */\n  get outputLines(): WorkflowLineEntity[] {\n    return this.data.outputLines;\n  }\n\n  get allLines(): WorkflowLineEntity[] {\n    return this.data.inputLines.concat(this.data.outputLines);\n  }\n\n  get availableLines(): WorkflowLineEntity[] {\n    return this.allLines.filter((line) => !line.isDrawing && !line.isHidden);\n  }\n\n  /**\n   * 输入节点\n   */\n  get inputNodes(): WorkflowNodeEntity[] {\n    return this.inputLines.map((l) => l.from!).filter(Boolean);\n  }\n\n  /**\n   * 所有输入节点\n   */\n  get allInputNodes(): WorkflowNodeEntity[] {\n    const nodeSet: Set<WorkflowNodeEntity> = new Set();\n\n    const handleNode = (node: WorkflowNodeEntity): void => {\n      if (nodeSet.has(node)) {\n        return;\n      }\n\n      nodeSet.add(node);\n\n      const { inputNodes } = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!;\n      if (!inputNodes || !inputNodes.length) {\n        return;\n      }\n\n      inputNodes.forEach((inputNode: WorkflowNodeEntity) => {\n        // 如果 outputNode 和当前 node 是父子节点，则不向下遍历\n        if (inputNode?.parent === node || node?.parent === inputNode) {\n          return;\n        }\n        handleNode(inputNode);\n      });\n    };\n\n    handleNode(this.entity);\n    nodeSet.delete(this.entity);\n\n    return Array.from(nodeSet);\n  }\n\n  /**\n   * 输出节点\n   */\n  get outputNodes(): WorkflowNodeEntity[] {\n    return this.outputLines.map((l) => l.to!).filter(Boolean);\n  }\n\n  /**\n   * 输入输出节点\n   */\n  get allOutputNodes(): WorkflowNodeEntity[] {\n    const nodeSet: Set<WorkflowNodeEntity> = new Set();\n\n    const handleNode = (node: WorkflowNodeEntity): void => {\n      if (nodeSet.has(node)) {\n        return;\n      }\n\n      nodeSet.add(node);\n\n      const { outputNodes } = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!;\n      if (!outputNodes || !outputNodes.length) {\n        return;\n      }\n\n      outputNodes.forEach((outputNode: WorkflowNodeEntity) => {\n        // 如果 outputNode 和当前 node 是父子节点，则不向下遍历\n        if (outputNode?.parent === node || node?.parent === outputNode) {\n          return;\n        }\n        handleNode(outputNode);\n      });\n    };\n\n    handleNode(this.entity);\n    nodeSet.delete(this.entity);\n\n    return Array.from(nodeSet);\n  }\n\n  addLine(line: WorkflowLineEntity): void {\n    if (line.from === this.entity) {\n      this.outputLines.push(line);\n    } else {\n      this.inputLines.push(line);\n    }\n    this.fireChange();\n  }\n\n  removeLine(line: WorkflowLineEntity): void {\n    const { inputLines, outputLines } = this;\n    const inputIndex = inputLines.indexOf(line);\n    const outputIndex = outputLines.indexOf(line);\n    if (inputIndex !== -1) {\n      inputLines.splice(inputIndex, 1);\n      this.fireChange();\n    }\n    if (outputIndex !== -1) {\n      outputLines.splice(outputIndex, 1);\n      this.fireChange();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/entity-datas/workflow-node-ports-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isEqual } from 'lodash-es';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport { EntityData, SizeData } from '@flowgram.ai/core';\n\nimport { type WorkflowPortType, getPortEntityId } from '../utils/statics';\nimport { type LinePoint, LinePointLocation, type WorkflowNodeMeta } from '../typings';\nimport { WorkflowPortEntity } from '../entities/workflow-port-entity';\nimport { type WorkflowNodeEntity, type WorkflowPort, type WorkflowPorts } from '../entities';\n\n/**\n * 节点的点位信息\n * portsData 只监听点位的数目和类型，不监听点位的 position 变化\n */\nexport class WorkflowNodePortsData extends EntityData {\n  public static readonly type = 'WorkflowNodePortsData';\n\n  public readonly entity: WorkflowNodeEntity;\n\n  /** 静态的 ports 数据 */\n  protected _staticPorts: WorkflowPorts = [];\n\n  /** 存储 port 实体的 id，用于判断 port 是否存在 */\n  protected _portIDSet = new Set<string>();\n\n  /** 上一次的 ports 数据，用于判断 ports 是否发生变化 */\n  protected _prePorts: WorkflowPorts;\n\n  constructor(entity: WorkflowNodeEntity) {\n    super(entity);\n    this.entity = entity;\n    const meta = entity.getNodeMeta<WorkflowNodeMeta>();\n    // 动态模式默认为空, 非动态模式默认左右两个点位\n    const defaultPorts: WorkflowPorts = meta.useDynamicPort\n      ? []\n      : [{ type: 'input' }, { type: 'output' }];\n    this._staticPorts = meta.defaultPorts?.slice() || defaultPorts;\n    this.updatePorts(this._staticPorts);\n    if (meta.useDynamicPort) {\n      this.toDispose.push(\n        // 只需要监听节点的大小，因为算的是相对位置\n        entity.getData!(SizeData)!.onDataChange(() => {\n          // 有可能节点被销毁了\n          if (entity.getData!(SizeData).width && entity.getData!(SizeData).height) {\n            this.updateDynamicPorts();\n          }\n        })\n      );\n    }\n    this.onDispose(() => {\n      this.allPorts.forEach((port) => port.dispose());\n    });\n  }\n\n  public getDefaultData(): any {\n    return {};\n  }\n\n  /**\n   * Update all ports data, includes static ports and dynamic ports\n   * @param ports\n   */\n  public updateAllPorts(ports?: WorkflowPorts) {\n    const meta = this.entity.getNodeMeta<WorkflowNodeMeta>();\n    if (ports) {\n      this._staticPorts = ports;\n    }\n    if (meta.useDynamicPort) {\n      this.updateDynamicPorts();\n    } else {\n      this.updatePorts(this._staticPorts);\n    }\n  }\n\n  /**\n   * @deprecated use `updateAllPorts` instead\n   */\n  public updateStaticPorts(ports: WorkflowPorts): void {\n    this.updateAllPorts(ports);\n  }\n\n  /**\n   * 动态计算点位，通过 dom 的 data-port-key\n   */\n  public updateDynamicPorts(): void {\n    const domNode = this.entity.getData(FlowNodeRenderData)!.node;\n    const elements = domNode.querySelectorAll<HTMLDivElement>('[data-port-id]');\n    const staticPorts: WorkflowPorts = this._staticPorts;\n    const dynamicPorts: WorkflowPorts = [];\n    if (elements.length > 0) {\n      dynamicPorts.push(\n        ...Array.from(elements).map((element) => ({\n          portID: element.getAttribute('data-port-id')!,\n          type: element.getAttribute('data-port-type')! as WorkflowPortType,\n          location: element.getAttribute('data-port-location')! as LinePointLocation,\n          targetElement: element,\n        }))\n      );\n    }\n    this.updatePorts(staticPorts.concat(dynamicPorts));\n  }\n\n  /**\n   * 根据 key 获取 port 实体\n   */\n  public getPortEntityByKey(\n    portType: WorkflowPortType,\n    portKey?: string | number\n  ): WorkflowPortEntity {\n    const entity = this.getOrCreatePortEntity({\n      type: portType,\n      portID: portKey,\n    });\n    return entity;\n  }\n\n  /**\n   * 更新 ports 数据\n   */\n  protected updatePorts(ports: WorkflowPorts): void {\n    if (!isEqual(this._prePorts, ports)) {\n      const portKeys = ports.map((port) => this.getPortId(port.type, port.portID));\n      this._portIDSet.forEach((portId) => {\n        if (!portKeys.includes(portId)) {\n          this.getPortEntity(portId)?.dispose();\n        }\n      });\n      ports.forEach((port) => this.updatePortEntity(port));\n      this._prePorts = ports;\n      this.fireChange();\n    }\n\n    // Note: 为什么调用 port.validate 不够，需要调用 line.validate\n    // 原因：假设有这样的连线：dynamic port → end 节点。\n    // line.validate 时，line.fromPort 可能为 undefined（未创建实体），导致 end 节点上的 port 未正确校验\n    // 所以需要在所有 port entities 准备完成后，通过再次调用 line.validate 来触发连线另一端的 port 更新\n    this.allPorts.forEach((port) => {\n      port.allLines.forEach((line) => {\n        line.validate();\n      });\n    });\n  }\n\n  /**\n   * 获取所有 port entities\n   */\n  public get allPorts(): WorkflowPortEntity[] {\n    return Array.from(this._portIDSet)\n      .map((portId) => this.getPortEntity(portId)!)\n      .filter(Boolean); // dispose 时，会获取不到 port\n  }\n\n  /**\n   * 获取输入点位\n   */\n  public get inputPorts(): WorkflowPortEntity[] {\n    return this.allPorts.filter((port) => port.portType === 'input');\n  }\n\n  /**\n   * 获取输出点位\n   */\n  public get outputPorts(): WorkflowPortEntity[] {\n    return this.allPorts.filter((port) => port.portType === 'output');\n  }\n\n  /**\n   * 获取输入点位置\n   */\n  public get inputPoints(): LinePoint[] {\n    return this.inputPorts.map((port) => port.point);\n  }\n\n  /**\n   * 获取输出点位置\n   */\n  public get outputPoints(): LinePoint[] {\n    return this.inputPorts.map((port) => port.point);\n  }\n\n  /**\n   * 根据 key 获取 输入点位置\n   */\n  public getInputPoint(key?: string | number): LinePoint {\n    return this.getPortEntityByKey('input', key).point;\n  }\n\n  /**\n   * 根据 key 获取输出点位置\n   */\n  public getOutputPoint(key?: string | number): LinePoint {\n    return this.getPortEntityByKey('output', key).point;\n  }\n\n  /**\n   * 获取 port 实体\n   */\n  protected getPortEntity(portId: string): WorkflowPortEntity | undefined {\n    if (!this._portIDSet.has(portId)) {\n      // 如果不是自身创建的 port，则返回 undefined\n      return undefined;\n    }\n    return this.entity.entityManager.getEntityById<WorkflowPortEntity>(portId)!;\n  }\n\n  /**\n   * 拼接 port 实体的 id\n   */\n  protected getPortId(portType: WorkflowPortType, portKey: string | number = ''): string {\n    return getPortEntityId(this.entity, portType, portKey);\n  }\n\n  /**\n   * 创建 port 实体\n   */\n  protected createPortEntity(portInfo: WorkflowPort): WorkflowPortEntity {\n    const id = this.getPortId(portInfo.type, portInfo.portID);\n    let portEntity = this.entity.entityManager.getEntityById<WorkflowPortEntity>(id);\n    if (!portEntity) {\n      portEntity = this.entity.entityManager.createEntity<WorkflowPortEntity>(WorkflowPortEntity, {\n        id,\n        node: this.entity,\n        ...portInfo,\n      });\n    }\n    portEntity.onDispose(() => {\n      this._portIDSet.delete(id);\n    });\n    this._portIDSet.add(id);\n    return portEntity;\n  }\n\n  /**\n   * 获取或创建 port 实体\n   */\n  protected getOrCreatePortEntity(portInfo: WorkflowPort): WorkflowPortEntity {\n    const id = this.getPortId(portInfo.type, portInfo.portID);\n    return this.getPortEntity(id) ?? this.createPortEntity(portInfo);\n  }\n\n  /**\n   * 更新 port 实体\n   */\n  protected updatePortEntity(portInfo: WorkflowPort): WorkflowPortEntity {\n    const portEntity = this.getOrCreatePortEntity(portInfo);\n    portEntity.update(portInfo);\n    return portEntity;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  useConfigEntity,\n  useService,\n  usePlayground,\n  useListenEvents,\n  usePlaygroundContainer,\n  usePlaygroundContext,\n  useEntities,\n  useEntityFromContext,\n  useEntityDataFromContext,\n  useRefresh,\n} from '@flowgram.ai/core';\nexport * from './typings';\nexport * from './use-node-render';\nexport * from './use-current-dom-node';\nexport * from './use-current-entity';\nexport * from './use-workflow-document';\nexport * from './use-playground-readonly-state';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/typings.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type NodeFormProps } from '@flowgram.ai/node';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { type WorkflowPortEntity } from '../entities';\n\nexport interface NodeRenderReturnType {\n  id: string;\n  type: string | number;\n  /**\n   * 当前节点\n   */\n  node: FlowNodeEntity;\n  /**\n   * 节点 data 数据\n   */\n  data: any;\n  /**\n   * 更新节点 data 数据\n   */\n  updateData: (newData: any) => void;\n  /**\n   * 节点选中\n   */\n  selected: boolean;\n  /**\n   * 节点激活\n   */\n  activated: boolean;\n  /**\n   * 节点展开\n   */\n  expanded: boolean;\n  /**\n   * 触发拖拽\n   * @param e\n   */\n  startDrag: (e: React.MouseEvent) => void;\n  /**\n   * 当前节点的点位信息\n   */\n  ports: WorkflowPortEntity[];\n  /**\n   * 删除节点\n   */\n  deleteNode: () => void;\n  /**\n   * 选中节点\n   * @param e\n   */\n  selectNode: (e: React.MouseEvent) => void;\n  /**\n   * 全局 readonly 状态\n   */\n  readonly: boolean;\n  /**\n   * 拖拽线条的目标 node id\n   */\n  linkingNodeId: string;\n  /**\n   * 节点 ref\n   */\n  nodeRef: React.MutableRefObject<HTMLDivElement | null>;\n  /**\n   * 节点 focus 事件\n   */\n  onFocus: () => void;\n  /**\n   * 节点 blur 事件\n   */\n  onBlur: () => void;\n  /**\n   * 渲染表单，只有节点引擎开启才能使用\n   */\n  form: NodeFormProps<any> | undefined;\n  /**\n   * 获取节点的扩展数据\n   */\n  getExtInfo<T = any>(): T;\n  /**\n   * 更新节点的扩展数据\n   * @param extInfo\n   */\n  updateExtInfo<T = any>(extInfo: T, fullUpdate?: boolean): void;\n  /**\n   * 展开/收起节点\n   * @param expanded\n   */\n  toggleExpand(): void;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-current-dom-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport { useEntityFromContext } from '@flowgram.ai/core';\n\nimport { type WorkflowNodeEntity } from '../entities';\n\n/**\n * 获取当前渲染的 dom 节点\n */\nexport function useCurrentDomNode(): HTMLDivElement {\n  const entity = useEntityFromContext<WorkflowNodeEntity>();\n  const renderData = entity.getData(FlowNodeRenderData)!;\n  return renderData.node;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-current-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEntityFromContext } from '@flowgram.ai/core';\n\nimport { type WorkflowNodeEntity } from '../entities';\n\n/**\n * 获取当前节点\n */\nexport function useCurrentEntity(): WorkflowNodeEntity {\n  return useEntityFromContext<WorkflowNodeEntity>();\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-node-render-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { NodeRenderReturnType } from './typings';\n\nexport const NodeRenderContext = React.createContext<NodeRenderReturnType>({} as any);\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\nimport { useCallback, useEffect, useRef, useState, useContext, useMemo } from 'react';\n\nimport { useObserve } from '@flowgram.ai/reactive';\nimport { getNodeForm } from '@flowgram.ai/node';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport {\n  MouseTouchEvent,\n  PlaygroundEntityContext,\n  useListenEvents,\n  useService,\n} from '@flowgram.ai/core';\n\nimport { WorkflowDragService, WorkflowSelectService } from '../service';\nimport { type WorkflowNodeEntity } from '../entities';\nimport { usePlaygroundReadonlyState } from './use-playground-readonly-state';\nimport { type NodeRenderReturnType } from './typings';\n\nfunction checkTargetDraggable(el: any): boolean {\n  return (\n    el &&\n    el.tagName !== 'INPUT' &&\n    el.tagName !== 'TEXTAREA' &&\n    !el.closest('.flow-canvas-not-draggable')\n  );\n}\n/**\n * - 下面的 firefox 为了修复一个 bug\n * - firefox 下 draggable 属性会影响节点 input 内容 focus：https://jsfiddle.net/Aydar/ztsvbyep/3/\n * - 该 bug 在 firefox 浏览器上存在了很久，需要作兼容：https://bugzilla.mozilla.org/show_bug.cgi?id=739071\n */\nconst isFirefox = typeof navigator !== 'undefined' && navigator?.userAgent?.includes?.('Firefox');\n\nexport function useNodeRender(nodeFromProps?: WorkflowNodeEntity): NodeRenderReturnType {\n  const node = nodeFromProps || useContext<WorkflowNodeEntity>(PlaygroundEntityContext);\n  const renderData = node.getData(FlowNodeRenderData)!;\n  const portsData = node.ports!;\n  const readonly = usePlaygroundReadonlyState();\n  const dragService = useService<WorkflowDragService>(WorkflowDragService);\n  const selectionService = useService<WorkflowSelectService>(WorkflowSelectService);\n  const isDragging = useRef(false);\n  const [formValueVersion, updateFormValueVersion] = useState<number>(0);\n  const formValueDependRef = useRef(false);\n  formValueDependRef.current = false;\n  const nodeRef = useRef<HTMLDivElement | null>(null);\n  const [linkingNodeId, setLinkingNodeId] = useState('');\n\n  useEffect(() => {\n    const disposable = dragService.onDragLineEventChange(({ type, onDragNodeId }) => {\n      if (type === 'onDrag') {\n        setLinkingNodeId(onDragNodeId || '');\n      } else {\n        setLinkingNodeId('');\n      }\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  const startDrag = useCallback(\n    (e: React.MouseEvent) => {\n      MouseTouchEvent.preventDefault(e);\n      if (!selectionService.isSelected(node.id)) {\n        selectNode(e);\n      }\n      if (!MouseTouchEvent.isTouchEvent(e as unknown as React.TouchEvent)) {\n        // 输入框不能拖拽\n        if (!checkTargetDraggable(e.target) || !checkTargetDraggable(document.activeElement)) {\n          return;\n        }\n      }\n      isDragging.current = true;\n      // 拖拽选中的节点\n      dragService.startDragSelectedNodes(e)?.finally(() =>\n        setTimeout(() => {\n          isDragging.current = false;\n        })\n      );\n    },\n    [dragService, node]\n  );\n  /**\n   * 单选节点\n   */\n  const selectNode = useCallback(\n    (e: React.MouseEvent) => {\n      // 触发了拖拽就不要再触发单选\n      if (isDragging.current) {\n        return;\n      }\n      // 追加选择\n      if (e.shiftKey) {\n        selectionService.toggleSelect(node);\n      } else {\n        selectionService.selectNode(node);\n      }\n      if (e.target) {\n        (e.target as HTMLDivElement).focus();\n      }\n    },\n    [node]\n  );\n  const deleteNode = useCallback(() => node.dispose(), [node]);\n  // 监听端口变化\n  useListenEvents(portsData.onDataChange);\n\n  const onFocus = useCallback(() => {\n    if (isFirefox) {\n      nodeRef.current?.setAttribute('draggable', 'false');\n    }\n  }, []);\n  const onBlur = useCallback(() => {\n    if (isFirefox) {\n      nodeRef.current?.setAttribute('draggable', 'true');\n    }\n  }, []);\n  const getExtInfo = useCallback(() => node.getExtInfo() as any, [node]);\n  const updateExtInfo = useCallback(\n    (data: any, fullUpdate?: boolean) => {\n      node.updateExtInfo(data, fullUpdate);\n    },\n    [node]\n  );\n  const form = useMemo(() => getNodeForm(node), [node]);\n  // Listen FormState change\n  const formState = useObserve<any>(form?.state);\n  const toggleExpand = useCallback(() => {\n    renderData.toggleExpand();\n  }, [renderData]);\n  const selected = selectionService.isSelected(node.id);\n  const activated = selectionService.isActivated(node.id);\n  const expanded = renderData.expanded;\n  useEffect(() => {\n    const toDispose = form?.onFormValuesChange(() => {\n      if (formValueDependRef.current) {\n        updateFormValueVersion((v) => v + 1);\n      }\n    });\n    return () => toDispose?.dispose();\n  }, [form]);\n\n  return useMemo(\n    () => ({\n      id: node.id,\n      type: node.flowNodeType,\n      get data() {\n        if (form) {\n          formValueDependRef.current = true;\n          return form.values;\n        }\n        return getExtInfo();\n      },\n      updateData(values: any) {\n        if (form) {\n          form.updateFormValues(values);\n        } else {\n          updateExtInfo(values, true);\n        }\n      },\n      node,\n      selected,\n      activated,\n      expanded,\n      startDrag,\n      get ports() {\n        return portsData.allPorts;\n      },\n      deleteNode,\n      selectNode,\n      readonly,\n      linkingNodeId,\n      nodeRef,\n      onFocus,\n      onBlur,\n      getExtInfo,\n      updateExtInfo,\n      toggleExpand,\n      get form() {\n        if (!form) return undefined;\n        return {\n          ...form,\n          get values() {\n            formValueDependRef.current = true;\n            return form.values!;\n          },\n          get state() {\n            return formState;\n          },\n        };\n      },\n    }),\n    [\n      node,\n      selected,\n      activated,\n      expanded,\n      startDrag,\n      deleteNode,\n      selectNode,\n      readonly,\n      linkingNodeId,\n      nodeRef,\n      onFocus,\n      onBlur,\n      getExtInfo,\n      updateExtInfo,\n      toggleExpand,\n      formValueVersion,\n    ]\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-playground-readonly-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { usePlayground, useRefresh } from '@flowgram.ai/core';\nimport { type Disposable } from '@flowgram.ai/utils';\n\n/**\n * 获取 readonly 状态\n */\nexport function usePlaygroundReadonlyState(listenChange?: boolean): boolean {\n  const playground = usePlayground();\n  const refresh = useRefresh();\n  useEffect(() => {\n    let dispose: Disposable | undefined = undefined;\n    if (listenChange) {\n      dispose = playground.config.onReadonlyOrDisabledChange(() => refresh());\n    }\n    return () => dispose?.dispose();\n  }, [listenChange]);\n  return playground.config.readonly;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/hooks/use-workflow-document.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService } from '@flowgram.ai/core';\n\nimport { WorkflowDocument } from '../workflow-document';\n\nexport function useWorkflowDocument(): WorkflowDocument {\n  return useService<WorkflowDocument>(WorkflowDocument);\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './workflow-commands';\nexport * from './hooks';\nexport * from './utils';\nexport * from './typings';\nexport * from './entities';\nexport * from './constants';\nexport * from './entity-datas';\nexport * from './service';\nexport * from './workflow-document';\nexport * from './workflow-document-container-module';\nexport * from './workflow-lines-manager';\nexport * from './workflow-document-option';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/layout/free-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport {\n  type IPoint,\n  PaddingSchema,\n  Rectangle,\n  type ScrollSchema,\n  SizeSchema,\n} from '@flowgram.ai/utils';\nimport {\n  type FlowDocument,\n  type FlowLayout,\n  type FlowNodeEntity,\n  FlowDocumentProvider,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';\n\nexport const FREE_LAYOUT_KEY = 'free-layout';\n/**\n * 自由画布布局\n */\n@injectable()\nexport class FreeLayout implements FlowLayout {\n  name = FREE_LAYOUT_KEY;\n\n  @inject(PlaygroundConfigEntity) playgroundConfig: PlaygroundConfigEntity;\n\n  @inject(FlowDocumentProvider)\n  protected documentProvider: FlowDocumentProvider;\n\n  get document(): FlowDocument {\n    return this.documentProvider();\n  }\n\n  /**\n   * 更新布局\n   */\n  update(): void {\n    if (this.document.root.getData(FlowNodeTransformData)?.localDirty) {\n      this.document.root.clearMemoGlobal();\n      // this.document.root.getData(FlowNodeTransformData)!.localDirty = false\n    }\n    // 自由画布同步同步大小, TODO 这个移动到 createWorkflowNode\n    // this.document.root.allChildren.forEach(this.syncTransform.bind(this))\n  }\n\n  syncTransform(node: FlowNodeEntity): void {\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n    if (!transform.localDirty) {\n      return;\n    }\n    node.clearMemoGlobal();\n    node.clearMemoLocal();\n    // 同步 size 给原始的 transform\n    transform.transform.update({\n      size: transform.data.size,\n    });\n    if (!node.parent) {\n      return;\n    }\n    node.parent.clearMemoGlobal();\n    node.parent.clearMemoLocal();\n    const parentTransform = node.parent.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    parentTransform.transform.fireChange();\n  }\n\n  /**\n   * 更新所有受影响的上下游节点\n   */\n  updateAffectedTransform(node: FlowNodeEntity): void {\n    const transformData = node.transform;\n    if (!transformData.localDirty) {\n      return;\n    }\n    const allParents = this.getAllParents(node);\n    const allBlocks = this.getAllBlocks(node).reverse();\n    const affectedNodes = [...allBlocks, ...allParents];\n    affectedNodes.forEach((node) => {\n      this.fireChange(node);\n    });\n  }\n\n  /**\n   * 获取节点的 padding 数据\n   * @param node\n   */\n  getPadding(node: FlowNodeEntity): PaddingSchema {\n    const { padding } = node.getNodeMeta();\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n    if (padding) {\n      return typeof padding === 'function' ? padding(transform) : padding;\n    }\n    return PaddingSchema.empty();\n  }\n\n  /**\n   * 默认滚动到 fitview 区域\n   * @param contentSize\n   */\n  getInitScroll(contentSize: SizeSchema): ScrollSchema {\n    const bounds = Rectangle.enlarge(\n      this.document.getAllNodes().map((node) => node.getData<TransformData>(TransformData).bounds)\n    ).pad(30, 30); // 留出 30 像素的边界\n    const viewport = this.playgroundConfig.getViewport(false);\n    const zoom = SizeSchema.fixSize(bounds, viewport);\n    return {\n      scrollX: (bounds.x + bounds.width / 2) * zoom - this.playgroundConfig.config.width / 2,\n      scrollY: (bounds.y + bounds.height / 2) * zoom - this.playgroundConfig.config.height / 2,\n    };\n  }\n\n  /**\n   * 获取默认输入点\n   */\n  getDefaultInputPoint(node: FlowNodeEntity): IPoint {\n    return node.getData<TransformData>(TransformData)!.bounds.leftCenter;\n  }\n\n  /**\n   * 获取默认输出点\n   */\n  getDefaultOutputPoint(node: FlowNodeEntity): IPoint {\n    return node.getData<TransformData>(TransformData)!.bounds.rightCenter;\n  }\n\n  /**\n   * 水平中心点\n   */\n  getDefaultNodeOrigin(): IPoint {\n    return { x: 0.5, y: 0 };\n  }\n\n  private getAllParents(node: FlowNodeEntity): FlowNodeEntity[] {\n    const parents: FlowNodeEntity[] = [];\n    let current = node.parent;\n\n    while (current) {\n      parents.push(current);\n      current = current.parent;\n    }\n\n    return parents;\n  }\n\n  private getAllBlocks(node: FlowNodeEntity): FlowNodeEntity[] {\n    return node.blocks.reduce<FlowNodeEntity[]>(\n      (acc, child) => [...acc, ...this.getAllBlocks(child)],\n      [node]\n    );\n  }\n\n  private fireChange(node?: FlowNodeEntity): void {\n    const transformData = node?.transform;\n    if (!node || !transformData?.localDirty) {\n      return;\n    }\n    node.clearMemoGlobal();\n    node.clearMemoLocal();\n    transformData.transform.fireChange();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/layout/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './free-layout';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './workflow-select-service';\nexport * from './workflow-hover-service';\nexport * from './workflow-drag-service';\nexport * from './workflow-reset-layout-service';\nexport * from './workflow-operation-base-service';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/workflow-drag-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { inject, injectable, postConstruct } from 'inversify';\nimport {\n  domUtils,\n  type IPoint,\n  PromiseDeferred,\n  Emitter,\n  type PositionSchema,\n  DisposableCollection,\n  Rectangle,\n  delay,\n  Disposable,\n  Point,\n} from '@flowgram.ai/utils';\nimport { FlowNodeTransformData, FlowNodeType, type FlowNodeEntity } from '@flowgram.ai/document';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport {\n  CommandService,\n  MouseTouchEvent,\n  PlaygroundConfigEntity,\n  PlaygroundDrag,\n  type PlaygroundDragEvent,\n  TransformData,\n} from '@flowgram.ai/core';\n\nimport { WorkflowLinesManager } from '../workflow-lines-manager';\nimport { WorkflowDocumentOptions } from '../workflow-document-option';\nimport { WorkflowDocument } from '../workflow-document';\nimport { LineEventProps, NodesDragEvent, OnDragLineEnd } from '../typings/workflow-drag';\nimport {\n  LinePointLocation,\n  WorkflowOperationBaseService,\n  type WorkflowNodeJSON,\n  type WorkflowNodeMeta,\n} from '../typings';\nimport {\n  type WorkflowLineEntity,\n  type WorkflowLinePortInfo,\n  type WorkflowNodeEntity,\n  type WorkflowPortEntity,\n} from '../entities';\nimport { WorkflowSelectService } from './workflow-select-service';\nimport { WorkflowHoverService } from './workflow-hover-service';\nimport { WorkflowPortType } from '../utils';\n\nconst DRAG_TIMEOUT = 100;\nconst DRAG_MIN_DELTA = 5;\nfunction checkDragSuccess(\n  time: number,\n  e: PlaygroundDragEvent,\n  originLine?: WorkflowLineEntity\n): boolean {\n  if (\n    !originLine ||\n    time > DRAG_TIMEOUT ||\n    Math.abs(e.endPos.x - e.startPos.x) >= DRAG_MIN_DELTA ||\n    Math.abs(e.endPos.y - e.startPos.y) >= DRAG_MIN_DELTA\n  ) {\n    return true;\n  }\n  return false;\n}\n\nfunction reverseLocation(sourceLocation: LinePointLocation): LinePointLocation {\n  switch (sourceLocation) {\n    case 'bottom':\n      return 'top';\n    case 'left':\n      return 'right';\n    case 'top':\n      return 'bottom';\n    case 'right':\n      return 'left';\n  }\n}\n\n@injectable()\nexport class WorkflowDragService {\n  @inject(PlaygroundConfigEntity)\n  protected playgroundConfig: PlaygroundConfigEntity;\n\n  @inject(WorkflowHoverService) protected hoverService: WorkflowHoverService;\n\n  @inject(WorkflowDocument) protected document: WorkflowDocument;\n\n  @inject(WorkflowLinesManager) protected linesManager: WorkflowLinesManager;\n\n  @inject(CommandService) protected commandService: CommandService;\n\n  @inject(WorkflowSelectService) protected selectService: WorkflowSelectService;\n\n  @inject(WorkflowOperationBaseService)\n  protected operationService: WorkflowOperationBaseService;\n\n  @inject(WorkflowDocumentOptions)\n  readonly options: WorkflowDocumentOptions;\n\n  private _onDragLineEventEmitter = new Emitter<LineEventProps>();\n\n  readonly onDragLineEventChange = this._onDragLineEventEmitter.event;\n\n  isDragging = false;\n\n  private _nodesDragEmitter = new Emitter<NodesDragEvent>();\n\n  readonly onNodesDrag = this._nodesDragEmitter.event;\n\n  protected _toDispose = new DisposableCollection();\n\n  private _droppableTransforms: FlowNodeTransformData[] = [];\n\n  private _dropNode?: FlowNodeEntity;\n\n  private posAdjusters: Set<\n    (params: { selectedNodes: WorkflowNodeEntity[]; position: IPoint }) => IPoint\n  > = new Set();\n\n  private _onDragLineEndCallbacks: Map<string, OnDragLineEnd> = new Map();\n\n  @postConstruct()\n  init() {\n    this._toDispose.pushAll([this._onDragLineEventEmitter, this._nodesDragEmitter]);\n    if (this.options.onDragLineEnd) {\n      this._toDispose.push(this.onDragLineEnd(this.options.onDragLineEnd));\n    }\n  }\n\n  dispose() {\n    this._toDispose.dispose();\n  }\n\n  /**\n   * 拖拽选中节点\n   * @param triggerEvent\n   */\n  async startDragSelectedNodes(triggerEvent: MouseEvent | React.MouseEvent): Promise<boolean> {\n    let { selectedNodes } = this.selectService;\n    if (selectedNodes.length === 0 || this.isDragging) {\n      return Promise.resolve(false);\n    }\n    if (\n      !this.document.options.enableReadonlyNodeDragging &&\n      (this.playgroundConfig.readonly || this.playgroundConfig.disabled)\n    ) {\n      return Promise.resolve(false);\n    }\n    this.isDragging = true;\n    // 节点整体开始位置\n    let startPosition = this.getNodesPosition(selectedNodes);\n    // 单个节点开始位置\n    let startPositions = selectedNodes.map((node) => {\n      const transform = node.getData(TransformData);\n      return { x: transform.position.x, y: transform.position.y };\n    });\n    let dragSuccess = false;\n    const startTime = Date.now();\n    const dragger = new PlaygroundDrag({\n      onDragStart: (dragEvent) => {\n        this._nodesDragEmitter.fire({\n          type: 'onDragStart',\n          nodes: selectedNodes,\n          startPositions,\n          dragEvent,\n          triggerEvent,\n          dragger,\n        });\n      },\n      onDrag: (dragEvent) => {\n        if (!dragSuccess && checkDragSuccess(Date.now() - startTime, dragEvent)) {\n          dragSuccess = true;\n        }\n\n        // 计算拖拽偏移量\n        const offset: IPoint = this.getDragPosOffset({\n          event: dragEvent,\n          selectedNodes,\n          startPosition,\n        });\n\n        const positions: PositionSchema[] = [];\n\n        selectedNodes.forEach((node, index) => {\n          const transform = node.getData(TransformData);\n          const nodeStartPosition = startPositions[index];\n          const newPosition = {\n            x: nodeStartPosition.x + offset.x,\n            y: nodeStartPosition.y + offset.y,\n          };\n          transform.update({\n            position: newPosition,\n          });\n          this.document.layout.updateAffectedTransform(node);\n          positions.push(newPosition);\n        });\n\n        this._nodesDragEmitter.fire({\n          type: 'onDragging',\n          nodes: selectedNodes,\n          startPositions,\n          positions,\n          dragEvent,\n          triggerEvent,\n          dragger,\n        });\n      },\n      onDragEnd: (dragEvent) => {\n        this.isDragging = false;\n        this._nodesDragEmitter.fire({\n          type: 'onDragEnd',\n          nodes: selectedNodes,\n          startPositions,\n          dragEvent,\n          triggerEvent,\n          dragger,\n        });\n        this.resetContainerInternalPosition(selectedNodes);\n      },\n    });\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(triggerEvent);\n    return dragger.start(clientX, clientY, this.playgroundConfig)?.then(() => dragSuccess);\n  }\n\n  /**\n   * 通过拖入卡片添加\n   * @param type\n   * @param event\n   * @param data 节点数据\n   */\n  async dropCard(\n    type: string,\n    event: { clientX: number; clientY: number },\n    data?: Partial<WorkflowNodeJSON>,\n    parent?: WorkflowNodeEntity\n  ): Promise<WorkflowNodeEntity | undefined> {\n    const mousePos = this.playgroundConfig.getPosFromMouseEvent(event);\n    if (!this.playgroundConfig.getViewport().contains(mousePos.x, mousePos.y)) {\n      // 鼠标范围不在画布之内\n      return;\n    }\n    const position = this.adjustSubNodePosition(type, parent, mousePos);\n\n    const node: WorkflowNodeEntity = await this.document.createWorkflowNodeByType(\n      type,\n      position,\n      data,\n      parent?.id\n    );\n    return node;\n  }\n\n  /**\n   * 拖拽卡片到画布\n   * 返回创建结果\n   * @param type\n   * @param event\n   */\n  async startDragCard(\n    type: string,\n    event: React.MouseEvent,\n    data: Partial<WorkflowNodeJSON>,\n    cloneNode?: (e: PlaygroundDragEvent) => HTMLDivElement // 创建拖拽的dom\n  ): Promise<WorkflowNodeEntity | undefined> {\n    let domNode: HTMLDivElement;\n    let startPos: IPoint = { x: 0, y: 0 };\n    const deferred = new PromiseDeferred<WorkflowNodeEntity | undefined>();\n    const dragger = new PlaygroundDrag({\n      onDragStart: (e) => {\n        const targetNode = event.currentTarget as HTMLDivElement;\n        domNode = cloneNode ? cloneNode(e) : (targetNode.cloneNode(true) as HTMLDivElement);\n        const bounds = targetNode.getBoundingClientRect();\n        startPos = { x: bounds.left + window.scrollX, y: bounds.top + window.scrollY };\n        domUtils.setStyle(domNode, {\n          zIndex: 1000,\n          position: 'absolute',\n          left: startPos.x,\n          top: startPos.y,\n          boxShadow: '0 6px 8px 0 rgba(28, 31, 35, .2)',\n        });\n        document.body.appendChild(domNode);\n        this.updateDroppableTransforms();\n      },\n      onDrag: (e) => {\n        const deltaX = e.endPos.x - e.startPos.x;\n        const deltaY = e.endPos.y - e.startPos.y;\n        const left = startPos.x + deltaX;\n        const right = startPos.y + deltaY;\n        domNode.style.left = `${left}px`;\n        domNode.style.top = `${right}px`;\n        // 节点类型拖拽碰撞检测\n        const { x, y } = this.playgroundConfig.getPosFromMouseEvent(e);\n        const draggingRect = new Rectangle(x, y, 170, 90);\n        const collisionTransform = this._droppableTransforms.find((transform) => {\n          const { bounds, entity } = transform;\n          const padding = this.document.layout.getPadding(entity);\n          const transformRect = new Rectangle(\n            bounds.x + padding.left + padding.right,\n            bounds.y,\n            bounds.width,\n            bounds.height\n          );\n          // 检测两个正方形是否相互碰撞\n          return Rectangle.intersects(draggingRect, transformRect);\n        });\n        this.updateDropNode(collisionTransform?.entity);\n      },\n      onDragEnd: async (e) => {\n        const dropNode = this._dropNode;\n        const { allowDrop } = this.canDropToNode({\n          dragNodeType: type,\n          dropNodeType: dropNode?.flowNodeType,\n          dropNode,\n        });\n        const dragNode = allowDrop ? await this.dropCard(type, e, data, dropNode) : undefined;\n        this.clearDrop();\n        if (dragNode) {\n          domNode.remove();\n          deferred.resolve(dragNode);\n        } else {\n          domNode.style.transition = 'all ease .2s';\n          domNode.style.left = `${startPos.x}px`;\n          domNode.style.top = `${startPos.y}px`;\n          const TIMEOUT = 200;\n          await delay(TIMEOUT);\n          domNode.remove();\n          deferred.resolve();\n        }\n      },\n    });\n    await dragger.start(event.clientX, event.clientY);\n    return deferred.promise;\n  }\n\n  /**\n   * 如果存在容器节点，且传入鼠标坐标，需要用容器的坐标减去传入的鼠标坐标\n   */\n  public adjustSubNodePosition(\n    subNodeType?: string,\n    containerNode?: WorkflowNodeEntity,\n    mousePos?: IPoint\n  ): IPoint {\n    if (!mousePos) {\n      return { x: 0, y: 0 };\n    }\n    if (!subNodeType || !containerNode || containerNode.flowNodeType === FlowNodeBaseType.ROOT) {\n      return mousePos;\n    }\n    const isParentEmpty = !containerNode.children || containerNode.children.length === 0;\n    const parentPadding = this.document.layout.getPadding(containerNode);\n    const containerWorldTransform = containerNode.transform.transform.worldTransform;\n    if (isParentEmpty) {\n      // 确保空容器节点不偏移\n      return {\n        x: 0,\n        y: parentPadding.top,\n      };\n    } else {\n      return {\n        x: mousePos.x - containerWorldTransform.tx,\n        y: mousePos.y - containerWorldTransform.ty,\n      };\n    }\n  }\n\n  /**\n   * 注册位置调整\n   */\n  public registerPosAdjuster(\n    adjuster: (params: { selectedNodes: WorkflowNodeEntity[]; position: IPoint }) => IPoint\n  ) {\n    this.posAdjusters.add(adjuster);\n    return {\n      dispose: () => this.posAdjusters.delete(adjuster),\n    };\n  }\n\n  /**\n   * 判断是否可以放置节点\n   */\n\n  public canDropToNode(params: {\n    dragNodeType?: FlowNodeType;\n    dragNode?: WorkflowNodeEntity;\n    dropNode?: WorkflowNodeEntity;\n    dropNodeType?: FlowNodeType;\n  }): {\n    allowDrop: boolean;\n    message?: string;\n    dropNode?: WorkflowNodeEntity;\n  } {\n    const { canDropToNode } = this.document.options;\n    const { dragNodeType, dropNode } = params;\n    if (canDropToNode) {\n      const result = canDropToNode(params);\n      if (result) {\n        return {\n          allowDrop: true,\n          dropNode,\n        };\n      }\n      return {\n        allowDrop: false,\n      };\n    }\n    if (!dragNodeType) {\n      return {\n        allowDrop: false,\n        message: 'Please select a node to drop',\n      };\n    }\n    return {\n      allowDrop: true,\n      dropNode,\n    };\n  }\n\n  /**\n   * 获取拖拽偏移\n   */\n  private getDragPosOffset(params: {\n    event: PlaygroundDragEvent;\n    selectedNodes: WorkflowNodeEntity[];\n    startPosition: IPoint;\n  }) {\n    const { event, selectedNodes, startPosition } = params;\n    const { finalScale } = this.playgroundConfig;\n    const mouseOffset: IPoint = {\n      x: (event.endPos.x - event.startPos.x) / finalScale,\n      y: (event.endPos.y - event.startPos.y) / finalScale,\n    };\n    const wholePosition: IPoint = {\n      x: startPosition.x + mouseOffset.x,\n      y: startPosition.y + mouseOffset.y,\n    };\n    const adjustedOffsets: IPoint[] = Array.from(this.posAdjusters.values()).map((adjuster) =>\n      adjuster({\n        selectedNodes,\n        position: wholePosition,\n      })\n    );\n    const offset: IPoint = adjustedOffsets.reduce(\n      (offset, adjustOffset) => ({\n        x: offset.x + adjustOffset.x,\n        y: offset.y + adjustOffset.y,\n      }),\n      mouseOffset\n    );\n    return offset;\n  }\n\n  private updateDroppableTransforms() {\n    this._droppableTransforms = this.document\n      .getRenderDatas(FlowNodeTransformData, false)\n      .filter((transform) => {\n        const { entity } = transform;\n        if (entity.originParent) {\n          return this.nodeSelectable(entity) && this.nodeSelectable(entity.originParent);\n        }\n        return this.nodeSelectable(entity);\n      })\n      .filter((transform) => this.isContainer(transform.entity));\n  }\n\n  /** 是否容器节点 */\n  private isContainer(node?: WorkflowNodeEntity): boolean {\n    return node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;\n  }\n\n  /**\n   * 获取节点整体位置\n   */\n  private getNodesPosition(nodes: WorkflowNodeEntity[]): IPoint {\n    const selectedBounds = Rectangle.enlarge(\n      nodes.map((n) => n.getData(FlowNodeTransformData)!.bounds)\n    );\n    const position: IPoint = {\n      x: selectedBounds.x,\n      y: selectedBounds.y,\n    };\n    return position;\n  }\n\n  private nodeSelectable(node: FlowNodeEntity) {\n    const selectable = node.getNodeMeta().selectable;\n    if (typeof selectable === 'function') {\n      return selectable(node);\n    } else {\n      return selectable;\n    }\n  }\n\n  private updateDropNode(node?: FlowNodeEntity) {\n    if (this._dropNode) {\n      if (this._dropNode.id === node?.id) {\n        return;\n      }\n      this.selectService.clear();\n    }\n    if (node) {\n      this.selectService.selectNode(node);\n    }\n    this._dropNode = node;\n  }\n\n  private clearDrop() {\n    if (this._dropNode) {\n      this.selectService.clear();\n    }\n    this._dropNode = undefined;\n    this._droppableTransforms = [];\n  }\n\n  private setLineColor(line: WorkflowLineEntity, color: string) {\n    line.highlightColor = color;\n    this.hoverService.clearHovered();\n  }\n\n  private checkDraggingPort(\n    isDrawingTo: boolean,\n    line: WorkflowLineEntity,\n    draggingNode: WorkflowNodeEntity,\n    draggingPort?: WorkflowPortEntity,\n    originLine?: WorkflowLineEntity\n  ): {\n    hasError: boolean;\n  } {\n    let successDrawing = false;\n    if (isDrawingTo) {\n      successDrawing = !!(\n        draggingPort &&\n        // 同一条线条则不用在判断 canAddLine\n        (originLine?.toPort === draggingPort ||\n          (draggingPort.portType === 'input' &&\n            this.linesManager.canAddLine(line.fromPort!, draggingPort, true)))\n      );\n    } else {\n      successDrawing = !!(\n        draggingPort &&\n        // 同一条线条则不用在判断 canAddLine\n        (originLine?.fromPort === draggingPort ||\n          (draggingPort.portType === 'output' &&\n            this.linesManager.canAddLine(draggingPort, line.toPort!, true)))\n      );\n    }\n\n    if (successDrawing) {\n      this.hoverService.updateHoveredKey(draggingPort!.id);\n      if (isDrawingTo) {\n        line.setToPort(draggingPort!);\n      } else {\n        line.setFromPort(draggingPort!);\n      }\n      this._onDragLineEventEmitter.fire({\n        type: 'onDrag',\n        onDragNodeId: draggingNode.id,\n      });\n      return {\n        hasError: false,\n      };\n    } else if (this.isContainer(draggingNode)) {\n      // 在容器内进行连线的情况，需忽略\n      return {\n        hasError: false,\n      };\n    } else {\n      this.setLineColor(line, this.linesManager.lineColor.error);\n      return {\n        hasError: true,\n      };\n    }\n  }\n\n  /**\n   * 容器内子节点总体位置重置为0\n   */\n  private resetContainerInternalPosition(nodes: WorkflowNodeEntity[]) {\n    const container = this.childrenOfContainer(nodes);\n    if (!container) {\n      return;\n    }\n    const bounds: Rectangle = Rectangle.enlarge(\n      container.blocks.map((node) => {\n        const x = node.transform.position.x - node.transform.bounds.width / 2;\n        const y = node.transform.position.y;\n        const width = node.transform.bounds.width;\n        const height = node.transform.bounds.height;\n        return new Rectangle(x, y, width, height);\n      })\n    );\n    const containerTransform = container.getData(TransformData);\n    this.operationService.updateNodePosition(container, {\n      x: containerTransform.position.x + bounds.x,\n      y: containerTransform.position.y + bounds.y,\n    });\n    this.document.layout.updateAffectedTransform(container);\n    container.blocks.forEach((node) => {\n      const transform = node.getData(TransformData);\n      this.operationService.updateNodePosition(node, {\n        x: transform.position.x - bounds.x,\n        y: transform.position.y - bounds.y,\n      });\n      this.document.layout.updateAffectedTransform(node);\n    });\n  }\n\n  private childrenOfContainer(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity | undefined {\n    if (nodes.length === 0) {\n      return;\n    }\n    const sourceContainer = nodes[0]?.parent;\n    if (!sourceContainer || sourceContainer.flowNodeType === FlowNodeBaseType.ROOT) {\n      return;\n    }\n    const valid = nodes.every((node) => node?.parent === sourceContainer);\n    if (!valid) {\n      return;\n    }\n    return sourceContainer;\n  }\n\n  /**\n   * 绘制线条\n   * @param opts\n   * @param event\n   */\n  async startDrawingLine(\n    port: WorkflowPortEntity,\n    event: { clientX: number; clientY: number },\n    originLine?: WorkflowLineEntity\n  ): Promise<{\n    dragSuccess?: boolean; // 是否拖拽成功，不成功则为选择节点\n    newLine?: WorkflowLineEntity; // 新的线条\n  }> {\n    const isDrawingTo = port.portType === 'output';\n    const isInActivePort = !originLine && port.isErrorPort() && port.disabled;\n    if (\n      originLine?.disabled ||\n      isInActivePort ||\n      this.playgroundConfig.readonly ||\n      this.playgroundConfig.disabled\n    ) {\n      return { dragSuccess: false, newLine: undefined };\n    }\n\n    this.selectService.clear();\n    const config = this.playgroundConfig;\n    const deferred = new PromiseDeferred<{\n      dragSuccess?: boolean;\n      newLine?: WorkflowLineEntity; // 新的线条\n    }>();\n    const preCursor = config.cursor;\n    let line: WorkflowLineEntity | undefined;\n    let newLineInfo: {\n      fromPort?: WorkflowPortEntity;\n      toPort?: WorkflowPortEntity;\n      hasError: boolean;\n    };\n    const startTime = Date.now();\n    let dragSuccess = false;\n    const dragger = new PlaygroundDrag({\n      onDrag: (e) => {\n        if (!line && checkDragSuccess(Date.now() - startTime, e, originLine)) {\n          // 隐藏原来的线条\n          if (originLine) {\n            originLine.highlightColor = this.linesManager.lineColor.hidden;\n          }\n          dragSuccess = true;\n          const pos = config.getPosFromMouseEvent(event);\n          // 创建临时的线条\n          if (isDrawingTo) {\n            line = this.linesManager.createLine({\n              from: port.node.id,\n              fromPort: port.portID,\n              data: originLine?.lineData,\n              drawingTo: {\n                x: pos.x,\n                y: pos.y,\n                location: port.location === 'right' ? 'left' : 'top',\n              },\n            });\n          } else {\n            line = this.linesManager.createLine({\n              to: port.node.id,\n              toPort: port.portID,\n              data: originLine?.lineData,\n              drawingFrom: {\n                x: pos.x,\n                y: pos.y,\n                location: port.location === 'left' ? 'right' : 'bottom',\n              },\n            });\n          }\n          if (!line) {\n            return;\n          }\n          config.updateCursor('grab');\n          line.highlightColor = originLine?.lockedColor || this.linesManager.lineColor.drawing;\n          this.hoverService.updateHoveredKey('');\n        }\n        if (!line) {\n          return;\n        }\n        const dragPos = config.getPosFromMouseEvent(e);\n        newLineInfo = this.updateDrawingLine(isDrawingTo, line, dragPos, originLine);\n      },\n\n      onDragEnd: async (e) => {\n        const dragPos = config.getPosFromMouseEvent(e);\n        const onDragLineEndCallbacks = Array.from(this._onDragLineEndCallbacks.values());\n        config.updateCursor(preCursor);\n        const { fromPort, toPort, hasError } = newLineInfo || {};\n        await Promise.all(\n          onDragLineEndCallbacks.map((callback) =>\n            callback({\n              fromPort,\n              toPort,\n              mousePos: dragPos,\n              line,\n              originLine,\n              event: e,\n            })\n          )\n        );\n        line?.dispose();\n        this._onDragLineEventEmitter.fire({\n          type: 'onDragEnd',\n        });\n        // 清除选中状态\n        if (originLine) {\n          originLine.highlightColor = '';\n        }\n        const end = () => {\n          originLine?.validate();\n          deferred.resolve({ dragSuccess });\n        };\n        if (dragSuccess) {\n          // Step 1: check same line\n          if (originLine && originLine.toPort === toPort && originLine.fromPort === fromPort) {\n            // 线条没变化则直接返回，不做处理\n            return end();\n          }\n          // 非 input 节点不能连接\n          if (\n            (toPort && toPort.portType !== 'input') ||\n            (fromPort && fromPort.portType !== 'output')\n          ) {\n            return end();\n          }\n          const newLinePortInfo: Required<WorkflowLinePortInfo> | undefined =\n            toPort && fromPort\n              ? {\n                  from: fromPort.node.id,\n                  fromPort: fromPort.portID,\n                  to: toPort.node.id,\n                  toPort: toPort.portID,\n                  data: originLine?.lineData,\n                }\n              : undefined;\n          // Step2: 检测 reset\n          const isReset = originLine && newLinePortInfo;\n          if (isReset && !this.linesManager.canReset(originLine, newLinePortInfo)) {\n            return end();\n          }\n          // Step 3: delete line\n          if (\n            originLine &&\n            (!this.linesManager.canRemove(originLine, newLinePortInfo, false) || hasError)\n          ) {\n            // 线条无法删除则返回，不再触发 canAddLine\n            return end();\n          } else {\n            originLine?.dispose();\n          }\n          //  Step 4:  add line\n          if (!newLinePortInfo || !this.linesManager.canAddLine(fromPort!, toPort!, false)) {\n            // 无法添加成功\n            return end();\n          }\n          const newLine = this.linesManager.createLine(newLinePortInfo);\n          if (!newLine) {\n            end();\n          }\n          deferred.resolve({\n            dragSuccess,\n            newLine,\n          });\n        } else {\n          end();\n        }\n      },\n    });\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);\n    await dragger.start(clientX, clientY, config);\n    return deferred.promise;\n  }\n\n  private updateDrawingLine(\n    isDrawingTo: boolean,\n    line: WorkflowLineEntity,\n    dragPos: IPoint,\n    originLine?: WorkflowLineEntity\n  ): { fromPort?: WorkflowPortEntity; toPort?: WorkflowPortEntity; hasError: boolean } {\n    let hasError = false;\n\n    const mouseNode = this.linesManager.getNodeFromMousePos(dragPos);\n    let toNode: WorkflowNodeEntity | undefined;\n    let toPort: WorkflowPortEntity | undefined;\n    let fromPort: WorkflowPortEntity | undefined;\n    let fromNode: WorkflowNodeEntity | undefined;\n\n    if (isDrawingTo) {\n      fromPort = line.fromPort!;\n      toNode = mouseNode;\n      toPort = this.linesManager.getPortFromMousePos(dragPos, 'input');\n      if (toNode && this.canBuildContainerLine(toNode, dragPos)) {\n        // 如果鼠标 hover 在 node 中的时候，默认连线到这个 node 的初始位置\n        toPort = this.getNearestPort(toNode, dragPos, 'input');\n        hasError = this.checkDraggingPort(isDrawingTo, line, toNode, toPort, originLine).hasError;\n      }\n      if (!toPort) {\n        line.setToPort(undefined);\n      } else if (!this.linesManager.canAddLine(fromPort, toPort, true)) {\n        hasError = true;\n        line.setToPort(undefined);\n      } else {\n        line.setToPort(toPort);\n      }\n\n      if (line.toPort) {\n        line.drawingTo = {\n          x: line.toPort.point.x,\n          y: line.toPort.point.y,\n          location: line.toPort.location,\n        };\n      } else {\n        line.drawingTo = {\n          x: dragPos.x,\n          y: dragPos.y,\n          location: reverseLocation(line.fromPort!.location),\n        };\n      }\n    } else {\n      toPort = line.toPort!;\n      fromNode = mouseNode;\n      fromPort = this.linesManager.getPortFromMousePos(dragPos, 'output');\n      if (fromNode && this.canBuildContainerLine(fromNode, dragPos)) {\n        // 如果鼠标 hover 在 node 中的时候，默认连线到这个 node 的初始位置\n        fromPort = this.getNearestPort(fromNode, dragPos, 'output');\n        hasError = this.checkDraggingPort(\n          isDrawingTo,\n          line,\n          fromNode,\n          fromPort,\n          originLine\n        ).hasError;\n      }\n      if (!fromPort) {\n        line.setFromPort(undefined);\n      } else if (!this.linesManager.canAddLine(fromPort, toPort, true)) {\n        hasError = true;\n        line.setFromPort(undefined);\n      } else {\n        line.setFromPort(fromPort);\n      }\n\n      if (line.fromPort) {\n        line.drawingFrom = {\n          x: line.fromPort.point.x,\n          y: line.fromPort.point.y,\n          location: line.fromPort.location,\n        };\n      } else {\n        line.drawingFrom = {\n          x: dragPos.x,\n          y: dragPos.y,\n          location: reverseLocation(line.toPort!.location),\n        };\n      }\n    }\n\n    this._onDragLineEventEmitter.fire({\n      type: 'onDrag',\n    });\n\n    if (hasError) {\n      this.setLineColor(line, this.linesManager.lineColor.error);\n    } else {\n      this.setLineColor(line, originLine?.lockedColor || this.linesManager.lineColor.drawing);\n    }\n    // 触发原 toPort 的校验\n    originLine?.validate();\n    line.validate();\n    return {\n      fromPort: fromPort,\n      toPort: toPort,\n      hasError,\n    };\n  }\n\n  /**\n   * 重新连接线条\n   * @param line\n   * @param e\n   */\n  async resetLine(line: WorkflowLineEntity, e: MouseEvent): Promise<void> {\n    const { fromPort, toPort } = line;\n    const mousePos = this.playgroundConfig.getPosFromMouseEvent(e);\n    const distanceFrom = Point.getDistance(fromPort!.point, mousePos);\n    const distanceTo = Point.getDistance(toPort!.point, mousePos);\n    const { dragSuccess } = await this.startDrawingLine(\n      distanceTo <= distanceFrom || !this.document.options.twoWayConnection ? fromPort! : toPort!,\n      e,\n      line\n    );\n    if (!dragSuccess) {\n      // 没有拖拽成功则表示为选中节点\n      this.selectService.select(line);\n    }\n  }\n\n  /** 线条拖拽结束 */\n  public onDragLineEnd(callback: OnDragLineEnd): Disposable {\n    const id = nanoid();\n    this._onDragLineEndCallbacks.set(id, callback);\n    return {\n      dispose: () => {\n        this._onDragLineEndCallbacks.delete(id);\n      },\n    };\n  }\n\n  /** 能否建立容器连线 */\n  private canBuildContainerLine(node: WorkflowNodeEntity, mousePos: IPoint): boolean {\n    const isContainer = this.isContainer(node);\n    if (!isContainer) {\n      return true;\n    }\n    const { padding, bounds } = node.transform;\n    const DEFAULT_DELTA = 10;\n    const leftDelta = (padding.left * 2) / 3 || DEFAULT_DELTA;\n    const rightDelta = (padding.right * 2) / 3 || DEFAULT_DELTA;\n    const bottomDelta = (padding.bottom * 2) / 3 || DEFAULT_DELTA;\n    const topDelta = (padding.top * 2) / 3 || DEFAULT_DELTA;\n    const rectangles = [\n      new Rectangle(bounds.x, bounds.y, leftDelta, bounds.height), // left\n      new Rectangle(bounds.x, bounds.y, bounds.width, topDelta), // top\n      new Rectangle(bounds.x, bounds.y + bounds.height - bottomDelta, bounds.width, bottomDelta), // bottom\n      new Rectangle(bounds.x + bounds.width - rightDelta, bounds.y, rightDelta, bounds.height), // right\n    ];\n    return rectangles.some((rect) => rect.contains(mousePos.x, mousePos.y));\n  }\n\n  /** 获取最近的 port */\n  private getNearestPort(\n    node: WorkflowNodeEntity,\n    mousePos: IPoint,\n    portType: WorkflowPortType = 'input'\n  ): WorkflowPortEntity {\n    const portsData = node.ports!;\n    const distanceSortedPorts = (\n      portType === 'input' ? portsData.inputPorts : portsData.outputPorts\n    ).sort((a, b) => Point.getDistance(mousePos, a.point) - Point.getDistance(mousePos, b.point));\n    return distanceSortedPorts[0];\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/workflow-hover-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Emitter, type PositionSchema } from '@flowgram.ai/utils';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport {\n  type WorkflowLineEntity,\n  type WorkflowNodeEntity,\n  type WorkflowPortEntity,\n} from '../entities';\n\n/**\n * 可 Hover 的节点 类型\n */\nexport type WorkflowEntityHoverable = WorkflowNodeEntity | WorkflowLineEntity | WorkflowPortEntity;\n\nexport interface HoverPosition {\n  position: PositionSchema;\n  target?: HTMLElement;\n}\n\n/** @deprecated */\nexport type WorkfloEntityHoverable = WorkflowEntityHoverable;\n/**\n * hover 状态管理\n */\n@injectable()\nexport class WorkflowHoverService {\n  @inject(EntityManager) protected entityManager: EntityManager;\n\n  protected onHoveredChangeEmitter = new Emitter<string>();\n\n  protected onUpdateHoverPositionEmitter = new Emitter<HoverPosition>();\n\n  readonly onHoveredChange = this.onHoveredChangeEmitter.event;\n\n  readonly onUpdateHoverPosition = this.onUpdateHoverPositionEmitter.event;\n\n  // 当前鼠标 hover 位置\n  hoveredPos: PositionSchema = { x: 0, y: 0 };\n\n  /**\n   * 当前 hovered 的 节点或者线条或者点\n   * 1: nodeId / lineId  （节点 / 线条）\n   * 2: nodeId:portKey  （节点连接点）\n   */\n  hoveredKey = '';\n\n  /**\n   * 更新 hover 的内容\n   * @param hoveredKey hovered key\n   */\n  updateHoveredKey(hoveredKey: string): void {\n    if (this.hoveredKey !== hoveredKey) {\n      this.hoveredKey = hoveredKey;\n      this.onHoveredChangeEmitter.fire(hoveredKey);\n    }\n  }\n\n  updateHoverPosition(position: PositionSchema, target?: HTMLElement): void {\n    this.hoveredPos = position;\n    this.onUpdateHoverPositionEmitter.fire({\n      position,\n      target,\n    });\n  }\n\n  /**\n   * 清空 hover 内容\n   */\n  clearHovered(): void {\n    this.updateHoveredKey('');\n  }\n\n  /**\n   *  判断是否 hover\n   * @param nodeId hoveredKey\n   * @returns 是否 hover\n   */\n  isHovered(nodeId: string): boolean {\n    return nodeId === this.hoveredKey;\n  }\n\n  isSomeHovered(): boolean {\n    return !!this.hoveredKey;\n  }\n\n  /**\n   * 获取被 hover 的节点或线条\n   * @deprecated use 'someHovered' instead\n   */\n  get hoveredNode(): WorkflowEntityHoverable | undefined {\n    return this.entityManager.getEntityById(this.hoveredKey);\n  }\n\n  /**\n   * 获取被 hover 的节点或线条\n   */\n  get someHovered(): WorkflowEntityHoverable | undefined {\n    return this.entityManager.getEntityById(this.hoveredKey);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/workflow-operation-base-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject } from 'inversify';\nimport { IPoint, Emitter } from '@flowgram.ai/utils';\nimport { FlowNodeEntityOrId, FlowOperationBaseServiceImpl } from '@flowgram.ai/document';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { WorkflowLinesManager } from '../workflow-lines-manager';\nimport { WorkflowDocument } from '../workflow-document';\nimport {\n  NodePostionUpdateEvent,\n  WorkflowOperationBaseService,\n} from '../typings/workflow-operation';\nimport { WorkflowJSON } from '../typings';\nimport { WorkflowNodeEntity, WorkflowLineEntity } from '../entities';\n\nexport class WorkflowOperationBaseServiceImpl\n  extends FlowOperationBaseServiceImpl\n  implements WorkflowOperationBaseService\n{\n  @inject(WorkflowDocument)\n  protected declare document: WorkflowDocument;\n\n  @inject(WorkflowLinesManager) linesManager: WorkflowLinesManager;\n\n  private onNodePostionUpdateEmitter = new Emitter<NodePostionUpdateEvent>();\n\n  public readonly onNodePostionUpdate = this.onNodePostionUpdateEmitter.event;\n\n  updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void {\n    const node = this.toNodeEntity(nodeOrId);\n\n    if (!node) {\n      return;\n    }\n\n    const transformData = node.getData(TransformData);\n    const oldPosition = {\n      x: transformData.position.x,\n      y: transformData.position.y,\n    };\n    transformData.update({\n      position,\n    });\n\n    this.onNodePostionUpdateEmitter.fire({\n      node,\n      oldPosition,\n      newPosition: position,\n    });\n  }\n\n  fromJSON(json: WorkflowJSON) {\n    if (this.document.disposed) return;\n    const workflowJSON: WorkflowJSON = {\n      nodes: json.nodes ?? [],\n      edges: json.edges ?? [],\n    };\n\n    const oldNodes = this.document.getAllNodes();\n    const oldEdges = this.linesManager.getAllLines();\n    const oldPositionMap = new Map<string, IPoint>(\n      oldNodes.map((node) => [\n        node.id,\n        {\n          x: node.transform.transform.position.x,\n          y: node.transform.transform.position.y,\n        },\n      ])\n    );\n\n    const newNodes: WorkflowNodeEntity[] = [];\n    const newEdges: WorkflowLineEntity[] = [];\n\n    // 逐层渲染\n    this.document.batchAddFromJSON(workflowJSON, {\n      onNodeCreated: (node) => newNodes.push(node),\n      onEdgeCreated: (edge) => newEdges.push(edge),\n    });\n\n    const newEdgeIDSet = new Set<string>(newEdges.map((edge) => edge.id));\n    oldEdges.forEach((edge) => {\n      // 清空旧线条\n      if (!newEdgeIDSet.has(edge.id)) {\n        edge.dispose();\n        return;\n      }\n    });\n\n    const newNodeIDSet = new Set<string>(newNodes.map((node) => node.id));\n    oldNodes.forEach((node) => {\n      // 清空旧节点\n      if (!newNodeIDSet.has(node.id)) {\n        node.dispose();\n        return;\n      }\n      // 记录现有节点位置变更\n      const oldPosition = oldPositionMap.get(node.id);\n      const newPosition = {\n        x: node.transform.transform.position.x,\n        y: node.transform.transform.position.y,\n      };\n      if (oldPosition && (oldPosition.x !== newPosition.x || oldPosition.y !== newPosition.y)) {\n        this.onNodePostionUpdateEmitter.fire({\n          node,\n          oldPosition,\n          newPosition,\n        });\n      }\n    });\n\n    // 批量触发画布更新\n    this.document.fireRender();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/workflow-reset-layout-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, postConstruct } from 'inversify';\nimport { PlaygroundConfigEntity } from '@flowgram.ai/core';\nimport { EntityManager } from '@flowgram.ai/core';\nimport { DisposableCollection, Emitter, type IPoint } from '@flowgram.ai/utils';\n\nimport { WorkflowDocument } from '../workflow-document';\nimport { layoutToPositions } from '../utils/layout-to-positions';\nimport { fitView } from '../utils';\nimport { WorkflowNodeEntity } from '../entities';\n\nexport type PositionMap = Record<string, IPoint>;\n\n/**\n * 重置布局服务\n */\n@injectable()\nexport class WorkflowResetLayoutService {\n  @inject(PlaygroundConfigEntity)\n  private _config: PlaygroundConfigEntity;\n\n  @inject(WorkflowDocument)\n  private _document: WorkflowDocument;\n\n  @inject(EntityManager)\n  private _entityManager: EntityManager;\n\n  private _resetLayoutEmitter = new Emitter<{\n    nodeIds: string[];\n    positionMap: PositionMap;\n    oldPositionMap: PositionMap;\n  }>();\n\n  /**\n   * reset layout事件\n   */\n  readonly onResetLayout = this._resetLayoutEmitter.event;\n\n  private _toDispose = new DisposableCollection();\n\n  /**\n   * 初始化\n   */\n  @postConstruct()\n  init() {\n    this._toDispose.push(this._resetLayoutEmitter);\n  }\n\n  /**\n   * 触发重置布局\n   * @param nodeIds 节点id\n   * @param positionMap 新布局数据\n   * @param oldPositionMap 老布局数据\n   */\n  fireResetLayout(nodeIds: string[], positionMap: PositionMap, oldPositionMap: PositionMap) {\n    this._resetLayoutEmitter.fire({\n      nodeIds,\n      positionMap,\n      oldPositionMap,\n    });\n  }\n\n  /**\n   * 根据数据重新布局\n   * @param positionMap\n   * @returns\n   */\n  async layoutToPositions(nodeIds: string[], positionMap: PositionMap) {\n    const nodes = nodeIds\n      .map(id => this._entityManager.getEntityById(id))\n      .filter(Boolean) as WorkflowNodeEntity[];\n    const positions = await layoutToPositions(nodes, positionMap);\n    fitView(this._document, this._config, true);\n    return positions;\n  }\n\n  /**\n   * 销毁\n   */\n  dispose() {\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/service/workflow-select-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport {\n  type Entity,\n  Playground,\n  SelectionService,\n  TransformData,\n  type PlaygroundConfigRevealOpts,\n} from '@flowgram.ai/core';\nimport { type Event, Rectangle, SizeSchema } from '@flowgram.ai/utils';\n\nimport { delay } from '../utils';\nimport { WorkflowNodeEntity } from '../entities';\nimport { type WorkfloEntityHoverable } from './workflow-hover-service';\n\n@injectable()\nexport class WorkflowSelectService {\n  @inject(SelectionService) protected selectionService: SelectionService;\n\n  @inject(Playground) protected playground: Playground;\n\n  get onSelectionChanged(): Event<void> {\n    return this.selectionService.onSelectionChanged;\n  }\n\n  get selection(): Entity[] {\n    return this.selectionService.selection;\n  }\n\n  set selection(entities: Entity[]) {\n    this.selectionService.selection = entities;\n  }\n\n  /**\n   * 当前激活的节点只能有一个\n   */\n  get activatedNode(): WorkflowNodeEntity | undefined {\n    const { selectedNodes } = this;\n    if (selectedNodes.length !== 1) {\n      return undefined;\n    }\n    return selectedNodes[0];\n  }\n\n  isSelected(id: string): boolean {\n    return this.selectionService.selection.some(s => s.id === id);\n  }\n\n  isActivated(id: string): boolean {\n    return this.activatedNode?.id === id;\n  }\n\n  /**\n   * 选中的节点\n   */\n  get selectedNodes(): WorkflowNodeEntity[] {\n    return this.selectionService.selection.filter(\n      n => n instanceof WorkflowNodeEntity,\n    ) as WorkflowNodeEntity[];\n  }\n\n  /**\n   * 选中\n   * @param node\n   */\n  selectNode(node: WorkflowNodeEntity): void {\n    this.selectionService.selection = [node];\n  }\n\n  toggleSelect(node: WorkflowNodeEntity): void {\n    if (this.selectionService.selection.includes(node)) {\n      this.selectionService.selection = this.selectionService.selection.filter(n => n !== node);\n    } else {\n      this.selectionService.selection = this.selectionService.selection.concat(node);\n    }\n  }\n\n  select(node: WorkfloEntityHoverable): void {\n    this.selectionService.selection = [node];\n  }\n\n  clear(): void {\n    this.selectionService.selection = [];\n  }\n\n  /**\n   *  选中并滚动到节点\n   * @param node\n   */\n  async selectNodeAndScrollToView(node: WorkflowNodeEntity, fitView?: boolean): Promise<void> {\n    this.selectNodeAndFocus(node);\n    const DELAY_TIME = 30;\n    // 等待节点渲染完成(一般用于刚添加的节点)\n    await delay(DELAY_TIME);\n\n    const scrollConfig: PlaygroundConfigRevealOpts = {\n      entities: [node],\n    };\n\n    if (fitView) {\n      const bounds = Rectangle.enlarge([node.getData<TransformData>(TransformData).bounds]).pad(\n        30,\n        30,\n      ); // 留出 30 像素的边界\n\n      const viewport = this.playground.config.getViewport(false);\n\n      const zoom = SizeSchema.fixSize(bounds, viewport);\n\n      scrollConfig.zoom = zoom;\n      scrollConfig.scrollToCenter = true;\n      scrollConfig.easing = true;\n    }\n\n    return this.playground.config.scrollToView(scrollConfig);\n  }\n\n  selectNodeAndFocus(node: WorkflowNodeEntity): void {\n    // 新添加的节点需要被选中\n    this.select(node);\n    // 拖进来需要让画布聚焦, 才能使用快捷键删除\n    this.playground.node.focus();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './workflow-json';\nexport * from './workflow-edge';\nexport * from './workflow-node';\nexport * from './workflow-registry';\nexport * from './workflow-line';\nexport * from './workflow-sub-canvas';\nexport * from './workflow-operation';\nexport * from './workflow-drag';\n\nexport const URLParams = Symbol('');\n\nexport interface URLParams {\n  [key: string]: string;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-drag.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\n\nimport { type PositionSchema } from '@flowgram.ai/utils';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundDrag, type PlaygroundDragEvent } from '@flowgram.ai/core';\n\nimport { type WorkflowLineEntity, type WorkflowPortEntity } from '../entities';\n\nexport interface LineEventProps {\n  type: 'onDrag' | 'onDragEnd';\n  onDragNodeId?: string;\n  event?: MouseEvent;\n}\n\ninterface INodesDragEvent {\n  type: string;\n  nodes: FlowNodeEntity[];\n  startPositions: PositionSchema[];\n  dragEvent: PlaygroundDragEvent;\n  triggerEvent: MouseEvent | React.MouseEvent;\n  dragger: PlaygroundDrag;\n}\n\nexport interface NodesDragStartEvent extends INodesDragEvent {\n  type: 'onDragStart';\n}\n\nexport interface NodesDragEndEvent extends INodesDragEvent {\n  type: 'onDragEnd';\n}\n\nexport interface NodesDraggingEvent extends INodesDragEvent {\n  type: 'onDragging';\n  positions: PositionSchema[];\n}\n\nexport type NodesDragEvent = NodesDragStartEvent | NodesDraggingEvent | NodesDragEndEvent;\n\nexport type onDragLineEndParams = {\n  fromPort?: WorkflowPortEntity;\n  toPort?: WorkflowPortEntity;\n  mousePos: PositionSchema;\n  line?: WorkflowLineEntity;\n  originLine?: WorkflowLineEntity;\n  event: PlaygroundDragEvent;\n};\n\nexport type OnDragLineEnd = (params: onDragLineEndParams) => Promise<void>;\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-edge.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * 边数据\n */\nexport interface WorkflowEdgeJSON {\n  sourceNodeID: string;\n  targetNodeID: string;\n  sourcePortID?: string | number;\n  targetPortID?: string | number;\n  data?: any;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-json.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type WorkflowLineEntity, type WorkflowNodeEntity } from '../entities';\nimport { type WorkflowNodeJSON } from './workflow-node';\nimport { type WorkflowEdgeJSON } from './workflow-edge';\n\nexport interface WorkflowJSON {\n  nodes: WorkflowNodeJSON[];\n  edges: WorkflowEdgeJSON[];\n}\n\nexport enum WorkflowContentChangeType {\n  /**\n   * 添加节点\n   */\n  ADD_NODE = 'ADD_NODE',\n  /**\n   * 删除节点\n   */\n  DELETE_NODE = 'DELETE_NODE',\n  /**\n   * 移动节点\n   */\n  MOVE_NODE = 'MOVE_NODE',\n  /**\n   * 节点数据更新 （表单引擎数据 或者 extInfo 数据）\n   */\n  NODE_DATA_CHANGE = 'NODE_DATA_CHANGE',\n  /**\n   * 添加线条\n   */\n  ADD_LINE = 'ADD_LINE',\n  /**\n   * 删除线条\n   */\n  DELETE_LINE = 'DELETE_LINE',\n  /**\n   * 线条数据修改\n   */\n  LINE_DATA_CHANGE = 'LINE_DATA_CHANGE',\n  /**\n   * 节点Meta信息变更\n   */\n  META_CHANGE = 'META_CHANGE',\n}\n\nexport interface WorkflowContentChangeEvent {\n  type: WorkflowContentChangeType;\n  /**\n   * 当前触发的元素的json数据，toJSON 需要主动触发\n   */\n  toJSON: () => any;\n  /**\n   * oldValue\n   */\n  oldValue?: any;\n  /*\n   * 当前的事件的 entity\n   */\n  entity: WorkflowNodeEntity | WorkflowLineEntity;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Rectangle, IPoint } from '@flowgram.ai/utils';\n\nimport { type WorkflowLineEntity } from '../entities';\n\nexport enum LineType {\n  BEZIER, // 贝塞尔曲线\n  LINE_CHART, // 折叠线\n  STRAIGHT, // 直线\n}\n\nexport type LineRenderType = LineType | string;\n\nexport type LinePointLocation = 'left' | 'top' | 'right' | 'bottom';\n\nexport interface LinePoint {\n  x: number;\n  y: number;\n  location: LinePointLocation;\n}\n\nexport interface LinePosition {\n  from: LinePoint;\n  to: LinePoint;\n}\n\nexport interface LineColor {\n  hidden: string;\n  default: string;\n  drawing: string;\n  hovered: string;\n  selected: string;\n  error: string;\n  flowing: string;\n}\n\nexport enum LineColors {\n  HIDDEN = 'var(--g-workflow-line-color-hidden,transparent)', // 隐藏线条\n  DEFUALT = 'var(--g-workflow-line-color-default,#4d53e8)',\n  DRAWING = 'var(--g-workflow-line-color-drawing, #5DD6E3)', // '#b5bbf8', // '#9197F1',\n  HOVER = 'var(--g-workflow-line-color-hover,#37d0ff)',\n  SELECTED = 'var(--g-workflow-line-color-selected,#37d0ff)',\n  ERROR = 'var(--g-workflow-line-color-error,red)',\n  FLOWING = 'var(--g-workflow-line-color-flowing,#4d53e8)', // 流动线条，默认使用主题色\n}\n\nexport interface LineCenterPoint {\n  x: number;\n  y: number;\n  labelX: number; // Relative to where the line begins\n  labelY: number; // Relative to where the line begins\n}\n\nexport interface WorkflowLineRenderContribution {\n  entity: WorkflowLineEntity;\n  path: string;\n  center?: LineCenterPoint;\n  bounds: Rectangle;\n  update: (params: { fromPos: LinePoint; toPos: LinePoint }) => void;\n  calcDistance: (pos: IPoint) => number;\n}\n\nexport type WorkflowLineRenderContributionFactory = (new (\n  entity: WorkflowLineEntity\n) => WorkflowLineRenderContribution) & {\n  type: LineRenderType;\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IPoint } from '@flowgram.ai/utils';\nimport type { FlowNodeJSON, FlowNodeMeta } from '@flowgram.ai/document';\n\nimport type { WorkflowNodeEntity, WorkflowPorts } from '../entities';\nimport type { WorkflowSubCanvas } from './workflow-sub-canvas';\nimport type { WorkflowEdgeJSON } from './workflow-edge';\n\n/**\n * 节点 meta 信息\n */\nexport interface WorkflowNodeMeta extends FlowNodeMeta {\n  position?: IPoint;\n  canvasPosition?: IPoint; // 子画布位置\n  deleteDisable?: boolean; // 是否禁用删除\n  copyDisable?: boolean; // 禁用复制\n  inputDisable?: boolean; // 禁用输入点\n  outputDisable?: boolean; // 禁用输出点\n  defaultPorts?: WorkflowPorts; // 默认点位\n  useDynamicPort?: boolean; // 使用动态点位，会计算 data-port-key\n  subCanvas?: (node: WorkflowNodeEntity) => WorkflowSubCanvas | undefined;\n  isContainer?: boolean; // 是否容器节点\n}\n\n/**\n * 节点数据\n */\nexport interface WorkflowNodeJSON extends FlowNodeJSON {\n  id: string;\n  type: string | number;\n  /**\n   * ui 数据\n   */\n  meta?: WorkflowNodeMeta;\n  /**\n   * 表单数据\n   */\n\n  data?: any;\n  /**\n   * 子节点\n   */\n  blocks?: WorkflowNodeJSON[];\n  /**\n   * 子节点间连线\n   */\n  edges?: WorkflowEdgeJSON[];\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-operation.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Event } from '@flowgram.ai/utils';\nimport {\n  FlowNodeEntity,\n  FlowNodeEntityOrId,\n  FlowOperationBaseService,\n} from '@flowgram.ai/document';\n\nimport { WorkflowJSON } from './workflow-json';\n\nexport interface NodePostionUpdateEvent {\n  node: FlowNodeEntity;\n  oldPosition: IPoint;\n  newPosition: IPoint;\n}\n\nexport interface WorkflowOperationBaseService extends FlowOperationBaseService {\n  /**\n   * 节点位置更新事件\n   */\n  readonly onNodePostionUpdate: Event<NodePostionUpdateEvent>;\n  /**\n   * 更新节点位置\n   * @param nodeOrId\n   * @param position\n   * @returns\n   */\n  updateNodePosition(nodeOrId: FlowNodeEntityOrId, position: IPoint): void;\n\n  /**\n   * 更新节点与线条\n   */\n  fromJSON(json: WorkflowJSON): void;\n}\n\nexport const WorkflowOperationBaseService = Symbol('WorkflowOperationBaseService');\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FormMeta } from '@flowgram.ai/node';\nimport type { FormMetaOrFormMetaGenerator } from '@flowgram.ai/form-core';\nimport type { FlowNodeRegistry } from '@flowgram.ai/document';\n\nimport type { WorkflowNodeEntity, WorkflowPortEntity } from '../entities';\nimport type { WorkflowNodeMeta } from './workflow-node';\nimport type { WorkflowLinesManager } from '../workflow-lines-manager';\n\n/**\n * 节点表单引擎配置\n */\nexport type WorkflowNodeFormMeta = FormMetaOrFormMetaGenerator | FormMeta;\n\n/**\n * 节点注册\n */\nexport interface WorkflowNodeRegistry extends FlowNodeRegistry<WorkflowNodeMeta> {\n  formMeta?: WorkflowNodeFormMeta;\n  canAddLine?: (\n    fromPort: WorkflowPortEntity,\n    toPort: WorkflowPortEntity,\n    lines: WorkflowLinesManager,\n    silent?: boolean\n  ) => boolean;\n}\n\nexport interface WorkflowNodeRenderProps {\n  node: WorkflowNodeEntity;\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/typings/workflow-sub-canvas.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeEntity } from '../entities';\n\n/**\n * 子画布配置\n */\nexport type WorkflowSubCanvas = {\n  isCanvas: boolean; // 是否画布节点\n  parentNode: WorkflowNodeEntity; // 父节点\n  canvasNode: WorkflowNodeEntity; // 画布节点\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/build-group-json.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\nimport { WorkflowJSON, WorkflowNodeJSON } from '../typings';\n\ninterface WorkflowGroupJSON extends WorkflowNodeJSON {\n  data: {\n    parentID?: string;\n    blockIDs?: string[];\n  };\n}\n\nexport const buildGroupJSON = (json: WorkflowJSON): WorkflowJSON => {\n  const { nodes, edges } = json;\n  const groupJSONs = nodes.filter(\n    (nodeJSON) => nodeJSON.type === FlowNodeBaseType.GROUP\n  ) as WorkflowGroupJSON[];\n\n  const nodeJSONMap = new Map<string, WorkflowNodeJSON>(nodes.map((n) => [n.id, n]));\n  const groupNodeJSONs = groupJSONs.map((groupJSON): WorkflowNodeJSON => {\n    const groupBlocks = (groupJSON.data.blockIDs ?? [])\n      .map((blockID) => nodeJSONMap.get(blockID))\n      .filter(Boolean) as WorkflowNodeJSON[];\n    const groupEdges = edges?.filter((edge) =>\n      groupBlocks.some((block) => block.id === edge.sourceNodeID || block.id === edge.targetNodeID)\n    );\n    const groupNodeJSON: WorkflowNodeJSON = {\n      ...groupJSON,\n      blocks: groupBlocks,\n      edges: groupEdges,\n    };\n    return groupNodeJSON;\n  });\n\n  const groupBlockSet = new Set(groupJSONs.map((groupJSON) => groupJSON.data.blockIDs).flat());\n  const processedNodes = nodes\n    .filter((nodeJSON) => !groupBlockSet.has(nodeJSON.id))\n    .concat(groupNodeJSONs);\n  return {\n    nodes: processedNodes,\n    edges,\n  };\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/compose.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { compose, composeAsync } from '@flowgram.ai/utils';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/fit-view.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { type PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';\n\nimport { type WorkflowDocument } from '../workflow-document';\n\nexport const fitView = (\n  doc: WorkflowDocument,\n  playgroundConfig: PlaygroundConfigEntity,\n  easing = true\n) => {\n  const bounds = Rectangle.enlarge(\n    doc.getAllNodes().map((node) => node.getData<TransformData>(TransformData).bounds)\n  );\n  // 留出 30 像素的边界\n  return playgroundConfig.fitView(bounds, easing, 30);\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/flow-node-form-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeFormData } from '@flowgram.ai/form-core';\nimport { FlowNodeEntity, FlowNodeJSON } from '@flowgram.ai/document';\n\nimport { type WorkflowDocument } from '../workflow-document';\nimport { WorkflowContentChangeType, type WorkflowNodeRegistry } from '../typings';\n\nexport function getFlowNodeFormData(node: FlowNodeEntity) {\n  return node.getData(FlowNodeFormData) as FlowNodeFormData;\n}\n\nexport function toFormJSON(node: FlowNodeEntity) {\n  const formData = node.getData(FlowNodeFormData) as FlowNodeFormData;\n  if (!formData || !(node.getNodeRegistry() as WorkflowNodeRegistry).formMeta) return undefined;\n  return formData.toJSON();\n}\n\nexport function initFormDataFromJSON(\n  node: FlowNodeEntity,\n  json: FlowNodeJSON,\n  isFirstCreate: boolean\n) {\n  const formData = node.getData(FlowNodeFormData)!;\n  const registry = node.getNodeRegistry();\n  const { formMeta } = registry;\n\n  if (formData && formMeta) {\n    if (isFirstCreate) {\n      formData.createForm(formMeta, json.data);\n      formData.onDataChange(() => {\n        (node.document as WorkflowDocument).fireContentChange({\n          type: WorkflowContentChangeType.NODE_DATA_CHANGE,\n          toJSON: () => formData.toJSON(),\n          entity: node,\n        });\n      });\n    } else {\n      formData.updateFormValues(json.data);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/get-anti-overlap-position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TransformData } from '@flowgram.ai/core';\nimport { type IPoint } from '@flowgram.ai/utils';\n\nimport { type WorkflowDocument } from '../workflow-document';\nimport { WorkflowNodeEntity } from '../entities';\n\n/**\n * 获取没有碰撞的位置\n * 距离很小时，xy 各偏移 30\n * @param position\n */\nexport function getAntiOverlapPosition(\n  doc: WorkflowDocument,\n  position: IPoint,\n  containerNode?: WorkflowNodeEntity,\n): IPoint {\n  let { x, y } = position;\n  const nodes = containerNode ? containerNode.collapsedChildren : doc.getAllNodes();\n  const positions = nodes\n    .map(n => {\n      const transform = n.getData<TransformData>(TransformData)!;\n      return { x: transform.position.x, y: transform.position.y };\n    })\n    .sort((a, b) => a.y - b.y);\n  const minDistance = 3;\n  for (const pos of positions) {\n    const { x: posX, y: posY } = pos;\n    if (y - posY < -minDistance) {\n      break;\n    }\n    const deltaX = Math.abs(x - posX);\n    const deltaY = Math.abs(y - posY);\n    if (deltaX <= minDistance && deltaY <= minDistance) {\n      x += 30;\n      y += 30;\n    }\n  }\n  return { x, y };\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/get-line-center.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\n\nimport { LineCenterPoint } from '../typings';\n\nexport function getLineCenter(\n  from: IPoint,\n  to: IPoint,\n  bbox: Rectangle,\n  linePadding: number\n): LineCenterPoint {\n  return {\n    x: bbox.center.x,\n    y: bbox.center.y,\n    labelX: bbox.center.x - bbox.x + linePadding,\n    labelY: bbox.center.y - bbox.y + linePadding,\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/get-url-params.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function getUrlParams(): Record<string, string> {\n  const paramsMap = new Map<string, string>();\n\n  location.search\n    .replace(/^\\?/, '')\n    .split('&')\n    .forEach((key) => {\n      if (!key) return;\n\n      const [k, v] = key.split('=');\n\n      if (k) {\n        // Decode URL-encoded parameter names and values\n        const decodedKey = decodeURIComponent(k.trim());\n        const decodedValue = v ? decodeURIComponent(v.trim()) : '';\n\n        // Prevent prototype pollution by filtering dangerous property names\n        const dangerousProps = [\n          '__proto__',\n          'constructor',\n          'prototype',\n          '__defineGetter__',\n          '__defineSetter__',\n          '__lookupGetter__',\n          '__lookupSetter__',\n          'hasOwnProperty',\n          'isPrototypeOf',\n          'propertyIsEnumerable',\n          'toString',\n          'valueOf',\n          'toLocaleString',\n        ];\n\n        if (dangerousProps.includes(decodedKey.toLowerCase())) {\n          return;\n        }\n\n        // Use Map to prevent prototype pollution\n        paramsMap.set(decodedKey, decodedValue);\n      }\n    });\n\n  // Convert Map to plain object while maintaining API compatibility\n  return Object.fromEntries(paramsMap);\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { bindConfigEntity } from '@flowgram.ai/core';\nexport { delay } from '@flowgram.ai/utils';\n\n/**\n * 让 entity 可以注入到类中\n *\n * @example\n * ```\n *    class SomeClass {\n *      @inject(PlaygroundConfigEntity) playgroundConfig: PlaygroundConfigEntity\n *    }\n * ```\n * @param bind\n * @param entityRegistry\n */\nexport { bindConfigEntity };\n\nexport { buildGroupJSON } from './build-group-json';\nexport { getLineCenter } from './get-line-center';\nexport * from './nanoid';\nexport * from './compose';\nexport * from './fit-view';\nexport * from './get-anti-overlap-position';\nexport * from './statics';\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/layout-to-positions.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport { TransformData, startTween } from '@flowgram.ai/core';\n\nimport { type WorkflowDocument } from '../workflow-document';\nimport { type WorkflowNodeEntity } from '../entities';\n\n/**\n * Coze 中节点坐标，以卡片顶部中间为原点。\n * autoLayout 计算出来的对齐的坐标以节点正中为原点，需要上移当前节点一般高度。\n * 即： newPosition.y - transform.bounds.height / 2\n * bounds 的原点坐标为左上角。\n */\nexport const layoutToPositions = async (\n  nodes: WorkflowNodeEntity[],\n  nodePositionMap: Record<string, IPoint>\n): Promise<Record<string, IPoint>> => {\n  // 缓存上次位置，用来还原位置\n  const newNodePositionMap: Record<string, IPoint> = {};\n  nodes.forEach((node) => {\n    const transform = node.getData(TransformData);\n    const nodeTransform = node.getData(FlowNodeTransformData);\n\n    newNodePositionMap[node.id] = {\n      x: transform.position.x,\n      y: transform.position.y + nodeTransform.bounds.height / 2,\n    };\n  });\n\n  return new Promise((resolve) => {\n    startTween({\n      from: { d: 0 },\n      to: { d: 100 },\n      duration: 300,\n      onUpdate: (v) => {\n        nodes.forEach((node) => {\n          const transform = node.getData(TransformData);\n          const deltaX = ((nodePositionMap[node.id].x - transform.position.x) * v.d) / 100;\n          const deltaY =\n            ((nodePositionMap[node.id].y - transform.bounds.height / 2 - transform.position.y) *\n              v.d) /\n            100;\n\n          transform.update({\n            position: {\n              x: transform.position.x + deltaX,\n              y: transform.position.y + deltaY,\n            },\n          });\n\n          const document = node.document as WorkflowDocument;\n          document.layout.updateAffectedTransform(node);\n        });\n      },\n      onComplete: () => {\n        resolve(newNodePositionMap);\n      },\n    });\n  });\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/location-config-to-point.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle, IPoint } from '@flowgram.ai/utils';\n\nimport { WorkflowPort } from '../entities';\n\nexport function locationConfigToPoint(\n  bounds: Rectangle,\n  config: Required<WorkflowPort>['locationConfig'],\n  _offset: IPoint = { x: 0, y: 0 }\n): IPoint {\n  const offset = { ..._offset };\n  if (config.left !== undefined) {\n    offset.x +=\n      typeof config.left === 'string' ? parseFloat(config.left) * 0.01 * bounds.width : config.left;\n  } else if (config.right !== undefined) {\n    offset.x +=\n      bounds.width -\n      (typeof config.right === 'string'\n        ? parseFloat(config.right) * 0.01 * bounds.width\n        : config.right);\n  }\n  if (config.top !== undefined) {\n    offset.y +=\n      typeof config.top === 'string' ? parseFloat(config.top) * 0.01 * bounds.height : config.top;\n  } else if (config.bottom !== undefined) {\n    offset.y +=\n      bounds.height -\n      (typeof config.bottom === 'string'\n        ? parseFloat(config.bottom) * 0.01 * bounds.height\n        : config.bottom);\n  }\n  return {\n    x: bounds.x + offset.x,\n    y: bounds.y + offset.y,\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/nanoid.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid as nanoidOrigin } from 'nanoid';\n\nexport function nanoid(n?: number): string {\n  return nanoidOrigin(n);\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/utils/statics.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\n\nimport { type WorkflowNodeEntity } from '../entities/workflow-node-entity';\nexport type WorkflowPortType = 'input' | 'output';\n\nexport const getPortEntityId = (\n  node: WorkflowNodeEntity,\n  portType: WorkflowPortType,\n  portID: string | number = '',\n): string => `port_${portType}_${node.id}_${portID}`;\n\nexport const WORKFLOW_LINE_ENTITY = 'WorkflowLineEntity';\n\nexport function domReactToBounds(react: DOMRect): Rectangle {\n  return new Rectangle(react.x, react.y, react.width, react.height);\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-commands.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowCommands {\n  DELETE_NODES = 'DELETE_NODES',\n  COPY_NODES = 'COPY_NODES',\n  PASTE_NODES = 'PASTE_NODES',\n  ZOOM_IN = 'ZOOM_IN',\n  ZOOM_OUT = 'ZOOM_OUT',\n  UNDO = 'UNDO',\n  REDO = 'REDO',\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-document-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\nimport { bindContributions } from '@flowgram.ai/utils';\nimport { FlowDocument, FlowDocumentContribution } from '@flowgram.ai/document';\n\nimport { WorkflowLinesManager } from './workflow-lines-manager';\nimport {\n  WorkflowDocumentOptions,\n  WorkflowDocumentOptionsDefault,\n} from './workflow-document-option';\nimport { WorkflowDocumentContribution } from './workflow-document-contribution';\nimport { WorkflowDocument, WorkflowDocumentProvider } from './workflow-document';\nimport { getUrlParams } from './utils/get-url-params';\nimport { URLParams, WorkflowOperationBaseService } from './typings';\nimport {\n  WorkflowDragService,\n  WorkflowHoverService,\n  WorkflowSelectService,\n  WorkflowResetLayoutService,\n  WorkflowOperationBaseServiceImpl,\n} from './service';\nimport { FreeLayout } from './layout';\n\nexport const WorkflowDocumentContainerModule = new ContainerModule(\n  (bind, unbind, isBound, rebind) => {\n    bind(WorkflowDocument).toSelf().inSingletonScope();\n    bind(WorkflowLinesManager).toSelf().inSingletonScope();\n    bind(FreeLayout).toSelf().inSingletonScope();\n    bind(WorkflowDragService).toSelf().inSingletonScope();\n    bind(WorkflowSelectService).toSelf().inSingletonScope();\n    bind(WorkflowHoverService).toSelf().inSingletonScope();\n    bind(WorkflowResetLayoutService).toSelf().inSingletonScope();\n    bind(WorkflowOperationBaseService).to(WorkflowOperationBaseServiceImpl).inSingletonScope();\n    bind(URLParams)\n      .toDynamicValue(() => getUrlParams())\n      .inSingletonScope();\n    bindContributions(bind, WorkflowDocumentContribution, [FlowDocumentContribution]);\n    bind(WorkflowDocumentOptions).toConstantValue({\n      ...WorkflowDocumentOptionsDefault,\n    });\n    rebind(FlowDocument).toService(WorkflowDocument);\n    bind(WorkflowDocumentProvider)\n      .toDynamicValue((ctx) => () => ctx.container.get(WorkflowDocument))\n      .inSingletonScope();\n  }\n);\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-document-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\nimport {\n  type FlowDocumentContribution,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\n\nimport { WorkflowDocument } from './workflow-document';\nimport { FreeLayout } from './layout';\nimport { WorkflowNodeLinesData, WorkflowNodePortsData } from './entity-datas';\n\n@injectable()\nexport class WorkflowDocumentContribution implements FlowDocumentContribution<WorkflowDocument> {\n  @inject(FreeLayout) freeLayout: FreeLayout;\n\n  registerDocument(document: WorkflowDocument): void {\n    // 注册节点数据\n    document.registerNodeDatas(\n      FlowNodeTransformData,\n      FlowNodeRenderData,\n      WorkflowNodePortsData,\n      WorkflowNodeLinesData,\n    );\n    document.registerLayout(this.freeLayout);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-document-option.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeErrorData } from '@flowgram.ai/form-core';\nimport { FlowDocumentOptions, FlowNodeTransformData, FlowNodeType } from '@flowgram.ai/document';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { type WorkflowLinesManager } from './workflow-lines-manager';\nimport { initFormDataFromJSON, toFormJSON } from './utils/flow-node-form-data';\nimport {\n  LineColor,\n  LineRenderType,\n  onDragLineEndParams,\n  WorkflowNodeJSON,\n  WorkflowNodeMeta,\n} from './typings';\nimport {\n  type WorkflowLineEntity,\n  type WorkflowLinePortInfo,\n  type WorkflowNodeEntity,\n  type WorkflowPortEntity,\n} from './entities';\n\nexport const WorkflowDocumentOptions = Symbol('WorkflowDocumentOptions');\n\n/**\n * 线条配置\n */\nexport interface WorkflowDocumentOptions extends FlowDocumentOptions {\n  cursors?: {\n    grab?: string;\n    grabbing?: string;\n  };\n  /** 双向连接 */\n  twoWayConnection?: boolean;\n  /** 允许拖拽只读节点 */\n  enableReadonlyNodeDragging?: boolean;\n  /** 线条颜色 */\n  lineColor?: Partial<LineColor>;\n  /** 是否显示错误线条 */\n  isErrorLine?: (\n    fromPort: WorkflowPortEntity | undefined,\n    toPort: WorkflowPortEntity | undefined,\n    lines: WorkflowLinesManager\n  ) => boolean;\n  /** 是否错误端口 */\n  isErrorPort?: (port: WorkflowPortEntity) => boolean;\n  /** 是否禁用端口 */\n  isDisabledPort?: (port: WorkflowPortEntity) => boolean;\n  /** 是否反转线条箭头 */\n  isReverseLine?: (line: WorkflowLineEntity) => boolean;\n  /** 是否隐藏线条箭头 */\n  isHideArrowLine?: (line: WorkflowLineEntity) => boolean;\n  /** 是否流动线条 */\n  isFlowingLine?: (line: WorkflowLineEntity) => boolean;\n  /** 是否禁用线条 */\n  isDisabledLine?: (line: WorkflowLineEntity) => boolean;\n  /** 拖拽线条结束 */\n  onDragLineEnd?: (params: onDragLineEndParams) => Promise<void>;\n  /** 获取线条渲染器 */\n  setLineRenderType?: (line: WorkflowLineEntity) => LineRenderType | undefined;\n  /** 设置线条样式 */\n  setLineClassName?: (line: WorkflowLineEntity) => string | undefined;\n  /** 能否添加线条 */\n  canAddLine?: (\n    fromPort: WorkflowPortEntity,\n    toPort: WorkflowPortEntity,\n    lines: WorkflowLinesManager,\n    silent?: boolean\n  ) => boolean;\n  /** 能否删除节点 */\n  canDeleteNode?: (node: WorkflowNodeEntity, silent?: boolean) => boolean;\n  /** 能否删除线条 */\n  canDeleteLine?: (\n    line: WorkflowLineEntity,\n    newLineInfo?: Required<Omit<WorkflowLinePortInfo, 'data'>>,\n    silent?: boolean\n  ) => boolean;\n  /**\n   * @param fromPort - 开始点\n   * @param oldToPort - 旧的连接点\n   * @param newToPort - 新的连接点\n   * @param lines - 线条管理器\n   */\n  canResetLine?: (\n    oldLine: WorkflowLineEntity,\n    newLineInfo: Required<WorkflowLinePortInfo>,\n    lines: WorkflowLinesManager\n  ) => boolean;\n  /**\n   * 是否允许拖入子画布 (loop or group)\n   * Whether to allow dragging into the sub-canvas (loop or group)\n   * @param params\n   */\n  canDropToNode?: (params: {\n    dragNodeType?: FlowNodeType;\n    dragNode?: WorkflowNodeEntity;\n    dropNode?: WorkflowNodeEntity;\n    dropNodeType?: FlowNodeType;\n  }) => boolean;\n}\n\nexport const WorkflowDocumentOptionsDefault: WorkflowDocumentOptions = {\n  // cursors: {\n  //   grab: 'url(\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMCAyMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMC40ODczIDIuNjIzNzhDOS45MDczMSAyLjYyMzc4IDkuNDM3MTMgMy4wOTM5NiA5LjQzNzEzIDMuNjczOTZWNS4xNDM3NkM5LjM5NDI4IDQuNDAyNzQgOC43Nzk3OCAzLjgxNTA0IDguMDI4MDIgMy44MTUwNEM3LjI0ODQ4IDMuODE1MDQgNi42MTY1MyA0LjQ0Njk5IDYuNjE2NTMgNS4yMjY1M1YxMS44Mjg5TDUuNjc0MTggMTEuMDA0OUM1LjE1NDg3IDEwLjU1MDkgNC40MDk1IDEwLjQ2MzYgMy43OTkzOCAxMC43ODU1TDMuNjk2OTQgMTAuODM5NkMzLjA2MjE3IDExLjE3NDUgMi45MjI2IDEyLjAyMjggMy40MTY2MiAxMi41NDM0TDcuMzM5NTkgMTYuNjc3NVYxNy4zMjU5QzcuMzM5NTkgMTcuNzg2MiA3LjcxMjY5IDE4LjE1OTMgOC4xNzI5MiAxOC4xNTkzSDEzLjgwODRDMTQuMjY4NyAxOC4xNTkzIDE0LjY0MTcgMTcuNzg2MiAxNC42NDE3IDE3LjMyNTlWMTYuNzkzNUMxNS44MDk0IDE1LjY0ODUgMTYuNDY3MyAxNC4wODE5IDE2LjQ2NzMgMTIuNDQ2NVYxMS40OTY3TDE2LjQ2NzEgNi42MzY4NUMxNi40NjcxIDUuOTU2MyAxNS45MTU0IDUuNDA0NjEgMTUuMjM0OCA1LjQwNDYxQzE0LjU1NDMgNS40MDQ2MSAxNC4wMDI2IDUuOTU2MyAxNC4wMDI2IDYuNjM2ODVMMTQuMDAyMSA1LjA0NzI4QzE0LjAwMjEgNC4zNjY3MyAxMy40NTA0IDMuODE1MDQgMTIuNzY5OCAzLjgxNTA0QzEyLjA4OTMgMy44MTUwNCAxMS41Mzc2IDQuMzY2NzMgMTEuNTM3NiA1LjA0NzI4TDExLjUzNzUgMy42NzM5NUMxMS41Mzc1IDMuMDkzOTYgMTEuMDY3MyAyLjYyMzc4IDEwLjQ4NzMgMi42MjM3OFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAuNDg3NCAxLjM3NDAyQzExLjM2MTIgMS4zNzQwMiAxMi4xMjExIDEuODYxMTggMTIuNTEwNSAyLjU3ODY4QzEyLjU5NTggMi41Njk4MyAxMi42ODIzIDIuNTY1MjggMTIuNzcgMi41NjUyOEMxMy44Mjc4IDIuNTY1MjggMTQuNzMxMSAzLjIyNzAxIDE1LjA4ODUgNC4xNTkxMUMxNS4xMzcgNC4xNTYyOSAxNS4xODU4IDQuMTU0ODYgMTUuMjM1IDQuMTU0ODZDMTYuNjA1OSA0LjE1NDg2IDE3LjcxNzIgNS4yNjYxOSAxNy43MTcyIDYuNjM3MDlMMTcuNzE3NCAxMi40NDY3QzE3LjcxNzQgMTQuMjM1NSAxNy4wNjQ0IDE1Ljk1NTkgMTUuODkxOSAxNy4yOTA0VjE3LjMyNjJDMTUuODkxOSAxOC40NzY4IDE0Ljk1OTEgMTkuNDA5NSAxMy44MDg1IDE5LjQwOTVIOC4xNzMwNkM3LjAyMjQ3IDE5LjQwOTUgNi4wODk3MyAxOC40NzY4IDYuMDg5NzMgMTcuMzI2MlYxNy4xNzY0TDIuNTEwMDMgMTMuNDA0MUMxLjQ0NTk5IDEyLjI4MjggMS43NDY2IDEwLjQ1NTUgMy4xMTM3OSA5LjczNDI0TDMuMjE2MjQgOS42ODAxOUMzLjg5MTY4IDkuMzIzODMgNC42NjE4NSA5LjI1NDAxIDUuMzY2NjYgOS40NTE5OFY1LjIyNjc4QzUuMzY2NjYgMy43NTY4NyA2LjU1ODI2IDIuNTY1MjggOC4wMjgxNiAyLjU2NTI4QzguMTcyOTMgMi41NjUyOCA4LjMxNDk5IDIuNTc2ODQgOC40NTM0NyAyLjU5OTA3QzguODM5NDMgMS44NzA0MiA5LjYwNTQ2IDEuMzc0MDIgMTAuNDg3NCAxLjM3NDAyWk0xMi40NDc2IDMuODU3ODdWOS40NzY0NkMxMi40NDc2IDkuNzI4NTIgMTIuMjQzMyA5LjkzMjg1IDExLjk5MTMgOS45MzI4NUMxMS43MzkyIDkuOTMyODUgMTEuNTM0OSA5LjcyODUyIDExLjUzNDkgOS40NzY0NlYzLjc5NjU1QzExLjUzNDkgMy43Nzk1NiAxMS41MzU4IDMuNzYyNzcgMTEuNTM3NiAzLjc0NjI2VjMuNjc0MkMxMS41Mzc2IDMuNDMyODIgMTEuNDU2MiAzLjIxMDQ2IDExLjMxOTMgMy4wMzMwOUMxMS4xMjcyIDIuNzg0MjggMTAuODI2MSAyLjYyNDAyIDEwLjQ4NzQgMi42MjQwMkMxMC4xMjM4IDIuNjI0MDIgOS44MDMzMiAyLjgwODg2IDkuNjE0ODMgMy4wODk3QzkuNTAyNjkgMy4yNTY3OSA5LjQzNzI2IDMuNDU3ODUgOS40MzcyNiAzLjY3NDJWMy43ODU3M0M5LjQzNzM1IDMuNzg5MzMgOS40MzczOSAzLjc5Mjk0IDkuNDM3MzkgMy43OTY1NVY5LjkwMTdDOS40MzczOSAxMC4xNTM3IDkuMjMzMDYgMTAuMzU4MSA4Ljk4MTAxIDEwLjM1ODFDOC43Mjg5NSAxMC4zNTgxIDguNTI0NjIgMTAuMTUzNyA4LjUyNDYyIDkuOTAxN1YzLjkwNTA3QzguNDE3NzMgMy44NjQ5IDguMzA0NjggMy44MzczMiA4LjE4NzI2IDMuODI0MTVDOC4xMzUwNCAzLjgxODI5IDguMDgxOTUgMy44MTUyOCA4LjAyODE2IDMuODE1MjhDNy4yNDg2MSAzLjgxNTI4IDYuNjE2NjYgNC40NDcyMyA2LjYxNjY2IDUuMjI2NzhWMTEuODI5Mkw1LjY3NDMxIDExLjAwNTJDNS41Nzg2OCAxMC45MjE2IDUuNDc1MzcgMTAuODUwNCA1LjM2NjY2IDEwLjc5MTlDNC44ODUwNiAxMC41MzI5IDQuMjk3MjggMTAuNTIzMSAzLjc5OTUyIDEwLjc4NThMMy42OTcwNyAxMC44Mzk4QzMuMDYyMzEgMTEuMTc0NyAyLjkyMjczIDEyLjAyMzEgMy40MTY3NSAxMi41NDM3TDcuMzM5NzMgMTYuNjc3N1YxNy4zMjYyQzcuMzM5NzMgMTcuNzg2NCA3LjcxMjgyIDE4LjE1OTUgOC4xNzMwNiAxOC4xNTk1SDEzLjgwODVDMTQuMjY4OCAxOC4xNTk1IDE0LjY0MTkgMTcuNzg2NCAxNC42NDE5IDE3LjMyNjJWMTYuNzkzOEMxNS43Mzc5IDE1LjcxOSAxNi4zODQ3IDE0LjI3MjggMTYuNDYgMTIuNzQ3QzE2LjQ2NDEgMTIuNjY0MSAxNi40NjY1IDEyLjU4MDkgMTYuNDY3MiAxMi40OTc1TDE2LjQ2NzQgMTIuNDQ2N0wxNi40NjcyIDYuNjM3MDlDMTYuNDY3MiA1Ljk2MjMgMTUuOTI0OCA1LjQxNDE5IDE1LjI1MjIgNS40MDQ5N0wxNS4yMzUgNS40MDQ4NkMxNS4xMjQ2IDUuNDA0ODYgMTUuMDE3NyA1LjQxOTM2IDE0LjkxNTkgNS40NDY1NlY5LjYwMjI2QzE0LjkxNTkgOS44NTQzMSAxNC43MTE2IDEwLjA1ODYgMTQuNDU5NSAxMC4wNTg2QzE0LjIwNzUgMTAuMDU4NiAxNC4wMDMxIDkuODU0MzEgMTQuMDAzMSA5LjYwMjI2VjYuNjA1MTRDMTQuMDAyOSA2LjYxNTc2IDE0LjAwMjcgNi42MjY0MSAxNC4wMDI3IDYuNjM3MDlWOS4yNzcwNUwxNC4wMDIyIDUuMDQ3NTJDMTQuMDAyMiA0Ljg2OTEzIDEzLjk2NDMgNC42OTk2IDEzLjg5NjEgNC41NDY1M0MxMy43MDY0IDQuMTIwNzIgMTMuMjgyMiAzLjgyMjM1IDEyLjc4NzYgMy44MTU0MUwxMi43NyAzLjgxNTI4QzEyLjY1ODQgMy44MTUyOCAxMi41NTA0IDMuODMwMSAxMi40NDc2IDMuODU3ODdaIiBmaWxsPSIjMUQxQzIzIi8+Cjwvc3ZnPg==\"), auto',\n  //   grabbing:\n  //     'url(\"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjEiIHZpZXdCb3g9IjAgMCAyMCAyMSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik02LjYxODE3IDUuNTk4NzVDNi42MTgxNyA0LjgxOTIgNy4yNTAxMiA0LjE4NzI2IDguMDI5NjcgNC4xODcyNkM4Ljc3ODczIDQuMTg3MjYgOS4zOTE1MiA0Ljc3MDc1IDkuNDM4MjkgNS41MDgwMUM5LjQ1OTkyIDQuOTQ3MSA5LjkyMTQ3IDQuNDk5MDIgMTAuNDg3NyA0LjQ5OTAyQzExLjA2NzcgNC40OTkwMiAxMS41Mzc4IDQuOTY5MiAxMS41Mzc4IDUuNTQ5MTlWOC43NjI0NkwxMS41Mzc5IDYuNzExNUMxMS41Mzc5IDYuMDMwOTUgMTIuMDg5NiA1LjQ3OTI2IDEyLjc3MDIgNS40NzkyNkMxMy40NTA3IDUuNDc5MjYgMTQuMDAyNCA2LjAzMDk1IDE0LjAwMjQgNi43MTE1TDE0LjAwMjQgOC43NjI0NkwxNC4wMDI5IDguMDE5ODNDMTQuMDAyOSA3LjMzOTI5IDE0LjU1NDYgNi43ODc1OSAxNS4yMzUyIDYuNzg3NTlDMTUuOTE1NyA2Ljc4NzU5IDE2LjQ2NzQgNy4zMzkyOCAxNi40Njc0IDguMDE5ODNWMTEuNDk3TDE2LjQ2NzUgMTIuNDQ2N0MxNi40Njc1IDE0LjA4MjEgMTUuODA5NiAxNS42NDg3IDE0LjY0MiAxNi43OTM4VjE3LjMyNjJDMTQuNjQyIDE3Ljc4NjQgMTQuMjY4OSAxOC4xNTk1IDEzLjgwODcgMTguMTU5NUg4LjE3MzE3QzcuNzEyOTMgMTguMTU5NSA3LjMzOTg0IDE3Ljc4NjQgNy4zMzk4NCAxNy4zMjYyVjE1Ljk0MjRMNS4zNDU2MiAxNC43NTM0QzQuNTg5MjQgMTQuMzAyNCA0LjEyNTkxIDEzLjQ4NjcgNC4xMjU4OSAxMi42MDYxTDQuMTI1ODMgOS4yODM4M0M0LjEyNTgyIDguOTU0MjcgNC4zMjAwMyA4LjY1NTY2IDQuNjIxMjkgOC41MjIwNUw2LjYxODE3IDcuNjM2MzRWNS41OTg3NVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTAuNDg3OCAzLjI0OTAyQzExLjI3OTYgMy4yNDkwMiAxMS45Nzc4IDMuNjQ5MDIgMTIuMzkxNyA0LjI1Nzk2QzEyLjUxNTEgNC4yMzkwNiAxMi42NDE2IDQuMjI5MjYgMTIuNzcwMyA0LjIyOTI2QzEzLjcyMjQgNC4yMjkyNiAxNC41NDkzIDQuNzY1MzEgMTQuOTY1NyA1LjU1MjA3QzE1LjA1NDMgNS41NDI1IDE1LjE0NDIgNS41Mzc1OSAxNS4yMzUzIDUuNTM3NTlDMTYuNjA2MiA1LjUzNzU5IDE3LjcxNzYgNi42NDg5MyAxNy43MTc2IDguMDE5ODNMMTcuNzE3NyAxMi40NDY3QzE3LjcxNzcgMTQuMjM1NSAxNy4wNjQ3IDE1Ljk1NTkgMTUuODkyMSAxNy4yOTA0VjE3LjMyNjJDMTUuODkyMSAxOC40NzY4IDE0Ljk1OTQgMTkuNDA5NSAxMy44MDg4IDE5LjQwOTVIOC4xNzMzMkM3LjAyMjczIDE5LjQwOTUgNi4wODk5OCAxOC40NzY4IDYuMDg5OTggMTcuMzI2MlYxNi42NTI0TDQuNzA1NjMgMTUuODI3QzMuNTcxMDYgMTUuMTUwNSAyLjg3NjA3IDEzLjkyNzEgMi44NzYwNCAxMi42MDYxTDIuODc1OTggOS4yODM4NUMyLjg3NTk2IDguNDU5OTYgMy4zNjE0OSA3LjcxMzQ1IDQuMTE0NjIgNy4zNzk0TDUuMzY4MzIgNi44MjMzM1Y1LjU5ODc1QzUuMzY4MzIgNC4xMjg4NSA2LjU1OTkxIDIuOTM3MjYgOC4wMjk4MiAyLjkzNzI2QzguNjA4MzEgMi45MzcyNiA5LjE0MzU1IDMuMTIxNyA5LjU4MDA1IDMuNDM1MDVDOS44NTg1MyAzLjMxNTMyIDEwLjE2NTQgMy4yNDkwMiAxMC40ODc4IDMuMjQ5MDJaTTEyLjQ0NzkgNS41MjE4NlY5LjQ3NTU3QzEyLjQ0NzkgOS43Mjc2MiAxMi4yNDM2IDkuOTMxOTUgMTEuOTkxNiA5LjkzMTk1QzExLjc1NjggOS45MzE5NSAxMS41NjM0IDkuNzU0NjUgMTEuNTM4IDkuNTI2NjNDMTEuNTM2MSA5LjUwOTg3IDExLjUzNTIgOS40OTI4MyAxMS41MzUyIDkuNDc1NTdWNS40NzE1OEMxMS41MTU0IDUuMjAwODMgMTEuMzkzIDQuOTU4NTggMTEuMjA2NiA0Ljc4MzU4QzExLjAxODggNC42MDcxMSAxMC43NjU5IDQuNDk5MDIgMTAuNDg3OCA0LjQ5OTAyQzEwLjQ3NjYgNC40OTkwMiAxMC40NjU0IDQuNDk5MTkgMTAuNDU0MiA0LjQ5OTU0QzkuOTAzNDcgNC41MTY4NCA5LjQ1OTY0IDQuOTU4MjQgOS40Mzg0NCA1LjUwODAxQzkuNDM4MiA1LjUwNDMgOS40Mzc5NSA1LjUwMDU4IDkuNDM3NjkgNS40OTY4OFY5LjkwMjQzQzkuNDM3NjkgMTAuMTU0NSA5LjIzMzM2IDEwLjM1ODggOC45ODEzMSAxMC4zNTg4QzguNzI5MjUgMTAuMzU4OCA4LjUyNDkyIDEwLjE1NDUgOC41MjQ5MiA5LjkwMjQzVjQuMjc2NTNDOC4zNzA4NiA0LjIxODgyIDguMjA0MDIgNC4xODcyNiA4LjAyOTgyIDQuMTg3MjZDNy4yNTAyNyA0LjE4NzI2IDYuNjE4MzIgNC44MTkyIDYuNjE4MzIgNS41OTg3NUw2LjYxODI3IDkuOTc1OTlDNi42MTgyNyAxMC4yMjggNi40MTM5NCAxMC40MzI0IDYuMTYxODkgMTAuNDMyNEM1LjkwOTgzIDEwLjQzMjQgNS43MDU1IDEwLjIyOCA1LjcwNTUgOS45NzU5OVY4LjA0MTIyTDQuNjIxNDQgOC41MjIwNUM0LjMyMDE4IDguNjU1NjYgNC4xMjU5NyA4Ljk1NDI3IDQuMTI1OTggOS4yODM4M0w0LjEyNjA0IDEyLjYwNjFDNC4xMjYwNiAxMy40ODY3IDQuNTg5MzkgMTQuMzAyNCA1LjM0NTc2IDE0Ljc1MzRMNy4zMzk5OCAxNS45NDI0VjE3LjMyNjJDNy4zMzk5OCAxNy43ODY0IDcuNzEzMDggMTguMTU5NSA4LjE3MzMyIDE4LjE1OTVIMTMuODA4OEMxNC4yNjkgMTguMTU5NSAxNC42NDIxIDE3Ljc4NjQgMTQuNjQyMSAxNy4zMjYyVjE2Ljc5MzhDMTUuNzM4MSAxNS43MTkgMTYuMzg1IDE0LjI3MjggMTYuNDYwMyAxMi43NDdDMTYuNDY0NiAxMi42NiAxNi40NjcgMTIuNTcyOCAxNi40Njc2IDEyLjQ4NTRMMTYuNDY3NyAxMi40NDY3TDE2LjQ2NzYgOC4wMTk4M0MxNi40Njc2IDcuMzQ1MDQgMTUuOTI1MiA2Ljc5NjkzIDE1LjI1MjUgNi43ODc3MUwxNS4yMzUzIDYuNzg3NTlDMTUuMTI1IDYuNzg3NTkgMTUuMDE4IDYuODAyMSAxNC45MTYyIDYuODI5MzFWOS42MDEzNkMxNC45MTYyIDkuODUzNDIgMTQuNzExOSAxMC4wNTc3IDE0LjQ1OTggMTAuMDU3N0MxNC4yMDc4IDEwLjA1NzcgMTQuMDAzNCA5Ljg1MzQxIDE0LjAwMzQgOS42MDEzNlY3Ljk4OTg1QzE0LjAwMzIgNy45OTk4MiAxNC4wMDMxIDguMDA5ODEgMTQuMDAzMSA4LjAxOTgzTDE0LjAwMzQgOS42MDEzNkwxNC4wMDI1IDYuNzExNUMxNC4wMDI1IDYuNDQ5NzQgMTMuOTIwOSA2LjIwNzA1IDEzLjc4MTggNi4wMDc0OEMxMy41NjIgNS42OTI0MiAxMy4xOTg5IDUuNDg0ODMgMTIuNzg3IDUuNDc5MzdMMTIuNzcwMyA1LjQ3OTI2QzEyLjY1ODggNS40NzkyNiAxMi41NTA3IDUuNDk0MDggMTIuNDQ3OSA1LjUyMTg2WiIgZmlsbD0iIzFEMUMyMyIvPgo8L3N2Zz4=\"), auto',\n  // },\n\n  fromNodeJSON(node, json, isFirstCreate) {\n    initFormDataFromJSON(node, json, isFirstCreate);\n    return;\n  },\n  toNodeJSON(node: WorkflowNodeEntity): WorkflowNodeJSON {\n    const nodeError = node.getData<FlowNodeErrorData>(FlowNodeErrorData)?.getError();\n    // 如果节点有错误，这里抛出错误，避免后面的代码执行异常\n    if (nodeError) {\n      throw nodeError;\n    }\n    const transform = node.getData<TransformData>(TransformData)!;\n\n    let formJSON = toFormJSON(node);\n    const metaData: Record<string, unknown> = {};\n\n    // 持久化子画布位置\n    const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(node);\n    if (subCanvas?.isCanvas === false) {\n      const canvasNodeTransform =\n        subCanvas.canvasNode.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      const { x, y } = canvasNodeTransform.transform.position;\n      metaData.canvasPosition = { x, y };\n    }\n\n    const json: WorkflowNodeJSON = {\n      id: node.id,\n      type: node.flowNodeType,\n      meta: {\n        position: { x: transform.position.x, y: transform.position.y },\n        ...metaData,\n      },\n      data: formJSON,\n    };\n    return json;\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-document.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { customAlphabet } from 'nanoid';\nimport { inject, injectable, optional, postConstruct } from 'inversify';\nimport { Emitter, type IPoint } from '@flowgram.ai/utils';\nimport { NodeEngineContext } from '@flowgram.ai/form-core';\nimport {\n  AddNodeData,\n  FlowDocument,\n  FlowNodeBaseType,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport {\n  injectPlaygroundContext,\n  PlaygroundConfigEntity,\n  PlaygroundContext,\n  PositionData,\n  TransformData,\n} from '@flowgram.ai/core';\n\nimport { WorkflowLinesManager } from './workflow-lines-manager';\nimport {\n  WorkflowDocumentOptions,\n  WorkflowDocumentOptionsDefault,\n} from './workflow-document-option';\nimport { getFlowNodeFormData } from './utils/flow-node-form-data';\nimport { buildGroupJSON, delay, fitView, getAntiOverlapPosition } from './utils';\nimport {\n  type WorkflowContentChangeEvent,\n  WorkflowContentChangeType,\n  WorkflowEdgeJSON,\n  type WorkflowJSON,\n  type WorkflowNodeJSON,\n  type WorkflowNodeMeta,\n  type WorkflowNodeRegistry,\n  WorkflowSubCanvas,\n} from './typings';\nimport { WorkflowSelectService } from './service/workflow-select-service';\nimport { FREE_LAYOUT_KEY, type FreeLayout } from './layout';\nimport { WorkflowNodeLinesData, WorkflowNodePortsData } from './entity-datas';\nimport {\n  WorkflowLineEntity,\n  WorkflowLinePortInfo,\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n} from './entities';\n\nconst nanoid = customAlphabet('1234567890', 5);\n\nexport const WorkflowDocumentProvider = Symbol('WorkflowDocumentProvider');\nexport type WorkflowDocumentProvider = () => WorkflowDocument;\n\n@injectable()\nexport class WorkflowDocument extends FlowDocument {\n  private _onContentChangeEmitter = new Emitter<WorkflowContentChangeEvent>();\n\n  protected readonly onLoadedEmitter = new Emitter<void>();\n\n  readonly onContentChange = this._onContentChangeEmitter.event;\n\n  private _onReloadEmitter = new Emitter<WorkflowDocument>();\n\n  readonly onReload = this._onReloadEmitter.event;\n\n  /**\n   * 数据加载完成\n   */\n  readonly onLoaded = this.onLoadedEmitter.event;\n\n  protected _loading = false;\n\n  @inject(WorkflowLinesManager) linesManager: WorkflowLinesManager;\n\n  @inject(PlaygroundConfigEntity) playgroundConfig: PlaygroundConfigEntity;\n\n  @injectPlaygroundContext() playgroundContext: PlaygroundContext;\n\n  @inject(WorkflowDocumentOptions)\n  options: WorkflowDocumentOptions = {};\n\n  @inject(NodeEngineContext) @optional() nodeEngineContext: NodeEngineContext;\n\n  @inject(WorkflowSelectService) selectServices: WorkflowSelectService;\n\n  get loading(): boolean {\n    return this._loading;\n  }\n\n  /**\n   * use `ctx.tools.fitView()` instead\n   * @deprecated\n   * @param easing\n   */\n  async fitView(easing?: boolean): Promise<void> {\n    return fitView(this, this.playgroundConfig, easing).then(() => {\n      this.linesManager.forceUpdate();\n    });\n  }\n\n  @postConstruct()\n  init(): void {\n    super.init();\n    this.currentLayoutKey = this.options.defaultLayout || FREE_LAYOUT_KEY;\n    this.linesManager.init(this);\n    this.playgroundConfig.getCursors = () => this.options.cursors;\n    this.linesManager.onAvailableLinesChange((e) => this.fireContentChange(e));\n    this.playgroundConfig.onReadonlyOrDisabledChange(({ readonly }) => {\n      if (this.nodeEngineContext) {\n        this.nodeEngineContext.readonly = readonly;\n      }\n    });\n  }\n\n  async load(): Promise<void> {\n    if (this.disposed) return;\n    this._loading = true;\n    await super.load();\n    this._loading = false;\n    this.onLoadedEmitter.fire();\n  }\n\n  /**\n   * @deprecated use `ctx.operation.fromJSON` instead\n   */\n  async reload(json: WorkflowJSON, delayTime = 0): Promise<void> {\n    if (this.disposed) return;\n    this._loading = true;\n    this.clear();\n    this.fromJSON(json);\n    // loading添加delay，避免reload时触发fireContentChange的副作用\n    await delay(delayTime);\n    this._loading = false;\n    this._onReloadEmitter.fire(this);\n  }\n\n  /**\n   * 从数据加载\n   * @param json\n   */\n  fromJSON(json: Partial<WorkflowJSON>, fireRender = true): void {\n    if (this.disposed) return;\n    const workflowJSON: WorkflowJSON = {\n      nodes: json.nodes ?? [],\n      edges: json.edges ?? [],\n    };\n    // 触发画布更新\n    this.entityManager.changeEntityLocked = true;\n\n    // 逐层渲染\n    this.batchAddFromJSON(workflowJSON);\n\n    this.entityManager.changeEntityLocked = false;\n    this.transformer.loading = false;\n    // 批量触发画布更新\n    if (fireRender) {\n      this.fireRender();\n    }\n  }\n\n  /**\n   * 清空画布\n   */\n  clear(): void {\n    this.getAllNodes().map((node) => node.dispose()); // 清空节点\n    this.linesManager.getAllLines().map((line) => line.dispose()); // 清空线条\n    this.getAllPorts().map((port) => port.dispose()); // 清空端口\n    this.selectServices.clear(); // 清空选择\n  }\n\n  /**\n   * 创建流程节点\n   * @param json\n   */\n  createWorkflowNode(\n    json: WorkflowNodeJSON,\n    /** @deprecated */\n    isClone: boolean = false,\n    parentID?: string\n  ): WorkflowNodeEntity {\n    return this._createWorkflowNode(json, { parentID });\n  }\n\n  /**\n   * 创建流程节点\n   * @param json\n   */\n  private _createWorkflowNode(\n    json: WorkflowNodeJSON,\n    options?: {\n      parentID?: string;\n      onNodeCreated?: (node: WorkflowNodeEntity) => void;\n      onEdgeCreated?: (edge: WorkflowLineEntity) => void;\n    }\n  ): WorkflowNodeEntity {\n    const { parentID, onNodeCreated, onEdgeCreated } = options ?? {};\n    // 是否是一个已经存在的节点\n    const existedNode = this.getNode(json.id);\n    const isExistedNode = existedNode && existedNode.flowNodeType === json.type;\n    const parent = this.getNode(parentID ?? this.root.id) ?? this.root;\n    const node = this.addNode(\n      {\n        ...json,\n        parent,\n      },\n      undefined,\n      true\n    ) as WorkflowNodeEntity;\n\n    const registry = node.getNodeRegistry() as WorkflowNodeRegistry;\n    const { formMeta } = registry;\n    const meta = node.getNodeMeta<WorkflowNodeMeta>();\n    const formData = getFlowNodeFormData(node);\n\n    const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n    const freeLayout = this.layout as FreeLayout;\n    if (!isExistedNode) {\n      transform.onDataChange(() => {\n        // TODO 这个有点难以理解，其实是为了同步size 数据\n        freeLayout.syncTransform(node);\n      });\n    }\n    let { position } = meta;\n    if (!position) {\n      // 获取默认的位置\n      position = this.getNodeDefaultPosition(json.type);\n    }\n\n    // 更新节点位置信息\n    node.getData(TransformData)!.update({\n      position,\n    });\n\n    // 初始化表单数据\n    if (formMeta && formData) {\n      if (!formData.formModel.initialized) {\n        // 如果表单数据在前置步骤（fromJSON）内已定义，则跳过表单初始化逻辑\n        formData.createForm(formMeta, json.data);\n\n        formData.onDataChange(() => {\n          this.fireContentChange({\n            type: WorkflowContentChangeType.NODE_DATA_CHANGE,\n            toJSON: () => formData.toJSON(),\n            entity: node,\n          });\n        });\n      } else {\n        formData.updateFormValues(json.data);\n      }\n    }\n    // 位置变更\n    const positionData = node.getData<PositionData>(PositionData)!;\n    if (!isExistedNode) {\n      positionData.onDataChange(() => {\n        this.fireContentChange({\n          type: WorkflowContentChangeType.MOVE_NODE,\n          toJSON: () => positionData.toJSON(),\n          entity: node,\n        });\n      });\n    }\n\n    const subCanvas = this.getNodeSubCanvas(node);\n\n    if (!isExistedNode && !subCanvas?.isCanvas) {\n      this.fireContentChange({\n        type: WorkflowContentChangeType.ADD_NODE,\n        entity: node,\n        toJSON: () => this.toNodeJSON(node),\n      });\n      node.onDispose(() => {\n        if (!node.parent || node.parent.flowNodeType === FlowNodeBaseType.ROOT) {\n          return;\n        }\n        const parentTransform = node.parent.getData(FlowNodeTransformData);\n        parentTransform.fireChange();\n      });\n      let lastDeleteNodeData: WorkflowNodeJSON | undefined;\n      node.preDispose.onDispose(() => {\n        lastDeleteNodeData = this.toNodeJSON(node);\n      });\n      node.onDispose(() => {\n        this.fireContentChange({\n          type: WorkflowContentChangeType.DELETE_NODE,\n          entity: node,\n          toJSON: () => lastDeleteNodeData,\n        });\n      });\n    }\n\n    // 若存在子节点，则创建子节点\n    if (json.blocks) {\n      this.batchAddFromJSON(\n        { nodes: json.blocks, edges: json.edges ?? [] },\n        {\n          parent: node,\n          onNodeCreated,\n          onEdgeCreated,\n        }\n      );\n    }\n    // 子画布联动\n    if (subCanvas) {\n      const canvasTransform = subCanvas.canvasNode.getData<TransformData>(TransformData);\n      canvasTransform.update({\n        position: subCanvas.parentNode.getNodeMeta()?.canvasPosition,\n      });\n      if (!isExistedNode) {\n        subCanvas.parentNode.onDispose(() => {\n          subCanvas.canvasNode.dispose();\n        });\n        subCanvas.canvasNode.onDispose(() => {\n          subCanvas.parentNode.dispose();\n        });\n      }\n    }\n    if (!isExistedNode) {\n      this.onNodeCreateEmitter.fire({\n        node,\n        data: json,\n        json,\n      });\n    } else {\n      this.onNodeUpdateEmitter.fire({\n        node,\n        data: json,\n        json,\n      });\n    }\n\n    return node;\n  }\n\n  /**\n   * 添加节点，如果节点已经存在则不会重复创建\n   * @param data\n   * @param addedNodes\n   */\n  addNode(\n    data: AddNodeData,\n    addedNodes?: WorkflowNodeEntity[],\n    ignoreCreateAndUpdateEvent?: boolean\n  ): WorkflowNodeEntity {\n    const { id, type = 'block', originParent, parent, meta, hidden, index } = data;\n    let node = this.getNode(id);\n    let isNew = false;\n    const register = this.getNodeRegistry(type, data.originParent);\n    // node 类型变化则全部删除重新来\n    if (node && node.flowNodeType !== data.type) {\n      node.dispose();\n      node = undefined;\n    }\n    if (!node) {\n      const { dataRegistries } = register;\n      node = this.entityManager.createEntity<WorkflowNodeEntity>(WorkflowNodeEntity, {\n        id,\n        document: this,\n        flowNodeType: type,\n        originParent,\n        meta,\n      });\n      this.options.preNodeCreate?.(node);\n      const datas = dataRegistries\n        ? this.nodeDataRegistries.concat(...dataRegistries)\n        : this.nodeDataRegistries;\n      node.addInitializeData(datas);\n      node.ports = node.getData(WorkflowNodePortsData);\n      node.lines = node.getData(WorkflowNodeLinesData);\n      node.onDispose(() => this.onNodeDisposeEmitter.fire({ node: node! }));\n      this.options.fromNodeJSON?.(node, data, true);\n      isNew = true;\n    } else {\n      this.options.fromNodeJSON?.(node, data, false);\n    }\n    // 初始化数据重制\n    node.initData({\n      originParent,\n      parent,\n      meta,\n      hidden,\n      index,\n    });\n    addedNodes?.push(node);\n    // 自定义创建逻辑\n    if (register.onCreate) {\n      const extendNodes = register.onCreate(node, data);\n      if (extendNodes && addedNodes) {\n        addedNodes.push(...extendNodes);\n      }\n    }\n\n    if (!ignoreCreateAndUpdateEvent) {\n      if (isNew) {\n        this.onNodeCreateEmitter.fire({\n          node,\n          data,\n          json: data,\n        });\n      } else {\n        this.onNodeUpdateEmitter.fire({ node, data, json: data });\n      }\n    }\n\n    return node;\n  }\n\n  get layout(): FreeLayout {\n    const layout = this.layouts.find((layout) => layout.name == this.currentLayoutKey);\n    if (!layout) {\n      throw new Error(`Unknown flow layout: ${this.currentLayoutKey}`);\n    }\n    return layout as FreeLayout;\n  }\n\n  /**\n   * 获取默认的 x y 坐标, 默认为当前画布可视区域中心\n   * @param type\n   * @protected\n   */\n  getNodeDefaultPosition(type: string | number): IPoint {\n    const { size } = this.getNodeRegistry(type).meta || {};\n    // 当前可视区域的中心位置\n    let position = this.playgroundConfig.getViewport(true).center;\n    if (size) {\n      position = {\n        x: position.x,\n        y: position.y - size.height / 2,\n      };\n    }\n    // 去掉叠加的\n    return getAntiOverlapPosition(this, position);\n  }\n\n  /**\n   * 通过类型创建节点, 如果没有提供position 则直接放在画布中间\n   * @param type\n   */\n  createWorkflowNodeByType(\n    type: string | number,\n    position?: IPoint,\n    json: Partial<WorkflowNodeJSON> = {},\n    parentID?: string\n  ): WorkflowNodeEntity {\n    let id: string = json.id as string;\n    if (id === undefined) {\n      // 保证 id 不要重复\n      do {\n        id = `1${nanoid()}`;\n      } while (this.entityManager.getEntityById(id));\n    } else {\n      if (this.entityManager.getEntityById(id)) {\n        throw new Error(`[WorkflowDocument.createWorkflowNodeByType] Node Id \"${id}\" duplicated.`);\n      }\n    }\n    return this._createWorkflowNode(\n      {\n        ...json,\n        id,\n        type,\n        meta: { position, ...json?.meta }, // TODO title 和 meta 要从注册数据去拿\n        data: json?.data,\n        blocks: json?.blocks,\n        edges: json?.edges,\n      },\n      { parentID }\n    );\n  }\n\n  getAllNodes(): WorkflowNodeEntity[] {\n    return this.entityManager\n      .getEntities<WorkflowNodeEntity>(WorkflowNodeEntity)\n      .filter((n) => n.id !== FlowNodeBaseType.ROOT);\n  }\n\n  getAllEdges(): WorkflowLineEntity[] {\n    return this.entityManager.getEntities<WorkflowLineEntity>(WorkflowLineEntity);\n  }\n\n  getAllPorts(): WorkflowPortEntity[] {\n    return this.entityManager\n      .getEntities<WorkflowPortEntity>(WorkflowPortEntity)\n      .filter((p) => p.node.id !== FlowNodeBaseType.ROOT);\n  }\n\n  /**\n   * 获取画布中的非游离节点\n   * 1. 开始节点\n   * 2. 从开始节点出发能走到的节点\n   * 3. 结束节点\n   * 4. 默认所有子画布内节点为游离节点\n   */\n  getAssociatedNodes(): WorkflowNodeEntity[] {\n    const allNode = this.getAllNodes();\n\n    const allLines = this.linesManager\n      .getAllLines()\n      .filter((line) => line.from && line.to)\n      .map((line) => ({\n        from: line.from!.id,\n        to: line.to!.id,\n      }));\n\n    const startNodeId = allNode.find((node) => node.isStart)?.id;\n    const endNodeId = allNode.find((node) => node.isNodeEnd)?.id;\n\n    // 子画布内节点无需开始/结束\n    const nodeInContainer = allNode\n      .filter((node) => node.parent?.getNodeMeta<WorkflowNodeMeta>().isContainer)\n      .map((node) => node.id);\n\n    const associatedCache = new Set(nodeInContainer);\n    if (endNodeId) {\n      associatedCache.add(endNodeId);\n    }\n    const bfs = (nodeId: string) => {\n      if (associatedCache.has(nodeId)) {\n        return;\n      }\n      associatedCache.add(nodeId);\n      const nextNodes = allLines.reduce((ids, { from, to }) => {\n        if (from === nodeId && !associatedCache.has(to)) {\n          ids.push(to);\n        }\n        return ids;\n      }, [] as string[]);\n\n      nextNodes.forEach(bfs);\n    };\n\n    if (startNodeId) {\n      bfs(startNodeId);\n    }\n\n    const associatedNodes = allNode.filter((node) => associatedCache.has(node.id));\n\n    return associatedNodes;\n  }\n\n  /**\n   * 触发渲染\n   */\n  fireRender() {\n    this.entityManager.fireEntityChanged(WorkflowNodeEntity.type);\n    this.entityManager.fireEntityChanged(WorkflowLineEntity.type);\n    this.entityManager.fireEntityChanged(WorkflowPortEntity.type);\n  }\n\n  fireContentChange(event: WorkflowContentChangeEvent): void {\n    if (this._loading || this.disposed || this.entityManager.changeEntityLocked) {\n      return;\n    }\n    this._onContentChangeEmitter.fire(event);\n  }\n\n  toNodeJSON(node: WorkflowNodeEntity): WorkflowNodeJSON {\n    // 如果是子画布，返回其父节点的JSON\n    const subCanvas = this.getNodeSubCanvas(node);\n    if (subCanvas?.isCanvas === true) {\n      return this.toNodeJSON(subCanvas.parentNode);\n    }\n\n    const json = this.toNodeJSONFromOptions(node);\n    const children = this.getNodeChildren(node);\n\n    // 计算子节点 JSON\n    const blocks = children.map((child) => this.toNodeJSON(child));\n\n    // 计算子线条 JSON\n    const linesMap = new Map<string, WorkflowEdgeJSON>();\n    children.forEach((child) => {\n      const childLinesData = child.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);\n      [...childLinesData.inputLines, ...childLinesData.outputLines]\n        .filter(Boolean)\n        .forEach((line) => {\n          const lineJSON = this.toLineJSON(line);\n          if (!lineJSON || linesMap.has(line.id)) {\n            return;\n          }\n          linesMap.set(line.id, lineJSON);\n        });\n    });\n    const edges = Array.from(linesMap.values()); // 使用 Map 防止线条重复\n\n    // 拼接 JSON\n    if (blocks.length > 0) json.blocks = blocks;\n    if (edges.length > 0) json.edges = edges;\n\n    return json;\n  }\n\n  /**\n   * 节点转换为JSON， 没有format的过程\n   * @param node\n   * @returns\n   */\n  private toNodeJSONFromOptions(node: WorkflowNodeEntity): WorkflowNodeJSON {\n    if (this.options.toNodeJSON) {\n      return this.options.toNodeJSON(node) as WorkflowNodeJSON;\n    }\n    return WorkflowDocumentOptionsDefault.toNodeJSON!(node) as WorkflowNodeJSON;\n  }\n\n  copyNode(\n    node: WorkflowNodeEntity,\n    newNodeId?: string | undefined,\n    format?: (json: WorkflowNodeJSON) => WorkflowNodeJSON,\n    position?: IPoint\n  ): WorkflowNodeEntity {\n    let json = this.toNodeJSON(node);\n    if (format) {\n      json = format(json);\n    }\n    position = position || {\n      x: json.meta!.position!.x + 30,\n      y: json.meta!.position!.y + 30,\n    };\n    return this._createWorkflowNode(\n      {\n        id: newNodeId || `1${nanoid()}`,\n        type: node.flowNodeType,\n        meta: {\n          ...json.meta,\n          position,\n        },\n        data: json.data,\n        blocks: json.blocks,\n        edges: json.edges,\n      },\n      {\n        parentID: node.parent?.id,\n      }\n    );\n  }\n\n  copyNodeFromJSON(\n    flowNodeType: string,\n    nodeJSON: WorkflowNodeJSON,\n    newNodeId?: string | undefined,\n    position?: IPoint,\n    parentID?: string\n  ): WorkflowNodeEntity {\n    position = position || {\n      x: nodeJSON.meta!.position!.x + 30,\n      y: nodeJSON.meta!.position!.y + 30,\n    };\n    return this._createWorkflowNode(\n      {\n        id: newNodeId || `1${nanoid()}`,\n        type: flowNodeType,\n        meta: {\n          ...nodeJSON.meta,\n          position,\n        },\n        data: nodeJSON.data,\n        blocks: nodeJSON.blocks,\n        edges: nodeJSON.edges,\n      },\n      {\n        parentID,\n      }\n    );\n  }\n\n  canRemove(node: WorkflowNodeEntity, silent?: boolean): boolean {\n    const meta = node.getNodeMeta<WorkflowNodeMeta>();\n    if (meta.deleteDisable) {\n      return false;\n    }\n    if (this.options.canDeleteNode && !this.options.canDeleteNode(node, silent)) {\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * 判断端口是否为错误态\n   */\n  isErrorPort(port: WorkflowPortEntity, defaultValue = false) {\n    if (typeof this.options.isErrorPort === 'function') {\n      return this.options.isErrorPort(port);\n    }\n\n    return defaultValue;\n  }\n\n  /**\n   * 导出数据\n   */\n  toJSON(): WorkflowJSON {\n    if (this.disposed) {\n      throw new Error(\n        'The WorkflowDocument has been disposed and it is no longer possible to call toJSON.'\n      );\n    }\n    const rootJSON = this.toNodeJSON(this.root);\n    const json = {\n      nodes: rootJSON.blocks ?? [],\n      edges: rootJSON.edges ?? [],\n    };\n    return json;\n  }\n\n  dispose() {\n    super.dispose();\n    this._onReloadEmitter.dispose();\n  }\n\n  /**\n   * 批量添加节点\n   * @deprecated use 'batchAddFromJSON' instead\n   * @param json\n   * @param options\n   */\n  public renderJSON(\n    json: WorkflowJSON,\n    options?: {\n      parent?: WorkflowNodeEntity;\n      /** @deprecated useless api */\n      isClone?: boolean;\n    }\n  ): {\n    nodes: WorkflowNodeEntity[];\n    edges: WorkflowLineEntity[];\n  } {\n    return this.batchAddFromJSON(json, options);\n  }\n\n  /**\n   * 批量添加节点\n   */\n  public batchAddFromJSON(\n    json: WorkflowJSON,\n    options?: {\n      parent?: WorkflowNodeEntity;\n      onNodeCreated?: (node: WorkflowNodeEntity) => void;\n      onEdgeCreated?: (edge: WorkflowLineEntity) => void;\n    }\n  ): {\n    nodes: WorkflowNodeEntity[];\n    edges: WorkflowLineEntity[];\n  } {\n    const { parent = this.root, onNodeCreated, onEdgeCreated } = options ?? {};\n    // 创建节点\n    const parentID = this.getNodeSubCanvas(parent)?.canvasNode.id ?? parent.id;\n    const processedJSON = buildGroupJSON(json);\n    const nodes = processedJSON.nodes.map((nodeJSON: WorkflowNodeJSON) =>\n      this._createWorkflowNode(nodeJSON, {\n        parentID,\n        onNodeCreated,\n        onEdgeCreated,\n      })\n    );\n    // 创建线条\n    const edges = processedJSON.edges\n      .map((edge) => this.createWorkflowLine(edge, parentID))\n      .filter(Boolean) as WorkflowLineEntity[];\n    // 触发回调\n    nodes.forEach((node) => options?.onNodeCreated?.(node));\n    edges.forEach((edge) => options?.onEdgeCreated?.(edge));\n    return { nodes, edges };\n  }\n\n  private getNodeSubCanvas(node: WorkflowNodeEntity): WorkflowSubCanvas | undefined {\n    if (!node) return;\n    const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(node);\n    return subCanvas;\n  }\n\n  private getNodeChildren(node: WorkflowNodeEntity): WorkflowNodeEntity[] {\n    if (!node || node.flowNodeType === FlowNodeBaseType.GROUP) return [];\n    const subCanvas = this.getNodeSubCanvas(node);\n    // get real children\n    const realChildren = subCanvas ? subCanvas.canvasNode.blocks : node.blocks;\n    // filter sub canvas node\n    const childrenWithoutSubCanvas = realChildren\n      .filter((child) => {\n        const childMeta = child.getNodeMeta<WorkflowNodeMeta>();\n        return !childMeta.subCanvas?.(node)?.isCanvas;\n      })\n      .filter(Boolean);\n    // flat group nodes\n    const children = childrenWithoutSubCanvas\n      .map((child) => {\n        if (child.flowNodeType === FlowNodeBaseType.GROUP) {\n          return [child, ...child.blocks];\n        }\n        return child;\n      })\n      .flat();\n    return children;\n  }\n\n  private toLineJSON(line: WorkflowLineEntity): WorkflowEdgeJSON | undefined {\n    const lineJSON = line.toJSON();\n    if (\n      !line.from ||\n      !line.info.from ||\n      !line.fromPort ||\n      !line.to ||\n      !line.info.to ||\n      !line.toPort\n    ) {\n      return;\n    }\n    // 父子节点之间连线，需替换子画布为父节点\n    const fromSubCanvas = this.getNodeSubCanvas(line.from);\n    const toSubCanvas = this.getNodeSubCanvas(line.to);\n    if (fromSubCanvas && !fromSubCanvas.isCanvas && toSubCanvas && toSubCanvas.isCanvas) {\n      // 忽略子画布与父节点的连线\n      return;\n    }\n    if (line.from === line.to.parent && fromSubCanvas) {\n      return {\n        ...lineJSON,\n        sourceNodeID: fromSubCanvas.parentNode.id,\n      };\n    }\n    if (line.to === line.from.parent && toSubCanvas) {\n      return {\n        ...lineJSON,\n        targetNodeID: toSubCanvas.parentNode.id,\n      };\n    }\n    return lineJSON;\n  }\n\n  private createWorkflowLine(\n    json: WorkflowEdgeJSON,\n    parentID?: string\n  ): WorkflowLineEntity | undefined {\n    const fromNode = this.getNode(json.sourceNodeID);\n    const toNode = this.getNode(json.targetNodeID);\n    // 脏数据清除\n    if (!fromNode || !toNode) {\n      return;\n    }\n    const lineInfo: WorkflowLinePortInfo = {\n      from: json.sourceNodeID,\n      fromPort: json.sourcePortID,\n      to: json.targetNodeID,\n      toPort: json.targetPortID,\n      data: json.data,\n    };\n    if (!parentID) {\n      return this.linesManager.createLine(lineInfo);\n    }\n    // 父子节点之间连线，需替换父节点为子画布\n    const canvasNode = this.getNode(parentID);\n    if (!canvasNode) {\n      return this.linesManager.createLine(lineInfo);\n    }\n    const parentSubCanvas = this.getNodeSubCanvas(canvasNode);\n    if (!parentSubCanvas) {\n      return this.linesManager.createLine(lineInfo);\n    }\n    if (lineInfo.from === parentSubCanvas.parentNode.id) {\n      return this.linesManager.createLine({\n        ...lineInfo,\n        from: parentSubCanvas.canvasNode.id,\n      });\n    }\n    if (lineInfo.to === parentSubCanvas.parentNode.id) {\n      return this.linesManager.createLine({\n        ...lineInfo,\n        to: parentSubCanvas.canvasNode.id,\n      });\n    }\n    return this.linesManager.createLine(lineInfo);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/src/workflow-lines-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { last } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { DisposableCollection, Emitter, type IPoint } from '@flowgram.ai/utils';\nimport { FlowNodeRenderData, FlowNodeTransformData } from '@flowgram.ai/document';\nimport { EntityManager, PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport { WorkflowDocumentOptions } from './workflow-document-option';\nimport { type WorkflowDocument } from './workflow-document';\nimport { WorkflowPortType } from './utils';\nimport {\n  LineColor,\n  LineColors,\n  LinePoint,\n  LineRenderType,\n  LineType,\n  type WorkflowLineRenderContributionFactory,\n} from './typings/workflow-line';\nimport {\n  type WorkflowContentChangeEvent,\n  WorkflowContentChangeType,\n  type WorkflowEdgeJSON,\n  WorkflowNodeRegistry,\n} from './typings';\nimport { WorkflowHoverService, WorkflowSelectService } from './service';\nimport { WorkflowNodeLinesData } from './entity-datas/workflow-node-lines-data';\nimport { WorkflowLineRenderData } from './entity-datas';\nimport {\n  LINE_HOVER_DISTANCE,\n  WorkflowLineEntity,\n  type WorkflowLinePortInfo,\n  type WorkflowNodeEntity,\n  WorkflowPortEntity,\n} from './entities';\n\n/**\n * 线条管理\n */\n@injectable()\nexport class WorkflowLinesManager {\n  protected document: WorkflowDocument;\n\n  protected toDispose = new DisposableCollection();\n  // 线条类型\n\n  protected _lineType: LineRenderType = LineType.BEZIER;\n\n  protected onAvailableLinesChangeEmitter = new Emitter<WorkflowContentChangeEvent>();\n\n  protected onForceUpdateEmitter = new Emitter<void>();\n\n  @inject(WorkflowHoverService) hoverService: WorkflowHoverService;\n\n  @inject(WorkflowSelectService) selectService: WorkflowSelectService;\n\n  @inject(EntityManager) protected readonly entityManager: EntityManager;\n\n  @inject(WorkflowDocumentOptions)\n  readonly options: WorkflowDocumentOptions;\n\n  /**\n   * 有效的线条被添加或者删除时候触发，未连上的线条不算\n   */\n  readonly onAvailableLinesChange = this.onAvailableLinesChangeEmitter.event;\n\n  /**\n   * 强制渲染 lines\n   */\n  readonly onForceUpdate = this.onForceUpdateEmitter.event;\n\n  readonly contributionFactories: WorkflowLineRenderContributionFactory[] = [];\n\n  init(doc: WorkflowDocument): void {\n    this.document = doc;\n  }\n\n  forceUpdate() {\n    this.onForceUpdateEmitter.fire();\n  }\n\n  get lineType() {\n    return this._lineType;\n  }\n\n  get lineColor(): LineColor {\n    const color: LineColor = {\n      default: LineColors.DEFUALT,\n      error: LineColors.ERROR,\n      hidden: LineColors.HIDDEN,\n      drawing: LineColors.DRAWING,\n      hovered: LineColors.HOVER,\n      selected: LineColors.SELECTED,\n      flowing: LineColors.FLOWING,\n    };\n    if (this.options.lineColor) {\n      Object.assign(color, this.options.lineColor);\n    }\n    return color;\n  }\n\n  switchLineType(newType?: LineRenderType): LineRenderType {\n    if (newType === undefined) {\n      if (this._lineType === LineType.BEZIER) {\n        newType = LineType.LINE_CHART;\n      } else {\n        newType = LineType.BEZIER;\n      }\n    }\n    if (newType !== this._lineType) {\n      this._lineType = newType;\n      // 更新线条数据\n      this.getAllLines().forEach((line) => {\n        line.getData(WorkflowLineRenderData).update();\n      });\n      window.requestAnimationFrame(() => {\n        // 触发线条重渲染\n        this.entityManager.fireEntityChanged(WorkflowLineEntity.type);\n      });\n    }\n    return this._lineType;\n  }\n\n  getAllLines(): WorkflowLineEntity[] {\n    return this.entityManager.getEntities(WorkflowLineEntity);\n  }\n\n  getAllAvailableLines(): WorkflowLineEntity[] {\n    return this.getAllLines().filter((l) => !l.isDrawing && !l.isHidden);\n  }\n\n  hasLine(portInfo: Omit<WorkflowLinePortInfo, 'data'>): boolean {\n    return !!this.entityManager.getEntityById<WorkflowLineEntity>(\n      WorkflowLineEntity.portInfoToLineId(portInfo)\n    );\n  }\n\n  getLine(portInfo: Omit<WorkflowLinePortInfo, 'data'>): WorkflowLineEntity | undefined {\n    return this.entityManager.getEntityById<WorkflowLineEntity>(\n      WorkflowLineEntity.portInfoToLineId(portInfo)\n    );\n  }\n\n  getLineById(id: string): WorkflowLineEntity | undefined {\n    return this.entityManager.getEntityById<WorkflowLineEntity>(id);\n  }\n\n  replaceLine(\n    oldPortInfo: Omit<WorkflowLinePortInfo, 'data'>,\n    newPortInfo: Omit<WorkflowLinePortInfo, 'data'>\n  ): WorkflowLineEntity {\n    const oldLine = this.getLine(oldPortInfo);\n    if (oldLine) {\n      oldLine.dispose();\n    }\n    return this.createLine(newPortInfo)!;\n  }\n\n  createLine(\n    options: {\n      drawingTo?: LinePoint; // 无连接的线条\n      drawingFrom?: LinePoint;\n      key?: string; // 自定义 key\n    } & WorkflowLinePortInfo\n  ): WorkflowLineEntity | undefined {\n    const { from, to, drawingTo, fromPort, drawingFrom, toPort, data } = options;\n    const available = Boolean(from && to);\n    const key = options.key || WorkflowLineEntity.portInfoToLineId(options);\n    let line = this.entityManager.getEntityById<WorkflowLineEntity>(key)!;\n    if (line) {\n      // 如果之前有线条，则先把颜色去掉\n      line.highlightColor = '';\n      line.validate();\n      return line;\n    }\n\n    const fromNode = from\n      ? this.entityManager\n          .getEntityById<WorkflowNodeEntity>(from)!\n          .getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)\n      : undefined;\n    const toNode = to\n      ? this.entityManager\n          .getEntityById<WorkflowNodeEntity>(to)!\n          .getData<WorkflowNodeLinesData>(WorkflowNodeLinesData)!\n      : undefined;\n\n    if (!fromNode && !toNode) {\n      // 非法情况\n      return;\n    }\n\n    this.isDrawing = Boolean(drawingTo || drawingFrom);\n    line = this.entityManager.createEntity<WorkflowLineEntity>(WorkflowLineEntity, {\n      id: key,\n      document: this.document,\n      linesManager: this,\n      from,\n      fromPort,\n      toPort,\n      to,\n      drawingTo,\n      drawingFrom,\n      data,\n    });\n\n    this.registerData(line);\n\n    fromNode?.addLine(line);\n    toNode?.addLine(line);\n    line.onDispose(() => {\n      this.isDrawing = false;\n      fromNode?.removeLine(line);\n      toNode?.removeLine(line);\n    });\n    line.onDispose(() => {\n      if (available) {\n        this.onAvailableLinesChangeEmitter.fire({\n          type: WorkflowContentChangeType.DELETE_LINE,\n          toJSON: () => line.toJSON(),\n          entity: line,\n        });\n      }\n    });\n    line.onLineDataChange(({ oldValue }) => {\n      this.onAvailableLinesChangeEmitter.fire({\n        type: WorkflowContentChangeType.LINE_DATA_CHANGE,\n        toJSON: () => line.toJSON(),\n        oldValue,\n        entity: line,\n      });\n    });\n    // 是否为有效的线条\n    if (available) {\n      this.onAvailableLinesChangeEmitter.fire({\n        type: WorkflowContentChangeType.ADD_LINE,\n        toJSON: () => line.toJSON(),\n        entity: line,\n      });\n    }\n    // 创建时检验 连线错误态 & 端口错误态\n    line.validate();\n    return line;\n  }\n\n  /**\n   * 获取线条中距离鼠标位置最近的线条和距离\n   * @param mousePos 鼠标位置\n   * @param minDistance 最小检测距离\n   * @returns 距离鼠标位置最近的线条 以及距离\n   */\n  getCloseInLineFromMousePos(\n    mousePos: IPoint,\n    minDistance: number = LINE_HOVER_DISTANCE\n  ): WorkflowLineEntity | undefined {\n    let targetLine: WorkflowLineEntity | undefined, targetLineDist: number | undefined;\n    this.getAllLines().forEach((line) => {\n      const dist = line.getHoverDist(mousePos);\n\n      if (dist <= minDistance && (!targetLineDist || targetLineDist >= dist)) {\n        targetLineDist = dist;\n        targetLine = line;\n      }\n    });\n    return targetLine;\n  }\n\n  /**\n   * 是否在调整线条\n   */\n  isDrawing = false;\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  isErrorLine(fromPort?: WorkflowPortEntity, toPort?: WorkflowPortEntity, defaultValue?: boolean) {\n    if (this.options.isErrorLine) {\n      return this.options.isErrorLine(fromPort, toPort, this);\n    }\n\n    return !!defaultValue;\n  }\n\n  isReverseLine(line: WorkflowLineEntity, defaultValue = false): boolean {\n    if (this.options.isReverseLine) {\n      return this.options.isReverseLine(line);\n    }\n\n    return defaultValue;\n  }\n\n  isHideArrowLine(line: WorkflowLineEntity, defaultValue = false): boolean {\n    if (this.options.isHideArrowLine) {\n      return this.options.isHideArrowLine(line);\n    }\n\n    return defaultValue;\n  }\n\n  isFlowingLine(line: WorkflowLineEntity, defaultValue = false): boolean {\n    if (this.options.isFlowingLine) {\n      return this.options.isFlowingLine(line);\n    }\n\n    return defaultValue;\n  }\n\n  isDisabledLine(line: WorkflowLineEntity, defaultValue = false): boolean {\n    if (this.options.isDisabledLine) {\n      return this.options.isDisabledLine(line);\n    }\n    return defaultValue;\n  }\n\n  setLineRenderType(line: WorkflowLineEntity): LineRenderType | undefined {\n    if (this.options.setLineRenderType) {\n      return this.options.setLineRenderType(line);\n    }\n    return undefined;\n  }\n\n  setLineClassName(line: WorkflowLineEntity): string | undefined {\n    if (this.options.setLineClassName) {\n      return this.options.setLineClassName(line);\n    }\n    return undefined;\n  }\n\n  getLineColor(line: WorkflowLineEntity): string | undefined {\n    // 隐藏的优先级比 hasError 高\n    if (line.isHidden) {\n      return this.lineColor.hidden;\n    }\n    // 颜色锁定\n    if (line.lockedColor) {\n      return line.lockedColor;\n    }\n    if (line.hasError) {\n      return this.lineColor.error;\n    }\n    if (line.highlightColor) {\n      return line.highlightColor;\n    }\n    if (line.drawingTo) {\n      return this.lineColor.drawing;\n    }\n    if (this.hoverService.isHovered(line.id)) {\n      return this.lineColor.hovered;\n    }\n    if (this.selectService.isSelected(line.id)) {\n      return this.lineColor.selected;\n    }\n    // 检查是否为流动线条\n    if (this.isFlowingLine(line)) {\n      return this.lineColor.flowing;\n    }\n    return this.lineColor.default;\n  }\n\n  canAddLine(fromPort: WorkflowPortEntity, toPort: WorkflowPortEntity, silent?: boolean): boolean {\n    if (\n      fromPort === toPort ||\n      fromPort.node === toPort.node ||\n      fromPort.portType !== 'output' ||\n      toPort.portType !== 'input' ||\n      fromPort.disabled ||\n      toPort.disabled\n    ) {\n      return false;\n    }\n    const fromCanAdd = fromPort.node.getNodeRegistry<WorkflowNodeRegistry>().canAddLine;\n    const toCanAdd = toPort.node.getNodeRegistry<WorkflowNodeRegistry>().canAddLine;\n    if (fromCanAdd && !fromCanAdd(fromPort, toPort, this, silent)) {\n      return false;\n    }\n    if (toCanAdd && !toCanAdd(fromPort, toPort, this, silent)) {\n      return false;\n    }\n    if (this.options.canAddLine) {\n      return this.options.canAddLine(fromPort, toPort, this, silent);\n    }\n    // 默认不能连接自己\n    return fromPort.node !== toPort.node;\n  }\n\n  toJSON(): WorkflowEdgeJSON[] {\n    return this.getAllLines()\n      .filter((l) => !l.isDrawing)\n      .map((l) => l.toJSON());\n  }\n\n  getPortById(portId: string): WorkflowPortEntity | undefined {\n    return this.entityManager.getEntityById<WorkflowPortEntity>(portId);\n  }\n\n  canRemove(\n    line: WorkflowLineEntity,\n    newLineInfo?: Required<Omit<WorkflowLinePortInfo, 'data'>>,\n    silent?: boolean\n  ): boolean {\n    if (\n      this.options &&\n      this.options.canDeleteLine &&\n      !this.options.canDeleteLine(line, newLineInfo, silent)\n    ) {\n      return false;\n    }\n    return true;\n  }\n\n  canReset(oldLine: WorkflowLineEntity, newLineInfo: Required<WorkflowLinePortInfo>): boolean {\n    if (\n      this.options &&\n      this.options.canResetLine &&\n      !this.options.canResetLine(oldLine, newLineInfo, this)\n    ) {\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * 根据鼠标位置找到 port\n   * @param pos\n   */\n  getPortFromMousePos(pos: IPoint, portType?: WorkflowPortType): WorkflowPortEntity | undefined {\n    const allNodes = this.getSortedNodes().reverse();\n    const allPorts = allNodes\n      .map((node) => {\n        if (!portType) {\n          return node.ports.allPorts;\n        }\n        return portType === 'input' ? node.ports.inputPorts : node.ports.outputPorts;\n      })\n      .flat();\n    const targetPort = allPorts.find((port) => port.isHovered(pos.x, pos.y));\n    if (targetPort) {\n      const containNodes = this.getContainNodesFromMousePos(pos);\n      const targetNode = last(containNodes);\n      // 点位可能会被节点覆盖\n      if (targetNode && targetNode !== targetPort.node) {\n        return;\n      }\n    }\n    return targetPort;\n  }\n\n  /**\n   * 根据鼠标位置找到 node\n   * @param pos - 鼠标位置\n   */\n  getNodeFromMousePos(pos: IPoint): WorkflowNodeEntity | undefined {\n    // 先挑选出 bounds 区域符合的 node\n    const { selection } = this.selectService;\n    const containNodes = this.getContainNodesFromMousePos(pos);\n    // 当有元素被选中的时候选中元素在顶层\n    if (selection?.length) {\n      const filteredNodes = containNodes.filter((node) =>\n        selection.some((_node) => node.id === _node.id)\n      );\n      if (filteredNodes?.length) {\n        return last(filteredNodes);\n      }\n    }\n    // 默认取最顶层的\n    return last(containNodes);\n  }\n\n  registerContribution(factory: WorkflowLineRenderContributionFactory): this {\n    this.contributionFactories.push(factory);\n    return this;\n  }\n\n  private registerData(line: WorkflowLineEntity) {\n    line.addData(WorkflowLineRenderData);\n  }\n\n  private getSortedNodes() {\n    return this.document.getAllNodes().sort((a, b) => this.getNodeIndex(a) - this.getNodeIndex(b));\n  }\n\n  /** 获取鼠标坐标位置的所有节点（stackIndex 从小到大排序） */\n  private getContainNodesFromMousePos(pos: IPoint): WorkflowNodeEntity[] {\n    const allNodes = this.getSortedNodes();\n    const zoom =\n      this.entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)?.config?.zoom ||\n      1;\n    const containNodes = allNodes\n      .map((node) => {\n        const { bounds } = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n        // 交互要求，节点边缘 4px 的时候就认为选中节点\n        if (\n          bounds\n            .clone()\n            .pad(4 / zoom)\n            .contains(pos.x, pos.y)\n        ) {\n          return node;\n        }\n      })\n      .filter(Boolean) as WorkflowNodeEntity[];\n    return containNodes;\n  }\n\n  private getNodeIndex(node: WorkflowNodeEntity): number {\n    const nodeRenderData = node.getData(FlowNodeRenderData);\n    return nodeRenderData.stackIndex;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"],\n  },\n  \"include\": [\n    \"./src\",\n    \"./__tests__\"\n  ],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    testTimeout: 30000,\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/canvas-engine/free-layout-core/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-document-container.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityManager } from '@flowgram.ai/core'\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n  FlowNodeTransformData,\n  FlowNodeTransitionData,\n} from '@flowgram.ai/document'\nimport { Container, decorate, injectable, type interfaces } from 'inversify'\n\nexport class FlowDocumentMockRegister implements FlowDocumentContribution {\n  registerDocument(document: FlowDocument) {\n    document.registerNodeDatas(FlowNodeTransformData, FlowNodeTransitionData)\n  }\n}\n\ndecorate(injectable(), FlowDocumentMockRegister)\n\nexport function createDocumentContainer(): interfaces.Container {\n  const container = new Container()\n  container.load(FlowDocumentContainerModule)\n  container.bind(EntityManager).toSelf()\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister)\n  return container\n}\n\nexport function createDocument(): FlowDocument {\n  return createDocumentContainer().get(FlowDocument)\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-drag-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const NOT_SCROLL_EVENT = {\n  clientX: 300,\n  clientY: 300,\n} as MouseEvent\n\nexport const SCROLL_BOTTOM_EVENT = {\n  clientX: 0,\n  clientY: 3020,\n} as MouseEvent\n\nexport const SCROLL_TOP_EVENT = {\n  clientX: 0,\n  clientY: 10,\n} as MouseEvent\n\nexport const SCROLL_RIGHT_EVENT = {\n  clientX: 3020,\n  clientY: 300,\n} as MouseEvent\n\nexport const SCROLL_LEFT_EVENT = {\n  clientX: 10,\n  clientY: 300,\n} as MouseEvent\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-json.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/document'\n\nexport const flowJson: FlowDocumentJSON = {\n  nodes: [\n    {\n      type: 'start',\n      id: 'start',\n    },\n    {\n      type: 'tryCatch',\n      id: 'tryCatch_f9fa62fa783',\n      blocks: [\n        {\n          id: 'branch_9fa62fa783d',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_fa62fa783d7',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n          blocks: [\n            {\n              type: 'createRecord',\n              id: 'createRecord_463df50d176',\n            },\n            {\n              type: 'createRecord',\n              id: 'createRecord_fb7a69ab5b8',\n            },\n          ]\n        },\n        {\n          id: 'branch_c57c09b038e',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_7c09b038e0b',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'loop',\n      id: 'while_4bd4950692a',\n      blocks: [\n        {\n          id: '$loopBranch$while_4bd4950692a',\n          blocks: [\n            {\n              type: 'createRecord',\n              id: 'createRecord_fb7a69ab5b8',\n            },\n          ]\n        },\n      ],\n    },\n    {\n      type: 'createRecord',\n      id: 'createRecord_6f8cad399fb',\n    },\n    {\n      type: 'loop',\n      id: 'forEach_4eeb9f9cde8',\n      blocks: [\n        {\n          id: '$loopBranch$forEach_4eeb9f9cde8',\n        },\n      ],\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'exclusiveSplit_d2bdee4eb90',\n      blocks: [\n        {\n          id: 'branch_008864cf1f9',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_08864cf1f9d',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'createRecord',\n      id: 'createRecord_c192e8f6d8d',\n    },\n    {\n      type: 'approval',\n      id: 'approval_fc79f9fa62f',\n      blocks: [\n        {\n          id: 'branch_c9c9f0a61f0',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_9c9f0a61f00',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'parallelSplit_2e05c1fc79f',\n      blocks: [\n        {\n          id: 'branch_8864cf1f9d3',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_864cf1f9d39',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_64cf1f9d393',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_4cf1f9d3938',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_cf1f9d39381',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'exclusiveSplit_d1c021e4362',\n      blocks: [\n        {\n          id: 'branch_a61f008864c',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_61f008864cf',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'exclusiveSplit_de37a4886e7',\n      blocks: [\n        {\n          id: 'branch_1f008864cf1',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_f008864cf1f',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'createRecord',\n      id: 'createRecord_25ab93c8764',\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'exclusiveSplit_15ef1db02e0',\n      blocks: [\n        {\n          id: 'branch_c9f0a61f008',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_9f0a61f0088',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_f0a61f00886',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_0a61f008864',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      type: 'createRecord',\n      id: 'createRecord_fc2f1d9ed41',\n    },\n    {\n      type: 'kunlun_all_all_lark_openapi_im_chats',\n      id: 'kunlun_all_all_lark_openapi_im_chats_5dd05c93e4e',\n    },\n    {\n      type: 'kunlun_all_all_byted_bmq_action',\n      id: 'kunlun_all_all_byted_bmq_action_f41ff46de8a',\n    },\n    {\n      type: 'kunlun_all_all_lark_openapi_doc_manage',\n      id: 'kunlun_all_all_lark_openapi_doc_manage_b789635bc6b',\n    },\n    {\n      type: 'kunlun_all_all_lark_open_spreadsheet',\n      id: 'kunlun_all_all_lark_open_spreadsheet_e8a7c39384d',\n    },\n    {\n      type: 'end',\n      id: 'end',\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-labels-mock-register.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type FlowNodeRegistry,\n  FlowTransitionLabelEnum,\n} from '@flowgram.ai/document';\nimport { FlowTextKey } from '../src/flow-renderer-registry';\n\n/**\n * 动态接入 mock register，测试 labels\n */\nexport const FlowLabelsMockRegister: FlowNodeRegistry = {\n  type: 'mock',\n  getLabels(transition) {\n    return [\n      {\n        type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n        offset: transition.transform.outputPoint,\n        props: {\n          side: 'left',\n        }\n      },\n      {\n        type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,\n        offset: transition.transform.outputPoint,\n        props: {\n          activateNode: {\n            getData: () => ({ hovered: true }),\n            isVertical: true,\n          }\n        }\n      },\n      {\n        type: FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,\n        offset: transition.transform.outputPoint,\n        props: {\n          activateNode: {\n            getData: () => ({ hovered: true }),\n            isVertical: true,\n          },\n        }\n      },\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.LOOP_WHILE_TEXT,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.TEXT_LABEL,\n        renderKey: FlowTextKey.CATCH_TEXT,\n        props: {\n          style: {\n            width: 100\n          }\n        },\n        rotate: '90deg',\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: FlowTransitionLabelEnum.CUSTOM_LABEL,\n        renderKey: FlowTextKey.LOOP_WHILE_TEXT,\n        offset: transition.transform.outputPoint,\n      },\n      {\n        type: 'unknown' as any,\n        offset: transition.transform.outputPoint,\n      },\n    ];\n  },\n};\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-mock-node-json.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const flowJson = {\n  nodes: [\n    {\n      type: 'start',\n      id: 'start',\n    },\n    {\n      type: 'mock',\n      id: 'mock',\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/flow-selected-nodes.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const FLOW_SELECTED_NODES = {\n  nodes: [\n    {\n      id: 'start',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'createRecord_613973143a5',\n      type: 'createRecord',\n      blocks: [],\n    },\n    {\n      id: 'exclusiveSplit_13973143a53',\n      type: 'dynamicSplit',\n      blocks: [\n        {\n          id: 'branch_0b5ee7b1189',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n        {\n          id: 'branch_b5ee7b11890',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n        },\n      ],\n    },\n    {\n      id: 'exclusiveSplit_30baf8b1da0',\n      type: 'dynamicSplit',\n      blocks: [\n        {\n          id: 'branch_33d40b5ee7b',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n          blocks: [\n            {\n              id: 'createRecord_897b61c55f3',\n              type: 'createRecord',\n              blocks: [],\n            },\n          ],\n        },\n        {\n          id: 'branch_3d40b5ee7b1',\n          meta: {\n            size: {\n              width: 280,\n              height: 28,\n            },\n          },\n          blocks: [\n            {\n              id: 'exclusiveSplit_d0070ce5d04',\n              type: 'dynamicSplit',\n              blocks: [\n                {\n                  id: 'branch_d40b5ee7b11',\n                  meta: {\n                    size: {\n                      width: 280,\n                      height: 28,\n                    },\n                  },\n                  blocks: [\n                    {\n                      id: 'createRecord_47e8fe1dfc3',\n                      type: 'createRecord',\n                      blocks: [],\n                    },\n                    {\n                      id: 'createRecord_32dcdd10274',\n                      type: 'createRecord',\n                      blocks: [],\n                    },\n                    {\n                      id: 'exclusiveSplit_a5579b3997d',\n                      type: 'dynamicSplit',\n                      blocks: [\n                        {\n                          id: 'branch_5ee7b11890c',\n                          meta: {\n                            size: {\n                              width: 280,\n                              height: 28,\n                            },\n                          },\n                          blocks: [\n                            {\n                              id: 'createRecord_b57b00eee94',\n                              type: 'createRecord',\n                              blocks: [],\n                            },\n                          ],\n                        },\n                        {\n                          id: 'branch_ee7b11890c1',\n                          meta: {\n                            size: {\n                              width: 280,\n                              height: 28,\n                            },\n                          },\n                        },\n                      ],\n                    },\n                  ]\n                },\n                {\n                  id: 'branch_40b5ee7b118',\n                  meta: {\n                    size: {\n                      width: 280,\n                      height: 28,\n                    },\n                  },\n                  blocks: [\n                    {\n                      id: 'tryCatch_cb31cd3f34f',\n                      type: 'tryCatch',\n                      blocks: [\n                        {\n                          id: 'branch_b31cd3f34fe',\n                          blocks: [\n                            {\n                              id: 'createRecord_a32ff708e68',\n                              type: 'createRecord',\n                              blocks: [],\n                            },\n                          ]\n                        },\n                        {\n                          id: 'branch_31cd3f34fec',\n                          blocks: [\n                            {\n                              id: 'createRecord_94cf09ad24b',\n                              type: 'createRecord',\n                              blocks: [],\n                            },\n                          ]\n                        },\n                      ],\n                    },\n                  ]\n                },\n              ],\n            },\n          ]\n        },\n      ],\n    },\n    {\n      id: 'end',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/mock-lines.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowTransitionLineEnum } from \"@flowgram.ai/document\"\n\nexport const mockDivergeLine1 = {\n  type: FlowTransitionLineEnum.DIVERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 0, y: 235},\n}\n\nexport const mockDivergeLine2 = {\n  type: FlowTransitionLineEnum.DIVERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 156, y: 232},\n}\n\nexport const mockDivergeLine3 = {\n  type: FlowTransitionLineEnum.DIVERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 141, y: 332},\n}\n\nexport const mockMergeLine3 = {\n  type: FlowTransitionLineEnum.MERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 0, y: 235},\n}\n\nexport const mockMergeLine4 = {\n  type: FlowTransitionLineEnum.MERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 141, y: 335},\n}\n\nexport const mockMergeLine5 = {\n  type: FlowTransitionLineEnum.MERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 150, y: 335},\n}\n\nexport const noRadiusLine = {\n  type: FlowTransitionLineEnum.DIVERGE_LINE,\n  from: {x: 140, y: 212},\n  to: {x: 141, y: 213},\n}\n\nexport const noTypeLine = {\n  type: undefined,\n  from: {x: 140, y: 212},\n  to: {x: 0, y: 312},\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/renderer.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityManager, PlaygroundConfigEntity, PlaygroundContainerModule } from '@flowgram.ai/core'\nimport { Container } from 'inversify'\n\nexport function createPlaygroundContainer(): Container {\n  const container = new Container()\n  container.load(PlaygroundContainerModule)\n  return container\n}\n\nexport function createPlaygroundConfigEntity(): PlaygroundConfigEntity {\n  return createPlaygroundContainer()\n    .get<EntityManager>(EntityManager)\n    .getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity, true)!\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__mocks__/setup-file.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata'\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/components/Adder.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { vi, describe, test, expect } from 'vitest';\nimport { render } from '@testing-library/react';\n\nimport Adder from '../../src/components/Adder';\n\nvi.mock('../../src/components/utils', () => ({\n  DEFAULT_LABEL_ACTIVATE_HEIGHT: 10,\n  getTransitionLabelHoverWidth() {\n    return 10;\n  },\n}));\n\ndescribe.skip('Adder', () => {\n  test('should render Adder correctly', () => {\n    const data = {\n      entity: {\n        document: {\n          renderState: {\n            getNodeDroppingId() {},\n            getDragStartEntity() {},\n          },\n          renderTree: {\n            getOriginInfo() {\n              return {\n                next: null,\n              };\n            },\n          },\n        },\n      },\n    } as any;\n\n    const rendererRegistry = {\n      getRendererComponent() {\n        return {\n          renderer() {\n            return 'hello';\n          },\n        };\n      },\n    } as any;\n\n    const { getByText } = render(<Adder data={data} rendererRegistry={rendererRegistry} />);\n    expect(getByText('hello')).toBeDefined();\n  });\n});\n\n// describe('getFlowRenderKey', () => {\n//   test('should getFlowRenderKey work correctly', () => {\n//     // branch\n//     expect(getFlowRenderKey(DRAGGING_TYPE.BRANCH, false, false)).toBe(FlowRendererKey.ADDER);\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.BRANCH, true, false)).toBe(FlowRendererKey.ADDER);\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.BRANCH, true, true)).toBe(FlowRendererKey.ADDER);\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.BRANCH, false, true)).toBe(FlowRendererKey.ADDER);\n\n//     // node\n//     expect(getFlowRenderKey(DRAGGING_TYPE.NODE, false, false)).toBe(\n//       FlowRendererKey.DRAGGABLE_ADDER,\n//     );\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.NODE, true, false)).toBe(\n//       FlowRendererKey.DRAG_HIGHLIGHT_ADDER,\n//     );\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.NODE, true, true)).toBe(FlowRendererKey.ADDER);\n\n//     expect(getFlowRenderKey(DRAGGING_TYPE.NODE, false, true)).toBe(FlowRendererKey.ADDER);\n//   });\n// });\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/components/rounded-turning-line.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { describe, test, expect, vi } from 'vitest';\nimport { Container, interfaces } from 'inversify';\nimport { render } from '@testing-library/react';\nimport {\n  FlowDocumentContainerModule,\n  FlowTransitionLine,\n  FlowTransitionLineEnum,\n} from '@flowgram.ai/document';\n\nimport RoundedTurningLine from '../../src/components/RoundedTurningLine';\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  // container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\nvi.mock('../../src/hooks/use-base-color.ts', () => ({\n  useBaseColor: () => ({\n    baseActivatedColor: '#fff',\n    baseColor: '#fff',\n  }),\n  BASE_DEFAULT_COLOR: '#BBBFC4',\n  BASE_DEFAULT_ACTIVATED_COLOR: '#82A7FC',\n}));\n\ndescribe('RoundedTurningLine', () => {\n  test('should render RoundedTurningLine correctly', () => {\n    const line: FlowTransitionLine = {\n      type: FlowTransitionLineEnum.ROUNDED_LINE,\n      from: {\n        x: 0,\n        y: 0,\n      },\n      to: {\n        x: 100,\n        y: 100,\n      },\n      vertices: [\n        {\n          x: 100,\n          y: 0,\n        },\n      ],\n    };\n\n    const { container } = render(<RoundedTurningLine {...line} />);\n    expect(container.querySelector('path')).toBeDefined();\n  });\n\n  test('should render RoundedTurningLine horizontal & arrow & active', () => {\n    const line: FlowTransitionLine = {\n      type: FlowTransitionLineEnum.ROUNDED_LINE,\n      from: {\n        x: 0,\n        y: 0,\n      },\n      to: {\n        x: 100,\n        y: 100,\n      },\n      activated: true,\n    };\n\n    const { container } = render(<RoundedTurningLine arrow={true} isHorizantal={true} {...line} />);\n    expect(container.querySelector('path')).toBeDefined();\n  });\n\n  test('should render with vertices', () => {\n    const line: FlowTransitionLine = {\n      type: FlowTransitionLineEnum.ROUNDED_LINE,\n      from: {\n        x: 0,\n        y: 0,\n      },\n      to: {\n        x: 100,\n        y: 100,\n      },\n      vertices: [\n        {\n          x: 0,\n          y: 30,\n        },\n        {\n          x: 0,\n          y: 30,\n          radiusX: 30,\n          radiusY: 30,\n        },\n        {\n          x: 0,\n          y: 30,\n          moveX: 10,\n          moveY: 10,\n        },\n        {\n          x: 50,\n          y: 50,\n        },\n      ],\n      activated: true,\n    };\n\n    const { container } = render(<RoundedTurningLine arrow={true} isHorizantal={true} {...line} />);\n    expect(container.querySelector('path')).toBeDefined();\n  });\n\n  test('should hide RoundedTurningLine', () => {\n    const line: FlowTransitionLine = {\n      type: FlowTransitionLineEnum.ROUNDED_LINE,\n      from: {\n        x: 0,\n        y: 0,\n      },\n      to: {\n        x: 100,\n        y: 100,\n      },\n    };\n\n    const { container } = render(<RoundedTurningLine hide={true} {...line} />);\n    expect(container.querySelector('path')).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/entities/__snapshots__/flow-drag-entities.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-drag-entity > flow drag scroll 1`] = `undefined`;\n\nexports[`flow-drag-entity > flow drag scroll 2`] = `0`;\n\nexports[`flow-drag-entity > flow drag scroll 3`] = `2`;\n\nexports[`flow-drag-entity > flow drag scroll 4`] = `3`;\n\nexports[`flow-drag-entity > flow drag scroll 5`] = `1`;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/entities/flow-drag-entities.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { Rectangle } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowNodeTransitionData,\n  FlowTransitionLabelEnum,\n  LABEL_SIDE_TYPE,\n} from '@flowgram.ai/document';\nimport { EntityManager, PlaygroundConfigEntity, PlaygroundContext } from '@flowgram.ai/core';\n\nimport { FlowDragEntity } from '../../src/entities/flow-drag-entity';\nimport { flowJson } from '../../__mocks__/flow-json.mock';\nimport {\n  NOT_SCROLL_EVENT,\n  SCROLL_BOTTOM_EVENT,\n  SCROLL_LEFT_EVENT,\n  SCROLL_RIGHT_EVENT,\n  SCROLL_TOP_EVENT,\n} from '../../__mocks__/flow-drag-entity';\nimport { createDocumentContainer } from '../../__mocks__/flow-document-container.mock';\n\n// layer 层 drag entity 单测\ndescribe('flow-drag-entity', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let flowDragEntity: FlowDragEntity;\n  let nodeTransition: FlowNodeTransitionData;\n  let firstBranchTransition: FlowNodeTransitionData;\n  const collisionRect = new Rectangle(-50, -50, 100, 100);\n  const notCollisionRect = new Rectangle(50, 50, 100, 100);\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    document = container.get<FlowDocument>(FlowDocument);\n    const entityManager = container.get<EntityManager>(EntityManager);\n    container.bind(PlaygroundContext).toConstantValue({});\n    document.fromJSON(flowJson);\n    flowDragEntity = new FlowDragEntity({ entityManager });\n    const playgroundConfigEntity =\n      entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity);\n    playgroundConfigEntity?.updateConfig({\n      clientX: 0,\n      clientY: 0,\n      width: 3000,\n      height: 3000,\n    });\n    const nodeEntity = document.getNode('$blockIcon$approval_fc79f9fa62f');\n    nodeTransition = nodeEntity?.getData<FlowNodeTransitionData>(\n      FlowNodeTransitionData\n    ) as FlowNodeTransitionData;\n    const firstBranchEntity = document.getNode('branch_8864cf1f9d3');\n    firstBranchTransition = firstBranchEntity?.getData<FlowNodeTransitionData>(\n      FlowNodeTransitionData\n    ) as FlowNodeTransitionData;\n  });\n\n  it('flow drag scroll', () => {\n    const el = global.document.createElement('div');\n    // 页面不滚动\n    expect(flowDragEntity.scrollDirection(NOT_SCROLL_EVENT, 0, 0)).toMatchSnapshot();\n    // 页面滚动\n    expect(flowDragEntity.scrollDirection(SCROLL_TOP_EVENT, 0, 0)).toMatchSnapshot();\n    expect(flowDragEntity.scrollDirection(SCROLL_LEFT_EVENT, 0, 0)).toMatchSnapshot();\n    expect(flowDragEntity.scrollDirection(SCROLL_RIGHT_EVENT, 0, 0)).toMatchSnapshot();\n    expect(flowDragEntity.scrollDirection(SCROLL_BOTTOM_EVENT, 0, 0)).toMatchSnapshot();\n    // 停止滚动\n    flowDragEntity.stopAllScroll();\n    expect(flowDragEntity.hasScroll).toEqual(false);\n  });\n\n  it('flow drag node collision true', () => {\n    // 测试默认 offset x y 为 0\n    expect(flowDragEntity.isCollision(nodeTransition, collisionRect, false)).toEqual({\n      hasCollision: true,\n      labelOffsetType: undefined,\n    });\n  });\n\n  it('flow drag node label empty', () => {\n    expect(flowDragEntity.isCollision(nodeTransition, notCollisionRect, false)).toEqual({\n      hasCollision: false,\n      labelOffsetType: undefined,\n    });\n  });\n\n  it('flow drag node collision false', () => {\n    const emptyLabelNodeTransition = {\n      ...nodeTransition,\n      labels: [{ type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL, offset: { x: 0, y: 0 } }],\n    } as FlowNodeTransitionData;\n    expect(flowDragEntity.isCollision(emptyLabelNodeTransition, collisionRect, false)).toEqual({\n      hasCollision: false,\n      labelOffsetType: undefined,\n    });\n  });\n\n  it('flow drag branch collision true', () => {\n    const preBranchNodeTransition = {\n      ...firstBranchTransition,\n      labels: [\n        {\n          type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n          props: {\n            side: LABEL_SIDE_TYPE.PRE_BRANCH,\n          } as any,\n          offset: { x: 0, y: 0 },\n        },\n      ],\n    } as FlowNodeTransitionData;\n    // 第一个分支场景，校验 labelOffsetType 场景\n    expect(flowDragEntity.isCollision(preBranchNodeTransition, collisionRect, true)).toEqual({\n      hasCollision: true,\n      labelOffsetType: LABEL_SIDE_TYPE.PRE_BRANCH,\n    });\n  });\n\n  it('flow drag branch collision false', () => {\n    const preBranchNodeTransition = {\n      ...firstBranchTransition,\n      labels: [\n        {\n          type: FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL,\n          props: {\n            side: LABEL_SIDE_TYPE.PRE_BRANCH,\n          } as any,\n          offset: { x: 0, y: 0 },\n        },\n      ],\n    } as FlowNodeTransitionData;\n    expect(flowDragEntity.isCollision(preBranchNodeTransition, notCollisionRect, true)).toEqual({\n      hasCollision: false,\n      labelOffsetType: LABEL_SIDE_TYPE.NORMAL_BRANCH,\n    });\n  });\n\n  it('flow drag dispose', () => {\n    flowDragEntity.dispose();\n    expect(flowDragEntity.toDispose.disposed).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/entities/flow-select-config-entity.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { FlowDocument, FlowNodeTransformData } from '@flowgram.ai/document';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { FlowSelectConfigEntity } from '../../src/entities/flow-select-config-entity';\nimport { FLOW_SELECTED_NODES } from '../../__mocks__/flow-selected-nodes.mock';\nimport { createDocumentContainer } from '../../__mocks__/flow-document-container.mock';\n\ndescribe('flow-select-config-entity', () => {\n  let document: FlowDocument;\n  let configEntity: FlowSelectConfigEntity;\n  let transformVisibles: FlowNodeTransformData[] = [];\n  beforeEach(() => {\n    const container = createDocumentContainer();\n    document = container.get(FlowDocument);\n    configEntity = container.get(EntityManager).getEntity(FlowSelectConfigEntity, true)!;\n    document.fromJSON(FLOW_SELECTED_NODES);\n    document.transformer.refresh();\n    transformVisibles = document\n      .getRenderDatas(FlowNodeTransformData, false)\n      .filter((transform) => {\n        const { entity } = transform;\n        if (entity.originParent) {\n          return entity.getNodeMeta().selectable && entity.originParent.getNodeMeta().selectable;\n        }\n        return entity.getNodeMeta().selectable;\n      });\n  });\n  it('base', () => {\n    expect(configEntity.selectedNodes.length).toEqual(0);\n    expect(configEntity.getSelectedBounds().width).toEqual(0);\n    const node = document.getNode('createRecord_47e8fe1dfc3')!;\n    configEntity.selectedNodes = [node];\n    expect(configEntity.selectedNodes.map((n) => n.id)).toEqual(['createRecord_47e8fe1dfc3']);\n    expect(configEntity.getSelectedBounds().width).toEqual(300);\n    configEntity.clearSelectedNodes();\n    expect(configEntity.getSelectedBounds().width).toEqual(0);\n  });\n  it('select from bounds', () => {\n    const bounds = new Rectangle(-150, 630, 300, 80);\n    configEntity.selectFromBounds(bounds, transformVisibles);\n    expect(configEntity.selectedNodes.map((n) => n.id)).toEqual(['exclusiveSplit_30baf8b1da0']);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/flow-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/__snapshots__/flow-drag-layer.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-drag-layer > test ready 1`] = `undefined`;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/__snapshots__/flow-label-layer.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-label-layer > test render 1`] = `\n<DocumentFragment>\n  <div\n    data-label-id=\"start\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-adder\"\n      data-from=\"start\"\n      data-testid=\"sdk.flowcanvas.line.adder\"\n      data-to=\"mock\"\n      style=\"width: 280px; height: 0px; display: flex; justify-content: center; align-items: center;\"\n    />\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-branch-draggable-adder\"\n    />\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  />\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-collapse\"\n      style=\"width: 280px; height: 32px; display: flex; justify-content: center; align-items: center;\"\n    />\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-collapse-adder\"\n      style=\"display: flex;\"\n    >\n      <div\n        class=\"flow-canvas-adder\"\n        data-from=\"mock\"\n        data-testid=\"sdk.flowcanvas.line.adder\"\n        data-to=\"\"\n        style=\"width: 40px; height: 32px; display: flex; justify-content: center; align-items: center;\"\n      />\n    </div>\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-collapse-adder\"\n    >\n      <div\n        class=\"flow-canvas-collapse\"\n        style=\"width: 280px; height: 20px; display: flex; justify-content: center; align-items: flex-end;\"\n      />\n      <div\n        class=\"flow-canvas-adder\"\n        data-from=\"mock\"\n        data-testid=\"sdk.flowcanvas.line.adder\"\n        data-to=\"\"\n        style=\"width: 280px; height: 20px; display: flex; justify-content: center; align-items: center;\"\n      />\n    </div>\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      class=\"flow-canvas-collapse-adder\"\n    >\n      <div\n        class=\"flow-canvas-collapse\"\n        style=\"width: 280px; height: 20px; display: flex; justify-content: center; align-items: flex-end;\"\n      />\n      <div\n        class=\"flow-canvas-adder\"\n        data-from=\"mock\"\n        data-testid=\"sdk.flowcanvas.line.adder\"\n        data-to=\"\"\n        style=\"width: 280px; height: 20px; display: flex; justify-content: center; align-items: center;\"\n      />\n    </div>\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      data-label-id=\"mock\"\n      style=\"font-size: 12px; color: rgb(187, 191, 196); text-align: center; white-space: nowrap; background-color: var(--g-editor-background); line-height: 20px;\"\n    >\n      123\n    </div>\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  >\n    <div\n      data-label-id=\"mock\"\n      style=\"font-size: 12px; color: rgb(187, 191, 196); text-align: center; white-space: nowrap; background-color: var(--g-editor-background); line-height: 20px; width: 100px; transform: rotate(90deg);\"\n    >\n      catch-text\n    </div>\n  </div>\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  />\n  <div\n    data-label-id=\"mock\"\n    style=\"position: absolute; left: 0px; top: 0px; transform: translate(-50%, -50%);\"\n  />\n</DocumentFragment>\n`;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/__snapshots__/flow-selector-box-layer.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`flow-selector-box-layer > test ready 1`] = `undefined`;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/flow-drag-layer.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { Container, decorate, injectable, type interfaces } from 'inversify';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n} from '@flowgram.ai/document';\nimport {\n  createDefaultPlaygroundConfig,\n  PlaygroundConfig,\n  PlaygroundContainerModule,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererRegistry } from '../../src/flow-renderer-registry';\nimport { FlowRendererContribution } from '../../src/flow-renderer-contribution';\nimport { FlowDragLayer, FlowRendererContainerModule } from '../../src';\nimport { flowJson } from '../../__mocks__/flow-json.mock';\nimport { FlowDocumentMockRegister } from '../../__mocks__/flow-document-container.mock';\n\nclass FlowRenderMockRegister implements FlowRendererContribution {\n  registerRenderer(registry: FlowRendererRegistry): void {\n    registry.registerLayers(FlowDragLayer);\n  }\n}\n\ndecorate(injectable(), FlowRenderMockRegister);\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\n// layer 层 drag entity 单测\ndescribe('flow-drag-layer', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let registry: FlowRendererRegistry;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.load(FlowRendererContainerModule);\n    container.load(PlaygroundContainerModule);\n    container.bind(FlowRendererContribution).to(FlowRenderMockRegister);\n    container.bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n\n    document = container.get<FlowDocument>(FlowDocument);\n    document.fromJSON(flowJson);\n    registry = container.get<FlowRendererRegistry>(FlowRendererRegistry);\n    registry.init();\n  });\n\n  // 测试初始化\n  // it('test ready', () => {\n  //   expect(registry.pipeline.renderer.layers.map(layer => layer?.onReady?.())).toMatchSnapshot();\n  // });\n\n  // 渲染\n  it('test ready', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      expect(layer.render?.()).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/flow-label-layer.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { Container, decorate, injectable, type interfaces } from 'inversify';\nimport { render } from '@testing-library/react';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n} from '@flowgram.ai/document';\nimport {\n  createDefaultPlaygroundConfig,\n  PlaygroundConfig,\n  PlaygroundContainerModule,\n} from '@flowgram.ai/core';\n\nimport { FlowLabelsLayer } from '../../src/layers/flow-labels-layer';\nimport { FlowRendererRegistry, FlowTextKey } from '../../src/flow-renderer-registry';\nimport { FlowRendererContribution } from '../../src/flow-renderer-contribution';\nimport { FlowRendererContainerModule } from '../../src/flow-renderer-container-module';\nimport { flowJson } from '../../__mocks__/flow-mock-node-json';\nimport { FlowLabelsMockRegister } from '../../__mocks__/flow-labels-mock-register';\nimport { FlowDocumentMockRegister } from '../../__mocks__/flow-document-container.mock';\n\nclass FlowRenderMockRegister implements FlowRendererContribution {\n  registerRenderer(registry: FlowRendererRegistry): void {\n    registry.registerLayers(FlowLabelsLayer);\n  }\n}\n\ndecorate(injectable(), FlowRenderMockRegister);\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\n// layer 层 drag entity 单测\ndescribe('flow-label-layer', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let registry: FlowRendererRegistry;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.load(FlowRendererContainerModule);\n    container.load(PlaygroundContainerModule);\n    container.bind(FlowRendererContribution).to(FlowRenderMockRegister);\n    container.bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n\n    document = container.get<FlowDocument>(FlowDocument);\n    document.init();\n    document.registerFlowNodes(\n      FlowLabelsMockRegister, // 通过 getLabel 方法 mock label\n    );\n    document.fromJSON(flowJson);\n    registry = container.get<FlowRendererRegistry>(FlowRendererRegistry);\n    registry.init();\n  });\n\n  // 测试初始化\n  it('test ready', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      layer?.onReady?.();\n      expect(layer.node.style.zIndex).toEqual('9');\n    });\n  });\n\n  // 缩放\n  it('test zoom', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      layer?.onZoom?.(2);\n      expect(layer.node!.style.transform).toEqual('scale(2)');\n    });\n  });\n\n  // FIXME: render 单测目前不全\n  // 渲染\n  it('test render', () => {\n    vi.mock('@flowgram.ai/core', async importOriginal => {\n      const contextMaker = {\n        makeFormItemMaterialContext: vi.fn().mockReturnValue('mock-context'),\n        isDragBranch: true,\n        labelSide: 'left',\n        isDroppableBranch: () => true,\n        dropNodeId: 'mock',\n        dragging: true,\n        isDroppableNode: () => true,\n      };\n      return {\n        // @ts-ignore\n        ...(await importOriginal()),\n        // mock Adder 组件里的 useService\n        useService: vi.fn().mockReturnValue(contextMaker),\n      };\n    });\n    const res: (JSX.Element | undefined)[] = [];\n\n    registry.pipeline.renderer.layers.forEach(layer => {\n      const render = (layer as any)?._render?.bind(layer);\n      // mock rendererRegistry\n      // @ts-ignore\n      layer.rendererRegistry = {\n        getRendererComponent: () => ({\n          renderer: () => null,\n        }),\n        getText: key => {\n          if (key === FlowTextKey.LOOP_WHILE_TEXT) {\n            return '123';\n          }\n          return;\n        },\n      };\n      res.push(render());\n    });\n\n    const app = render(<>{res}</>);\n    expect(app.asFragment()).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/flow-lines-layer.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { Container, decorate, injectable, type interfaces } from 'inversify';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n} from '@flowgram.ai/document';\nimport {\n  createDefaultPlaygroundConfig,\n  PlaygroundConfig,\n  PlaygroundConfigEntity,\n  PlaygroundContainerModule,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererRegistry } from '../../src/flow-renderer-registry';\nimport { FlowRendererContribution } from '../../src/flow-renderer-contribution';\nimport { FlowRendererContainerModule } from '../../src/flow-renderer-container-module';\nimport { FlowLinesLayer, FlowNodesTransformLayer } from '../../src';\nimport { flowJson } from '../../__mocks__/flow-json.mock';\nimport { FlowDocumentMockRegister } from '../../__mocks__/flow-document-container.mock';\n\nclass FlowRenderMockRegister implements FlowRendererContribution {\n  registerRenderer(registry: FlowRendererRegistry): void {\n    registry.registerLayers(FlowLinesLayer);\n  }\n}\n\ndecorate(injectable(), FlowRenderMockRegister);\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\ndescribe('flow-lines-layer', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let registry: FlowRendererRegistry;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.load(FlowRendererContainerModule);\n    container.load(PlaygroundContainerModule);\n    container.bind(FlowRendererContribution).to(FlowRenderMockRegister);\n    container.bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n\n    document = container.get<FlowDocument>(FlowDocument);\n    document.init();\n    document.fromJSON(flowJson);\n    registry = container.get<FlowRendererRegistry>(FlowRendererRegistry);\n    registry.init();\n\n    // Mock the ResizeObserver\n    const ResizeObserverMock = vi.fn(() => ({\n      observe: vi.fn(),\n      unobserve: vi.fn(),\n      disconnect: vi.fn(),\n    }));\n\n    // Stub the global ResizeObserver\n    vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n  });\n\n  // 测试初始化\n  it('test ready', () => {\n    registry.pipeline.renderer.layers.forEach((layer) => {\n      (layer as FlowLinesLayer).onReady();\n      expect(layer.node.style.zIndex).toEqual('1');\n    });\n  });\n\n  // 缩放\n  it('test zoom', () => {\n    const config = container.get<PlaygroundConfigEntity>(PlaygroundConfigEntity);\n    config.updateConfig({ zoom: 2 });\n    registry.pipeline.renderer.layers.forEach((layer) => {\n      const linesLayer = layer as FlowLinesLayer;\n      linesLayer.onZoom();\n      expect(linesLayer.viewBox).toEqual('0 0 500 500');\n    });\n  });\n\n  // FIXME: render 单测目前不全\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/flow-selector-box-layer.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { Container, decorate, injectable, type interfaces } from 'inversify';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n} from '@flowgram.ai/document';\nimport {\n  createDefaultPlaygroundConfig,\n  PlaygroundConfig,\n  PlaygroundContainerModule,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererRegistry } from '../../src/flow-renderer-registry';\nimport { FlowRendererContribution } from '../../src/flow-renderer-contribution';\nimport {\n  FlowSelectorBoxLayer,\n  FlowRendererContainerModule,\n  FlowSelectConfigEntity,\n} from '../../src';\nimport { flowJson } from '../../__mocks__/flow-json.mock';\nimport { FlowDocumentMockRegister } from '../../__mocks__/flow-document-container.mock';\n\nclass FlowRenderMockRegister implements FlowRendererContribution {\n  registerRenderer(registry: FlowRendererRegistry): void {\n    registry.registerLayers(FlowSelectorBoxLayer);\n  }\n}\n\ndecorate(injectable(), FlowRenderMockRegister);\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\n// box layer 单测\ndescribe('flow-selector-box-layer', () => {\n  let container = createDocumentContainer();\n  let document: any;\n  let registry: FlowRendererRegistry;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.load(FlowRendererContainerModule);\n    container.load(PlaygroundContainerModule);\n    container.bind(FlowRendererContribution).to(FlowRenderMockRegister);\n    container.bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n    container.bind(FlowSelectConfigEntity).toSelf().inSingletonScope();\n\n    document = container.get<typeof FlowDocument>(FlowDocument as any);\n    document.fromJSON(flowJson);\n    registry = container.get<FlowRendererRegistry>(FlowRendererRegistry);\n    registry.init();\n  });\n\n  // 渲染, FIXME 补充单测\n  it('test ready', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      // layer.onReady();\n      expect(layer.render?.()).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/layers/flow-transform-layer.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { Container, decorate, injectable, type interfaces } from 'inversify';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n  FlowDocumentContribution,\n} from '@flowgram.ai/document';\nimport {\n  createDefaultPlaygroundConfig,\n  PlaygroundConfig,\n  PlaygroundContainerModule,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererRegistry } from '../../src/flow-renderer-registry';\nimport { FlowRendererContribution } from '../../src/flow-renderer-contribution';\nimport { FlowRendererContainerModule } from '../../src/flow-renderer-container-module';\nimport { FlowNodesTransformLayer } from '../../src';\nimport { flowJson } from '../../__mocks__/flow-json.mock';\nimport { FlowDocumentMockRegister } from '../../__mocks__/flow-document-container.mock';\n\nclass FlowRenderMockRegister implements FlowRendererContribution {\n  registerRenderer(registry: FlowRendererRegistry): void {\n    registry.registerLayers(FlowNodesTransformLayer);\n  }\n}\n\ndecorate(injectable(), FlowRenderMockRegister);\n\nfunction createDocumentContainer(): interfaces.Container {\n  const container = new Container();\n  container.load(FlowDocumentContainerModule);\n  container.bind(FlowDocumentContribution).to(FlowDocumentMockRegister);\n  return container;\n}\n\n// layer 层 drag entity 单测\ndescribe('flow-transform-layer', () => {\n  let container = createDocumentContainer();\n  let document: FlowDocument;\n  let registry: FlowRendererRegistry;\n\n  beforeEach(() => {\n    container = createDocumentContainer();\n    container.load(FlowRendererContainerModule);\n    container.load(PlaygroundContainerModule);\n    container.bind(FlowRendererContribution).to(FlowRenderMockRegister);\n    container.bind(PlaygroundConfig).toConstantValue(createDefaultPlaygroundConfig());\n\n    document = container.get<FlowDocument>(FlowDocument);\n    document.init();\n    document.fromJSON(flowJson);\n    registry = container.get<FlowRendererRegistry>(FlowRendererRegistry);\n    registry.init();\n\n    // Mock the ResizeObserver\n    const ResizeObserverMock = vi.fn(() => ({\n      observe: vi.fn(),\n      unobserve: vi.fn(),\n      disconnect: vi.fn(),\n    }));\n\n    // Stub the global ResizeObserver\n    vi.stubGlobal('ResizeObserver', ResizeObserverMock);\n  });\n\n  // 测试初始化\n  it('test ready', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      (layer as FlowNodesTransformLayer).onReady();\n      expect(layer.node.style.zIndex).toEqual('10');\n    });\n  });\n\n  // 缩放\n  it('test zoom', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      (layer as FlowNodesTransformLayer).onZoom(2);\n      expect(layer.node!.style.transform).toEqual('scale(2)');\n    });\n  });\n\n  // FIXME: render 单测目前不全\n  // 渲染\n  it('test render', () => {\n    registry.pipeline.renderer.layers.forEach(layer => {\n      // const autorun = registry.pipeline.renderer.layerAutorunMap.get(layer);\n      // autorun?.();\n      (layer as FlowNodesTransformLayer).updateNodesBounds();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/utils/element.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, expect, it } from 'vitest';\n\nimport { isHidden, isRectInit } from '../../src/utils/element';\n\ndescribe('test isHidden', () => {\n  it('isHidden true', () => {\n    vi.stubGlobal('getComputedStyle', () => ({\n      display: 'none',\n    }));\n    const mockElement = {\n      offsetParent: null,\n    };\n\n    const res = isHidden(mockElement as unknown as HTMLElement);\n    expect(res).toEqual(true);\n  });\n  it('isHidden false', () => {\n    vi.stubGlobal('getComputedStyle', () => ({\n      display: 'block',\n    }));\n    const mockElement1 = {\n      offsetParent: true,\n    };\n\n    const res = isHidden(mockElement1 as unknown as HTMLElement);\n    expect(res).toEqual(false);\n  });\n});\n\ndescribe('isRectInit', () => {\n  it('should return false when input is undefined', () => {\n    expect(isRectInit(undefined)).toBe(false);\n  });\n\n  it('should return false when all properties are 0', () => {\n    const emptyRect: DOMRect = {\n      bottom: 0,\n      height: 0,\n      left: 0,\n      right: 0,\n      top: 0,\n      width: 0,\n      x: 0,\n      y: 0,\n      toJSON: () => ({}),\n    };\n    expect(isRectInit(emptyRect)).toBe(false);\n  });\n\n  it('should return true when any property is not 0', () => {\n    const validRect: DOMRect = {\n      bottom: 100,\n      height: 100,\n      left: 0,\n      right: 100,\n      top: 0,\n      width: 100,\n      x: 0,\n      y: 0,\n      toJSON: () => ({}),\n    };\n    expect(isRectInit(validRect)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/utils/find-selected-nodes.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowDocument } from '@flowgram.ai/document';\n\nimport { findSelectedNodes } from '../../src/utils/find-selected-nodes';\nimport { FLOW_SELECTED_NODES } from '../../__mocks__/flow-selected-nodes.mock';\nimport { createDocument } from '../../__mocks__/flow-document-container.mock';\n\nfunction selectNodes(document: FlowDocument, nodeIds: string[]): string[] {\n  const nodes = nodeIds.map(n => document.getNode(n));\n  return findSelectedNodes(nodes).map(n => n.id);\n}\n\ndescribe('find selected nodes', () => {\n  let document: FlowDocument;\n  beforeEach(() => {\n    document = createDocument();\n    document.fromJSON(FLOW_SELECTED_NODES);\n  });\n  /**\n   * 同分支选择\n   */\n  it('some branch', () => {\n    const res = selectNodes(document, [\n      'createRecord_47e8fe1dfc3',\n      'createRecord_32dcdd10274',\n      'exclusiveSplit_a5579b3997d',\n    ]);\n    expect(res).toEqual([\n      'createRecord_47e8fe1dfc3',\n      'createRecord_32dcdd10274',\n      'exclusiveSplit_a5579b3997d',\n    ]);\n  });\n  /**\n   * 同分支下再选择子节点\n   */\n  it('some branch with sub branch', () => {\n    const res = selectNodes(document, [\n      'createRecord_47e8fe1dfc3',\n      'createRecord_32dcdd10274',\n      'createRecord_b57b00eee94', // 这个属于 \"exclusiveSplit_a5579b3997d\" 的子节点\n    ]);\n    expect(res).toEqual([\n      'createRecord_47e8fe1dfc3',\n      'createRecord_32dcdd10274',\n      'exclusiveSplit_a5579b3997d',\n    ]);\n  });\n  /**\n   * 跨分支选择\n   */\n  it('different branch', () => {\n    const res = selectNodes(document, ['createRecord_897b61c55f3', 'createRecord_b57b00eee94']);\n    expect(res).toEqual(['exclusiveSplit_30baf8b1da0']);\n    const res2 = selectNodes(document, ['createRecord_897b61c55f3', 'createRecord_47e8fe1dfc3']);\n    expect(res2).toEqual(['exclusiveSplit_30baf8b1da0']);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/utils/get-vertices.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { calcEllipseY, getHorizontalVertices, getVertices } from '../../src/components/utils';\nimport {\n  mockDivergeLine1,\n  mockDivergeLine2,\n  mockDivergeLine3,\n  mockMergeLine3,\n  mockMergeLine4,\n  mockMergeLine5,\n  noRadiusLine,\n  noTypeLine,\n} from '../../__mocks__/mock-lines';\n\ndescribe('test Vertices', () => {\n  it('calcEllipseY', () => {\n    expect(calcEllipseY(1, 1, 3)).toEqual(0);\n  });\n  // 垂直布局\n  it('getVertices diverge_line', () => {\n    expect(() => getVertices(undefined as any)).toThrowError();\n    // 正常线条\n    const res1 = getVertices(mockDivergeLine1);\n    expect(res1).toEqual([\n      { x: 140, y: 215, radiusY: 3 },\n      { x: 0, y: 215 },\n    ]);\n    // radiusYCount = 1\n    const res2 = getVertices(mockDivergeLine2);\n    expect(res2).toEqual([{ x: 156, y: 212, radiusY: 20 }]);\n    // radiusYCount > 1 & radiusXCount < 1\n    const res3 = getVertices(mockDivergeLine3);\n    expect(res3).toEqual([\n      { x: 140, y: 232, moveX: 0.5 },\n      { x: 141, y: 232, moveX: 0.5 },\n    ]);\n  });\n  it('getVertices merge_line', () => {\n    // merge_line radiusYCount < 2\n    const res3 = getVertices(mockMergeLine3);\n    expect(res3).toEqual([{ x: 140, y: 235 }]);\n    // merge_line radiusYCount > 2 & radiusXCount < 2\n    const res4 = getVertices(mockMergeLine4);\n    expect(res4).toEqual([\n      { x: 140, y: 315, moveX: 0.5 },\n      { x: 141, y: 315, moveX: 0.5 },\n    ]);\n    // merge_line radiusYCount > 2 & radiusXCount > 2\n    const res5 = getVertices(mockMergeLine5);\n    expect(res5).toEqual([\n      { x: 140, y: 315, moveX: 5 },\n      { x: 150, y: 315, moveX: 5 },\n    ]);\n  });\n\n  it('getVertices no radius line', () => {\n    const noRadiusRes = getVertices(noRadiusLine);\n    expect(noRadiusRes).toEqual([]);\n    const noTypeRes = getVertices(noTypeLine as any);\n    expect(noTypeRes).toEqual([]);\n  });\n\n  // 水平布局\n  it('getHorizontalVertices diverge_line', () => {\n    expect(() => getHorizontalVertices(undefined as any)).toThrowError();\n    // 正常线条\n    const res1 = getHorizontalVertices(mockDivergeLine1);\n    expect(res1).toEqual([\n      { x: 160, y: 212, moveY: 11.5 },\n      { x: 160, y: 235, moveY: 11.5 },\n    ]);\n    // radiusYCount = 1\n    const res2 = getHorizontalVertices(mockDivergeLine2);\n    expect(res2).toEqual([{ x: 156, y: 212, radiusX: 16 }]);\n    // radiusYCount > 1 & radiusXCount < 2\n    const res3 = getHorizontalVertices(mockDivergeLine3, 0.9);\n    expect(res3).toEqual([\n      { x: 121, y: 212, radiusX: -19 },\n      { x: 121, y: 332 },\n    ]);\n  });\n\n  it('getHorizontalVertices merge_line', () => {\n    // merge_line radiusYCount < 2\n    const res3 = getHorizontalVertices(mockMergeLine3);\n    expect(res3).toEqual([\n      { x: -20, y: 212, moveY: 11.5 },\n      { x: -20, y: 235, moveY: 11.5 },\n    ]);\n    // merge_line radiusYCount > 2 & radiusXCount < 2\n    const res4 = getHorizontalVertices(mockMergeLine4, 0.9);\n    expect(res4).toEqual([{ x: 141, y: 212 }]);\n    // merge_line radiusYCount > 2 & radiusXCount > 2\n    const res5 = getHorizontalVertices(mockMergeLine5);\n    expect(res5).toEqual([]);\n  });\n\n  it('getHorizontalVertices no radius line', () => {\n    const noRadiusRes = getHorizontalVertices(noRadiusLine);\n    expect(noRadiusRes).toEqual([]);\n    const noTypeRes = getHorizontalVertices(noTypeLine as any);\n    expect(noTypeRes).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/__tests__/utils/scroll-limit.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\n\nimport { scrollLimit } from '../../src/utils/scroll-limit';\nimport { createPlaygroundConfigEntity } from '../../__mocks__/renderer.mock';\n\ntest('scroll limit', () => {\n  const config = createPlaygroundConfigEntity();\n  config.updateConfig({\n    width: 1668,\n    height: 527,\n    clientX: 60,\n    clientY: 89,\n    scrollX: 18,\n    scrollY: -14,\n  });\n  const initScrollData = { scrollX: 100, scrollY: -100 };\n  const res = scrollLimit(initScrollData, [new Rectangle(0, 0, 10, 10)], config, () => ({\n    scrollX: config.config.scrollX,\n    scrollY: config.config.scrollY,\n  }));\n  expect(res).toEqual({ scrollX: 18, scrollY: -14 });\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  --g-selection-background: #336df4;\n  --g-editor-background: #f2f3f5;\n  --g-playground-select: var(--g-selection-background);\n  --g-playground-hover: var(--g-selection-background);\n  --g-playground-line: var(--g-selection-background);\n  --g-playground-blur: #999;\n  --g-playground-ruler-select: #3794ff;\n  --g-playground-selectBox-outline: var(--g-selection-background);\n  --g-playground-selectBox-background: rgba(51, 109, 244, 0.1);\n  --g-playground-select-hover-background: rgba(51, 109, 244, 0.1);\n  --g-playground-select-control-size: 12px;\n}\n\n:global {\n  .gedit-playground {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n    z-index: 10;\n    overflow: hidden;\n    user-select: none;\n    outline: none;\n    box-sizing: border-box;\n    background-color: var(--g-editor-background);\n  }\n\n  .gedit-playground-scroll-right {\n    position: absolute;\n    right: 2px;\n    height: 100vh;\n    width: 7px;\n    z-index: 10;\n  }\n\n  .gedit-playground-scroll-bottom {\n    position: absolute;\n    bottom: 2px;\n    width: 100vw;\n    height: 7px;\n    z-index: 10;\n  }\n\n  .gedit-playground-scroll-right-block {\n    position: absolute;\n    opacity: 0.3;\n    border-radius: 3.5px;\n  }\n\n  .gedit-playground-scroll-right-block:hover {\n    opacity: 0.6;\n  }\n\n  .gedit-playground-scroll-bottom-block {\n    position: absolute;\n    opacity: 0.3;\n    border-radius: 3.5px;\n  }\n\n  .gedit-playground-scroll-bottom-block:hover {\n    opacity: 0.6;\n  }\n\n  .gedit-playground-scroll-hidden {\n    opacity: 0;\n  }\n\n  .gedit-playground * {\n    box-sizing: border-box;\n  }\n\n  .gedit-playground-loading {\n    position: absolute;\n    color: white;\n    left: 50%;\n    top: 50%;\n    z-index: 100;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    transition: opacity 0.8s;\n    flex-direction: column;\n    text-align: center;\n    opacity: 0.8;\n  }\n\n  .gedit-hidden {\n    display: none;\n  }\n\n  .gedit-playground-pipeline {\n    position: absolute;\n    overflow: visible;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n  }\n\n  .gedit-playground-pipeline::before {\n    content: '';\n    position: absolute;\n    width: 1px;\n    height: 100%;\n    left: 0;\n    top: 0;\n  }\n\n  .gedit-playground-layer {\n    position: absolute;\n    overflow: visible;\n  }\n\n  .gedit-selector-box {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: 33;\n    outline: 1px solid var(--g-playground-selectBox-outline);\n    background-color: var(--g-playground-selectBox-background);\n  }\n\n  .gedit-selector-box-block {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: 9999;\n    display: none;\n    background-color: rgba(0, 0, 0, 0);\n  }\n\n  .gedit-selector-bounds-background {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 0;\n    height: 0;\n    outline: 1px solid var(--g-playground-selectBox-outline);\n    background-color: #f0f4ff;\n  }\n\n  .gedit-selector-bounds-foreground {\n    position: absolute;\n    left: 0;\n    top: 0;\n    width: 0;\n    height: 0;\n    z-index: 33;\n    background: rgba(255, 255, 255, 0);\n  }\n\n  .gedit-flow-activity-node {\n    position: absolute;\n  }\n\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/renderer\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"index.module.less\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/i18n\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/Adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useState } from 'react';\n\nimport {\n  type AdderProps,\n  type FlowNodeTransitionData,\n  type FlowNodeEntity,\n  FlowDragService,\n} from '@flowgram.ai/document';\nimport { useService } from '@flowgram.ai/core';\n\nimport { FlowRendererKey, type FlowRendererRegistry } from '../flow-renderer-registry';\nimport { getTransitionLabelHoverHeight, getTransitionLabelHoverWidth } from './utils';\n\ninterface PropsType {\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  hoverWidth?: number;\n  hoverHeight?: number;\n  // 业务自定义 props\n  [key: string]: unknown;\n}\n\n// export only for tests\nexport const getFlowRenderKey = (\n  node: FlowNodeEntity,\n  { dragService }: { dragService?: FlowDragService },\n) => {\n  if (dragService && dragService.dragging && dragService.isDroppableNode(node)) {\n    if (dragService.dropNodeId === node.id) {\n      return FlowRendererKey.DRAG_HIGHLIGHT_ADDER;\n    }\n    return FlowRendererKey.DRAGGABLE_ADDER;\n  }\n\n  return FlowRendererKey.ADDER;\n};\n\n/**\n * Adder 高亮热区扩散目的：\n * ux 调研的时候不少用户反馈点看的不是很清楚（初始点较小）\n * 因此给的解决办法是加深加大 icon  再加扩大 hover 热区\n *\n * Adder 模块高亮规则：\n * 取前后节点宽度的最大值为高亮区域宽度\n * 高度固定为 32px\n */\nexport default function Adder(props: PropsType) {\n  const {\n    data,\n    rendererRegistry,\n    hoverHeight = getTransitionLabelHoverHeight(data),\n    hoverWidth = getTransitionLabelHoverWidth(data),\n    ...restProps\n  } = props;\n\n  const [hoverActivated, setHoverActivated] = useState(false);\n\n  const handleMouseEnter = useCallback(() => setHoverActivated(true), []);\n  const handleMouseLeave = useCallback(() => setHoverActivated(false), []);\n\n  const node = data.entity;\n\n  const dragService = useService<FlowDragService>(FlowDragService);\n\n  // 根据拖拽条件转换状态\n  const flowRenderKey = getFlowRenderKey(node, { dragService });\n\n  const adder = rendererRegistry.getRendererComponent(flowRenderKey);\n  const from = node;\n  // 获取 originTree 的 to 节点\n  const to = data.entity.document.renderTree.getOriginInfo(node).next;\n  // 实际渲染的 to 节点\n  const renderTo = node.next;\n\n  const child = React.createElement(\n    adder.renderer as (props: AdderProps) => JSX.Element,\n    {\n      node,\n      from,\n      to,\n      renderTo,\n      hoverActivated,\n      setHoverActivated,\n      hoverWidth,\n      hoverHeight,\n      ...restProps,\n    } as AdderProps,\n  );\n\n  return (\n    // eslint-disable-next-line react/jsx-filename-extension\n    <div\n      className=\"flow-canvas-adder\"\n      data-testid=\"sdk.flowcanvas.line.adder\"\n      data-from={from.id}\n      data-to={to?.id ?? ''}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        width: hoverWidth,\n        height: hoverHeight,\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n      }}\n    >\n      {child}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/BranchDraggableRenderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  type AdderProps,\n  type FlowNodeTransitionData,\n  type LABEL_SIDE_TYPE,\n  FlowDragService,\n} from '@flowgram.ai/document';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { useService } from '@flowgram.ai/core';\n\nimport { FlowRendererKey, type FlowRendererRegistry } from '../flow-renderer-registry';\n\ninterface PropsType {\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  hoverHeight?: number;\n  side?: LABEL_SIDE_TYPE;\n  // 业务自定义 props\n  [key: string]: unknown;\n}\n\nconst getFlowRenderKey = (\n  node: FlowNodeEntity,\n  { dragService, side }: { dragService: FlowDragService; side?: LABEL_SIDE_TYPE },\n) => {\n  if (\n    dragService.isDragBranch &&\n    side &&\n    dragService.labelSide === side &&\n    dragService.isDroppableBranch(node, side)\n  ) {\n    if (dragService.dropNodeId === node.id) {\n      // 元素拖拽区域激活\n      return FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER;\n    }\n    // 节点元素拖拽，展示可被拖入区域为添加节点位置\n    return FlowRendererKey.DRAGGABLE_ADDER;\n  }\n\n  // 默认不展示\n  return '';\n};\n\n/**\n * 分支可被拖拽进入区域样式渲染\n */\nexport default function BranchDraggableRenderer(props: PropsType) {\n  const { data, rendererRegistry, side, ...restProps } = props;\n\n  const node = data.entity;\n\n  const dragService = useService<FlowDragService>(FlowDragService);\n\n  const flowRenderKey = getFlowRenderKey(node, { side, dragService });\n\n  if (!flowRenderKey) {\n    return null;\n  }\n  const adder = rendererRegistry.getRendererComponent(flowRenderKey);\n  const from = node;\n  // 获取 originTree 的 to 节点\n  const to = data.entity.document.renderTree.getOriginInfo(node).next;\n  // 实际渲染的 to 节点\n  const renderTo = node.next;\n\n  const child = React.createElement(\n    adder.renderer as (props: AdderProps) => JSX.Element,\n    {\n      node,\n      from,\n      to,\n      renderTo,\n      ...restProps,\n    } as AdderProps,\n  );\n\n  return <div className=\"flow-canvas-branch-draggable-adder\">{child}</div>;\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/Collapse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useCallback } from 'react';\n\nimport {\n  type CollapseProps,\n  FlowNodeRenderData,\n  type FlowNodeTransitionData,\n} from '@flowgram.ai/document';\n\nimport { FlowRendererKey, type FlowRendererRegistry } from '../flow-renderer-registry';\nimport { getTransitionLabelHoverHeight, getTransitionLabelHoverWidth } from './utils';\n\ninterface PropsType extends Partial<CollapseProps> {\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  hoverHeight?: number;\n  hoverWidth?: number;\n  wrapperStyle?: React.CSSProperties;\n  // 业务自定义 props\n  [key: string]: unknown;\n}\n\nexport default function Collapse(props: PropsType) {\n  const {\n    data,\n    rendererRegistry,\n    forceVisible,\n    hoverHeight = getTransitionLabelHoverHeight(data),\n    hoverWidth = getTransitionLabelHoverWidth(data),\n    wrapperStyle,\n    ...restProps\n  } = props;\n  const { activateNode } = restProps;\n\n  const [hoverActivated, setHoverActivated] = useState(false);\n  const activateData = activateNode?.getData(FlowNodeRenderData);\n\n  const handleMouseEnter = useCallback(() => {\n    setHoverActivated(true);\n    activateData?.toggleMouseEnter();\n  }, []);\n\n  const handleMouseLeave = useCallback(() => {\n    setHoverActivated(false);\n    activateData?.toggleMouseLeave();\n  }, []);\n\n  const collapseOpener = rendererRegistry.getRendererComponent(FlowRendererKey.COLLAPSE);\n  const node = data.entity;\n\n  const child = React.createElement(\n    collapseOpener.renderer as (props: CollapseProps) => JSX.Element,\n    {\n      node,\n      collapseNode: node,\n      ...restProps,\n      hoverActivated,\n    } as CollapseProps,\n  );\n\n  const isChildVisible = data.collapsed || activateData?.hovered || hoverActivated || forceVisible;\n\n  return (\n    <div\n      className=\"flow-canvas-collapse\"\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        width: hoverWidth,\n        height: hoverHeight,\n        display: 'flex',\n        justifyContent: 'center',\n        alignItems: 'center',\n        ...wrapperStyle,\n      }}\n    >\n      {isChildVisible ? child : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/CollapseAdder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState, useCallback } from 'react';\n\nimport {\n  type CollapseAdderProps,\n  FlowNodeRenderData,\n  type FlowNodeTransitionData,\n} from '@flowgram.ai/document';\n\nimport { type FlowRendererRegistry } from '../flow-renderer-registry';\nimport Collapse from './Collapse';\nimport Adder from './Adder';\n\ninterface PropsType extends Partial<CollapseAdderProps> {\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  // 业务自定义 props\n  [key: string]: unknown;\n}\n\n/**\n * 加号和收起复合 Label\n * @param props\n * @returns\n */\nexport default function CollapseAdder(props: PropsType) {\n  const { data, rendererRegistry, ...restProps } = props;\n  const { activateNode } = restProps;\n\n  // 收起展开按钮是否可见\n  const [hoverActivated, setHoverActivated] = useState(false);\n\n  const activateData = activateNode?.getData(FlowNodeRenderData);\n\n  const handleMouseEnter = useCallback(() => {\n    setHoverActivated(true);\n  }, []);\n\n  const handleMouseLeave = useCallback(() => {\n    setHoverActivated(false);\n  }, []);\n\n  const isVertical = activateNode?.isVertical;\n  const activated = activateData?.hovered || hoverActivated;\n  if (isVertical) {\n    return (\n      <div\n        className=\"flow-canvas-collapse-adder\"\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n      >\n        {(activated || data.collapsed) && (\n          <Collapse\n            forceVisible\n            {...props}\n            wrapperStyle={{\n              alignItems: 'flex-end',\n            }}\n            hoverHeight={20}\n          />\n        )}\n        {!data.collapsed && (\n          <Adder {...props} hoverHeight={activated ? 20 : 40} hoverActivated={activated} />\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      className=\"flow-canvas-collapse-adder\"\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        display: data.collapsed ? 'block' : 'flex',\n      }}\n    >\n      {(activated || data.collapsed) && (\n        <Collapse\n          forceVisible\n          {...props}\n          wrapperStyle={{\n            justifyContent: 'flex-end',\n          }}\n          hoverWidth={20}\n        />\n      )}\n      {!data.collapsed && (\n        <Adder {...props} hoverWidth={activated ? 20 : 40} hoverActivated={activated} />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/CustomLine.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { FlowTransitionLine } from '@flowgram.ai/document';\n\nimport { type FlowRendererRegistry } from '../flow-renderer-registry';\n\ninterface PropsType extends FlowTransitionLine {\n  rendererRegistry: FlowRendererRegistry;\n}\n\nfunction CustomLine(props: PropsType): JSX.Element {\n  const { renderKey, rendererRegistry, ...line } = props;\n\n  if (!renderKey) {\n    return <></>;\n  }\n\n  const renderer = rendererRegistry.getRendererComponent(renderKey);\n\n  if (!renderer) {\n    return <></>;\n  }\n\n  const Component = renderer.renderer as (props: FlowTransitionLine) => JSX.Element;\n\n  return <Component lineId={props.lineId} {...line} />;\n}\n\nexport default CustomLine;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/LabelsRenderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { type IPoint, Rectangle } from '@flowgram.ai/utils';\nimport {\n  type CustomLabelProps,\n  type FlowNodeTransitionData,\n  type FlowTransitionLabel,\n  FlowTransitionLabelEnum,\n} from '@flowgram.ai/document';\n\nimport { type FlowRendererRegistry } from '../flow-renderer-registry';\nimport CollapseAdder from './CollapseAdder';\nimport Collapse from './Collapse';\nimport BranchDraggableRenderer from './BranchDraggableRenderer';\nimport Adder from './Adder';\n\nexport interface LabelOpts {\n  // eslint-disable-next-line react/no-unused-prop-types\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  isViewportVisible: (bounds: Rectangle) => boolean;\n  labelsSave: JSX.Element[];\n  getLabelColor: (activated?: boolean) => string;\n}\n\nconst TEXT_LABEL_STYLE: React.CSSProperties = {\n  fontSize: 12,\n  color: '#8F959E',\n  textAlign: 'center',\n  whiteSpace: 'nowrap',\n  backgroundColor: 'var(--g-editor-background)',\n  lineHeight: '20px',\n};\n\nconst LABEL_MAX_WIDTH = 150;\nconst LABEL_MAX_HEIGHT = 60;\n\nfunction getLabelBounds(offset: IPoint) {\n  return new Rectangle(\n    offset.x - LABEL_MAX_WIDTH / 2,\n    offset.y - LABEL_MAX_HEIGHT / 2,\n    LABEL_MAX_WIDTH,\n    LABEL_MAX_HEIGHT\n  );\n}\n\nexport function createLabels(labelProps: LabelOpts): void {\n  const { data, rendererRegistry, labelsSave, getLabelColor } = labelProps;\n  const { labels, renderData } = data || {};\n  const { activated } = renderData || {};\n\n  // 标签绘制逻辑\n  const renderLabel = (label: FlowTransitionLabel, index: number) => {\n    const { offset, renderKey, props, rotate, origin, type } = label || {};\n    const offsetX = offset.x;\n    const offsetY = offset.y;\n\n    let child = null;\n    switch (type) {\n      case FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL:\n        child = (\n          <BranchDraggableRenderer\n            labelId={label.labelId || labelProps.data.entity.id}\n            rendererRegistry={rendererRegistry}\n            data={data}\n            {...props}\n          />\n        );\n        break;\n      case FlowTransitionLabelEnum.ADDER_LABEL:\n        child = (\n          <Adder\n            labelId={label.labelId || labelProps.data.entity.id}\n            rendererRegistry={rendererRegistry}\n            data={data}\n            {...props}\n          />\n        );\n        break;\n\n      case FlowTransitionLabelEnum.COLLAPSE_LABEL:\n        child = (\n          <Collapse\n            labelId={label.labelId || labelProps.data.entity.id}\n            rendererRegistry={rendererRegistry}\n            data={data}\n            {...props}\n          />\n        );\n        break;\n\n      case FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL:\n        child = (\n          <CollapseAdder\n            labelId={label.labelId || labelProps.data.entity.id}\n            rendererRegistry={rendererRegistry}\n            data={data}\n            {...props}\n          />\n        );\n        break;\n\n      case FlowTransitionLabelEnum.TEXT_LABEL:\n        if (!renderKey) {\n          return null;\n        }\n        const text = rendererRegistry.getText(renderKey) || renderKey;\n        child = (\n          <div\n            data-label-id={label.labelId || labelProps.data.entity.id}\n            style={{\n              ...TEXT_LABEL_STYLE,\n              ...props?.style,\n              color: getLabelColor(activated),\n              transform: rotate ? `rotate(${rotate})` : undefined,\n            }}\n          >\n            {text}\n          </div>\n        );\n        break;\n\n      case FlowTransitionLabelEnum.CUSTOM_LABEL:\n        if (!renderKey) {\n          return null;\n        }\n        try {\n          const renderer = rendererRegistry.getRendererComponent(renderKey);\n          child = React.createElement(\n            renderer.renderer as (props: any) => JSX.Element,\n            {\n              node: data.entity,\n              labelId: label.labelId || labelProps.data.entity.id,\n              ...props,\n            } as CustomLabelProps\n          );\n        } catch (err) {\n          console.error(err);\n          child = renderKey;\n        }\n        break;\n      default:\n        break;\n    }\n\n    const originX = typeof origin?.[0] === 'number' ? origin?.[0] : 0.5;\n    const originY = typeof origin?.[1] === 'number' ? origin?.[1] : 0.5;\n\n    return (\n      <div\n        key={`${data.entity.id}${index}`}\n        data-label-id={label.labelId || labelProps.data.entity.id}\n        style={{\n          position: 'absolute',\n          left: offsetX,\n          top: offsetY,\n          transform: `translate(-${originX * 100}%, -${originY * 100}%)`,\n        }}\n      >\n        {child}\n      </div>\n    );\n  };\n\n  labels.forEach((label, index) => {\n    if (labelProps.isViewportVisible(getLabelBounds(label.offset))) {\n      labelsSave.push(renderLabel(label, index) as JSX.Element);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/LinesRenderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport {\n  FlowDragService,\n  type FlowNodeTransitionData,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n  DefaultSpacingKey,\n} from '@flowgram.ai/document';\nimport { getDefaultSpacing } from '@flowgram.ai/document';\n\nimport { type FlowRendererRegistry } from '../flow-renderer-registry';\nimport StraightLine from './StraightLine';\nimport RoundedTurningLine from './RoundedTurningLine';\nimport CustomLine from './CustomLine';\n\nexport interface PropsType {\n  data: FlowNodeTransitionData;\n  rendererRegistry: FlowRendererRegistry;\n  isViewportVisible: (bounds: Rectangle) => boolean;\n  linesSave: JSX.Element[];\n  dragService: FlowDragService;\n}\n\nexport function createLines(props: PropsType): void {\n  const { data, rendererRegistry, linesSave, dragService } = props;\n  const { lines, entity } = data || {};\n\n  const radius = getDefaultSpacing(entity, DefaultSpacingKey.ROUNDED_LINE_RADIUS);\n  const xRadius = getDefaultSpacing(entity, DefaultSpacingKey.ROUNDED_LINE_X_RADIUS);\n  const yRadius = getDefaultSpacing(entity, DefaultSpacingKey.ROUNDED_LINE_Y_RADIUS);\n\n  // 线条绘制逻辑\n  const renderLine = (line: FlowTransitionLine, index: number) => {\n    const { renderData } = data;\n    const { isVertical } = data.entity;\n    const { lineActivated } = renderData || {};\n\n    const draggingLineHide =\n      (line.type === FlowTransitionLineEnum.DRAGGING_LINE || line.isDraggingLine) &&\n      !dragService.isDroppableBranch(data.entity, line.side);\n\n    const draggingLineActivated =\n      (line.type === FlowTransitionLineEnum.DRAGGING_LINE || line.isDraggingLine) &&\n      data.entity?.id === dragService.dropNodeId &&\n      line.side === dragService.labelSide;\n\n    switch (line.type) {\n      case FlowTransitionLineEnum.STRAIGHT_LINE:\n        return (\n          <StraightLine\n            key={`${data.entity.id}_${index}`}\n            lineId={data.entity.id}\n            activated={lineActivated}\n            {...line}\n          />\n        );\n\n      case FlowTransitionLineEnum.DIVERGE_LINE:\n      case FlowTransitionLineEnum.DRAGGING_LINE:\n      case FlowTransitionLineEnum.MERGE_LINE:\n      case FlowTransitionLineEnum.ROUNDED_LINE:\n        return (\n          <RoundedTurningLine\n            key={`${data.entity.id}_${index}`}\n            lineId={data.entity.id}\n            isHorizontal={!isVertical}\n            activated={lineActivated || draggingLineActivated}\n            radius={radius}\n            {...line}\n            xRadius={xRadius}\n            yRadius={yRadius}\n            hide={draggingLineHide}\n          />\n        );\n\n      case FlowTransitionLineEnum.CUSTOM_LINE:\n        return (\n          <CustomLine\n            key={`${data.entity.id}_${index}`}\n            lineId={data.entity.id}\n            {...line}\n            rendererRegistry={rendererRegistry}\n          />\n        );\n\n      default:\n        break;\n    }\n\n    return undefined;\n  };\n  lines.forEach((line, index) => {\n    const bounds = Rectangle.createRectangleWithTwoPoints(line.from, line.to).pad(10);\n    if (props.isViewportVisible(bounds)) {\n      const jsxEl = renderLine(line, index) as JSX.Element;\n      if (jsxEl) linesSave.push(jsxEl);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/MarkerActivatedArrow.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { useBaseColor } from '../hooks/use-base-color';\n\nexport const MARK_ACTIVATED_ARROW_ID = '$marker_arrow_activated$';\n// export const MARK_ACTIVATED_ARROW_URL = `url(#${MARK_ACTIVATED_ARROW_ID})`;\n\nfunction MarkerActivatedArrow(props: { id?: string }): JSX.Element {\n  const { baseActivatedColor } = useBaseColor();\n  return (\n    <marker\n      data-line-id={props.id}\n      id={props.id || MARK_ACTIVATED_ARROW_ID}\n      markerWidth=\"11\"\n      markerHeight=\"14\"\n      refX=\"10\"\n      refY=\"7\"\n      orient=\"auto\"\n    >\n      <path\n        d=\"M9.6 5.2C10.8 6.1 10.8 7.9 9.6 8.8L3.6 13.3C2.11672 14.4125 0 13.3541 0 11.5L0 2.5C0 0.645898 2.11672 -0.412461 3.6 0.7L9.6 5.2Z\"\n        fill={baseActivatedColor}\n      />\n    </marker>\n  );\n}\n\n// version 变化才触发组件更新\nexport default MarkerActivatedArrow;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/MarkerArrow.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { useBaseColor } from '../hooks/use-base-color';\n\nexport const MARK_ARROW_ID = '$marker_arrow$';\n// export const MARK_ARROW_URL = `url(#${MARK_ARROW_ID})`;\n\nfunction MarkerArrow(props: { id: string }): JSX.Element {\n  const { baseColor } = useBaseColor();\n  return (\n    <marker\n      data-line-id={props.id}\n      id={props.id || MARK_ARROW_ID}\n      markerWidth=\"11\"\n      markerHeight=\"14\"\n      refX=\"10\"\n      refY=\"7\"\n      orient=\"auto\"\n    >\n      <path\n        d=\"M9.6 5.2C10.8 6.1 10.8 7.9 9.6 8.8L3.6 13.3C2.11672 14.4125 0 13.3541 0 11.5L0 2.5C0 0.645898 2.11672 -0.412461 3.6 0.7L9.6 5.2Z\"\n        fill={baseColor}\n      />\n    </marker>\n  );\n}\n\n// version变化才触发组件更新\nexport default MarkerArrow;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/RoundedTurningLine.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { isNil } from 'lodash-es';\nimport { Point } from '@flowgram.ai/utils';\nimport { type FlowTransitionLine } from '@flowgram.ai/document';\nimport { useService } from '@flowgram.ai/core';\n\nimport { useBaseColor } from '../hooks/use-base-color';\nimport { DEFAULT_LINE_ATTRS, DEFAULT_RADIUS, getHorizontalVertices, getVertices } from './utils';\nimport MarkerArrow, { MARK_ARROW_ID } from './MarkerArrow';\nimport MarkerActivatedArrow, { MARK_ACTIVATED_ARROW_ID } from './MarkerActivatedArrow';\nimport { FlowRendererKey, FlowRendererRegistry } from '../flow-renderer-registry';\n\ninterface PropsType extends FlowTransitionLine {\n  radius?: number;\n  hide?: boolean;\n  xRadius?: number;\n  yRadius?: number;\n}\n\nfunction MarkerDefs(props: { id: string; activated?: boolean }): JSX.Element {\n  const renderRegistry = useService(FlowRendererRegistry);\n  const ArrowRenderer = renderRegistry?.tryToGetRendererComponent(\n    props.activated ? FlowRendererKey.MARKER_ACTIVATE_ARROW : FlowRendererKey.MARKER_ARROW\n  );\n  if (ArrowRenderer) {\n    return <ArrowRenderer.renderer {...props} />;\n  }\n  if (props.activated) {\n    return (\n      <defs>\n        <MarkerActivatedArrow id={props.id} />\n      </defs>\n    );\n  }\n  return (\n    <defs>\n      <MarkerArrow id={props.id} />\n    </defs>\n  );\n}\n/**\n * 圆角转弯线\n */\nfunction RoundedTurningLine(props: PropsType): JSX.Element | null {\n  const { vertices, radius = DEFAULT_RADIUS, hide, xRadius, yRadius, ...line } = props;\n  const { from, to, arrow, activated, style } = line || {};\n  const { baseActivatedColor, baseColor } = useBaseColor();\n\n  // 如果没有 vertices，根据线条类型计算转折点\n  const realVertices =\n    vertices ||\n    (props.isHorizontal\n      ? getHorizontalVertices(line, xRadius, yRadius)\n      : getVertices(line, xRadius, yRadius));\n  const middleStr: string = useMemo(\n    () =>\n      realVertices\n        .map((point, idx) => {\n          const prev = realVertices[idx - 1] || from;\n          const next = realVertices[idx + 1] || to;\n\n          // 前后 delta 变化\n          const prevDelta = { x: Math.abs(prev.x - point.x), y: Math.abs(prev.y - point.y) };\n          const nextDelta = { x: Math.abs(next.x - point.x), y: Math.abs(next.y - point.y) };\n\n          // 不是垂直直角的拐弯线报错\n          const isRightAngleX = prevDelta.x === 0 && nextDelta.y === 0;\n          const isRightAngleY = prevDelta.y === 0 && nextDelta.x === 0;\n          const isRightAngle = isRightAngleX || isRightAngleY;\n\n          if (!isRightAngle) {\n            console.error(`vertex ${point.x},${point.y} is not right angle`);\n          }\n\n          // 圆角入点和出点为 control 往两个方向移动一段距离，距离不够 radius 为短距离\n          const inPoint = new Point().copyFrom(point);\n          const outPoint = new Point().copyFrom(point);\n          const radiusX = isNil(point.radiusX) ? radius : point.radiusX;\n          const radiusY = isNil(point.radiusY) ? radius : point.radiusY;\n          let rx = radiusX;\n          let ry = radiusY;\n\n          if (isRightAngleX) {\n            ry = Math.min(prevDelta.y, radiusY);\n            const moveY = isNil(point.moveY) ? ry : point.moveY;\n            inPoint.y += from.y < point.y ? -moveY : +moveY;\n\n            rx = Math.min(nextDelta.x, radiusX);\n            const moveX = isNil(point.moveX) ? rx : point.moveX;\n            outPoint.x += to.x < point.x ? -moveX : +moveX;\n          }\n\n          if (isRightAngleY) {\n            rx = Math.min(prevDelta.x, radiusX);\n            const moveX = isNil(point.moveX) ? rx : point.moveX;\n            inPoint.x += from.x < point.x ? -moveX : +moveX;\n\n            ry = Math.min(nextDelta.y, radiusY);\n            const moveY = isNil(point.moveY) ? ry : point.moveY;\n            outPoint.y += to.y < point.y ? -moveY : +moveY;\n          }\n\n          // radius overflow 策略为截断，则回复 rx, ry 为原始 radius\n          if (point.radiusOverflow === 'truncate') {\n            rx = radiusX;\n            ry = radiusY;\n          }\n\n          // 是否是顺时针？\n          // - 基于 AB 和 AC 的向量叉积\n          // - A 点：inPoint, B 点：point, C 点：outPoint\n          const crossProduct =\n            (point.x - inPoint.x) * (outPoint.y - inPoint.y) -\n            (point.y - inPoint.y) * (outPoint.x - inPoint.x);\n          const isClockWise = crossProduct > 0;\n\n          // 控制点为当前节点\n          return `L ${inPoint.x} ${inPoint.y} A ${rx} ${ry} 0 0 ${isClockWise ? 1 : 0} ${\n            outPoint.x\n          } ${outPoint.y}`;\n        })\n        .join(' '),\n    [realVertices]\n  );\n\n  if (hide) {\n    return null;\n  }\n\n  const pathStr = `M ${from.x} ${from.y} ${middleStr} L ${to.x} ${to.y}`;\n  const markerId = activated\n    ? `${MARK_ACTIVATED_ARROW_ID}${props.lineId}`\n    : `${MARK_ARROW_ID}${props.lineId}`;\n\n  return (\n    <>\n      {arrow ? <MarkerDefs id={markerId} activated={activated} /> : null}\n      <path\n        data-line-id={props.lineId}\n        d={pathStr}\n        {...DEFAULT_LINE_ATTRS}\n        stroke={activated ? baseActivatedColor : baseColor}\n        {...(arrow\n          ? {\n              markerEnd: `url(#${markerId})`,\n            }\n          : {})}\n        style={style}\n      ></path>\n    </>\n  );\n}\n\n// version 变化才触发组件更新\nexport default RoundedTurningLine;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/StraightLine.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { FlowTransitionLine } from '@flowgram.ai/document';\n\nimport { useBaseColor } from '../hooks/use-base-color';\nimport { DEFAULT_LINE_ATTRS } from './utils';\n\nfunction StraightLine(props: FlowTransitionLine): JSX.Element {\n  const { from, to, activated, style } = props;\n  const { baseColor, baseActivatedColor } = useBaseColor();\n\n  return (\n    <path\n      data-line-id={props.lineId}\n      d={`M ${from.x} ${from.y} L ${to.x} ${to.y}`}\n      {...DEFAULT_LINE_ATTRS}\n      stroke={activated ? baseActivatedColor : baseColor}\n      style={style}\n    />\n  );\n}\n\n// version 变化才触发组件更新\nexport default StraightLine;\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/components/utils.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\n\nimport {\n  FlowNodeTransformData,\n  type FlowNodeTransitionData,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n  type Vertex,\n  DefaultSpacingKey,\n  DEFAULT_SPACING,\n} from '@flowgram.ai/document';\n\nimport { BASE_DEFAULT_COLOR } from '../hooks/use-base-color';\n\nexport const DEFAULT_LINE_ATTRS: React.SVGProps<SVGPathElement> = {\n  stroke: BASE_DEFAULT_COLOR,\n  fill: 'transparent',\n  strokeLinecap: 'round',\n  strokeLinejoin: 'round',\n};\n\n// 默认的圆角半径\nexport const DEFAULT_RADIUS = DEFAULT_SPACING[DefaultSpacingKey.ROUNDED_LINE_RADIUS];\n\n// 小圆角\nexport const MINI_RADIUS = 10;\n\n// 默认 label 激活高度\nexport const DEFAULT_LABEL_ACTIVATE_HEIGHT = 32;\n\n/**\n * 根据椭圆方程计算 y 坐标\n *\n * x^2 / rx^2 + y^2 / ry^2 = 1\n */\nexport const calcEllipseY = (x: number, rx: number, ry: number) =>\n  Math.sqrt(ry ** 2 * (1 - x ** 2 / rx ** 2));\n\n/**\n * 获取转弯线的转折点 (水平布局)\n */\nexport function getHorizontalVertices(\n  line: FlowTransitionLine,\n  xRadius = 16,\n  yRadius = 20\n): Vertex[] {\n  const { from, to, type } = line || {};\n\n  // 空间可以容纳的圆角数\n  const deltaY = Math.abs(to.y - from.y);\n  const deltaX = Math.abs(to.x - from.x);\n\n  const radiusXCount = deltaX / xRadius;\n  const radiusYCount = deltaY / yRadius;\n\n  let res: Vertex[] = [];\n\n  // 容纳不下一个圆角，直接连线\n  if (radiusXCount < 1) {\n    return [];\n  }\n\n  switch (type) {\n    case FlowTransitionLineEnum.DIVERGE_LINE:\n    case FlowTransitionLineEnum.DRAGGING_LINE:\n      if (radiusXCount <= 1) {\n        return [\n          {\n            x: to.x,\n            y: from.y,\n            radiusX: deltaX,\n          },\n        ];\n      }\n      res = [\n        {\n          x: from.x + yRadius,\n          y: from.y,\n        },\n        {\n          x: from.x + yRadius,\n          y: to.y,\n        },\n      ];\n      if (radiusXCount < 2) {\n        const firstRadius = deltaX - yRadius;\n        res = [\n          {\n            x: from.x + firstRadius,\n            y: from.y,\n            // 第一个圆角收缩 y 半径\n            radiusX: firstRadius,\n          },\n          {\n            x: from.x + firstRadius,\n            y: to.y,\n          },\n        ];\n      }\n\n      // y 轴空间不足处理\n      if (radiusYCount < 2) {\n        res[0].moveY = deltaY / 2;\n        res[1].moveY = deltaY / 2;\n      }\n\n      return res;\n\n    case FlowTransitionLineEnum.MERGE_LINE:\n      // 聚合线 y 轴空间不足时直接连上\n      if (radiusXCount < 2) {\n        return [\n          {\n            x: to.x,\n            y: from.y,\n          },\n        ];\n      }\n\n      res = [\n        {\n          x: to.x - yRadius,\n          y: from.y,\n        },\n        {\n          x: to.x - yRadius,\n          y: to.y,\n        },\n      ];\n\n      // y 轴空间不足处理\n      if (radiusYCount < 2) {\n        res[0].moveY = deltaY / 2;\n        res[1].moveY = deltaY / 2;\n      }\n\n      return res;\n\n    default:\n      break;\n  }\n\n  return [];\n}\n/**\n * 获取转弯线的转折点 (垂直布局)\n */\nexport function getVertices(line: FlowTransitionLine, xRadius = 16, yRadius = 20): Vertex[] {\n  const { from, to, type } = line || {};\n\n  // 空间可以容纳的圆角数\n  const deltaY = Math.abs(to.y - from.y);\n  const deltaX = Math.abs(to.x - from.x);\n\n  const radiusYCount = deltaY / yRadius;\n  const radiusXCount = deltaX / xRadius;\n\n  let res: Vertex[] = [];\n\n  // 容纳不下一个圆角，直接连线\n  if (radiusYCount < 1) {\n    return [];\n  }\n\n  switch (type) {\n    case FlowTransitionLineEnum.DIVERGE_LINE:\n    case FlowTransitionLineEnum.DRAGGING_LINE:\n      if (radiusYCount <= 1) {\n        return [\n          {\n            x: to.x,\n            y: from.y,\n            radiusY: deltaY,\n          },\n        ];\n      }\n      res = [\n        {\n          x: from.x,\n          y: from.y + yRadius,\n        },\n        {\n          x: to.x,\n          y: from.y + yRadius,\n        },\n      ];\n      if (radiusYCount < 2) {\n        const firstRadius = deltaY - yRadius;\n        res = [\n          {\n            x: from.x,\n            y: from.y + firstRadius,\n            // 第一个圆角收缩 y 半径\n            radiusY: firstRadius,\n          },\n          {\n            x: to.x,\n            y: from.y + firstRadius,\n          },\n        ];\n      }\n\n      // x 轴空间不足处理\n      if (radiusXCount < 2) {\n        res[0].moveX = deltaX / 2;\n        res[1].moveX = deltaX / 2;\n      }\n\n      return res;\n\n    case FlowTransitionLineEnum.MERGE_LINE:\n      // 聚合线 y 轴空间不足时直接连上\n      if (radiusYCount < 2) {\n        return [\n          {\n            x: from.x,\n            y: to.y,\n          },\n        ];\n      }\n\n      res = [\n        {\n          x: from.x,\n          y: to.y - yRadius,\n        },\n        {\n          x: to.x,\n          y: to.y - yRadius,\n        },\n      ];\n\n      // x 轴空间不足处理\n      if (radiusXCount < 2) {\n        res[0].moveX = deltaX / 2;\n        res[1].moveX = deltaX / 2;\n      }\n\n      return res;\n\n    default:\n      break;\n  }\n\n  return [];\n}\n\n// 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区\nexport function getTransitionLabelHoverWidth(data: FlowNodeTransitionData) {\n  const { isVertical } = data.entity;\n  if (isVertical) {\n    const nextWidth =\n      data.entity.next?.firstChild && !data.entity.next.isInlineBlocks\n        ? data.entity.next.firstChild!.getData(FlowNodeTransformData)!.size.width\n        : data.entity.next?.getData(FlowNodeTransformData)!.size.width;\n\n    // 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区\n    const maxWidth = Math.max(\n      data.entity.getData(FlowNodeTransformData)?.size.width ??\n        DEFAULT_SPACING[DefaultSpacingKey.HOVER_AREA_WIDTH],\n      nextWidth || 0\n    );\n\n    return maxWidth;\n  }\n  if (data.transform.next) {\n    return data.transform.next.inputPoint.x - data.transform.outputPoint.x;\n  }\n  return DEFAULT_LABEL_ACTIVATE_HEIGHT;\n}\n\nexport function getTransitionLabelHoverHeight(data: FlowNodeTransitionData) {\n  const { isVertical } = data.entity;\n  if (isVertical) {\n    if (data.transform.next) {\n      return data.transform.next.inputPoint.y - data.transform.outputPoint.y;\n    }\n    return DEFAULT_LABEL_ACTIVATE_HEIGHT;\n  }\n  const nextHeight =\n    data.entity.next?.firstChild && !data.entity.next.isInlineBlocks\n      ? data.entity.next.firstChild!.getData(FlowNodeTransformData)!.size.height\n      : data.entity.next?.getData(FlowNodeTransformData)!.size.height;\n\n  // 获取上一个节点和下一个节点中较宽的宽度作为 hover 热区\n  const maxHeight = Math.max(\n    data.entity.getData(FlowNodeTransformData)?.size.height || 280,\n    nextHeight || 0\n  );\n\n  return maxHeight;\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/entities/README.md",
    "content": "## 画布渲染相关 entity 数据\n\n这里存放的是和画布渲染强耦合的数据\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/entities/flow-drag-entity.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport {\n  type FlowNodeTransitionData,\n  FlowTransitionLabelEnum,\n  LABEL_SIDE_TYPE,\n} from '@flowgram.ai/document';\nimport { ConfigEntity, type EntityOpts, PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport { DEFAULT_LABEL_ACTIVATE_HEIGHT } from '../components/utils';\n\nconst BRANCH_HOVER_HEIGHT = 64;\n\ninterface FlowDragEntityConfig extends EntityOpts {}\n\nenum ScrollDirection {\n  TOP,\n  BOTTOM,\n  LEFT,\n  RIGHT,\n}\n\nconst SCROLL_DELTA = 4;\n\nconst SCROLL_INTERVAL = 20;\n\nconst SCROLL_BOUNDING = 20;\n\nconst EDITOR_LEFT_BAR_WIDTH = 60;\n\nexport interface CollisionRetType {\n  hasCollision: boolean;\n  labelOffsetType?: LABEL_SIDE_TYPE;\n}\n\nexport class FlowDragEntity extends ConfigEntity<FlowDragEntityConfig> {\n  private playgroundConfigEntity: PlaygroundConfigEntity;\n\n  static type = 'FlowDragEntity';\n\n  private containerX = 0;\n\n  private containerY = 0;\n\n  private _scrollXInterval: { interval: number; origin: number } | undefined;\n\n  private _scrollYInterval: { interval: number; origin: number } | undefined;\n\n  get hasScroll(): boolean {\n    return Boolean(this._scrollXInterval || this._scrollYInterval);\n  }\n\n  constructor(conf: any) {\n    super(conf);\n    this.playgroundConfigEntity = this.entityManager.getEntity<PlaygroundConfigEntity>(\n      PlaygroundConfigEntity,\n      true\n    )!;\n  }\n\n  isCollision(\n    transition: FlowNodeTransitionData,\n    rect: Rectangle,\n    isBranch: boolean\n  ): CollisionRetType {\n    const scale = this.playgroundConfigEntity.finalScale || 0;\n    if (isBranch) {\n      return this.isBranchCollision(transition, rect, scale);\n    }\n    return this.isNodeCollision(transition, rect, scale);\n  }\n\n  // 检测节点维度碰撞方法\n  isNodeCollision(\n    transition: FlowNodeTransitionData,\n    rect: Rectangle,\n    scale: number\n  ): CollisionRetType {\n    const { labels } = transition;\n    const { isVertical } = transition.entity;\n\n    const hasCollision = labels.some((label) => {\n      if (\n        !label ||\n        ![\n          FlowTransitionLabelEnum.ADDER_LABEL,\n          FlowTransitionLabelEnum.COLLAPSE_ADDER_LABEL,\n        ].includes(label.type)\n      ) {\n        return false;\n      }\n\n      const hoverWidth = isVertical\n        ? transition.transform.bounds.width\n        : DEFAULT_LABEL_ACTIVATE_HEIGHT;\n      const hoverHeight = isVertical\n        ? DEFAULT_LABEL_ACTIVATE_HEIGHT\n        : transition.transform.bounds.height;\n\n      const labelRect = new Rectangle(\n        (label.offset.x - hoverWidth / 2) * scale,\n        (label.offset.y - hoverHeight / 2) * scale,\n        hoverWidth * scale,\n        hoverHeight * scale\n      );\n      // 检测两个正方形是否相互碰撞\n      return Rectangle.intersects(labelRect, rect);\n    });\n\n    return {\n      hasCollision,\n      // 节点不关心 offsetType\n      labelOffsetType: undefined,\n    };\n  }\n\n  // 检测分支维度碰撞\n  isBranchCollision(\n    transition: FlowNodeTransitionData,\n    rect: Rectangle,\n    scale: number\n  ): CollisionRetType {\n    const { labels } = transition;\n    const { isVertical } = transition.entity;\n\n    let labelOffsetType: LABEL_SIDE_TYPE = LABEL_SIDE_TYPE.NORMAL_BRANCH;\n    const hasCollision = labels.some((label) => {\n      if (!label || label.type !== FlowTransitionLabelEnum.BRANCH_DRAGGING_LABEL) {\n        return false;\n      }\n      const hoverHeight = isVertical ? BRANCH_HOVER_HEIGHT : label.width || 0;\n      // BRANCH_DRAGGING_LABEL 类型的 label 一定存在 width 属性\n      const hoverWidth = isVertical ? label.width || 0 : BRANCH_HOVER_HEIGHT;\n\n      const labelRect = new Rectangle(\n        (label.offset.x - hoverWidth / 2) * scale,\n        (label.offset.y - hoverHeight / 2) * scale,\n        hoverWidth * scale,\n        hoverHeight * scale\n      );\n      // 检测两个正方形是否相互碰撞\n      const collision = Rectangle.intersects(labelRect, rect);\n      if (collision) {\n        labelOffsetType = label.props!.side;\n      }\n      return collision;\n    });\n\n    return {\n      hasCollision,\n      labelOffsetType,\n    };\n  }\n\n  private _startScrollX(origin: number, added: boolean): void {\n    if (this._scrollXInterval) {\n      return;\n    }\n    const interval = window.setInterval(() => {\n      const current = this._scrollXInterval;\n      if (!current) return;\n      // eslint-disable-next-line no-multi-assign\n      const scrollX = (current.origin = added\n        ? current.origin + SCROLL_DELTA\n        : current.origin - SCROLL_DELTA);\n      this.playgroundConfigEntity.updateConfig({\n        scrollX,\n      });\n      const playgroundConfig = this.playgroundConfigEntity.config;\n      if (playgroundConfig?.scrollX === scrollX) {\n        if (added) {\n          this.containerX += SCROLL_DELTA;\n        } else {\n          this.containerX -= SCROLL_DELTA;\n        }\n      }\n    }, SCROLL_INTERVAL);\n    this._scrollXInterval = { interval, origin };\n  }\n\n  private _stopScrollX(): void {\n    if (this._scrollXInterval) {\n      clearInterval(this._scrollXInterval.interval);\n      this._scrollXInterval = undefined;\n    }\n  }\n\n  private _startScrollY(origin: number, added: boolean): void {\n    if (this._scrollYInterval) {\n      return;\n    }\n    const interval = window.setInterval(() => {\n      const current = this._scrollYInterval;\n      if (!current) return;\n      // eslint-disable-next-line no-multi-assign\n      const scrollY = (current.origin = added\n        ? current.origin + SCROLL_DELTA\n        : current.origin - SCROLL_DELTA);\n      this.playgroundConfigEntity.updateConfig({\n        scrollY,\n      });\n      const playgroundConfig = this.playgroundConfigEntity.config;\n      if (playgroundConfig?.scrollY === scrollY) {\n        if (added) {\n          this.containerY += SCROLL_DELTA;\n        } else {\n          this.containerY -= SCROLL_DELTA;\n        }\n      }\n    }, SCROLL_INTERVAL);\n    this._scrollYInterval = { interval, origin };\n  }\n\n  private _stopScrollY(): void {\n    if (this._scrollYInterval) {\n      clearInterval(this._scrollYInterval.interval);\n      this._scrollYInterval = undefined;\n    }\n  }\n\n  stopAllScroll(): void {\n    this._stopScrollX();\n    this._stopScrollY();\n  }\n\n  scrollDirection(e: MouseEvent, x: number, y: number): ScrollDirection | undefined {\n    const playgroundConfig = this.playgroundConfigEntity.config;\n    const currentScrollX = playgroundConfig.scrollX;\n    const currentScrollY = playgroundConfig.scrollY;\n    this.containerX = x;\n    this.containerY = y;\n    const clientRect = this.playgroundConfigEntity.playgroundDomNode.getBoundingClientRect();\n\n    const mouseToBottom = playgroundConfig.height + clientRect.y - e.clientY;\n    if (mouseToBottom < SCROLL_BOUNDING) {\n      this._startScrollY(currentScrollY, true);\n      return ScrollDirection.BOTTOM;\n    }\n    const mouseToTop = e.clientY - clientRect.y;\n    if (mouseToTop < SCROLL_BOUNDING) {\n      this._startScrollY(currentScrollY, false);\n      return ScrollDirection.TOP;\n    }\n    this._stopScrollY();\n    const mouseToRight = playgroundConfig.width + clientRect.x - e.clientX;\n    if (mouseToRight < SCROLL_BOUNDING) {\n      this._startScrollX(currentScrollX, true);\n      return ScrollDirection.RIGHT;\n    }\n    const mouseToLeft = e.clientX - clientRect.x;\n    if (mouseToLeft < SCROLL_BOUNDING + EDITOR_LEFT_BAR_WIDTH) {\n      this._startScrollX(currentScrollX, false);\n      return ScrollDirection.LEFT;\n    }\n    this._stopScrollX();\n\n    return undefined;\n  }\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/entities/flow-select-config-entity.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport { ConfigEntity } from '@flowgram.ai/core';\nimport { Compare, Rectangle } from '@flowgram.ai/utils';\n\nimport { findSelectedNodes } from '../utils/find-selected-nodes';\n\ninterface FlowSelectConfigEntityData {\n  selectedNodes: FlowNodeEntity[];\n}\n\nconst BOUNDS_PADDING_DEFAULT = 10;\n\n/**\n * 圈选节点相关数据存储\n */\nexport class FlowSelectConfigEntity extends ConfigEntity<FlowSelectConfigEntityData> {\n  static type = 'FlowSelectConfigEntity';\n\n  boundsPadding = BOUNDS_PADDING_DEFAULT;\n\n  getDefaultConfig(): FlowSelectConfigEntityData {\n    return {\n      selectedNodes: [],\n    };\n  }\n\n  get selectedNodes(): FlowNodeEntity[] {\n    return this.config.selectedNodes;\n  }\n\n  /**\n   * 选中节点\n   * @param nodes\n   */\n  set selectedNodes(nodes: FlowNodeEntity[]) {\n    nodes = findSelectedNodes(nodes);\n    // if (nodes.length === 1 && nodes[0].flowNodeType === FlowNodeBaseType.END) {\n    //   nodes = [];\n    // }\n    if (\n      nodes.length !== this.config.selectedNodes.length ||\n      nodes.some(n => !this.config.selectedNodes.includes(n))\n    ) {\n      this.config.selectedNodes.forEach(oldNode => {\n        if (!nodes.includes(oldNode)) {\n          oldNode.getData(FlowNodeRenderData)!.activated = false;\n        }\n      });\n      // 高亮选中的节点\n      nodes.forEach(node => {\n        node.getData(FlowNodeRenderData)!.activated = true;\n      });\n      if (Compare.isArrayShallowChanged(this.config.selectedNodes, nodes)) {\n        this.updateConfig({\n          selectedNodes: nodes,\n        });\n      }\n    }\n  }\n\n  /**\n   * 清除选中节点\n   */\n  clearSelectedNodes() {\n    if (this.config.selectedNodes.length === 0) return;\n    this.config.selectedNodes.forEach(node => {\n      node.getData(FlowNodeRenderData)!.activated = false;\n    });\n    this.updateConfig({\n      selectedNodes: [],\n    });\n  }\n\n  /**\n   * 通过选择框选中节点\n   * @param rect\n   * @param transforms\n   */\n  selectFromBounds(rect: Rectangle, transforms: FlowNodeTransformData[]): void {\n    const selectedNodes: FlowNodeEntity[] = [];\n    transforms.forEach(transform => {\n      if (Rectangle.intersects(rect, transform.bounds)) {\n        if (transform.entity.originParent) {\n          selectedNodes.push(transform.entity.originParent);\n        } else {\n          selectedNodes.push(transform.entity);\n        }\n      }\n    });\n    this.selectedNodes = selectedNodes;\n  }\n\n  /**\n   * 获取选中节点外围的最大边框\n   */\n  getSelectedBounds(): Rectangle {\n    const nodes = this.selectedNodes;\n    if (nodes.length === 0) {\n      return Rectangle.EMPTY;\n    }\n    return Rectangle.enlarge(nodes.map(n => n.getData(FlowNodeTransformData)!.bounds)).pad(\n      this.boundsPadding,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/entities/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-drag-entity';\nexport * from './flow-select-config-entity';\nexport * from './selector-box-config-entity';\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/entities/selector-box-config-entity.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  PositionSchema,\n  SizeSchema,\n  ConfigEntity,\n  PlaygroundDragEvent,\n} from '@flowgram.ai/core';\nimport { Rectangle } from '@flowgram.ai/utils';\n\nexport interface SelectorBoxConfigData extends PlaygroundDragEvent {\n  disabled?: boolean; // 是否禁用选择框\n}\n\n/**\n * 选择框配置\n */\nexport class SelectorBoxConfigEntity extends ConfigEntity<SelectorBoxConfigData> {\n  static type = 'SelectorBoxConfigEntity';\n\n  get dragInfo(): PlaygroundDragEvent {\n    return this.config;\n  }\n\n  setDragInfo(info: PlaygroundDragEvent): void {\n    this.updateConfig(info);\n  }\n\n  get disabled(): boolean {\n    return this.config && !!this.config.disabled;\n  }\n\n  set disabled(disabled: boolean) {\n    this.updateConfig({\n      disabled,\n    });\n  }\n\n  get isStart(): boolean {\n    return this.dragInfo.isStart;\n  }\n\n  get isMoving(): boolean {\n    return this.dragInfo.isMoving;\n  }\n\n  get position(): PositionSchema {\n    const { dragInfo } = this;\n    return {\n      x: dragInfo.startPos.x < dragInfo.endPos.x ? dragInfo.startPos.x : dragInfo.endPos.x,\n      y: dragInfo.startPos.y < dragInfo.endPos.y ? dragInfo.startPos.y : dragInfo.endPos.y,\n    };\n  }\n\n  get size(): SizeSchema {\n    const { dragInfo } = this;\n    return {\n      width: Math.abs(dragInfo.startPos.x - dragInfo.endPos.x),\n      height: Math.abs(dragInfo.startPos.y - dragInfo.endPos.y),\n    };\n  }\n\n  get collapsed(): boolean {\n    const { size } = this;\n    return size.width === 0 && size.height === 0;\n  }\n\n  collapse(): void {\n    this.setDragInfo({\n      ...this.dragInfo,\n      isMoving: false,\n      isStart: false,\n    });\n  }\n\n  toRectangle(scale: number): Rectangle {\n    const { position, size } = this;\n    return new Rectangle(\n      position.x / scale,\n      position.y / scale,\n      size.width / scale,\n      size.height / scale,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/flow-renderer-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { FlowRendererResizeObserver } from './flow-renderer-resize-observer';\nimport { FlowRendererRegistry } from './flow-renderer-registry';\n\nexport const FlowRendererContainerModule = new ContainerModule(bind => {\n  bind(FlowRendererRegistry).toSelf().inSingletonScope();\n  bind(FlowRendererResizeObserver).toSelf().inSingletonScope();\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/flow-renderer-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowRendererRegistry } from './flow-renderer-registry';\n\nexport const FlowRendererContribution = Symbol('FlowRendererContribution');\n\nexport interface FlowRendererContribution {\n  registerRenderer?(registry: FlowRendererRegistry): void;\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/flow-renderer-registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, multiInject, optional } from 'inversify';\nimport { I18n } from '@flowgram.ai/i18n';\nimport { type Layer, type LayerRegistry, PipelineRegistry } from '@flowgram.ai/core';\n\nimport { FlowRendererContribution } from './flow-renderer-contribution';\n\nexport enum FlowRendererComponentType {\n  REACT, // react 组件\n  DOM, // dom 组件\n  TEXT, // 文案\n}\n\nexport enum FlowRendererKey {\n  NODE_RENDER = 'node-render', // 节点渲染\n  ADDER = 'adder', // 添加按钮渲染\n  COLLAPSE = 'collapse', // 节点展开收起标签（包含展开态和收起态）\n  BRANCH_ADDER = 'branch-adder', // 分支添加按钮\n  TRY_CATCH_COLLAPSE = 'try-catch-collapse', // 错误处理分支整体收起\n  DRAG_NODE = 'drag-node', // 拖拽节点\n  DRAGGABLE_ADDER = 'draggable-adder', // 拖拽可被拖入\n  DRAG_HIGHLIGHT_ADDER = 'drag-highlight-adder', // 拖拽高亮\n  DRAG_BRANCH_HIGHLIGHT_ADDER = 'drag-branch-highlight-adder', // 分支拖拽添加高亮\n  SELECTOR_BOX_POPOVER = 'selector-box-popover', // 选择框右上角菜单\n  /**\n   * @deprecated\n   */\n  CONTEXT_MENU_POPOVER = 'context-menu-popover', // 右键菜单\n  SUB_CANVAS = 'sub-canvas', // 子画布渲染\n\n  SLOT_ADDER = 'slot-adder', // 插槽添加按钮\n  SLOT_LABEL = 'slot-label', // 插槽标签\n  SLOT_COLLAPSE = 'slot-collapse', // 插槽收起按钮渲染\n\n  // 工作流线条箭头自定义渲染\n  ARROW_RENDERER = 'arrow-renderer', // 工作流线条箭头渲染器\n\n  // 下边两个不一定存在\n  MARKER_ARROW = 'marker-arrow', // loop 的默认箭头\n  MARKER_ACTIVATE_ARROW = 'marker-active-arrow', // loop 的激活态箭头\n}\n\nexport enum FlowTextKey {\n  // 循环节点相关\n  LOOP_END_TEXT = 'loop-end-text', // 文案：循环结束\n  LOOP_TRAVERSE_TEXT = 'loop-traverse-text', // 文案：循环遍历\n  LOOP_WHILE_TEXT = 'loop-while-text', // 文案：满足条件时\n  // TryCatch 相关\n  TRY_START_TEXT = 'try-start-text', // 文案：监控开始\n  TRY_END_TEXT = 'try-end-text', // 文案：监控结束\n  CATCH_TEXT = 'catch-text', // 发生错误\n}\n\nexport interface FlowRendererComponent {\n  type: FlowRendererComponentType;\n  renderer: (props?: any) => any;\n}\n\n/**\n * 命令分类\n */\nexport enum FlowRendererCommandCategory {\n  SELECTOR_BOX = 'SELECTOR_BOX', // 选择框\n}\n\n@injectable()\nexport class FlowRendererRegistry {\n  private componentsMap = new Map<string, FlowRendererComponent>();\n\n  private textMap = new Map<string, string>();\n\n  @multiInject(FlowRendererContribution)\n  @optional()\n  private contribs: FlowRendererContribution[] = [];\n\n  @inject(PipelineRegistry) readonly pipeline: PipelineRegistry;\n\n  init() {\n    this.contribs.forEach((contrib) => contrib.registerRenderer?.(this));\n  }\n\n  /**\n   * 注册 组件数据\n   */\n  registerRendererComponents(\n    renderKey: FlowRendererKey | string,\n    comp: FlowRendererComponent\n  ): void {\n    this.componentsMap.set(renderKey, comp);\n  }\n\n  registerReactComponent(renderKey: FlowRendererKey | string, renderer: (props: any) => any): void {\n    this.componentsMap.set(renderKey, {\n      type: FlowRendererComponentType.REACT,\n      renderer,\n    });\n  }\n\n  /**\n   * 注册文案\n   */\n  registerText(configs: Record<FlowTextKey | string, string>): void {\n    Object.entries(configs).forEach(([key, value]) => {\n      this.textMap.set(key, value);\n    });\n  }\n\n  getText(textKey: string) {\n    return I18n.t(textKey, { defaultValue: '' }) || this.textMap.get(textKey);\n  }\n\n  /**\n   * TODO: support memo\n   */\n  public getRendererComponent(renderKey: FlowRendererKey | string): FlowRendererComponent {\n    const comp = this.componentsMap.get(renderKey);\n    if (!comp) {\n      throw new Error(`Unknown render key ${renderKey}`);\n    }\n    return comp;\n  }\n\n  tryToGetRendererComponent(\n    renderKey: FlowRendererKey | string\n  ): FlowRendererComponent | undefined {\n    return this.componentsMap.get(renderKey);\n  }\n\n  /**\n   * 注册画布层\n   */\n  registerLayers(...layerRegistries: LayerRegistry[]): void {\n    layerRegistries.forEach((layer) => this.pipeline.registerLayer(layer));\n  }\n\n  /**\n   * 根据配置注册画布\n   * @param layerRegistry\n   * @param options\n   */\n  registerLayer<P extends Layer = Layer>(\n    layerRegistry: LayerRegistry<Layer>,\n    options?: P['options']\n  ): void {\n    this.pipeline.registerLayer(layerRegistry, options);\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/flow-renderer-resize-observer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { type FlowNodeTransformData } from '@flowgram.ai/document';\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { isHidden, isRectInit } from './utils/element';\n\n/**\n * 监听 dom 元素的 size 变化，用于画布节点的大小变化重新计算\n */\n@injectable()\nexport class FlowRendererResizeObserver {\n  /**\n   * 监听元素 size，并同步到 transform\n   * @param el\n   * @param transform\n   */\n  observe(el: HTMLElement, transform: FlowNodeTransformData): Disposable {\n    const observer = new ResizeObserver(entries => {\n      /**\n       * NOTICE: 不加 window.requestAnimationFrame\n       * 会导致 \"ResizeObserver loop completed with undelivered notifications.\" 报错\n       * 这个报错在 chrome 和 firefox 是默认被忽略的，但本地调试会被编译工具弹窗打断\n       */\n      window.requestAnimationFrame(() => {\n        if (!Array.isArray(entries) || !entries.length) {\n          return;\n        }\n        const entry = entries[0];\n        const { contentRect, target } = entry;\n        // 元素宽高未计算时，不更新节点 size\n        const isContentRectInit = isRectInit(contentRect);\n        // 目标节点脱离 DOM 树，忽略本次变更\n        const isLeaveDOMTree = !target.parentNode;\n        // IDE 环境下画布元素可能 display none，这时候会监听到元素宽高 0 导致闪屏\n        // 此情况下不作 resize 重渲染\n        const isHiddenElement = isHidden(target.parentNode as HTMLElement);\n        if (isContentRectInit && !isLeaveDOMTree && !isHiddenElement) {\n          // 更新节点 size 数据\n          transform.size = {\n            width: Math.round(contentRect.width * 10) / 10,\n            height: Math.round(contentRect.height * 10) / 10,\n          };\n        }\n      });\n    });\n    observer.observe(el);\n    return Disposable.create(() => {\n      observer.unobserve(el);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/hooks/use-base-color.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ConstantKeys, FlowDocumentOptions } from '@flowgram.ai/document';\nimport { useService } from '@flowgram.ai/core';\n\nexport const BASE_DEFAULT_COLOR = '#BBBFC4';\nexport const BASE_DEFAULT_ACTIVATED_COLOR = '#82A7FC';\n\nexport function useBaseColor(): { baseColor: string; baseActivatedColor: string } {\n  const options = useService<FlowDocumentOptions>(FlowDocumentOptions);\n  return {\n    baseColor: options.constants?.[ConstantKeys.BASE_COLOR] || BASE_DEFAULT_COLOR,\n    baseActivatedColor:\n      options.constants?.[ConstantKeys.BASE_ACTIVATED_COLOR] || BASE_DEFAULT_ACTIVATED_COLOR,\n  };\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './entities';\nexport * from './layers';\nexport * from './flow-renderer-contribution';\nexport * from './flow-renderer-registry';\nexport * from './flow-renderer-container-module';\n\nexport { ScrollBarEvents } from './utils';\nexport { MARK_ARROW_ID } from './components/MarkerArrow';\nexport { MARK_ACTIVATED_ARROW_ID } from './components/MarkerActivatedArrow';\nexport { useBaseColor } from './hooks/use-base-color';\nexport { createLines } from './components/LinesRenderer';\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-context-menu-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport {\n  CommandRegistry,\n  ContextMenuService,\n  EditorState,\n  EditorStateConfigEntity,\n  Layer,\n  observeEntity,\n  PipelineLayerPriority,\n  PlaygroundConfigEntity,\n  SelectionService,\n} from '@flowgram.ai/core';\n\nimport {\n  FlowRendererCommandCategory,\n  FlowRendererKey,\n  FlowRendererRegistry,\n} from '../flow-renderer-registry';\nimport { SelectorBoxConfigEntity } from '../entities/selector-box-config-entity';\nimport { FlowSelectConfigEntity } from '../entities/flow-select-config-entity';\n\n/**\n * 流程右键菜单\n */\n@injectable()\nexport class FlowContextMenuLayer extends Layer {\n  @inject(CommandRegistry) readonly commandRegistry: CommandRegistry;\n\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  @inject(ContextMenuService) readonly contextMenuService: ContextMenuService;\n\n  @observeEntity(FlowSelectConfigEntity) protected flowSelectConfigEntity: FlowSelectConfigEntity;\n\n  @inject(SelectionService) readonly selectionService: SelectionService;\n\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  @observeEntity(SelectorBoxConfigEntity)\n  protected selectorBoxConfigEntity: SelectorBoxConfigEntity;\n\n  readonly node = domUtils.createDivWithClass('gedit-context-menu-layer');\n\n  readonly nodeRef = React.createRef<{\n    setVisible: (v: boolean) => void;\n  }>();\n\n  isEnabled(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n    return (\n      !this.config.disabled &&\n      !this.config.readonly &&\n      currentState === EditorState.STATE_SELECT &&\n      !this.selectorBoxConfigEntity.disabled\n    );\n  }\n\n  onReady(): void {\n    // 这个是覆盖到节点上边的，所以要比 flow-nodes-content-layer 大\n    this.node!.style.zIndex = '30';\n    this.node!.style.display = 'block';\n    // 监听鼠标右键\n    this.toDispose.pushAll([\n      this.listenPlaygroundEvent(\n        'contextmenu',\n        (e: MouseEvent): boolean | undefined => {\n          if (!this.isEnabled()) return;\n          this.contextMenuService.rightPanelVisible = true;\n          const bounds = this.flowSelectConfigEntity.getSelectedBounds();\n          if (bounds.width === 0 || bounds.height === 0) {\n            return;\n          }\n          e.stopPropagation();\n          e.preventDefault();\n\n          this.nodeRef.current?.setVisible(true);\n          const clientBounds = this.playgroundConfigEntity.getClientBounds();\n          const dragBlockX = e.clientX - (this.pipelineNode.offsetLeft || 0) - clientBounds.x;\n          const dragBlockY = e.clientY - (this.pipelineNode.offsetTop || 0) - clientBounds.y;\n          this.node.style.left = `${dragBlockX}px`;\n          this.node.style.top = `${dragBlockY}px`;\n        },\n        PipelineLayerPriority.BASE_LAYER\n      ),\n      this.listenPlaygroundEvent('mousedown', () => {\n        this.nodeRef.current?.setVisible(false);\n        this.contextMenuService.rightPanelVisible = false;\n      }),\n    ]);\n  }\n\n  onScroll() {\n    this.nodeRef.current?.setVisible(false);\n  }\n\n  onZoom() {\n    this.nodeRef.current?.setVisible(false);\n  }\n\n  /**\n   * Destroy\n   */\n  dispose(): void {\n    super.dispose();\n  }\n\n  /**\n   * 渲染工具栏\n   */\n  renderCommandMenus(): JSX.Element[] {\n    return this.commandRegistry.commands\n      .filter((cmd) => cmd.category === FlowRendererCommandCategory.SELECTOR_BOX)\n      .map((cmd) => {\n        const CommandRenderer = this.rendererRegistry.getRendererComponent(\n          (cmd.icon as string) || cmd.id\n        )?.renderer;\n        return (\n          <CommandRenderer\n            key={cmd.id}\n            command={cmd}\n            isContextMenu\n            disabled={!this.commandRegistry.isEnabled(cmd.id)}\n            onClick={(e: any) => this.commandRegistry.executeCommand(cmd.id, e)}\n          />\n        );\n      })\n      .filter((c) => c);\n  }\n\n  render(): JSX.Element {\n    const SelectorBoxPopover = this.rendererRegistry.getRendererComponent(\n      FlowRendererKey.CONTEXT_MENU_POPOVER\n    ).renderer;\n    return <SelectorBoxPopover ref={this.nodeRef} content={this.renderCommandMenus()} />;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-debug-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowNodeEntity,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';\n\nimport { getScrollViewport } from '../utils';\n\nlet rgbTimes = 0;\n\nfunction randomColor(percent: number): string {\n  const max = Math.min((percent / 10) * 255, 255);\n  rgbTimes += 1;\n  // rgb 轮询就可以错开颜色\n  const rgb = rgbTimes % 3;\n  const random = () => Math.floor(Math.random() * max);\n  return `rgb(${rgb === 0 ? random() : 0}, ${rgb === 1 ? random() : 0}, ${\n    rgb === 2 ? random() : 0\n  })`;\n}\n\n/**\n * 调试用，会绘出所有节点的边界\n */\n@injectable()\nexport class FlowDebugLayer extends Layer {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) _transforms: FlowNodeTransformData[];\n\n  get transforms(): FlowNodeTransformData[] {\n    return this.document.getRenderDatas<FlowNodeTransformData>(FlowNodeTransformData);\n  }\n\n  node = document.createElement('div') as HTMLElement;\n\n  viewport = domUtils.createDivWithClass('gedit-flow-debug-bounds');\n\n  boundsNodes = domUtils.createDivWithClass('gedit-flow-debug-bounds');\n\n  pointsNodes = domUtils.createDivWithClass('gedit-flow-debug-points');\n\n  versionNodes = domUtils.createDivWithClass('gedit-flow-debug-versions gedit-hidden');\n\n  /**\n   * ?debug=xxxx, 则返回 xxxx\n   */\n  filterKey = window.location.search.match(/debug=([^&]+)/)?.[1] || '';\n\n  protected originLine = document.createElement('div') as HTMLDivElement;\n\n  domCache = new WeakMap<\n    FlowNodeTransformData,\n    {\n      color: string;\n      bbox: HTMLDivElement;\n      version: HTMLDivElement;\n      input: HTMLDivElement;\n      output: HTMLDivElement;\n    }\n  >();\n\n  onReady() {\n    this.node!.style.zIndex = '20';\n    domUtils.setStyle(this.originLine, {\n      position: 'absolute',\n      width: 1,\n      height: '100%',\n      left: this.pipelineNode.style.left,\n      top: 0,\n      borderLeft: '1px dashed rgba(255, 0, 0, 0.5)',\n    });\n    this.pipelineNode.parentElement!.appendChild(this.originLine);\n    this.node.appendChild(this.viewport);\n    this.node.appendChild(this.versionNodes);\n    this.node.appendChild(this.boundsNodes);\n    this.node.appendChild(this.pointsNodes);\n    this.renderScrollViewportBounds();\n  }\n\n  onScroll() {\n    this.originLine.style.left = this.pipelineNode.style.left;\n    this.renderScrollViewportBounds();\n  }\n\n  onResize() {\n    this.renderScrollViewportBounds();\n  }\n\n  onZoom(scale: number) {\n    this.node!.style.transform = `scale(${scale})`;\n    this.renderScrollViewportBounds();\n  }\n\n  createBounds(transform: FlowNodeTransformData, color: string, depth: number): void {\n    // 根据 debug=xxxx 进行匹配过滤\n    if (this.filterKey && transform.key.indexOf(this.filterKey) === -1) return;\n    let cache = this.domCache.get(transform)!;\n    const { bounds, inputPoint, outputPoint } = transform;\n    if (!cache) {\n      const bbox = domUtils.createDivWithClass('') as HTMLDivElement;\n      const input = domUtils.createDivWithClass('') as HTMLDivElement;\n      const output = domUtils.createDivWithClass('') as HTMLDivElement;\n      const version = domUtils.createDivWithClass('') as HTMLDivElement;\n      bbox.title = transform.key;\n      input.title = transform.key + '(input)';\n      output.title = transform.key + '(output)';\n      version.title = transform.key;\n      this.boundsNodes.appendChild(bbox);\n      this.pointsNodes.appendChild(input);\n      this.pointsNodes.appendChild(output);\n      this.versionNodes.appendChild(version);\n      transform.onDispose(() => {\n        bbox.remove();\n        input.remove();\n        output.remove();\n      });\n      cache = { bbox, input, output, version, color };\n      this.domCache.set(transform, cache);\n    }\n    domUtils.setStyle(cache.version, {\n      position: 'absolute',\n      marginLeft: '-9px',\n      marginTop: '-10px',\n      borderRadius: 12,\n      background: '#f54a45',\n      padding: 4,\n      color: 'navajowhite',\n      display: transform.renderState.hidden ? 'none' : 'block',\n      zIndex: depth + 1000,\n      left: bounds.center.x,\n      top: bounds.center.y,\n    });\n    cache.version.innerHTML = transform.version.toString();\n    domUtils.setStyle(cache.input, {\n      position: 'absolute',\n      width: 10,\n      height: 10,\n      marginLeft: -5,\n      marginTop: -5,\n      borderRadius: 5,\n      left: inputPoint.x,\n      top: inputPoint.y,\n      opacity: 0.4,\n      zIndex: depth,\n      backgroundColor: cache.color,\n      whiteSpace: 'nowrap',\n      overflow: 'visible',\n    });\n    cache.input.innerHTML = `${inputPoint.x},${inputPoint.y}`;\n    domUtils.setStyle(cache.output, {\n      position: 'absolute',\n      width: 10,\n      height: 10,\n      marginLeft: -5,\n      marginTop: -5,\n      borderRadius: 5,\n      left: outputPoint.x,\n      top: outputPoint.y,\n      opacity: 0.4,\n      zIndex: depth,\n      backgroundColor: cache.color,\n      whiteSpace: 'nowrap',\n      overflow: 'visible',\n    });\n    cache.output.innerHTML = `${outputPoint.x},${outputPoint.y}`;\n    domUtils.setStyle(cache.bbox, {\n      position: 'absolute',\n      width: bounds.width,\n      height: bounds.height,\n      left: bounds.left,\n      top: bounds.top,\n      opacity: `${depth / 30}`,\n      backgroundColor: cache.color,\n    });\n  }\n\n  /**\n   * 显示 viewport 可滚动区域\n   */\n  renderScrollViewportBounds() {\n    const viewportBounds = getScrollViewport(\n      {\n        scrollX: this.config.config.scrollX,\n        scrollY: this.config.config.scrollY,\n      },\n      this.config\n    );\n    domUtils.setStyle(this.viewport, {\n      position: 'absolute',\n      width: viewportBounds.width - 2,\n      height: viewportBounds.height - 2,\n      left: viewportBounds.left + 1,\n      top: viewportBounds.top + 1,\n      border: '1px solid rgba(200, 200, 255, 0.5)',\n    });\n  }\n\n  autorun() {\n    if (this.documentTransformer.loading) return;\n    this.documentTransformer.refresh();\n    // let lastDepth = 0\n    let color = randomColor(0);\n    this.document.traverse((entity, depth) => {\n      const transform = entity.getData<FlowNodeTransformData>(FlowNodeTransformData)!;\n      // if (lastDepth !== depth) {\n      //   // 层级变化则更新颜色\n      // }\n      color = randomColor(depth);\n      this.createBounds(transform, color, depth);\n      // lastDepth = depth\n    });\n    this.renderScrollViewportBounds();\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-drag-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { Rectangle, Xor } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  FlowNodeTransitionData,\n  FlowRendererStateEntity,\n  type LABEL_SIDE_TYPE,\n  FlowDragService,\n  FlowNodeJSON,\n} from '@flowgram.ai/document';\nimport {\n  EditorState,\n  EditorStateConfigEntity,\n  Layer,\n  observeEntity,\n  observeEntityDatas,\n  PlaygroundConfigEntity,\n} from '@flowgram.ai/core';\nimport { PlaygroundDrag } from '@flowgram.ai/core';\n\nimport {\n  type FlowRendererComponent,\n  FlowRendererKey,\n  FlowRendererRegistry,\n} from '../flow-renderer-registry';\nimport { type CollisionRetType, FlowDragEntity } from '../entities/flow-drag-entity';\nimport { FlowSelectConfigEntity } from '../entities';\n\n// 移动超过一定距离后触发拖拽生效\nconst DRAG_OFFSET = 10;\n\nconst DEFAULT_DRAG_OFFSET_X = 8;\nconst DEFAULT_DRAG_OFFSET_Y = 8;\n\ninterface Position {\n  x: number;\n  y: number;\n}\n\ntype StartDragProps = {\n  dragEntities?: FlowNodeEntity[];\n} & Xor<\n  {\n    dragStartEntity: FlowNodeEntity;\n  },\n  {\n    dragJSON: FlowNodeJSON;\n    isBranch?: boolean;\n    onCreateNode: (json: FlowNodeJSON, dropEntity: FlowNodeEntity) => Promise<FlowNodeEntity>;\n  }\n>;\n\nexport interface FlowDragOptions {\n  onDrop?: (opts: { dragNodes: FlowNodeEntity[]; dropNode: FlowNodeEntity }) => void;\n  canDrop?: (\n    opts: {\n      dropNode: FlowNodeEntity;\n      isBranch?: boolean;\n    } & Xor<\n      {\n        dragNodes: FlowNodeEntity[];\n      },\n      {\n        dragJSON: FlowNodeJSON;\n      }\n    >\n  ) => boolean;\n}\n/**\n * 监听节点的激活状态\n */\n@injectable()\nexport class FlowDragLayer extends Layer<FlowDragOptions> {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @inject(FlowDragService) readonly flowDragService: FlowDragService;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData) transforms: FlowNodeTransformData[];\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  @observeEntity(FlowDragEntity)\n  protected flowDragConfigEntity: FlowDragEntity;\n\n  @observeEntity(FlowRendererStateEntity)\n  protected flowRenderStateEntity: FlowRendererStateEntity;\n\n  @observeEntity(FlowSelectConfigEntity)\n  protected selectConfigEntity: FlowSelectConfigEntity;\n\n  private initialPosition: Position;\n\n  private disableDragScroll: Boolean = false;\n\n  private dragJSON?: FlowNodeJSON;\n\n  private onCreateNode?: (\n    json: FlowNodeJSON,\n    dropEntity: FlowNodeEntity\n  ) => Promise<FlowNodeEntity>;\n\n  dragOffset = {\n    x: DEFAULT_DRAG_OFFSET_X,\n    y: DEFAULT_DRAG_OFFSET_Y,\n  };\n\n  get transitions(): FlowNodeTransitionData[] {\n    const result: FlowNodeTransitionData[] = [];\n    this.document.traverse((entity) => {\n      result.push(entity.getData<FlowNodeTransitionData>(FlowNodeTransitionData)!);\n    });\n    return result;\n  }\n\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  get dragStartEntity() {\n    return this.flowRenderStateEntity.getDragStartEntity()!;\n  }\n\n  set dragStartEntity(entity: FlowNodeEntity | undefined) {\n    this.flowRenderStateEntity.setDragStartEntity(entity);\n  }\n\n  get dragEntities() {\n    return this.flowRenderStateEntity.getDragEntities()!;\n  }\n\n  set dragEntities(entities: FlowNodeEntity[]) {\n    this.flowRenderStateEntity.setDragEntities(entities);\n  }\n\n  private dragNodeComp: FlowRendererComponent;\n\n  containerRef = React.createRef<HTMLDivElement>();\n\n  draggingNodeMask = document.createElement('div');\n\n  protected isGrab(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n    return currentState === EditorState.STATE_GRAB;\n  }\n\n  setDraggingStatus(status: boolean): void {\n    if (this.flowDragService.nodeDragIdsWithChildren.length) {\n      this.flowDragService.nodeDragIdsWithChildren.forEach((_id) => {\n        const node = this.entityManager.getEntityById(_id);\n        const data = node?.getData<FlowNodeRenderData>(FlowNodeRenderData)!;\n        data.dragging = status;\n      });\n    }\n    this.flowRenderStateEntity.setDragging(status);\n  }\n\n  dragEnable(e: MouseEvent) {\n    return (\n      Math.abs(e.clientX - this.initialPosition.x) > DRAG_OFFSET ||\n      Math.abs(e.clientY - this.initialPosition.y) > DRAG_OFFSET\n    );\n  }\n\n  handleMouseMove(event: MouseEvent) {\n    if ((this.dragJSON || this.dragStartEntity) && this.dragEnable(event)) {\n      // 变更拖拽节点的位置\n      this.setDraggingStatus(true);\n      const scale = this.playgroundConfigEntity.finalScale;\n\n      if (this.containerRef.current) {\n        const dragNode = this.containerRef.current.children?.[0];\n        const clientBounds = this.playgroundConfigEntity.getClientBounds();\n        const dragBlockX =\n          event.clientX -\n          (this.pipelineNode.offsetLeft || 0) -\n          clientBounds.x -\n          (dragNode.clientWidth - this.dragOffset.x) * scale;\n        const dragBlockY =\n          event.clientY -\n          (this.pipelineNode.offsetTop || 0) -\n          clientBounds.y -\n          (dragNode.clientHeight - this.dragOffset.y) * scale;\n\n        // 获取节点状态是节点类型还是分支类型\n        const isBranch = this.flowDragService.isDragBranch;\n\n        // 节点类型拖拽碰撞检测\n        const draggingRect = new Rectangle(\n          dragBlockX,\n          dragBlockY,\n          dragNode.clientWidth * scale,\n          dragNode.clientHeight * scale\n        );\n        let side: LABEL_SIDE_TYPE | undefined;\n        const collisionTransition = this.transitions.find((transition) => {\n          // 过滤已被折叠 label\n          if (transition?.entity?.parent?.collapsed) {\n            return false;\n          }\n          const { hasCollision, labelOffsetType } = this.flowDragConfigEntity.isCollision(\n            transition,\n            draggingRect,\n            isBranch\n          ) as CollisionRetType;\n          side = labelOffsetType;\n          return hasCollision;\n        });\n        if (\n          collisionTransition &&\n          (isBranch\n            ? this.flowDragService.isDroppableBranch(collisionTransition.entity, side)\n            : this.flowDragService.isDroppableNode(collisionTransition.entity)) &&\n          (!this.options.canDrop ||\n            this.options.canDrop({\n              dragNodes: this.dragEntities,\n              dropNode: collisionTransition.entity,\n              isBranch,\n            }))\n        ) {\n          // 设置碰撞的 label id\n          this.flowRenderStateEntity.setNodeDroppingId(collisionTransition.entity.id);\n        } else {\n          // 没有碰撞清空 highlight\n          this.flowRenderStateEntity.setNodeDroppingId('');\n        }\n\n        // 判断拖拽种类是节点类型还是分支类型\n        this.flowRenderStateEntity.setDragLabelSide(side);\n\n        this.containerRef.current.style.visibility = 'visible';\n        this.pipelineNode.parentElement!.appendChild(this.draggingNodeMask);\n\n        this.containerRef.current.style.left = `${\n          dragBlockX + this.pipelineNode.offsetLeft + clientBounds.x + window.scrollX\n        }px`;\n        this.containerRef.current.style.top = `${\n          dragBlockY + this.pipelineNode.offsetTop + clientBounds.y + window.scrollY\n        }px`;\n        this.containerRef.current.style.transformOrigin = 'top left';\n        this.containerRef.current.style.transform = `scale(${scale})`;\n\n        if (!this.disableDragScroll) {\n          this.flowDragConfigEntity.scrollDirection(event, dragBlockX, dragBlockY);\n        }\n      }\n    }\n  }\n\n  async handleMouseUp() {\n    this.setDraggingStatus(false);\n    if (this.dragStartEntity || this.dragJSON) {\n      const activatedNodeId = this.flowDragService.dropNodeId;\n\n      if (activatedNodeId) {\n        if (this.flowDragService.isDragBranch) {\n          if (this.dragJSON) {\n            await this.flowDragService.dropCreateNode(this.dragJSON, this.onCreateNode);\n          } else {\n            this.flowDragService.dropBranch();\n          }\n        } else {\n          if (this.dragJSON) {\n            await this.flowDragService.dropCreateNode(this.dragJSON, this.onCreateNode);\n          } else {\n            this.flowDragService.dropNode();\n          }\n          this.selectConfigEntity.clearSelectedNodes();\n        }\n      }\n\n      // 清空碰撞 id\n      this.flowRenderStateEntity.setNodeDroppingId('');\n      this.flowRenderStateEntity.setDragLabelSide();\n      this.flowRenderStateEntity.setIsBranch(false);\n      this.dragStartEntity = undefined;\n      this.dragEntities = [];\n\n      // 滚动停止\n      this.flowDragConfigEntity.stopAllScroll();\n    }\n\n    this.disableDragScroll = false;\n    this.dragJSON = undefined;\n    if (this.containerRef.current) {\n      this.containerRef.current.style.visibility = 'hidden';\n      if (this.pipelineNode.parentElement!.contains(this.draggingNodeMask)) {\n        this.pipelineNode.parentElement!.removeChild(this.draggingNodeMask);\n      }\n    }\n  }\n\n  protected _dragger = new PlaygroundDrag({\n    onDrag: (e) => {\n      this.handleMouseMove(e);\n    },\n    onDragEnd: () => {\n      this.handleMouseUp();\n    },\n    stopGlobalEventNames: ['contextmenu'],\n  });\n\n  /**\n   * 开始拖拽事件\n   * @param e\n   */\n  async startDrag(\n    e: { clientX: number; clientY: number },\n    {\n      dragStartEntity: startEntityFromProps,\n      dragEntities,\n      dragJSON,\n      isBranch,\n      onCreateNode,\n    }: StartDragProps,\n    options?: {\n      dragOffsetX?: number;\n      dragOffsetY?: number;\n      disableDragScroll?: boolean;\n    }\n  ) {\n    // 1. 避免按住空格拖动滚动场景覆盖，context disabled 会出现在画布编辑被抢锁时候触发\n    if (this.isGrab() || this.config.disabled || this.config.readonly) {\n      return;\n    }\n\n    this.disableDragScroll = Boolean(options?.disableDragScroll);\n    this.dragJSON = dragJSON;\n    this.onCreateNode = onCreateNode;\n    this.flowRenderStateEntity.setIsBranch(Boolean(isBranch));\n\n    this.dragOffset.x = options?.dragOffsetX || DEFAULT_DRAG_OFFSET_X;\n    this.dragOffset.y = options?.dragOffsetY || DEFAULT_DRAG_OFFSET_Y;\n\n    const type = startEntityFromProps?.flowNodeType || dragJSON?.type;\n\n    const isIcon = type === FlowNodeBaseType.BLOCK_ICON;\n    const isOrderIcon = type === FlowNodeBaseType.BLOCK_ORDER_ICON;\n\n    const dragStartEntity =\n      isIcon || isOrderIcon ? startEntityFromProps!.parent! : startEntityFromProps;\n\n    // 部分节点不支持拖拽\n    if (dragStartEntity && !dragStartEntity!.getData(FlowNodeRenderData).draggable) {\n      return;\n    }\n\n    this.initialPosition = {\n      x: e.clientX,\n      y: e.clientY,\n    };\n\n    this.dragStartEntity = dragStartEntity;\n    this.dragEntities = dragEntities || (this.dragStartEntity ? [this.dragStartEntity!] : []);\n\n    return this._dragger.start(e.clientX, e.clientY);\n  }\n\n  onReady() {\n    this.draggingNodeMask.style.width = '100%';\n    this.draggingNodeMask.style.height = '100%';\n    this.draggingNodeMask.style.position = 'absolute';\n    this.draggingNodeMask.classList.add('dragging-node');\n    this.draggingNodeMask.style.zIndex = '99';\n    this.draggingNodeMask.style.cursor = 'pointer';\n\n    this.dragNodeComp = this.rendererRegistry.getRendererComponent(FlowRendererKey.DRAG_NODE);\n    // 监听拖入事件\n    if (this.options.onDrop) {\n      this.toDispose.push(this.flowDragService.onDrop(this.options.onDrop));\n    }\n  }\n\n  dispose(): void {\n    this._dragger.dispose();\n    super.dispose();\n  }\n\n  render() {\n    // styled-component component type 为 any\n    const DragComp: any = this.dragNodeComp.renderer;\n\n    return ReactDOM.createPortal(\n      <div\n        ref={this.containerRef}\n        style={{ position: 'absolute', zIndex: 99999, visibility: 'hidden' }}\n        onMouseEnter={(e) => e.stopPropagation()}\n      >\n        <DragComp\n          dragJSON={this.dragJSON}\n          dragStart={this.dragStartEntity}\n          dragNodes={this.dragEntities}\n        />\n      </div>,\n      document.body\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-labels-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { throttle } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowNodeEntity,\n  FlowNodeTransitionData,\n  FlowRendererStateEntity,\n} from '@flowgram.ai/document';\nimport { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';\n\nimport { useBaseColor } from '../hooks/use-base-color';\nimport { FlowRendererRegistry } from '../flow-renderer-registry';\nimport { createLabels } from '../components/LabelsRenderer';\n\n@injectable()\nexport class FlowLabelsLayer extends Layer {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  node = domUtils.createDivWithClass('gedit-flow-labels-layer');\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntity(FlowRendererStateEntity)\n  readonly flowRenderState: FlowRendererStateEntity;\n\n  /**\n   * 监听 transition 变化\n   */\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransitionData)\n  _transitions: FlowNodeTransitionData[];\n\n  get transitions(): FlowNodeTransitionData[] {\n    return this.document.getRenderDatas<FlowNodeTransitionData>(FlowNodeTransitionData);\n  }\n\n  /**\n   * 监听缩放，目前采用整体缩放\n   * @param scale\n   */\n  onZoom(scale: number) {\n    this.node!.style.transform = `scale(${scale})`;\n  }\n\n  /**\n   * 可视区域变化\n   */\n  onViewportChange: ReturnType<typeof throttle> = throttle(() => {\n    this.render();\n  }, 100);\n\n  onReady() {\n    // 图层顺序调整：节点 > label > 线条\n    // 节点 z-index: 10\n    this.node.style.zIndex = '9';\n  }\n\n  /**\n   * 监听readonly和 disabled 状态 并刷新layer, 并刷新\n   */\n  onReadonlyOrDisabledChange() {\n    this.render();\n  }\n\n  render() {\n    const labels: JSX.Element[] = [];\n    if (this.documentTransformer?.loading) return <></>;\n    this.documentTransformer?.refresh?.();\n    const { baseActivatedColor, baseColor } = useBaseColor();\n    const isViewportVisible = this.config.isViewportVisible.bind(this.config);\n    this.transitions.forEach((transition) => {\n      createLabels({\n        data: transition,\n        rendererRegistry: this.rendererRegistry,\n        isViewportVisible,\n        labelsSave: labels,\n        getLabelColor: (activated) => (activated ? baseActivatedColor : baseColor),\n      });\n    });\n    // 这里采用扁平化的 react 结构性能更高\n    return <>{labels}</>;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-lines-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { groupBy, throttle } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowNodeEntity,\n  FlowNodeTransitionData,\n  FlowRendererStateEntity,\n  FlowDragService,\n} from '@flowgram.ai/document';\nimport { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';\n\nimport { FlowRendererRegistry } from '../flow-renderer-registry';\nimport { createLines } from '../components/LinesRenderer';\n\n@injectable()\nexport class FlowLinesLayer extends Layer {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @inject(FlowDragService)\n  protected readonly dragService: FlowDragService;\n\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  node = domUtils.createDivWithClass('gedit-flow-lines-layer');\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntity(FlowRendererStateEntity)\n  readonly flowRenderState: FlowRendererStateEntity;\n\n  /**\n   * 监听 transition 变化\n   */\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransitionData)\n  _transitions: FlowNodeTransitionData[];\n\n  get transitions(): FlowNodeTransitionData[] {\n    return this.document.getRenderDatas<FlowNodeTransitionData>(FlowNodeTransitionData);\n  }\n\n  /**\n   * 可视区域变化\n   */\n  onViewportChange: ReturnType<typeof throttle> = throttle(() => {\n    this.render();\n  }, 100);\n\n  onZoom() {\n    const svgContainer = this.node!.querySelector('svg.flow-lines-container')!;\n    svgContainer?.setAttribute?.('viewBox', this.viewBox);\n  }\n\n  onReady() {\n    this.node.style.zIndex = '1';\n  }\n\n  get viewBox(): string {\n    const ratio = 1000 / this.config.finalScale;\n    return `0 0 ${ratio} ${ratio}`;\n  }\n\n  render(): JSX.Element {\n    const allLines: JSX.Element[] = [];\n    const isViewportVisible = this.config.isViewportVisible.bind(this.config);\n    // 还没初始化\n    if (this.documentTransformer.loading) return <></>;\n    this.documentTransformer.refresh();\n\n    this.transitions.forEach((transition) => {\n      createLines({\n        data: transition,\n        rendererRegistry: this.rendererRegistry,\n        isViewportVisible,\n        linesSave: allLines,\n        dragService: this.dragService,\n      });\n    });\n\n    // svg 没有 z-index，只能通过顺序来设置前后层级\n    // 通过将 activated 的项排到最后，防止 hover 层级覆盖\n    const { activateLines = [], normalLines = [] } = groupBy(allLines, (line) =>\n      line.props.activated ? 'activateLines' : 'normalLines'\n    );\n    const resultLines = [...normalLines, ...activateLines];\n\n    return (\n      <svg\n        className=\"flow-lines-container\"\n        width=\"1000\"\n        height=\"1000\"\n        overflow=\"visible\"\n        viewBox={this.viewBox}\n        xmlns=\"http://www.w3.org/2000/svg\"\n      >\n        {resultLines}\n      </svg>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-nodes-content-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { Cache, type CacheOriginItem, domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport {\n  Layer,\n  observeEntity,\n  observeEntityDatas,\n  PlaygroundEntityContext,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererKey, FlowRendererRegistry } from '../flow-renderer-registry';\n\ninterface NodePortal extends CacheOriginItem {\n  id: string;\n  Portal: () => JSX.Element;\n}\n\n/**\n * 渲染节点内容\n */\n@injectable()\nexport class FlowNodesContentLayer extends Layer {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeRenderData)\n  _renderStates: FlowNodeRenderData[];\n\n  get renderStatesVisible(): FlowNodeRenderData[] {\n    return this.document.getRenderDatas<FlowNodeRenderData>(FlowNodeRenderData, false);\n  }\n\n  private renderMemoCache = new WeakMap<any, any>();\n\n  node = domUtils.createDivWithClass('gedit-flow-nodes-layer');\n\n  getPortalRenderer(data: FlowNodeRenderData): (props: any) => JSX.Element {\n    const meta = data.entity.getNodeMeta();\n    const renderer = this.rendererRegistry.getRendererComponent(\n      (meta.renderKey as FlowRendererKey) || FlowRendererKey.NODE_RENDER\n    );\n    const reactRenderer = renderer.renderer as any;\n    let memoCache = this.renderMemoCache.get(reactRenderer);\n    if (!memoCache) {\n      memoCache = React.memo(reactRenderer);\n      this.renderMemoCache.set(reactRenderer, memoCache);\n    }\n    return memoCache;\n  }\n\n  /**\n   * 监听缩放，目前采用整体缩放\n   * @param scale\n   */\n  onZoom(scale: number) {\n    this.node!.style.transform = `scale(${scale})`;\n  }\n\n  dispose(): void {\n    this.reactPortals.dispose();\n    super.dispose();\n  }\n\n  protected reactPortals = Cache.create<NodePortal, FlowNodeRenderData>(\n    (data?: FlowNodeRenderData) => {\n      const { node, entity } = data!;\n      const { config } = this;\n      const PortalRenderer = this.getPortalRenderer(data!);\n\n      function Portal(): JSX.Element {\n        React.useEffect(() => {\n          // 第一次加载需要把宽高通知\n          if (!entity.getNodeMeta().autoResizeDisable && node.clientWidth && node.clientHeight) {\n            const transform = entity.getData<FlowNodeTransformData>(FlowNodeTransformData);\n            if (transform)\n              transform.size = {\n                width: node.clientWidth,\n                height: node.clientHeight,\n              };\n          }\n        }, [entity, node]);\n        // 这里使用 portal，改 dom 样式不会引起 react 重新渲染\n        return ReactDOM.createPortal(\n          <PlaygroundEntityContext.Provider value={entity}>\n            <PortalRenderer\n              node={entity}\n              version={data?.version}\n              activated={data?.activated}\n              readonly={config.readonly}\n              disabled={config.disabled}\n            />\n          </PlaygroundEntityContext.Provider>,\n          node\n        );\n      }\n\n      return {\n        id: node.id || entity.id,\n        dispose: () => {\n          // TODO, 删除逻辑由 node 去控制了\n        },\n        Portal,\n      } as NodePortal;\n    }\n  );\n\n  onReady() {\n    this.node!.style.zIndex = '10';\n  }\n\n  /**\n   * 监听readonly和 disabled 状态 并刷新layer, 并刷新节点\n   */\n  onReadonlyOrDisabledChange() {\n    this.render();\n  }\n\n  getPortals(): NodePortal[] {\n    return this.reactPortals.getMoreByItems(this.renderStatesVisible);\n  }\n\n  render() {\n    if (this.documentTransformer.loading) return <></>;\n    this.documentTransformer.refresh();\n\n    // 从缓存获取节点\n    return (\n      <>\n        {this.getPortals().map((portal) => (\n          <portal.Portal key={portal.id} />\n        ))}\n      </>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-nodes-transform-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Cache, type Disposable, domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowNodeEntity,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';\n// import { throttle } from 'lodash-es'\n\nimport { FlowRendererResizeObserver } from '../flow-renderer-resize-observer';\n\ninterface TransformRenderCache {\n  updateBounds(): void;\n}\n\nexport interface FlowNodesTransformLayerOptions {\n  renderElement?: HTMLElement | (() => HTMLElement | undefined);\n}\n\n/**\n * 渲染节点位置\n */\n@injectable()\nexport class FlowNodesTransformLayer extends Layer<FlowNodesTransformLayerOptions> {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  @inject(FlowRendererResizeObserver)\n  readonly resizeObserver: FlowRendererResizeObserver;\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData)\n  _transforms: FlowNodeTransformData[];\n\n  node = domUtils.createDivWithClass('gedit-flow-nodes-layer');\n\n  get transformVisibles(): FlowNodeTransformData[] {\n    return this.document.getRenderDatas<FlowNodeTransformData>(FlowNodeTransformData, false);\n  }\n\n  /**\n   * 监听缩放，目前采用整体缩放\n   * @param scale\n   */\n  onZoom(scale: number) {\n    this.node!.style.transform = `scale(${scale})`;\n  }\n\n  dispose(): void {\n    this.renderCache.dispose();\n    super.dispose();\n  }\n\n  // onViewportChange() {\n  //   this.throttleUpdate()\n  // }\n\n  // throttleUpdate = throttle(() => {\n  //   this.renderCache.getFromCache().forEach((cache) => cache.updateBounds())\n  // }, 100)\n\n  protected renderCache = Cache.create<TransformRenderCache, FlowNodeTransformData>(\n    (transform?: FlowNodeTransformData) => {\n      const { renderState } = transform!;\n      const { node } = renderState;\n      const { entity } = transform!;\n      node.id = entity.id;\n      let resizeDispose: Disposable | undefined;\n      const append = () => {\n        if (resizeDispose) return;\n        // 监听 dom 节点的大小变化\n        this.renderElement.appendChild(node);\n        if (!entity.getNodeMeta().autoResizeDisable) {\n          resizeDispose = this.resizeObserver.observe(node, transform!);\n        }\n      };\n      const dispose = () => {\n        if (!resizeDispose) return;\n        // 脱离文档流，但是 react 组件会保留\n        if (node.parentElement) {\n          this.renderElement.removeChild(node);\n        }\n        resizeDispose.dispose();\n        resizeDispose = undefined;\n      };\n      append();\n      return {\n        dispose,\n        updateBounds: () => {\n          const { bounds } = transform!;\n          // 保留2位小数\n          const rawX: number = parseFloat(node.style.left);\n          const rawY: number = parseFloat(node.style.top);\n          if (!this.isCoordEqual(rawX, bounds.x) || !this.isCoordEqual(rawY, bounds.y)) {\n            node.style.left = `${bounds.x}px`;\n            node.style.top = `${bounds.y}px`;\n          }\n        },\n      };\n    }\n  );\n\n  private isCoordEqual(a: number, b: number) {\n    const browserCoordEpsilon = 0.05; // 浏览器处理坐标的精度误差: 两位小数四舍五入\n    return Math.abs(a - b) < browserCoordEpsilon;\n  }\n\n  onReady() {\n    this.node!.style.zIndex = '10';\n  }\n\n  get visibeBounds() {\n    return this.transformVisibles.map((transform) => transform.bounds);\n  }\n\n  /**\n   * 更新节点的 bounds 数据\n   */\n  updateNodesBounds() {\n    this.renderCache\n      .getMoreByItems(this.transformVisibles)\n      .forEach((render) => render.updateBounds());\n  }\n\n  autorun() {\n    // 更新节点偏移数据 O(n) TODO 这个更新会从 render 里移除改成自动触发\n    if (this.documentTransformer.loading) return;\n    this.documentTransformer.refresh();\n    this.updateNodesBounds();\n  }\n\n  private get renderElement(): HTMLElement {\n    if (typeof this.options.renderElement === 'function') {\n      const element = this.options.renderElement();\n      if (element) {\n        return element;\n      }\n    } else if (typeof this.options.renderElement !== 'undefined') {\n      return this.options.renderElement as HTMLElement;\n    }\n    return this.node;\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-scroll-bar-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, optional } from 'inversify';\nimport { FlowDocument, FlowNodeTransformData } from '@flowgram.ai/document';\nimport {\n  Layer,\n  observeEntity,\n  PlaygroundConfigEntity,\n  PlaygroundDrag,\n} from '@flowgram.ai/core';\nimport { domUtils, Rectangle } from '@flowgram.ai/utils';\n// import {\n//   FlowDocument,\n//   FlowDocumentTransformerEntity,\n//   FlowNodeTransformData,\n// } from '@flowgram.ai/document'\n\nimport { ScrollBarEvents } from '../utils/scroll-bar-events';\nimport { getScrollViewport } from '../utils';\n\n// 中间区域边框宽度\nconst BORDER_WIDTH = 2;\n\n// 右下角预留的 offset\nconst BLOCK_OFFSET = 11;\n\n// 滚动条样式宽\nconst SCROLL_BAR_WIDTH = '7px';\n\n// 滚动条显示状态\nenum ScrollBarVisibility {\n  Show = 'show',\n  Hidden = 'hidden',\n}\n\nexport interface ScrollBarOptions {\n  /**\n   * 显示滚动条的时机，可选常驻或滚动时显示\n   */\n  showScrollBars: 'whenScrolling' | 'always';\n  getBounds(): Rectangle;\n}\n\n/**\n * 渲染滚动条 layer\n */\n@injectable()\nexport class FlowScrollBarLayer extends Layer<ScrollBarOptions> {\n  @optional()\n  @inject(ScrollBarEvents)\n  readonly events?: ScrollBarEvents;\n\n  @inject(FlowDocument) @optional() flowDocument?: FlowDocument;\n\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  // @observeEntity(FlowDocumentTransformerEntity) readonly documentTransformer: FlowDocumentTransformerEntity\n\n  // 右滚动区域\n  readonly rightScrollBar = domUtils.createDivWithClass('gedit-playground-scroll-right');\n\n  // 右滚动条\n  readonly rightScrollBarBlock = domUtils.createDivWithClass('gedit-playground-scroll-right-block');\n\n  // 底滚动区域\n  readonly bottomScrollBar = domUtils.createDivWithClass('gedit-playground-scroll-bottom');\n\n  // 底滚动条\n  readonly bottomScrollBarBlock = domUtils.createDivWithClass(\n    'gedit-playground-scroll-bottom-block',\n  );\n\n  // 最左边的位置\n  private mostLeft: number;\n\n  // 最右边的位置\n  private mostRight: number;\n\n  // 最上面的位置\n  private mostTop: number;\n\n  // 最下面的位置\n  private mostBottom: number;\n\n  // 视区宽度\n  private viewportWidth: number;\n\n  // 视区高度\n  private viewportHeight: number;\n\n  // 元素宽高\n  private width: number;\n\n  private height: number;\n\n  // 底部滚动条宽度\n  private scrollBottomWidth: number;\n\n  // 右侧滚动条高度\n  private scrollRightHeight: number;\n\n  // 缩放比\n  private scale: number;\n\n  // 总滚动距离\n  private sum = 0;\n\n  // 初始 x 轴滚动距离\n  private initialScrollX = 0;\n\n  // 初始 y 轴滚动距离\n  private initialScrollY = 0;\n\n  // 隐藏滚动条的时延\n  private hideTimeout: number | undefined;\n\n  // 浏览器视图宽度\n  get clientViewportWidth(): number {\n    return this.viewportWidth * this.scale - BLOCK_OFFSET;\n  }\n\n  // 浏览器视图高度\n  get clientViewportHeight(): number {\n    return this.viewportHeight * this.scale - BLOCK_OFFSET;\n  }\n\n  // 视图的完整宽度\n  get viewportFullWidth(): number {\n    return this.mostLeft - this.mostRight;\n  }\n\n  // 视图的完整高度\n  get viewportFullHeight(): number {\n    return this.mostTop - this.mostBottom;\n  }\n\n  // 视图的可移动宽度\n  get viewportMoveWidth(): number {\n    return this.mostLeft - this.mostRight + this.width;\n  }\n\n  // 视图的可移动高度\n  get viewportMoveHeight(): number {\n    return this.mostTop - this.mostBottom + this.height;\n  }\n\n  getToLeft(scrollX: number): number {\n    return ((scrollX - this.mostRight) / this.viewportMoveWidth) * this.clientViewportWidth;\n  }\n\n  getToTop(scrollY: number): number {\n    return ((scrollY - this.mostBottom) / this.viewportMoveHeight) * this.clientViewportHeight;\n  }\n\n  clickRightScrollBar(e: MouseEvent) {\n    e.preventDefault();\n    e.stopPropagation();\n    const ratio = 1 - (e?.y || 0) / this.clientViewportHeight;\n    const scrollY = (this.mostTop - this.viewportFullHeight * ratio) * this.scale;\n\n    // 滚动到指定位置\n    this.playgroundConfigEntity.scroll(\n      {\n        scrollY,\n      },\n      false,\n    );\n  }\n\n  clickBottomScrollBar(e: MouseEvent) {\n    e.preventDefault();\n    e.stopPropagation();\n    const ratio = 1 - (e?.x || 0) / this.clientViewportWidth;\n    const scrollX = (this.mostLeft - this.viewportFullWidth * ratio) * this.scale;\n\n    // 滚动到指定位置\n    this.playgroundConfigEntity.scroll(\n      {\n        scrollX,\n      },\n      false,\n    );\n  }\n\n  onBoardingToast() {\n    // onBoarding 逻辑，滚动条指示优化，弹出 toast\n    this.events?.dragStart();\n  }\n\n  protected bottomGrabDragger = new PlaygroundDrag({\n    onDragStart: e => {\n      this.config.updateCursor('grabbing');\n      this.sum = 0;\n      this.initialScrollX = this.config.getViewport().x;\n      this.onBoardingToast();\n    },\n    onDrag: e => {\n      this.sum += e.movingDelta.x;\n      this.playgroundConfigEntity.scroll(\n        {\n          scrollX:\n            (this.initialScrollX +\n              (this.sum * this.viewportFullWidth) /\n                (this.clientViewportWidth - this.scrollBottomWidth)) *\n            this.scale,\n        },\n        false,\n      );\n    },\n    onDragEnd: e => {\n      this.config.updateCursor('default');\n    },\n  });\n\n  protected rightGrabDragger = new PlaygroundDrag({\n    onDragStart: e => {\n      this.config.updateCursor('grabbing');\n      this.sum = 0;\n      this.initialScrollY = this.config.getViewport().y;\n      this.onBoardingToast();\n    },\n    onDrag: e => {\n      this.sum += e.movingDelta.y;\n      this.playgroundConfigEntity.scroll(\n        {\n          scrollY:\n            (this.initialScrollY +\n              (this.sum * this.viewportFullHeight) /\n                (this.clientViewportHeight - this.scrollRightHeight)) *\n            this.scale,\n        },\n        false,\n      );\n    },\n    onDragEnd: e => {\n      this.config.updateCursor('default');\n    },\n  });\n\n  protected changeScrollBarVisibility(scrollBar: HTMLDivElement, status: ScrollBarVisibility) {\n    const addClassName =\n      status === ScrollBarVisibility.Show\n        ? 'gedit-playground-scroll-show'\n        : 'gedit-playground-scroll-hidden';\n    const delClassName =\n      status === ScrollBarVisibility.Show\n        ? 'gedit-playground-scroll-hidden'\n        : 'gedit-playground-scroll-show';\n    domUtils.addClass(scrollBar, addClassName);\n    domUtils.delClass(scrollBar, delClassName);\n  }\n\n  onReady() {\n    if (!this.options.getBounds) {\n      this.options = {\n        getBounds: () => {\n          const document = this.flowDocument;\n          if (!document) return Rectangle.EMPTY;\n          document.transformer.refresh();\n          return document.root.getData(FlowNodeTransformData)!.bounds;\n        },\n        showScrollBars: 'whenScrolling',\n      };\n    }\n    this.pipelineNode.parentNode!.appendChild(this.rightScrollBar);\n    this.pipelineNode.parentNode!.appendChild(this.rightScrollBarBlock);\n    this.pipelineNode.parentNode!.appendChild(this.bottomScrollBar);\n    this.pipelineNode.parentNode!.appendChild(this.bottomScrollBarBlock);\n    // 模拟滚动条点击时的滚动\n    this.rightScrollBar.onclick = this.clickRightScrollBar.bind(this);\n    this.bottomScrollBar.onclick = this.clickBottomScrollBar.bind(this);\n\n    // 滚动时才显示滚动条 则要监听鼠标事件 hover 的时候也要显示\n    if (this.options.showScrollBars === 'whenScrolling') {\n      this.rightScrollBar.addEventListener('mouseenter', (e: MouseEvent) => {\n        this.changeScrollBarVisibility(this.rightScrollBarBlock, ScrollBarVisibility.Show);\n      });\n      this.rightScrollBar.addEventListener('mouseleave', (e: MouseEvent) => {\n        this.changeScrollBarVisibility(this.rightScrollBarBlock, ScrollBarVisibility.Hidden);\n      });\n      this.bottomScrollBar.addEventListener('mouseenter', (e: MouseEvent) => {\n        this.changeScrollBarVisibility(this.bottomScrollBarBlock, ScrollBarVisibility.Show);\n      });\n      this.bottomScrollBar.addEventListener('mouseleave', (e: MouseEvent) => {\n        this.changeScrollBarVisibility(this.bottomScrollBarBlock, ScrollBarVisibility.Hidden);\n      });\n    }\n\n    // 监听拖拽滚动\n    this.bottomScrollBarBlock.addEventListener('mousedown', (e: MouseEvent) => {\n      this.bottomGrabDragger.start(e.clientX, e.clientY);\n      e.stopPropagation();\n    });\n    this.rightScrollBarBlock.addEventListener('mousedown', (e: MouseEvent) => {\n      this.rightGrabDragger.start(e.clientX, e.clientY);\n      e.stopPropagation();\n    });\n  }\n\n  autorun() {\n    // if (this.documentTransformer.loading) return null\n    // this.documentTransformer.refresh()\n    if (this.hideTimeout) {\n      clearTimeout(this.hideTimeout);\n    }\n\n    // 中间活动区域宽高\n    const viewportBounds = getScrollViewport(\n      {\n        scrollX: this.config.config.scrollX,\n        scrollY: this.config.config.scrollY,\n      },\n      this.config,\n    );\n\n    // 画布视区宽高\n    const viewport = this.config.getViewport();\n    // 计算视区的时候预留 11px，防止右下角滚动条重叠\n    this.viewportWidth = viewport.width;\n    this.viewportHeight = viewport.height;\n    // 中间部分元素的宽高\n    const rootBounds = this.options.getBounds(); // this.document.root.getData(FlowNodeTransformData)!.bounds\n    this.width = rootBounds?.width || 0;\n    this.height = rootBounds?.height || 0;\n    // 中间部分元素的左右间距\n    const paddingLeftRight = (this.viewportWidth - viewportBounds.width) / 2 - BORDER_WIDTH;\n    // 中间部分元素的上下间距\n    const paddingTopBottom = (this.viewportHeight - viewportBounds.height) / 2 - BORDER_WIDTH;\n\n    // 画布可滚动总长度\n    const canvasTotalWidth = this.width + viewportBounds.width;\n    const canvasTotalHeight = this.height + viewportBounds.height;\n    // 根据当前滚动距离计算 滚动条距离边界间距\n    // 中间元素初始的偏移位置：\n    const initialOffsetX = rootBounds.x;\n    const initialOffsetY = rootBounds.y;\n    // 最左边的位置\n    this.mostLeft = this.width + initialOffsetX - paddingLeftRight;\n    // 最右边的位置\n    this.mostRight = this.mostLeft - canvasTotalWidth;\n    // 最上面的位置\n    this.mostTop = this.height + initialOffsetY - paddingTopBottom;\n    // 最下面的位置\n    this.mostBottom = this.mostTop - canvasTotalHeight;\n\n    this.scale = this.config.finalScale;\n\n    const calcViewportWidth = this.clientViewportWidth;\n    const calcViewportHeight = this.clientViewportHeight;\n    // 计算公式：\n    // 可视区域 - 滚动条的长度 / 可视区域 = 可视区域 / 画布可滚动距离\n    // 底部滚动条宽度\n    this.scrollBottomWidth =\n      calcViewportWidth -\n      (calcViewportWidth * (this.mostLeft - this.mostRight)) / this.viewportMoveWidth;\n    // 右侧滚动条高度\n    this.scrollRightHeight =\n      calcViewportHeight -\n      (calcViewportHeight * (this.mostTop - this.mostBottom)) / this.viewportMoveHeight;\n\n    // 计算滚动条滚动位移的距离\n    // 可滚动区域：canvasTotalWidth - scrollBottomWidth\n    const bottomBarToLeft = this.getToLeft(viewport.x);\n    const rightBarToTop = this.getToTop(viewport.y);\n\n    // 设置右侧的滚动条内的 block 样式\n    domUtils.setStyle(this.rightScrollBarBlock, {\n      right: 2,\n      top: rightBarToTop,\n      background: '#1F2329',\n      zIndex: 10,\n      height: this.scrollRightHeight,\n      width: SCROLL_BAR_WIDTH,\n    });\n    // 设置底部的滚动条内的 block 样式\n    domUtils.setStyle(this.bottomScrollBarBlock, {\n      left: bottomBarToLeft,\n      bottom: 2,\n      background: '#1F2329',\n      zIndex: 10,\n      height: SCROLL_BAR_WIDTH,\n      width: this.scrollBottomWidth,\n    });\n    this.changeScrollBarVisibility(this.rightScrollBarBlock, ScrollBarVisibility.Show);\n    this.changeScrollBarVisibility(this.bottomScrollBarBlock, ScrollBarVisibility.Show);\n\n    // 滚动时才显示滚动条\n    // 定时器在 1s 后隐藏滚动条\n    if (this.options.showScrollBars === 'whenScrolling') {\n      this.hideTimeout = window.setTimeout(() => {\n        this.changeScrollBarVisibility(this.rightScrollBarBlock, ScrollBarVisibility.Hidden);\n        this.changeScrollBarVisibility(this.bottomScrollBarBlock, ScrollBarVisibility.Hidden);\n        this.hideTimeout = undefined;\n      }, 1000);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-scroll-limit-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { FlowDocument, FlowNodeTransformData } from '@flowgram.ai/document';\nimport { Layer } from '@flowgram.ai/core';\nimport { ScrollSchema } from '@flowgram.ai/utils';\n\nimport { scrollLimit } from '../utils';\n\n/**\n * 控制滚动边界\n */\n@injectable()\nexport class FlowScrollLimitLayer extends Layer {\n  @inject(FlowDocument) readonly document: FlowDocument;\n\n  getInitScroll(): ScrollSchema {\n    return this.document.layout.getInitScroll(this.pipelineNode.getBoundingClientRect());\n  }\n\n  onReady(): void {\n    const initScroll = () => this.getInitScroll();\n    this.config.updateConfig(initScroll());\n    this.config.addScrollLimit(scroll =>\n      scrollLimit(\n        scroll,\n        [this.document.root.getData(FlowNodeTransformData)!.bounds],\n        this.config,\n        initScroll,\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-selector-bounds-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { FlowNodeEntity, FlowNodeRenderData, FlowNodeTransformData } from '@flowgram.ai/document';\nimport {\n  CommandRegistry,\n  EditorState,\n  EditorStateConfigEntity,\n  Layer,\n  LayerOptions,\n  PlaygroundConfig,\n  observeEntity,\n  observeEntityDatas,\n} from '@flowgram.ai/core';\n\nimport { FlowRendererKey, FlowRendererRegistry } from '../flow-renderer-registry';\nimport { FlowSelectConfigEntity, SelectorBoxConfigEntity } from '../entities';\n\nexport interface SelectorBoxPopoverProps {\n  bounds: Rectangle;\n  config: PlaygroundConfig;\n  flowSelectConfig: FlowSelectConfigEntity;\n  commandRegistry: CommandRegistry;\n  children?: React.ReactNode;\n}\n\nexport interface FlowSelectorBoundsLayerOptions extends LayerOptions {\n  ignoreOneSelect?: boolean;\n  ignoreChildrenLength?: boolean;\n  boundsPadding?: number; // 边框留白，默认 10\n  disableBackground?: boolean; // 禁用背景框\n  backgroundClassName?: string; // 节点下边\n  foregroundClassName?: string; // 节点上边\n  SelectorBoxPopover?: React.FC<SelectorBoxPopoverProps>; // 选择框工具层\n  CustomBoundsRenderer?: React.FC<SelectorBoxPopoverProps>; // 自定义渲染\n}\n\n/**\n * 流程节点被框选后的边界区域渲染\n */\n@injectable()\nexport class FlowSelectorBoundsLayer extends Layer<FlowSelectorBoundsLayerOptions> {\n  @inject(FlowRendererRegistry) readonly rendererRegistry: FlowRendererRegistry;\n\n  @inject(CommandRegistry) readonly commandRegistry: CommandRegistry;\n\n  @observeEntity(FlowSelectConfigEntity)\n  protected flowSelectConfigEntity: FlowSelectConfigEntity;\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  @observeEntity(SelectorBoxConfigEntity)\n  protected selectorBoxConfigEntity: SelectorBoxConfigEntity;\n\n  /**\n   * 需要监听节点的展开和收起状态，重新绘制边框\n   */\n  @observeEntityDatas(FlowNodeEntity, FlowNodeRenderData)\n  renderStates: FlowNodeRenderData[];\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData)\n  _transforms: FlowNodeTransformData[];\n\n  readonly node = domUtils.createDivWithClass('gedit-selector-bounds-layer');\n\n  readonly selectBoundsBackground = domUtils.createDivWithClass('gedit-selector-bounds-background');\n\n  onReady(): void {\n    // 这个是覆盖到节点上边的，所以要比 flow-nodes-content-layer 大\n    this.node!.style.zIndex = '20';\n    const { firstChild } = this.pipelineNode;\n    if (this.options.boundsPadding !== undefined) {\n      this.flowSelectConfigEntity.boundsPadding = this.options.boundsPadding;\n    }\n    if (this.options.backgroundClassName) {\n      this.selectBoundsBackground.classList.add(this.options.backgroundClassName);\n    }\n    // 这里创建一个空 layer 用于放背景\n    const selectorBoundsLayer = domUtils.createDivWithClass(\n      'gedit-selector-bounds-background-layer gedit-playground-layer'\n    );\n    selectorBoundsLayer.appendChild(this.selectBoundsBackground);\n    // 背景框需要在节点的下边\n    this.pipelineNode.insertBefore(selectorBoundsLayer, firstChild);\n  }\n\n  onZoom(scale: number) {\n    this.node!.style.transform = `scale(${scale})`;\n    this.selectBoundsBackground.parentElement!.style.transform = `scale(${scale})`;\n  }\n\n  onViewportChange() {\n    // 需要调整 bounds 菜单的位置\n    this.render();\n  }\n\n  isEnabled(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n    return currentState === EditorState.STATE_SELECT;\n  }\n\n  // /**\n  //  * 渲染工具栏\n  //  */\n  // renderCommandMenus(): JSX.Element[] {\n  //   return this.commandRegistry.commands\n  //     .filter(cmd => cmd.category === FlowRendererCommandCategory.SELECTOR_BOX)\n  //     .map(cmd => {\n  //       const CommandRenderer = this.rendererRegistry.getRendererComponent(\n  //         (cmd.icon as string) || cmd.id,\n  //       )?.renderer;\n  //\n  //       return (\n  //         // eslint-disable-next-line react/jsx-filename-extension\n  //         <CommandRenderer\n  //           key={cmd.id}\n  //           disabled={!this.commandRegistry.isEnabled(cmd.id)}\n  //           command={cmd}\n  //           onClick={(e: any) => this.commandRegistry.executeCommand(cmd.id, e)}\n  //         />\n  //       );\n  //     })\n  //     .filter(c => c);\n  // }\n\n  render(): JSX.Element {\n    const {\n      ignoreOneSelect,\n      ignoreChildrenLength,\n      SelectorBoxPopover: SelectorBoxPopoverFromOpts,\n      disableBackground,\n      CustomBoundsRenderer,\n    } = this.options;\n\n    const bounds = this.flowSelectConfigEntity.getSelectedBounds();\n    const selectedNodes = this.flowSelectConfigEntity.selectedNodes;\n\n    const bg = this.selectBoundsBackground;\n    const isDragging = !this.selectorBoxConfigEntity.isStart;\n\n    if (\n      bounds.width === 0 ||\n      bounds.height === 0 ||\n      // 选中单个的时候不显示\n      (ignoreOneSelect &&\n        selectedNodes.length === 1 &&\n        // 选中的节点不包含多个子节点\n        (ignoreChildrenLength || (selectedNodes[0] as FlowNodeEntity).childrenLength <= 1))\n    ) {\n      domUtils.setStyle(bg, {\n        display: 'none',\n      });\n      return <></>;\n    }\n    if (CustomBoundsRenderer) {\n      return (\n        <CustomBoundsRenderer\n          bounds={bounds}\n          config={this.config}\n          flowSelectConfig={this.flowSelectConfigEntity}\n          commandRegistry={this.commandRegistry}\n        />\n      );\n    }\n    const style = {\n      display: 'block',\n      left: bounds.left,\n      top: bounds.top,\n      width: bounds.width,\n      height: bounds.height,\n    };\n    if (!disableBackground) {\n      domUtils.setStyle(bg, style);\n    }\n    let foregroundClassName = 'gedit-selector-bounds-foreground';\n    if (this.options.foregroundClassName) {\n      foregroundClassName += ' ' + this.options.foregroundClassName;\n    }\n    const SelectorBoxPopover =\n      SelectorBoxPopoverFromOpts ||\n      this.rendererRegistry.tryToGetRendererComponent(FlowRendererKey.SELECTOR_BOX_POPOVER)\n        ?.renderer;\n    if (!isDragging || !SelectorBoxPopover)\n      return <div className={foregroundClassName} style={style} />;\n    return (\n      <SelectorBoxPopover\n        bounds={bounds}\n        config={this.config}\n        flowSelectConfig={this.flowSelectConfigEntity}\n        commandRegistry={this.commandRegistry}\n      >\n        <div className={foregroundClassName} style={style} />\n      </SelectorBoxPopover>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/flow-selector-box-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils, PositionSchema } from '@flowgram.ai/utils';\nimport { FlowDocument, FlowNodeEntity, FlowNodeTransformData } from '@flowgram.ai/document';\nimport {\n  ContextMenuService,\n  EditorState,\n  EditorStateConfigEntity,\n  Layer,\n  LayerOptions,\n  observeEntity,\n  PipelineLayerPriority,\n  PlaygroundConfigEntity,\n  PlaygroundDrag,\n  SelectionService,\n} from '@flowgram.ai/core';\n\nimport { FlowSelectConfigEntity, SelectorBoxConfigEntity } from '../entities';\n\nexport interface FlowSelectorBoxOptions extends LayerOptions {\n  /**\n   * 默认不提供则为点击空白地方可以框选\n   * @param e\n   * @param entity\n   */\n  canSelect?: (e: MouseEvent, entity: SelectorBoxConfigEntity) => boolean;\n}\n/**\n * 流程选择框\n */\n@injectable()\nexport class FlowSelectorBoxLayer extends Layer<FlowSelectorBoxOptions> {\n  @inject(FlowDocument)\n  protected flowDocument: FlowDocument;\n\n  @inject(ContextMenuService)\n  readonly contextMenuService: ContextMenuService;\n\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  @inject(SelectionService) readonly selectionService: SelectionService;\n\n  @observeEntity(SelectorBoxConfigEntity)\n  protected selectorBoxConfigEntity: SelectorBoxConfigEntity;\n\n  @observeEntity(FlowSelectConfigEntity)\n  protected selectConfigEntity: FlowSelectConfigEntity;\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  readonly node = domUtils.createDivWithClass('gedit-selector-box-layer');\n\n  /**\n   * 选择框\n   */\n  protected selectorBox = this.createDOMCache('gedit-selector-box');\n\n  /**\n   * 用于遮挡鼠标，避免触发 hover\n   */\n  protected selectorBoxBlock = this.createDOMCache('gedit-selector-box-block');\n\n  protected transformVisibles: FlowNodeTransformData[];\n\n  /**\n   * 拖动选择框\n   */\n  protected selectboxDragger = new PlaygroundDrag({\n    onDragStart: (e) => {\n      this.selectConfigEntity.clearSelectedNodes();\n      const mousePos = this.playgroundConfigEntity.getPosFromMouseEvent(e);\n      this.transformVisibles = this.flowDocument\n        .getRenderDatas(FlowNodeTransformData, false)\n        .filter((transform) => {\n          const { entity } = transform;\n          if (entity.originParent) {\n            return (\n              this.nodeSelectable(entity, mousePos) &&\n              this.nodeSelectable(entity.originParent, mousePos)\n            );\n          }\n          return this.nodeSelectable(entity, mousePos);\n        });\n      this.selectorBoxConfigEntity.setDragInfo(e);\n      this.updateSelectorBox(this.selectorBoxConfigEntity);\n    },\n    onDrag: (e) => {\n      this.selectorBoxConfigEntity.setDragInfo(e);\n      // 更新选择框\n      this.selectConfigEntity.selectFromBounds(\n        this.selectorBoxConfigEntity.toRectangle(this.playgroundConfigEntity.finalScale),\n        this.transformVisibles\n      );\n      this.updateSelectorBox(this.selectorBoxConfigEntity);\n    },\n    onDragEnd: (e) => {\n      this.selectorBoxConfigEntity.setDragInfo(e);\n      this.transformVisibles.length = 0;\n      this.updateSelectorBox(this.selectorBoxConfigEntity);\n    },\n  });\n\n  onReady(): void {\n    if (!this.options.canSelect) {\n      this.options.canSelect = (e: MouseEvent) => {\n        const target = e.target as HTMLElement | undefined;\n        // 默认点击空白地方可以框选\n        return target === this.pipelineNode || target === this.playgroundNode;\n      };\n    }\n    // 将选中的节点同步到全局\n    // TODO 后续要统一到 selection service\n    this.toDispose.pushAll([\n      this.selectConfigEntity.onConfigChanged(() => {\n        this.selectionService.selection = this.selectConfigEntity.selectedNodes;\n      }),\n      this.selectionService.onSelectionChanged(() => {\n        const selectedNodes = this.selectionService.selection.filter(\n          (entity) => entity instanceof FlowNodeEntity\n        );\n\n        this.selectConfigEntity.selectedNodes = selectedNodes as FlowNodeEntity[];\n      }),\n    ]);\n    this.listenPlaygroundEvent(\n      'mousedown',\n      (e: MouseEvent): boolean | undefined => {\n        if (!this.isEnabled()) return;\n        // 自定义拦截选择框事件\n        if (this.options.canSelect && !this.options.canSelect(e, this.selectorBoxConfigEntity)) {\n          return;\n        }\n\n        const currentState = this.editorStateConfig.getCurrentState();\n\n        // 鼠标友好模式，框选后，再次点击其他地方或者框选其他地方，需要清空已有选择的节点\n        if (currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) {\n          this.selectConfigEntity.clearSelectedNodes();\n        }\n\n        // const target = e.target as HTMLElement | undefined;\n        // TODO 下边这些特化逻辑迁移到固定布局逻辑\n        // const linesLayer = document.querySelector('.gedit-flow-lines-layer');\n        // const toolsTarget = document.querySelector('.flow-canvas-selector-box-tools');\n        // const isInTools = toolsTarget && (toolsTarget === target || toolsTarget.contains(target!));\n        // 保证 service 更新后进行是否清除的计算\n        // setTimeout(() => {\n        //   // 如果点击到选中区域的菜单栏\n        //   if (!isInTools && !this.contextMenuService.rightPanelVisible) {\n        //     // 取消之前的选择状态\n        //     this.selectConfigEntity.clearSelectedNodes();\n        //   }\n        // }, 0);\n        // if (\n        //   target === this.pipelineNode ||\n        //   target === this.playgroundNode // 点击空白区域\n        //   linesLayer?.contains(target!) // 点击 svg 线条\n        //   target?.classList.contains('flow-canvas-adder') || // 点击添加按钮的留白区域\n        //   target?.classList.contains('flow-canvas-block-icon') // 点击添加按钮的留白区域\n        // ) {\n        //   return true;\n        // }\n        this.selectboxDragger.start(e.clientX, e.clientY, this.config);\n        return true;\n      },\n      PipelineLayerPriority.BASE_LAYER\n    );\n  }\n\n  isEnabled(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n    const isMouseFriendly = currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;\n\n    return (\n      !this.config.disabled &&\n      !this.config.readonly &&\n      // 鼠标友好模式下，需要按下 shift 启动框选\n      ((isMouseFriendly && this.editorStateConfig.isPressingShift) ||\n        currentState === EditorState.STATE_SELECT) &&\n      !this.selectorBoxConfigEntity.disabled\n    );\n  }\n\n  /**\n   * Destroy\n   */\n  dispose(): void {\n    this.selectorBox.dispose();\n    this.selectorBoxBlock.dispose();\n    super.dispose();\n  }\n\n  protected updateSelectorBox(selector: SelectorBoxConfigEntity): void {\n    const node = this.selectorBox.get();\n    const block = this.selectorBoxBlock.get();\n    // 非可用状态且在 moving 则关闭选择框\n    if (!this.isEnabled() && selector.isMoving) {\n      this.selectorBoxConfigEntity.collapse();\n    }\n    if (!this.isEnabled() || !selector.isMoving) {\n      node.setStyle({\n        display: 'none',\n      });\n      block.setStyle({\n        display: 'none',\n      });\n    } else {\n      node.setStyle({\n        display: 'block',\n        left: selector.position.x,\n        top: selector.position.y,\n        width: selector.size.width,\n        height: selector.size.height,\n      });\n      // 这是遮挡滑块，防止触发节点 hover\n      block.setStyle({\n        display: 'block',\n        left: selector.position.x - 10,\n        top: selector.position.y - 10,\n        width: selector.size.width + 20,\n        height: selector.size.height + 20,\n      });\n    }\n  }\n\n  private nodeSelectable(node: FlowNodeEntity, mousePos: PositionSchema) {\n    const selectable = node.getNodeMeta().selectable;\n    if (typeof selectable === 'function') {\n      return selectable(node, mousePos);\n    } else {\n      return selectable;\n    }\n  }\n\n  // autorun(): void {\n  //   this.updateSelectorBox(this.selectorBoxConfigEntity);\n  // }\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/layers/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-nodes-transform-layer';\nexport * from './flow-nodes-content-layer';\nexport * from './flow-lines-layer';\nexport * from './flow-labels-layer';\nexport * from './flow-debug-layer';\nexport * from './flow-scroll-bar-layer';\nexport * from './flow-drag-layer';\nexport * from './flow-selector-box-layer';\nexport * from './flow-selector-bounds-layer';\nexport * from './flow-context-menu-layer';\nexport * from './flow-scroll-limit-layer';\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/utils/element.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nexport const isHidden = (dom?: HTMLElement) => {\n  if (!dom || isNil(dom?.offsetParent)) {\n    return true;\n  }\n  const style = window.getComputedStyle(dom);\n  if (style?.display === 'none') {\n    return true;\n  }\n  return false;\n};\n\nexport const isRectInit = (rect?: DOMRect): boolean => {\n  if (!rect) {\n    return false;\n  }\n  // 检查所有属性是否都为0,表示DOMRect未初始化\n  if (\n    rect.bottom === 0 &&\n    rect.height === 0 &&\n    rect.left === 0 &&\n    rect.right === 0 &&\n    rect.top === 0 &&\n    rect.width === 0 &&\n    rect.x === 0 &&\n    rect.y === 0\n  ) {\n    return false;\n  }\n  return true;\n};\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/utils/find-selected-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { uniq } from 'lodash-es';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\n\nfunction getNodePath(node: FlowNodeEntity): FlowNodeEntity[] {\n  const path: FlowNodeEntity[] = [node];\n  node = node.parent as FlowNodeEntity;\n  while (node) {\n    path.push(node);\n    node = node.parent as FlowNodeEntity;\n  }\n  return path.reverse();\n}\n\n/**\n * 过滤掉画布节点, 有 originParent，都是非独立节点\n * @param entity\n */\nfunction findRealEntity(entity: FlowNodeEntity): FlowNodeEntity {\n  while (entity.originParent) {\n    entity = entity.originParent;\n  }\n  return entity;\n}\n/**\n * 生成选中节点的路径\n * 如\n *   [\n *     'root',\n *     'exclusiveSplit_30baf8b1da0',\n *     'exclusiveSplit_d0070ce5d04',\n *     'createRecord_47e8fe1dfc3'\n *   ],\n *   [\n *     'root',\n *     'exclusiveSplit_30baf8b1da0',\n *     'exclusiveSplit_d0070ce5d04',\n *     'createRecord_32dcdd10274'\n *   ],\n *   [\n *     'root',\n *     'exclusiveSplit_30baf8b1da0',\n *     'exclusiveSplit_d0070ce5d04',\n *     'exclusiveSplit_a5579b3997d', // 这里产生分叉\n *     'createRecord_b57b00eee94' // 父亲节点分叉了，这里就忽略了\n *   ]\n * ]\n * 1. 相同分支的节点，选择每个节点\n * 2. 跨分支的节点选择共同的父节点\n */\nexport function findSelectedNodes(nodes: FlowNodeEntity[]): FlowNodeEntity[] {\n  if (nodes.length === 0) return [];\n  /**\n   * 生成节点的路径\n   */\n  const nodePathList: FlowNodeEntity[][] = nodes.map((n) => getNodePath(n));\n  /**\n   * 只需要比较最小的路径\n   */\n  const minLength = Math.min(...nodePathList.map((n) => n.length));\n  let index = 0;\n  let selectedItems: FlowNodeEntity[] = [];\n  /**\n   * 从二维数组的每一层打平去看，看看有没有分叉，如果有分叉就在当前层停止并作为选中的节点\n   */\n  while (index < minLength) {\n    // eslint-disable-next-line no-loop-func\n    selectedItems = uniq(nodePathList.map((p) => p[index]));\n    // 存在分叉\n    if (selectedItems.length > 1) {\n      break;\n    }\n    index += 1;\n  }\n  return uniq(selectedItems.map((item) => findRealEntity(item)));\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './scroll-limit';\nexport * from './scroll-bar-events';\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/utils/scroll-bar-events.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * 滚动条点击事件监听\n */\nexport const ScrollBarEvents = Symbol('ScrollBarEvents');\n\nexport interface ScrollBarEvents {\n  dragStart: () => void;\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/src/utils/scroll-limit.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { type PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nexport interface ScrollData {\n  scrollX: number;\n  scrollY: number;\n}\n\n// viewport 缩小 30 像素\nconst SCROLL_LIMIT_PADDING = -120;\n\nexport function getScrollViewport(\n  scrollData: ScrollData,\n  config: PlaygroundConfigEntity\n): Rectangle {\n  const scale = config.finalScale;\n  return new Rectangle(\n    scrollData.scrollX / scale,\n    scrollData.scrollY / scale,\n    config.config.width / scale,\n    config.config.height / scale\n  ).pad(SCROLL_LIMIT_PADDING / scale, SCROLL_LIMIT_PADDING / scale);\n}\n\n/**\n * 限制滚动\n */\nexport function scrollLimit(\n  scroll: ScrollData,\n  boundsList: Rectangle[],\n  config: PlaygroundConfigEntity,\n  initScroll: () => ScrollData\n): ScrollData {\n  scroll = { ...scroll };\n  const configData = config.config;\n  const oldScroll = { scrollX: configData.scrollX, scrollY: configData.scrollY };\n  // 画布 size 还没初始化滚动不限制\n  if (boundsList.length === 0 || configData.width === 0 || configData.height === 0) return scroll;\n  const viewport = getScrollViewport(scroll, config);\n  const isVisible = boundsList.find((bounds) => Rectangle.isViewportVisible(bounds, viewport));\n  if (!isVisible) {\n    const oldViewport = getScrollViewport(oldScroll, config);\n    const isOldVisible = boundsList.find((bounds) =>\n      Rectangle.isViewportVisible(bounds, oldViewport)\n    );\n    // 如果之前也是不可见就不阻止\n    if (!isOldVisible) {\n      return initScroll();\n    }\n    return oldScroll;\n  }\n  return scroll;\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\n      \"vitest/globals\"\n    ],\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"**/__tests__/**\",\n    \"**/__mocks__/**\"\n  ]\n}\n"
  },
  {
    "path": "packages/canvas-engine/renderer/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/canvas-engine/renderer/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/client/editor/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/client/editor/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/editor\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/history-node-plugin\": \"workspace:*\",\n    \"@flowgram.ai/i18n-plugin\": \"workspace:*\",\n    \"@flowgram.ai/materials-plugin\": \"workspace:*\",\n    \"@flowgram.ai/node\": \"workspace:*\",\n    \"@flowgram.ai/node-core-plugin\": \"workspace:*\",\n    \"@flowgram.ai/node-variable-plugin\": \"workspace:*\",\n    \"@flowgram.ai/playground-react\": \"workspace:*\",\n    \"@flowgram.ai/redux-devtool-plugin\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"@flowgram.ai/shortcuts-plugin\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@flowgram.ai/variable-plugin\": \"workspace:*\",\n    \"@flowgram.ai/reactive\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/flow-editor-client-plugins.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { createNodeClientPlugins } from './node-client/create-node-client-plugins';\nimport { FlowEditorClient } from './flow-editor-client';\n\nexport const createFlowEditorClientPlugin = definePluginCreator<{}>({\n  onBind({ bind }) {\n    bind(FlowEditorClient).toSelf().inSingletonScope();\n  },\n});\n\nexport const createFlowEditorClientPlugins = () => [\n  ...createNodeClientPlugins(),\n  createFlowEditorClientPlugin({}),\n];\n"
  },
  {
    "path": "packages/client/editor/src/clients/flow-editor-client.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\nimport { type FormItem } from '@flowgram.ai/form-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { Playground, PlaygroundConfigRevealOpts } from '@flowgram.ai/core';\n\nimport { FocusNodeFormItemOptions, NodeClient } from './node-client';\n\ninterface FocusNodeOptions {\n  zoom?: PlaygroundConfigRevealOpts['zoom'];\n  easing?: PlaygroundConfigRevealOpts['easing']; // 是否开启缓动，默认开启\n  easingDuration?: PlaygroundConfigRevealOpts['easingDuration']; // 默认 500 ms\n  scrollToCenter?: PlaygroundConfigRevealOpts['scrollToCenter']; // 是否滚动到中心\n}\n\n@injectable()\nexport class FlowEditorClient {\n  @inject(NodeClient) readonly nodeClient: NodeClient;\n\n  @inject(Playground) readonly playground: Playground;\n\n  focusNodeFormItem(formItem: FormItem, options?: FocusNodeFormItemOptions) {\n    this.nodeClient.nodeFocusService.focusNodeFormItem(formItem, options);\n  }\n\n  focusNode(node: FlowNodeEntity, options?: FocusNodeOptions) {\n    this.playground.scrollToView({ entities: [node], ...options });\n  }\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node-client';\nexport * from './flow-editor-client';\nexport * from './flow-editor-client-plugins';\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/create-node-client-plugins.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { NodeFocusService } from './node-focus-service';\nimport { NodeClient } from './node-client';\nimport { createNodeHighlightPlugin } from './highlight/create-node-highlight-plugin';\n\nexport const createNodeClientPlugin = definePluginCreator<{}>({\n  onBind({ bind }) {\n    bind(NodeFocusService).toSelf().inSingletonScope();\n    bind(NodeClient).toSelf().inSingletonScope();\n  },\n});\n\nexport const createNodeClientPlugins = () => [\n  createNodeHighlightPlugin({}),\n  createNodeClientPlugin({}),\n];\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const DEFAULT_HIGHLIGHT_COLOR = 'rgba(238, 245, 40, 0.5)';\nexport const DEFAULT_HIGHLIGHT_PADDING = 0;\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/create-node-highlight-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { createHighlightStyle, removeHighlightStyle } from './highlight-style';\n\nexport const createNodeHighlightPlugin = definePluginCreator<{}>({\n  onInit() {\n    createHighlightStyle();\n  },\n  onDispose() {\n    removeHighlightStyle();\n  },\n});\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/highlight-form-item.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItem } from '@flowgram.ai/form-core';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\n\nimport { HIGHLIGHT_CLASSNAME } from './highlight-style';\nimport { DEFAULT_HIGHLIGHT_PADDING } from './constants';\n\nexport interface HighLightOptions {\n  padding?: number;\n  overlayClassName?: string;\n}\n\nexport function highlightFormItem(\n  formItem: FormItem,\n  options?: HighLightOptions,\n): HTMLDivElement | undefined {\n  const parent =\n    formItem.formModel.flowNodeEntity.getData<FlowNodeRenderData>(FlowNodeRenderData).node;\n  const target = formItem.domRef.current;\n\n  if (!target) {\n    return undefined;\n  }\n\n  const overlay = document.createElement('div');\n\n  const { padding = DEFAULT_HIGHLIGHT_PADDING, overlayClassName } = options || {};\n\n  overlay.style.position = 'absolute';\n  overlay.style.top = '0';\n  overlay.style.left = '0';\n  overlay.style.width = '100%';\n  overlay.style.height = '100%';\n  overlay.style.zIndex = '9999';\n\n  parent.appendChild(overlay);\n\n  const parentRect = parent.getBoundingClientRect();\n  const targetRect = target.getBoundingClientRect();\n\n  overlay.style.top = targetRect.top - parentRect.top - padding + 'px';\n  overlay.style.left = targetRect.left - parentRect.left - padding + 'px';\n  overlay.style.width = targetRect.width + padding * 2 + 'px';\n  overlay.style.height = targetRect.height + padding * 2 + 'px';\n\n  overlay.className = overlayClassName || HIGHLIGHT_CLASSNAME;\n  setTimeout(() => {\n    overlay.remove();\n  }, 2000);\n  return overlay;\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/highlight-style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const HIGHLIGHT_CLASSNAME = 'flowide-highlight';\n\nconst styleText = `\n@keyframes flowide-fade {\n  from {\n   opacity: 1.0;\n  }\n  to {\n    opacity: 0;\n  }\n}\n@-webkit-keyframes flowide-fade {\n  from {\n   opacity: 1.0;\n  }\n  to {\n    opacity: 0;\n  }\n}\n.${HIGHLIGHT_CLASSNAME} {\n  background-color: rgba(238, 245, 40, 0.5);\n  animation: flowide-fade 2s 1 forwards;\n  -webkit-animation: flowide-fade 2s 1 forwards;\n}\n`;\n\nlet styleDom: HTMLStyleElement | undefined;\n\nexport function createHighlightStyle(): void {\n  if (styleDom) return;\n  styleDom = document.createElement('style');\n  styleDom.innerHTML = styleText;\n  document.head.appendChild(styleDom);\n}\n\nexport function removeHighlightStyle(): void {\n  styleDom?.remove();\n  styleDom = undefined;\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { highlightFormItem, HighLightOptions } from './highlight-form-item';\nexport { useHighlight } from './use-highlight';\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/highlight/use-highlight.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useRef } from 'react';\n\nimport { FormModel } from '@flowgram.ai/form-core';\n\ninterface HighlightProps {\n  form: FormModel;\n  path: string;\n}\n\nexport function useHighlight(props: HighlightProps) {\n  const ref = useRef<any>(null);\n  const { form, path } = props;\n  const formItem = form.getFormItemByPath(path);\n  if (!formItem) {\n    return null;\n  }\n  formItem.domRef = ref;\n  return ref;\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './highlight';\nexport * from './node-client';\nexport * from './node-focus-service';\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/node-client.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\n\nimport { NodeFocusService } from './node-focus-service';\n\n@injectable()\nexport class NodeClient {\n  @inject(NodeFocusService) nodeFocusService: NodeFocusService;\n}\n"
  },
  {
    "path": "packages/client/editor/src/clients/node-client/node-focus-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\nimport { type FormItem } from '@flowgram.ai/form-core';\nimport { Playground, PlaygroundConfigRevealOpts } from '@flowgram.ai/core';\n\nimport { highlightFormItem, HighLightOptions } from './highlight';\n\nexport type FocusNodeCanvasOptions = PlaygroundConfigRevealOpts;\n\nexport interface FocusNodeFormItemOptions {\n  canvas?: FocusNodeCanvasOptions;\n  highlight?: boolean | HighLightOptions;\n}\n\n@injectable()\nexport class NodeFocusService {\n  @inject(Playground) readonly playground: Playground;\n\n  protected previousOverlay: HTMLDivElement | undefined;\n\n  protected currentPromise: Promise<void> | undefined;\n\n  highlightNodeFormItem(formItem: FormItem, options?: HighLightOptions) {\n    this.previousOverlay = highlightFormItem(formItem, options);\n  }\n\n  focusNodeFormItem(formItem: FormItem, options?: FocusNodeFormItemOptions): Promise<void> {\n    const node = formItem.formModel.flowNodeEntity;\n    const { canvas = {}, highlight } = options || {};\n    if (this.previousOverlay) {\n      this.previousOverlay.remove();\n      this.previousOverlay = undefined;\n    }\n    const currentPromise = this.playground\n      .scrollToView({ entities: [node], scrollToCenter: true, ...canvas })\n      .then(() => {\n        if (!formItem || !highlight || this.currentPromise !== currentPromise) {\n          return;\n        }\n        this.highlightNodeFormItem(formItem, typeof highlight === 'boolean' ? {} : highlight);\n      });\n    this.currentPromise = currentPromise;\n    return this.currentPromise;\n  }\n}\n"
  },
  {
    "path": "packages/client/editor/src/components/editor-provider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useCallback } from 'react';\n\nimport { interfaces } from 'inversify';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport {\n  PlaygroundReactProvider,\n  createPluginContextDefault,\n  SelectionService,\n} from '@flowgram.ai/core';\n\nimport { EditorPluginContext, EditorProps, createDefaultPreset } from '../preset';\n\nexport const EditorProvider: React.FC<EditorProps> = (props: EditorProps) => {\n  const { children, ...others } = props;\n  const preset = useMemo(() => createDefaultPreset(others), []);\n  const customPluginContext = useCallback(\n    (container: interfaces.Container) =>\n      ({\n        ...createPluginContextDefault(container),\n        get document(): FlowDocument {\n          return container.get<FlowDocument>(FlowDocument);\n        },\n        get selection(): SelectionService {\n          return container.get<SelectionService>(SelectionService);\n        },\n      } as EditorPluginContext),\n    []\n  );\n  return (\n    <PlaygroundReactProvider plugins={preset} customPluginContext={customPluginContext}>\n      {children}\n    </PlaygroundReactProvider>\n  );\n};\n"
  },
  {
    "path": "packages/client/editor/src/components/editor-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PlaygroundReactRenderer as EditorRenderer } from '@flowgram.ai/core';\n"
  },
  {
    "path": "packages/client/editor/src/components/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { EditorProps } from '../preset';\nimport { EditorRenderer } from './editor-renderer';\nimport { EditorProvider } from './editor-provider';\n\n/**\n * 画布编辑器\n * @param props\n * @constructor\n */\nexport const Editor: React.FC<EditorProps> = (props: EditorProps) => {\n  const { children, ...otherProps } = props;\n  return (\n    <EditorProvider {...otherProps}>\n      <EditorRenderer>{children}</EditorRenderer>\n    </EditorProvider>\n  );\n};\n"
  },
  {
    "path": "packages/client/editor/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './editor-provider';\nexport * from './editor-renderer';\nexport * from './editor';\n"
  },
  {
    "path": "packages/client/editor/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './use-flow-editor';\n"
  },
  {
    "path": "packages/client/editor/src/hooks/use-flow-editor.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService } from '@flowgram.ai/core';\n\nimport { FlowEditorClient } from '../clients';\n\nexport function useFlowEditor(): FlowEditorClient {\n  return useService(FlowEditorClient);\n}\n"
  },
  {
    "path": "packages/client/editor/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\nimport { FormModelV2 } from '@flowgram.ai/node';\n\n/* 核心 模块导出 */\nexport * from '@flowgram.ai/utils';\nexport * from '@flowgram.ai/core';\nexport * from '@flowgram.ai/document';\nexport * from '@flowgram.ai/renderer';\nexport * from '@flowgram.ai/variable-plugin';\nexport * from '@flowgram.ai/shortcuts-plugin';\nexport * from '@flowgram.ai/node-core-plugin';\nexport * from '@flowgram.ai/i18n-plugin';\nexport {\n  ReactiveState,\n  ReactiveBaseState,\n  Tracker,\n  useReactiveState,\n  useReadonlyReactiveState,\n  useObserve,\n  observe,\n} from '@flowgram.ai/reactive';\nexport {\n  type interfaces,\n  injectable,\n  postConstruct,\n  named,\n  Container,\n  ContainerModule,\n  AsyncContainerModule,\n  inject,\n  multiInject,\n} from 'inversify';\n\nexport { FlowNodeFormData, NodeRender, type NodeRenderProps } from '@flowgram.ai/form-core';\n\nexport type {\n  FormState,\n  FieldState,\n  FieldArrayRenderProps,\n  FieldRenderProps,\n  FormRenderProps,\n  Validate,\n  FormControl,\n  FieldName,\n  FieldError,\n  FieldWarning,\n  IField,\n  IFieldArray,\n  IForm,\n  Errors,\n  Warnings,\n} from '@flowgram.ai/form';\n\nexport {\n  Form,\n  Field,\n  FieldArray,\n  useForm,\n  useField,\n  useCurrentField,\n  useCurrentFieldState,\n  useFieldValidate,\n  useWatch,\n  ValidateTrigger,\n  FeedbackLevel,\n} from '@flowgram.ai/form';\nexport * from '@flowgram.ai/node';\nexport { FormModelV2 as FormModel };\n\n/**\n * 固定布局模块导出\n */\nexport * from './preset';\nexport * from './components';\nexport * from './hooks';\nexport * from './clients';\n\n/**\n * Plugin 导出\n */\n\nexport * from '@flowgram.ai/node-variable-plugin';\n\nexport { createPlaygroundReactPreset } from '@flowgram.ai/playground-react';\n"
  },
  {
    "path": "packages/client/editor/src/preset/editor-default-preset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport { FlowNodeScope, getNodePrivateScope, getNodeScope } from '@flowgram.ai/variable-plugin';\nimport { FlowRendererContainerModule, FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport { createReduxDevToolPlugin } from '@flowgram.ai/redux-devtool-plugin';\nimport { createNodeVariablePlugin } from '@flowgram.ai/node-variable-plugin';\nimport { createNodeCorePlugin } from '@flowgram.ai/node-core-plugin';\nimport { getNodeForm, NodeFormProps } from '@flowgram.ai/node';\nimport { createMaterialsPlugin } from '@flowgram.ai/materials-plugin';\nimport { createI18nPlugin } from '@flowgram.ai/i18n-plugin';\nimport { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin';\nimport { FlowDocumentContainerModule } from '@flowgram.ai/document';\nimport { createPlaygroundPlugin, Plugin, PluginsProvider } from '@flowgram.ai/core';\n\nimport { createFlowEditorClientPlugins } from '../clients/flow-editor-client-plugins';\nimport { EditorPluginContext, EditorProps } from './editor-props';\n\nexport function createDefaultPreset<CTX extends EditorPluginContext = EditorPluginContext>(\n  opts: EditorProps<CTX>,\n  plugins: Plugin[] = []\n): PluginsProvider<CTX> {\n  return (ctx: CTX) => {\n    opts = { ...EditorProps.DEFAULT, ...opts };\n    /**\n     * i18n support\n     */\n    if (opts.i18n) {\n      plugins.push(createI18nPlugin(opts.i18n));\n    }\n    /**\n     * 默认注册顶层 flow editor client plugin\n     */\n    plugins.push(...createFlowEditorClientPlugins());\n\n    /**\n     * 注册 Redux 开发者工具\n     */\n    if (opts.reduxDevTool?.enable) {\n      plugins.push(createReduxDevToolPlugin(opts.reduxDevTool));\n    }\n\n    /**\n     * 注册画布模块\n     */\n    const defaultContainerModules: interfaces.ContainerModule[] = [\n      FlowDocumentContainerModule, // 默认文档\n      FlowRendererContainerModule, // 默认渲染\n    ];\n    /**\n     * 注册物料\n     */\n    plugins.push(createMaterialsPlugin(opts.materials || {}));\n\n    /**\n     * 注册节点引擎\n     */\n    if (opts.nodeEngine && opts.nodeEngine.enable !== false) {\n      plugins.push(createNodeCorePlugin({ materials: opts.nodeEngine.materials }));\n\n      if (opts.variableEngine?.enable) {\n        plugins.push(createNodeVariablePlugin({}));\n      }\n\n      if (opts.history?.enable && opts.history?.enableChangeNode !== false) {\n        plugins.push(createHistoryNodePlugin({}));\n      }\n    }\n    /**\n     * 画布生命周期注册\n     */\n    plugins.push(\n      createPlaygroundPlugin<CTX>({\n        onInit: (ctx) => {\n          if (opts.nodeRegistries) {\n            ctx.document.registerFlowNodes(...opts.nodeRegistries);\n          }\n          // 自定义画布内部常量\n          if (opts.constants) {\n            ctx.document.options.constants = opts.constants;\n          }\n          if (opts.getNodeDefaultRegistry) {\n            ctx.document.options.getNodeDefaultRegistry = opts.getNodeDefaultRegistry;\n          }\n          ctx.document.options.preNodeCreate = (node) => {\n            /**\n             * Define node.form\n             */\n            if (opts.nodeEngine && opts.nodeEngine.enable !== false) {\n              let cache: NodeFormProps<any> | undefined;\n              Object.defineProperty(node, 'form', {\n                get: () => {\n                  if (cache) return cache;\n                  cache = getNodeForm(node);\n                  return cache;\n                },\n              });\n            }\n\n            /**\n             * Define node.scope & node.privateScope\n             */\n            if (opts.variableEngine && opts.variableEngine.enable !== false) {\n              let cache: FlowNodeScope | undefined;\n              let privateCache: FlowNodeScope | undefined;\n\n              Object.defineProperty(node, 'scope', {\n                get: () => {\n                  if (cache) return cache;\n                  cache = getNodeScope(node);\n                  return cache;\n                },\n              });\n              Object.defineProperty(node, 'privateScope', {\n                get: () => {\n                  if (privateCache) return privateCache;\n                  privateCache = getNodePrivateScope(node);\n                  return privateCache;\n                },\n              });\n            }\n          };\n          ctx.get<FlowRendererRegistry>(FlowRendererRegistry).init();\n        },\n        onReady(ctx) {\n          if (opts.initialData) {\n            ctx.document.fromJSON(opts.initialData);\n          }\n          if (opts.readonly) {\n            ctx.playground.config.readonly = opts.readonly;\n          }\n          ctx.document.load().then(() => {\n            if (opts.onLoad) opts.onLoad(ctx);\n          });\n        },\n        onDispose(ctx) {\n          ctx.document.dispose();\n        },\n        containerModules: defaultContainerModules,\n      })\n    );\n\n    return plugins;\n  };\n}\n"
  },
  {
    "path": "packages/client/editor/src/preset/editor-props.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeScope, VariablePluginOptions } from '@flowgram.ai/variable-plugin';\nimport { ReduxDevToolPluginOptions } from '@flowgram.ai/redux-devtool-plugin';\nimport { PlaygroundReactProps, SelectionService } from '@flowgram.ai/playground-react';\nimport { NodeCorePluginOptions } from '@flowgram.ai/node-core-plugin';\nimport { type NodeFormProps } from '@flowgram.ai/node';\nimport { MaterialsPluginOptions } from '@flowgram.ai/materials-plugin';\nimport { I18nPluginOptions } from '@flowgram.ai/i18n-plugin';\nimport { HistoryPluginOptions } from '@flowgram.ai/history';\nimport { FormMetaOrFormMetaGenerator } from '@flowgram.ai/form-core';\nimport {\n  FlowDocument,\n  FlowDocumentJSON,\n  FlowNodeEntity,\n  type FlowNodeJSON,\n  FlowNodeRegistry,\n  FlowNodeType,\n} from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\n\ndeclare module '@flowgram.ai/document' {\n  interface FlowNodeEntity {\n    form: NodeFormProps<any> | undefined;\n    scope: FlowNodeScope | undefined;\n    privateScope: FlowNodeScope | undefined;\n  }\n}\n\nexport interface EditorPluginContext extends PluginContext {\n  document: FlowDocument;\n  selection: SelectionService;\n}\n\nexport interface EditorProps<\n  CTX extends EditorPluginContext = EditorPluginContext,\n  JSON = FlowDocumentJSON\n> extends PlaygroundReactProps<CTX> {\n  /**\n   * Initialize data\n   * 初始化数据\n   */\n  initialData?: JSON;\n  /**\n   * whether it is readonly\n   * 是否为 readonly\n   */\n  readonly?: boolean;\n  /**\n   * node registries\n   * 节点定义\n   */\n  nodeRegistries?: FlowNodeRegistry[];\n  /**\n   * Get the default node registry, which will be merged with the 'nodeRegistries'\n   * 提供默认的节点注册，这个会和 nodeRegistries 做合并\n   */\n  getNodeDefaultRegistry?: (type: FlowNodeType) => FlowNodeRegistry;\n  /**\n   * Node engine configuration\n   */\n  nodeEngine?: NodeCorePluginOptions & {\n    /**\n     * Default formMeta\n     */\n    createDefaultFormMeta?: (node: FlowNodeEntity) => FormMetaOrFormMetaGenerator;\n    /**\n     * Enable node engine\n     */\n    enable?: boolean;\n  };\n  /**\n   * By default, all nodes are expanded\n   * 默认是否展开所有节点\n   */\n  allNodesDefaultExpanded?: boolean;\n  /**\n   * Canvas material, Used to customize react components\n   * 画布物料, 用于自定义 react 组件\n   */\n  materials?: MaterialsPluginOptions;\n  /**\n   * 画布数据加载完成, 请使用 onAllLayersRendered 替代\n   * @deprecated\n   * */\n  onLoad?: (ctx: CTX) => void;\n  /**\n   * 是否开启变量引擎\n   * Variable engine enable\n   */\n  variableEngine?: VariablePluginOptions;\n  /**\n   * Redo/Undo enable\n   */\n  history?: HistoryPluginOptions<CTX> & { disableShortcuts?: boolean };\n\n  /**\n   * redux devtool configuration\n   */\n  reduxDevTool?: ReduxDevToolPluginOptions;\n\n  /**\n   * Scroll configuration\n   * 滚动配置\n   */\n  scroll?: {\n    enableScrollLimit?: boolean; // 开启滚动限制\n    disableScrollBar?: boolean; //  关闭滚动条\n    disableScroll?: boolean; // 禁止滚动\n  };\n\n  /**\n   * Node data transformation, called by ctx.document.fromJSON\n   * 节点数据转换, 由 ctx.document.fromJSON 调用\n   * @param node - current node\n   * @param json - Current node json data\n   */\n  toNodeJSON?(node: FlowNodeEntity, json: FlowNodeJSON): FlowNodeJSON;\n  /**\n   * Node data transformation, called by ctx.document.toJSON\n   * 节点数据转换, 由 ctx.document.toJSON 调用\n   * @param node - current node\n   * @param json - Current node json data\n   * @param isFirstCreate - Whether it is created for the first time, If document.fromJSON is recalled, but the node already exists, isFirstCreate is false\n   */\n  fromNodeJSON?(node: FlowNodeEntity, json: FlowNodeJSON, isFirstCreate: boolean): FlowNodeJSON;\n  /**\n   * Canvas internal constant customization\n   * 画布内部常量自定义\n   */\n  constants?: Record<string, any>;\n  /**\n   * i18n\n   * 国际化\n   */\n  i18n?: I18nPluginOptions;\n}\n\nexport namespace EditorProps {\n  /**\n   * 默认配置\n   */\n  export const DEFAULT: EditorProps = {\n    background: {},\n  };\n}\n"
  },
  {
    "path": "packages/client/editor/src/preset/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './editor-props';\nexport * from './editor-default-preset';\n"
  },
  {
    "path": "packages/client/editor/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [],\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/client/editor/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/client/editor/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__mocks__/flow.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '../src';\n\nexport const emptyMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n\nexport const formMock: FlowDocumentJSON = {\n  nodes: [{\n    id: 'noop_0',\n    type: 'noop',\n    data: {\n      title: 'noop title',\n    },\n    blocks: [],\n  }]\n}\n\nexport const formMock2: FlowDocumentJSON = {\n  nodes: [{\n    id: 'noop_0',\n    type: 'noop',\n    data: {\n      title: 'noop title changed',\n    },\n    blocks: [],\n  }]\n}\n\nexport const baseWithDataMock: FlowDocumentJSON = {\n  nodes: [ {\n      id: 'start_0',\n      type: 'start',\n      data: {\n        title: 'start title',\n      },\n      blocks: [],\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      data: {\n        title: 'dynamic title',\n      },\n      blocks: [\n        { id: 'block_0', data: { title: '' }, blocks: [], type: 'block' },\n        { id: 'block_1',data: { title: '' }, blocks: [], type: 'block'},\n        { id: 'block_2',data: { title: '' },blocks: [], type: 'block' }\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'end title',\n      },\n      blocks: [],\n    },\n  ]\n}\n\nexport const baseWithDataMock2: FlowDocumentJSON = {\n  nodes: [ {\n    id: 'start_0',\n    type: 'start',\n    data: {\n      title: 'start title changed',\n    },\n    blocks: [],\n  },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      data: {\n        title: 'dynamic title changed',\n      },\n      blocks: [\n        { id: 'block_3', data: { title: '' }, blocks: [], type: 'block' },\n        { id: 'block_4',data: { title: '' }, blocks: [], type: 'block'},\n        { id: 'block_2',data: { title: 'title changed' },blocks: [], type: 'block' }\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      data: {\n        title: 'end title changed',\n      },\n      blocks: [],\n    },\n  ]\n}\n\n\nexport const baseMock: FlowDocumentJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      blocks: [],\n    },\n    {\n      id: 'dynamicSplit_0',\n      type: 'dynamicSplit',\n      blocks: [\n        { id: 'block_0', data: {}, blocks: [], type: 'block' },\n        { id: 'block_1',data: {}, blocks: [], type: 'block'},\n        { id: 'block_2',data: {},blocks: [], type: 'block' }\n      ],\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      blocks: [],\n    },\n  ],\n};\n\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__mocks__/form.mock.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {Field, FieldRenderProps, FormMeta, FormRenderProps} from \"@flowgram.ai/editor\";\n\n\nexport const render = ({ form }: FormRenderProps<FormData>) => {\n  return (\n    <>\n      <Field name=\"name\">\n        {({ field: { value, onChange } }: FieldRenderProps<string>) => (\n          <>\n            <input value={value} onChange={onChange} />\n          </>\n        )}\n      </Field>\n    </>\n  );\n}\n\nexport const formMock: FormMeta = {\n  render\n};\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/__snapshots__/fixed-layout-preset.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`fixed-layout-preset > custom fromNodeJSON and toNodeJSON 1`] = `\n{\n  \"nodes\": [\n    {\n      \"blocks\": [],\n      \"data\": {\n        \"isFirstCreate\": true,\n        \"runningTimes\": 1,\n        \"title\": \"start title\",\n      },\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": true,\n            \"runningTimes\": 1,\n            \"title\": \"\",\n          },\n          \"id\": \"block_0\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": true,\n            \"runningTimes\": 1,\n            \"title\": \"\",\n          },\n          \"id\": \"block_1\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": true,\n            \"runningTimes\": 1,\n            \"title\": \"\",\n          },\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"data\": {\n        \"isFirstCreate\": true,\n        \"runningTimes\": 1,\n        \"title\": \"dynamic title\",\n      },\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"blocks\": [],\n      \"data\": {\n        \"isFirstCreate\": true,\n        \"runningTimes\": 1,\n        \"title\": \"end title\",\n      },\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`fixed-layout-preset > custom fromNodeJSON and toNodeJSON 2`] = `\n{\n  \"nodes\": [\n    {\n      \"blocks\": [],\n      \"data\": {\n        \"isFirstCreate\": false,\n        \"runningTimes\": 1,\n        \"title\": \"start title changed\",\n      },\n      \"id\": \"start_0\",\n      \"type\": \"start\",\n    },\n    {\n      \"blocks\": [\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": true,\n            \"runningTimes\": 1,\n            \"title\": \"\",\n          },\n          \"id\": \"block_3\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": true,\n            \"runningTimes\": 1,\n            \"title\": \"\",\n          },\n          \"id\": \"block_4\",\n          \"type\": \"block\",\n        },\n        {\n          \"blocks\": [],\n          \"data\": {\n            \"isFirstCreate\": false,\n            \"runningTimes\": 1,\n            \"title\": \"title changed\",\n          },\n          \"id\": \"block_2\",\n          \"type\": \"block\",\n        },\n      ],\n      \"data\": {\n        \"isFirstCreate\": false,\n        \"runningTimes\": 1,\n        \"title\": \"dynamic title changed\",\n      },\n      \"id\": \"dynamicSplit_0\",\n      \"type\": \"dynamicSplit\",\n    },\n    {\n      \"blocks\": [],\n      \"data\": {\n        \"isFirstCreate\": false,\n        \"runningTimes\": 1,\n        \"title\": \"end title changed\",\n      },\n      \"id\": \"end_0\",\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`fixed-layout-preset > nodeEngine(v2) toJSON 1`] = `\n{\n  \"nodes\": [\n    {\n      \"blocks\": [],\n      \"data\": {\n        \"title\": \"noop title2\",\n      },\n      \"id\": \"noop_0\",\n      \"type\": \"noop\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/create-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport { HistoryService } from '@flowgram.ai/fixed-history-plugin';\nimport {\n  createPlaygroundContainer,\n  Playground,\n  loadPlugins,\n  PluginContext,\n  createPluginContextDefault,\n  FlowDocument,\n  EditorProps,\n} from '@flowgram.ai/editor';\n\nimport {\n  FixedLayoutPluginContext,\n  FixedLayoutProps,\n  FlowOperationService,\n  createFixedLayoutPreset,\n} from '../src';\n\nexport function createContainer(opts: FixedLayoutProps): interfaces.Container {\n  const container = createPlaygroundContainer();\n\n  const playground = container.get(Playground);\n  const preset = createFixedLayoutPreset(opts);\n  const customPluginContext = (container: interfaces.Container) =>\n    ({\n      ...createPluginContextDefault(container),\n      get document(): FlowDocument {\n        return container.get<FlowDocument>(FlowDocument);\n      },\n    } as FixedLayoutPluginContext);\n\n  const ctx = customPluginContext(container);\n  container.rebind(PluginContext).toConstantValue(ctx);\n  loadPlugins(preset(ctx), container);\n  playground.init();\n  return container;\n}\n\nexport function createHistoryContainer(props: EditorProps = {}) {\n  const container = createContainer({\n    history: {\n      enable: true,\n    },\n    ...props,\n  });\n\n  const flowDocument = container.get<FlowDocument>(FlowDocument);\n  const flowOperationService = container.get<FlowOperationService>(FlowOperationService);\n  const historyService = container.get<HistoryService>(HistoryService);\n\n  return {\n    flowDocument,\n    flowOperationService,\n    historyService,\n  };\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/fixed-layout-preset.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowDocument, FlowNodeFormData } from '@flowgram.ai/editor';\n\nimport { baseWithDataMock, baseWithDataMock2, formMock, formMock2 } from '../__mocks__/flow.mock';\nimport { createContainer } from './create-container';\n\ndescribe('fixed-layout-preset', () => {\n  let flowDocument: FlowDocument;\n  beforeEach(() => {\n    const container = createContainer({});\n    flowDocument = container.get(FlowDocument);\n  });\n  it('fromJSON and toJSON', () => {\n    flowDocument.fromJSON(baseWithDataMock);\n    expect(flowDocument.toJSON()).toEqual(baseWithDataMock);\n    // reload data\n    flowDocument.fromJSON(baseWithDataMock2);\n    expect(flowDocument.toJSON()).toEqual(baseWithDataMock2);\n  });\n  it('custom fromNodeJSON and toNodeJSON', () => {\n    const container = createContainer({\n      fromNodeJSON: (node, json, isFirstCreate) => {\n        if (!json.data) {\n          json.data = {};\n        }\n        json.data = { ...json.data, isFirstCreate };\n        return json;\n      },\n      toNodeJSON(node, json) {\n        json.data.runningTimes = (json.data.runningTimes || 0) + 1;\n        return json;\n      },\n    });\n    container.get(FlowDocument).fromJSON(baseWithDataMock);\n    expect(container.get(FlowDocument).toJSON()).toMatchSnapshot();\n    container.get(FlowDocument).fromJSON(baseWithDataMock2);\n    expect(container.get(FlowDocument).toJSON()).toMatchSnapshot();\n  });\n  it('nodeEngine(v2) toJSON', async () => {\n    const container = createContainer({\n      nodeEngine: {},\n      nodeRegistries: [\n        {\n          type: 'noop',\n          formMeta: {\n            render: () => undefined,\n          },\n        },\n      ],\n    });\n    flowDocument = container.get(FlowDocument);\n    flowDocument.fromJSON(formMock);\n    expect(flowDocument.toJSON()).toEqual(formMock);\n    const { formModel } = flowDocument.getNode('noop_0').getData(FlowNodeFormData);\n    expect(formModel.getFormItemByPath('title').value).toEqual('noop title');\n    formModel.getFormItemByPath('title').value = 'noop title2';\n    expect(flowDocument.toJSON()).toMatchSnapshot();\n    flowDocument.fromJSON(formMock2);\n    expect(flowDocument.toJSON()).toEqual(formMock2);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/flow-operation-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { FlowDocument, FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { getNodeChildrenIds } from '../utils';\nimport { createContainer } from '../create-container';\nimport { FlowOperationService } from '../../src/types';\nimport { baseMock } from '../../__mocks__/flow.mock';\n\ndescribe('flow-operation-service', () => {\n  let flowOperationService: FlowOperationService;\n  let flowDocument: FlowDocument;\n  beforeEach(() => {\n    const container = createContainer({});\n    flowDocument = container.get(FlowDocument);\n    flowOperationService = container.get(FlowOperationService);\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('addFromNode', () => {\n    const type = 'test';\n    const nodeJSON = {\n      id: 'test',\n      type,\n    };\n    const added = flowOperationService.addFromNode('start_0', nodeJSON);\n    const node = flowDocument.getNode(added.id) as FlowNodeEntity;\n    expect(node).toBe(added);\n    expect(added.id).toEqual(nodeJSON.id);\n  });\n\n  it('deleteNode', () => {\n    const id = 'dynamicSplit_0';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n  });\n\n  it('delete block by deleteNode', () => {\n    const id = 'block_0';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n  });\n\n  it('addNode', () => {\n    const nodeJSON = {\n      id: 'test-node',\n      type: 'test',\n    };\n    const parent = flowDocument.getNode('start_0');\n    const added = flowOperationService.addNode(nodeJSON, {\n      parent,\n    });\n    const entity = flowDocument.getNode(added.id);\n    expect(entity).toBe(added);\n    expect(entity?.parent).toBe(parent);\n    expect(entity?.originParent).toBeUndefined();\n  });\n\n  it('addBlock', () => {\n    const target = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n    const added = flowOperationService.addBlock(target, {\n      id: 'test-block',\n      type: 'test-block',\n    });\n    const entity = flowDocument.getNode(added.id);\n    expect(entity).toBe(added);\n    expect(entity?.parent?.id).toEqual('$inlineBlocks$dynamicSplit_0');\n    expect(entity?.originParent).toBe(target);\n  });\n\n  it('deleteNodes', () => {\n    const parent = flowDocument.getNode('start_0');\n    const added = flowOperationService.addNode(\n      {\n        id: 'delete-node',\n        type: 'test',\n      },\n      {\n        parent,\n      },\n    );\n\n    flowOperationService.deleteNodes([added]);\n    expect(flowDocument.getNode(added.id)).toBeUndefined();\n  });\n\n  it('createGroup ungroup', () => {\n    const node1 = flowOperationService.addFromNode('start_0', {\n      id: 'add',\n      type: 'add',\n    });\n    const node2 = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n    // TODO 这里需要优化，理论上createGroup不应该依赖渲染，现在createGroup内部有一个index的校验，但index在transformer中被设置对了\n    flowDocument.transformer.refresh();\n    const group = flowOperationService.createGroup([node1, node2]) as FlowNodeEntity;\n    const root = flowDocument.getNode('root');\n    expect(root?.collapsedChildren.map(c => c.id)).toEqual(['start_0', group.id, 'end_0']);\n    expect(group.collapsedChildren.map(c => c.id)).toEqual([node1.id, node2.id]);\n    flowOperationService.ungroup(group);\n    expect(root?.collapsedChildren.map(c => c.id)).toEqual([\n      'start_0',\n      'add',\n      'dynamicSplit_0',\n      'end_0',\n    ]);\n  });\n\n  it('moveNode', () => {\n    flowOperationService.moveNode('block_1', {\n      index: 2,\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2', 'block_1']);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/add-block.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service addNode', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('addBlock', async () => {\n    const block0 = flowDocument.getNode('block_0') as FlowNodeEntity;\n    const block1 = flowDocument.getNode('block_1') as FlowNodeEntity;\n    const block2 = flowDocument.getNode('block_2') as FlowNodeEntity;\n\n    const target = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n\n    // 测试添加分支\n    const added = flowOperationService.addBlock(target, {\n      id: 'test-block',\n      type: 'test-block',\n    });\n    const entity = flowDocument.getNode(added.id);\n    const children = target.collapsedChildren[1].children;\n\n    expect(entity).toBe(added);\n    expect(entity?.parent?.id).toEqual('$inlineBlocks$dynamicSplit_0');\n    expect(entity?.originParent).toBe(target);\n    expect(children).toEqual([block0, block1, block2, added]);\n\n    // 测试添加分支，index为0\n    const added0 = flowOperationService.addBlock(\n      target,\n      {\n        id: 'test-block0',\n        type: 'test-block0',\n      },\n      {\n        index: 0,\n      },\n    );\n\n    expect(children).toEqual([added0, block0, block1, block2, added]);\n\n    // 测试undo\n    await historyService.undo();\n    expect(children).toEqual([block0, block1, block2, added]);\n    await historyService.undo();\n    expect(children).toEqual([block0, block1, block2]);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/add-from-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('addFromNode', () => {\n    const id = 'test-id';\n    const type = 'test';\n    const nodeJSON = {\n      id,\n      type,\n    };\n    const added = flowOperationService.addFromNode('start_0', nodeJSON);\n    expect(added.id).toEqual(id);\n    const node = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node).toBe(added);\n\n    historyService.undo();\n    const node2 = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node2).toBeUndefined();\n  });\n\n  it('add first node in a block by addFromNode', () => {\n    const id = 'test-id';\n    const blockIconId = '$blockOrderIcon$block_1';\n    const added = flowOperationService.addFromNode(blockIconId, {\n      id,\n      type: 'test',\n    });\n    expect(added.id).toEqual(id);\n    const node = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node).toBe(added);\n    expect(node.pre?.id).toEqual(blockIconId);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/add-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { getRootChildrenIds } from '../../utils';\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock, emptyMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service addNode', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  it('addNode simple', async () => {\n    flowDocument.fromJSON(emptyMock);\n    flowOperationService.addNode(\n      {\n        type: 'test',\n        id: 'test',\n      },\n      {\n        parent: flowDocument.getNode('root'),\n        index: 1,\n      },\n    );\n    expect(getRootChildrenIds(flowDocument)).toEqual(['start_0', 'test', 'end_0']);\n\n    // 测试undo\n    await historyService.undo();\n\n    expect(getRootChildrenIds(flowDocument)).toEqual(['start_0', 'end_0']);\n  });\n\n  it('addNode composed', async () => {\n    flowDocument.fromJSON(baseMock);\n    const nodeJSON = {\n      id: 'test-node',\n      type: 'test',\n    };\n    const parent = flowDocument.getNode('start_0') as FlowNodeEntity;\n    const added = flowOperationService.addNode(nodeJSON, {\n      parent,\n    });\n    const entity = flowDocument.getNode(added.id);\n    expect(entity).toBe(added);\n    expect(entity?.parent).toBe(parent);\n    expect(entity?.originParent).toBeUndefined();\n    expect(parent.collapsedChildren).toEqual([added]);\n\n    // 测试hidden\n    const added1 = flowOperationService.addNode(\n      {\n        id: 'test-node1',\n        type: 'test1',\n      },\n      {\n        parent,\n        hidden: true,\n      },\n    );\n    const entity1 = flowDocument.getNode(added1.id) as FlowNodeEntity;\n    expect(entity1).toBe(added1);\n    expect(entity1.hidden).toBe(true);\n    expect(parent.collapsedChildren).toEqual([added, added1]);\n\n    // 测试index添加\n    const added2 = flowOperationService.addNode(\n      {\n        id: 'test-node2',\n        type: 'test2',\n      },\n      {\n        parent,\n        index: 1,\n      },\n    );\n    expect(parent.collapsedChildren).toEqual([added, added2, added1]);\n\n    // 测试undo\n    await historyService.undo();\n    expect(flowDocument.getNode(added2.id)).toBeUndefined();\n    expect(parent.collapsedChildren).toEqual([added, added1]);\n\n    await historyService.undo();\n    expect(flowDocument.getNode(added1.id)).toBeUndefined();\n    expect(parent.collapsedChildren).toEqual([added]);\n\n    await historyService.undo();\n    expect(flowDocument.getNode(added.id)).toBeUndefined();\n    expect(parent.collapsedChildren).toEqual([]);\n  });\n\n  it('add loop children by addNode', async () => {\n    flowDocument.fromJSON(emptyMock);\n    const loop = flowOperationService.addFromNode('start_0', {\n      id: 'test-loop',\n      type: 'loop',\n    });\n    expect(loop.id).toEqual('test-loop');\n    const loopJson = flowDocument.toJSON();\n    const child = flowOperationService.addNode(\n      {\n        id: 'loop-child1',\n        type: 'test',\n      },\n      {\n        parent: loop,\n        index: 0,\n      },\n    ) as FlowNodeEntity;\n\n    expect(child.id).toEqual('loop-child1');\n    expect(child.pre?.id).toEqual('$loopRightEmpty$test-loop');\n    expect(child.parent?.id).toEqual('$block$test-loop');\n    const str = flowDocument.toString();\n\n    await historyService.undo();\n    expect(flowDocument.toJSON()).toEqual(loopJson);\n\n    await historyService.redo();\n    expect(flowDocument.toString()).toEqual(str);\n  });\n\n  it('add dynamic split children by addNode', async () => {\n    flowDocument.fromJSON(baseMock);\n    const child = flowOperationService.addNode(\n      {\n        id: 'block_test',\n        type: 'block',\n      },\n      {\n        parent: 'dynamicSplit_0',\n        index: 0,\n      },\n    ) as FlowNodeEntity;\n\n    expect(child.id).toEqual('block_test');\n    expect(child.pre?.id).toBeUndefined();\n    expect(child.next?.id).toBe('block_0');\n    expect(child.originParent?.id).toBe('dynamicSplit_0');\n    expect(child.parent?.id).toBe('$inlineBlocks$dynamicSplit_0');\n    const str = flowDocument.toString();\n\n    await historyService.undo();\n    expect(flowDocument.toJSON()).toEqual(baseMock);\n\n    await historyService.redo();\n    expect(flowDocument.toString()).toEqual(str);\n  });\n\n  it('add block children by addNode', async () => {\n    flowDocument.fromJSON(baseMock);\n    const child = flowOperationService.addNode(\n      {\n        id: 'test',\n        type: 'test',\n      },\n      {\n        parent: 'block_0',\n        index: 0,\n      },\n    ) as FlowNodeEntity;\n\n    expect(child.id).toEqual('test');\n    expect(child.pre?.id).toBe('$blockOrderIcon$block_0');\n    expect(child.next?.id).toBeUndefined();\n    expect(child.originParent?.id).toBeUndefined();\n    expect(child.parent?.id).toBe('block_0');\n    const str = flowDocument.toString();\n\n    await historyService.undo();\n    expect(flowDocument.toJSON()).toEqual(baseMock);\n\n    await historyService.redo();\n    expect(flowDocument.toString()).toEqual(str);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/apply.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\nimport { FlowNodeEntity, OperationType } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service apply', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  it('apply deleteNodes', async () => {\n    flowDocument.fromJSON(baseMock);\n    const id = 'dynamicSplit_0';\n    const node = flowDocument.getNode(id) as FlowNodeEntity;\n    flowOperationService.apply({\n      type: OperationType.deleteNodes,\n      value: {\n        fromId: 'start_0',\n        nodes: [node.toJSON()],\n      },\n    });\n    expect(flowDocument.getNode(id)).toBeUndefined();\n\n    historyService.undo();\n    expect(flowDocument.toJSON()).toEqual(baseMock);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/create-group.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('createGroup', async () => {\n    const node1 = flowOperationService.addFromNode('start_0', {\n      id: 'add',\n      type: 'add',\n    });\n    const node2 = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n    flowDocument.transformer.refresh();\n\n    const json = flowDocument.toJSON();\n    const group = flowOperationService.createGroup([node1, node2]) as FlowNodeEntity;\n\n    // 分组创建后 json 变化（group 不再作为系统节点）\n    expect(flowDocument.toJSON()).not.toEqual(json);\n    const root = flowDocument.getNode('root');\n    expect(root?.collapsedChildren.map((c) => c.id)).toEqual(['start_0', group.id, 'end_0']);\n    expect(group.collapsedChildren.map((c) => c.id)).toEqual([node1.id, node2.id]);\n\n    await historyService.undo();\n    expect(root?.collapsedChildren.map((c) => c.id)).toEqual([\n      'start_0',\n      'add',\n      'dynamicSplit_0',\n      'end_0',\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/delete-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('deleteNode', () => {\n    const id = 'dynamicSplit_0';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n\n    historyService.undo();\n    const node1 = flowDocument.getNode(id);\n    expect(node1?.id).toEqual(id);\n  });\n\n  it('delete first block by deleteNode', () => {\n    const id = 'block_0';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n\n    historyService.undo();\n    const node1 = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node1.id).toEqual(id);\n    const pre = node1.pre;\n    expect(pre).toBeUndefined();\n    expect(node1.next?.id).toEqual('block_1');\n    expect(node1.parent?.id).toEqual('$inlineBlocks$dynamicSplit_0');\n    expect(node1.originParent?.id).toEqual('dynamicSplit_0');\n  });\n\n  it('delete intermediate block by deleteNode', () => {\n    const id = 'block_1';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n\n    historyService.undo();\n    const node1 = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node1.id).toEqual(id);\n    expect(node1.pre?.id).toEqual('block_0');\n    expect(node1.next?.id).toEqual('block_2');\n    expect(node1.parent?.id).toEqual('$inlineBlocks$dynamicSplit_0');\n    expect(node1.originParent?.id).toEqual('dynamicSplit_0');\n  });\n\n  it('delete last block by deleteNode', () => {\n    const id = 'block_2';\n    flowOperationService.deleteNode(id);\n    const node = flowDocument.getNode(id);\n    expect(node).toBeUndefined();\n\n    historyService.undo();\n    const node1 = flowDocument.getNode(id) as FlowNodeEntity;\n    expect(node1.id).toEqual(id);\n    expect(node1.pre?.id).toEqual('block_1');\n    expect(node1.next).toBeUndefined();\n    expect(node1.parent?.id).toEqual('$inlineBlocks$dynamicSplit_0');\n    expect(node1.originParent?.id).toEqual('dynamicSplit_0');\n  });\n\n  it('delete empty group by deleteNode', async () => {\n    const node = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n    const group = flowOperationService.createGroup([node]) as FlowNodeEntity;\n    const renderStruct = flowDocument.toString();\n\n    historyService.transact(() => {\n      flowOperationService.deleteNode(node);\n      flowOperationService.deleteNode(group);\n    });\n    await historyService.undo();\n    expect(flowDocument.toString()).toEqual(renderStruct);\n  });\n\n  it('redo test delete block by deleteNode', async () => {\n    flowOperationService.deleteNode('block_1');\n    const str = flowDocument.toString();\n    await historyService.undo();\n    await historyService.redo();\n    expect(flowDocument.toString()).toEqual(str);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/delete-nodes.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\n\nimport { getNodeChildrenIds } from '../../utils';\nimport { createHistoryContainer } from '../../create-container';\nimport { emptyMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service deleteNodes', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  let toDelete1, toDelete2, toDelete3;\n\n  function getRootChildrenIds() {\n    return getNodeChildrenIds(flowDocument.getNode('root'));\n  }\n\n  beforeEach(() => {\n    flowDocument.fromJSON(emptyMock);\n    // start_0 -> to-delete1 -> to-delete2 -> to-delete3 -> end_0\n    toDelete3 = flowOperationService.addFromNode('start_0', {\n      id: 'to-delete3',\n      type: 'test',\n    });\n\n    toDelete2 = flowOperationService.addFromNode('start_0', {\n      id: 'to-delete2',\n      type: 'test',\n    });\n\n    toDelete1 = flowOperationService.addFromNode('start_0', {\n      id: 'to-delete1',\n      type: 'test',\n    });\n  });\n\n  it('delete order nodes', async () => {\n    const toDelete1JSON = toDelete1.toJSON();\n    const toDelete2JSON = toDelete2.toJSON();\n\n    flowOperationService.deleteNodes(['to-delete1', toDelete2]);\n    expect(flowDocument.getNode('to-delete1')).toBeUndefined();\n    expect(flowDocument.getNode('to-delete2')).toBeUndefined();\n\n    expect(getRootChildrenIds()).toEqual(['start_0', 'to-delete3', 'end_0']);\n\n    await historyService.undo();\n    expect(flowDocument.getNode('to-delete1')?.toJSON()).toEqual(toDelete1JSON);\n    expect(flowDocument.getNode('to-delete2')?.toJSON()).toEqual(toDelete2JSON);\n\n    expect(getRootChildrenIds()).toEqual([\n      'start_0',\n      'to-delete1',\n      'to-delete2',\n      'to-delete3',\n      'end_0',\n    ]);\n  });\n\n  it('delete reverse nodes', async () => {\n    const toDelete1JSON = toDelete1.toJSON();\n    const toDelete2JSON = toDelete2.toJSON();\n\n    flowOperationService.deleteNodes([toDelete2, 'to-delete1']);\n    expect(flowDocument.getNode('to-delete1')).toBeUndefined();\n    expect(flowDocument.getNode('to-delete2')).toBeUndefined();\n\n    expect(getRootChildrenIds()).toEqual(['start_0', 'to-delete3', 'end_0']);\n\n    await historyService.undo();\n    expect(flowDocument.getNode('to-delete1')?.toJSON()).toEqual(toDelete1JSON);\n    expect(flowDocument.getNode('to-delete2')?.toJSON()).toEqual(toDelete2JSON);\n\n    expect(getRootChildrenIds()).toEqual([\n      'start_0',\n      'to-delete1',\n      'to-delete2',\n      'to-delete3',\n      'end_0',\n    ]);\n  });\n\n  it('delete random nodes', async () => {\n    const toDelete1JSON = toDelete1.toJSON();\n    const toDelete3JSON = toDelete3.toJSON();\n\n    flowOperationService.deleteNodes([toDelete3, 'to-delete1']);\n    expect(flowDocument.getNode('to-delete1')).toBeUndefined();\n    expect(flowDocument.getNode('to-delete3')).toBeUndefined();\n\n    expect(getRootChildrenIds()).toEqual(['start_0', 'to-delete2', 'end_0']);\n\n    await historyService.undo();\n    expect(flowDocument.getNode('to-delete1')?.toJSON()).toEqual(toDelete1JSON);\n    expect(flowDocument.getNode('to-delete3')?.toJSON()).toEqual(toDelete3JSON);\n\n    expect(getRootChildrenIds()).toEqual([\n      'start_0',\n      'to-delete1',\n      'to-delete2',\n      'to-delete3',\n      'end_0',\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/move-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { getNodeChildrenIds } from '../../utils';\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service moveNode', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('move block with parent', async () => {\n    flowOperationService.moveNode('block_1', {\n      parent: '$inlineBlocks$dynamicSplit_0',\n      index: 2,\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2', 'block_1']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']);\n  });\n\n  it('move block without parent', async () => {\n    flowOperationService.moveNode('block_1', {\n      index: 2,\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2', 'block_1']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']);\n  });\n\n  it('move block with index', async () => {\n    flowOperationService.moveNode('block_0', {\n      index: 1,\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_1', 'block_0', 'block_2']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']);\n  });\n\n  it('move block without index', async () => {\n    flowOperationService.moveNode('block_1');\n    const split = flowDocument.getNode('dynamicSplit_0');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2', 'block_1']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']);\n  });\n\n  it('move block to other parent', async () => {\n    flowDocument.addFromNode('dynamicSplit_0', {\n      id: 'dynamicSplit_1',\n      type: 'dynamicSplit',\n      blocks: [{ id: 'block_3' }, { id: 'block_4' }, { id: 'block_5' }],\n    });\n\n    flowOperationService.moveNode('block_1', {\n      parent: '$inlineBlocks$dynamicSplit_1',\n      index: 1,\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    const split1 = flowDocument.getNode('dynamicSplit_1');\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2']);\n    expect(getNodeChildrenIds(split1, true)).toEqual(['block_3', 'block_1', 'block_4', 'block_5']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_1', 'block_2']);\n    expect(getNodeChildrenIds(split1, true)).toEqual(['block_3', 'block_4', 'block_5']);\n  });\n\n  it('move block to other parent which has no children', async () => {\n    flowDocument.addFromNode('dynamicSplit_0', {\n      id: 'dynamicSplit_1',\n      type: 'dynamicSplit',\n      blocks: [],\n    });\n\n    flowOperationService.moveNode('block_1', {\n      parent: '$inlineBlocks$dynamicSplit_1',\n    });\n    const split = flowDocument.getNode('dynamicSplit_0');\n    const split1 = flowDocument.getNode('dynamicSplit_1');\n\n    expect(getNodeChildrenIds(split, true)).toEqual(['block_0', 'block_2']);\n    expect(getNodeChildrenIds(split1, true)).toEqual(['block_1']);\n\n    expect(historyService.canUndo()).toBe(true);\n  });\n\n  it('move node without parent and index', async () => {\n    const root = flowDocument.getNode('root');\n    flowOperationService.moveNode('start_0');\n    expect(getNodeChildrenIds(root)).toEqual(['dynamicSplit_0', 'end_0', 'start_0']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'end_0']);\n  });\n\n  it('move node with parent and without index', async () => {\n    const root = flowDocument.getNode('root');\n    const block0 = flowDocument.getNode('block_0');\n\n    flowOperationService.addNode(\n      { id: 'test0', type: 'test' },\n      {\n        parent: block0,\n      }\n    );\n\n    flowDocument.addFromNode('start_0', {\n      type: 'test',\n      id: 'test1',\n    });\n\n    flowOperationService.moveNode('test1', { parent: 'block_0' });\n\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'end_0']);\n    expect(getNodeChildrenIds(block0)).toEqual(['$blockOrderIcon$block_0', 'test0', 'test1']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'test1', 'dynamicSplit_0', 'end_0']);\n    expect(getNodeChildrenIds(block0)).toEqual(['$blockOrderIcon$block_0', 'test0']);\n  });\n\n  it('move node with parent and index', async () => {\n    const root = flowDocument.getNode('root');\n    flowDocument.addFromNode('dynamicSplit_0', {\n      type: 'test',\n      id: 'test',\n    });\n\n    // 向后移动\n    flowOperationService.moveNode('dynamicSplit_0', {\n      index: 2,\n    });\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'test', 'dynamicSplit_0', 'end_0']);\n\n    // 向前移动\n    flowOperationService.moveNode('dynamicSplit_0', {\n      index: 1,\n    });\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'test', 'end_0']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'test', 'dynamicSplit_0', 'end_0']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', 'dynamicSplit_0', 'test', 'end_0']);\n  });\n\n  it('move node to group', async () => {\n    const group = flowOperationService.createGroup([\n      flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity,\n    ]) as FlowNodeEntity;\n    const root = flowDocument.getNode('root');\n    flowOperationService.moveNode('start_0', {\n      parent: group,\n    });\n    expect(getNodeChildrenIds(root)).toEqual([group.id, 'end_0']);\n    expect(getNodeChildrenIds(group)).toEqual(['dynamicSplit_0', 'start_0']);\n\n    await historyService.undo();\n    expect(getNodeChildrenIds(root)).toEqual(['start_0', group.id, 'end_0']);\n    expect(getNodeChildrenIds(group)).toEqual(['dynamicSplit_0']);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/set-form-value.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, it, expect } from 'vitest';\nimport { FlowNodeFormData, FormModelV2 } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { formMock } from '../../../__mocks__/form.mock';\nimport { emptyMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service changeFormData', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer({\n    nodeEngine: {},\n    nodeRegistries: [\n      {\n        type: 'formV2',\n        formMeta: formMock,\n      },\n    ],\n  });\n\n  beforeEach(() => {\n    flowDocument.fromJSON(emptyMock);\n  });\n\n  it('setFormValue', async () => {\n    const formNode = flowOperationService.addFromNode('start_0', {\n      type: 'formV2',\n      id: 'form',\n    });\n\n    // TODO 新引擎需要渲染后才会createField, 这里先手动模拟下\n    const formModel = formNode?.getData(FlowNodeFormData)?.getFormModel() as FormModelV2;\n    formModel.nativeFormModel?.createField('name');\n\n    flowOperationService.setFormValue(formNode, 'name', 'test');\n    expect(formNode.toJSON()?.data?.name).toEqual('test');\n    await historyService.undo();\n    expect(formNode.toJSON()?.data?.name).toEqual(undefined);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/transact.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, it, expect } from 'vitest';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service transact', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('startTransaction endTransaction', async () => {\n    flowOperationService.startTransaction();\n    ['block_0', 'block_1'].forEach(id => {\n      flowOperationService.deleteNode(id);\n    });\n\n    flowOperationService.addBlock('dynamicSplit_0', {\n      id: 'test-block1',\n    });\n    flowOperationService.addBlock('dynamicSplit_0', {\n      id: 'test-block2',\n    });\n    flowOperationService.endTransaction();\n\n    await historyService.undo();\n    expect(historyService.undoRedoService.canUndo()).toEqual(false);\n    expect(flowDocument.toJSON()).toEqual(baseMock);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/services/history-operation-service/ungroup.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nimport { createHistoryContainer } from '../../create-container';\nimport { baseMock } from '../../../__mocks__/flow.mock';\n\ndescribe('history-operation-service', () => {\n  const { flowDocument, flowOperationService, historyService } = createHistoryContainer();\n  beforeEach(() => {\n    flowDocument.fromJSON(baseMock);\n  });\n\n  it('ungroup', async () => {\n    const node1 = flowOperationService.addFromNode('start_0', {\n      id: 'add',\n      type: 'add',\n    });\n    const node2 = flowDocument.getNode('dynamicSplit_0') as FlowNodeEntity;\n    flowDocument.transformer.refresh();\n    const group = flowOperationService.createGroup([node1, node2]) as FlowNodeEntity;\n    const root = flowDocument.getNode('root');\n\n    flowOperationService.ungroup(group);\n    expect(root?.collapsedChildren.map(c => c.id)).toEqual([\n      'start_0',\n      'add',\n      'dynamicSplit_0',\n      'end_0',\n    ]);\n\n    await historyService.undo();\n    expect(root?.collapsedChildren.map(c => c.id)).toEqual(['start_0', group.id, 'end_0']);\n  });\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/__tests__/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocument, FlowNodeEntity } from '@flowgram.ai/editor';\n\nexport function getNodeChildrenIds(node: FlowNodeEntity | undefined, isBranch: boolean = false) {\n  if (!node) {\n    return [];\n  }\n\n  if (isBranch) {\n    return getNodeChildrenIds(\n      node.collapsedChildren.find(c => c.id === `$inlineBlocks$${node.id}`),\n    );\n  }\n\n  return node?.collapsedChildren.map(c => c.id);\n}\n\nexport function getRootChildrenIds(flowDocument: FlowDocument) {\n  return getNodeChildrenIds(flowDocument.getNode('root'));\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  --g-selection-background: #4d53e8;\n  --g-editor-background: #f2f3f5;\n  --g-playground-select: var(--g-selection-background);\n  --g-playground-hover: var(--g-selection-background);\n  --g-playground-line: var(--g-selection-background);\n  --g-playground-blur: #999;\n  --g-playground-selectBox-outline: var(--g-selection-background);\n  --g-playground-selectBox-background: rgba(141, 144, 231, 0.1);\n  --g-playground-select-hover-background: rgba(77, 83, 232, 0.1);\n  --g-playground-select-control-size: 12px;\n}\n\n.gedit-playground {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n  z-index: 10;\n  overflow: hidden;\n  user-select: none;\n  outline: none;\n  box-sizing: border-box;\n  background-color: var(--g-editor-background);\n}\n\n.gedit-playground .flow-lines-container {\n  overflow: visible;\n}\n\n.gedit-transition-ease {\n  transition: left, top 0.3s ease;\n}\n\n.gedit-playground-scroll-right {\n  position: absolute;\n  right: 2px;\n  height: 100vh;\n  width: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-bottom {\n  position: absolute;\n  bottom: 2px;\n  width: 100vw;\n  height: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-right-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-right-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-bottom-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-bottom-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-hidden {\n  opacity: 0;\n}\n\n.gedit-playground-loading {\n  position: absolute;\n  color: white;\n  left: 50%;\n  top: 50%;\n  z-index: 100;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  transition: opacity 0.8s;\n  flex-direction: column;\n  text-align: center;\n  opacity: 0.8;\n}\n\n.gedit-hidden {\n  display: none;\n}\n\n.gedit-playground-pipeline {\n  position: absolute;\n  overflow: visible;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-pipeline::before {\n  content: '';\n  position: absolute;\n  width: 1px;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-layer {\n  position: absolute;\n  overflow: visible;\n}\n\n.gedit-selector-box {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: var(--g-playground-selectBox-background);\n}\n\n.gedit-selector-box-block {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 9999;\n  display: none;\n  background-color: rgba(0, 0, 0, 0);\n}\n\n.gedit-selector-bounds-background {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: #f0f4ff;\n}\n\n.gedit-selector-bounds-foreground {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  background: rgba(255, 255, 255, 0);\n}\n\n.gedit-flow-activity-node {\n  position: absolute;\n}\n\n.gedit-grid-svg {\n  display: block;\n  position: absolute;\n  left: 20px;\n  top: 20px;\n  width: 0;\n  height: 0;\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/fixed-layout-editor\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"require\": \"./dist/index.js\",\n      \"import\": \"./dist/esm/index.js\"\n    },\n    \"./index.css\": {\n      \"import\": \"./index.css\",\n      \"require\": \"./index.css\"\n    }\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"index.css\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/editor\": \"workspace:*\",\n    \"@flowgram.ai/fixed-drag-plugin\": \"workspace:*\",\n    \"@flowgram.ai/fixed-history-plugin\": \"workspace:*\",\n    \"@flowgram.ai/fixed-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/reactive\": \"workspace:*\",\n    \"@flowgram.ai/select-box-plugin\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/components/fixed-layout-editor-provider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useCallback, forwardRef } from 'react';\n\nimport { interfaces } from 'inversify';\nimport { HistoryService } from '@flowgram.ai/history';\nimport {\n  FlowDocument,\n  createPluginContextDefault,\n  PlaygroundReactProvider,\n  ClipboardService,\n  SelectionService,\n  Playground,\n} from '@flowgram.ai/editor';\n\nimport { FlowOperationService } from '../types';\nimport {\n  createFixedLayoutPreset,\n  FixedLayoutPluginContext,\n  FixedLayoutPluginTools,\n  FixedLayoutProps,\n} from '../preset';\n\nexport const FixedLayoutEditorProvider = forwardRef<FixedLayoutPluginContext, FixedLayoutProps>(\n  function FixedLayoutEditorProvider(props: FixedLayoutProps, ref) {\n    const { parentContainer, children, ...others } = props;\n    const preset = useMemo(() => createFixedLayoutPreset(others), []);\n    const customPluginContext = useCallback(\n      (container: interfaces.Container) =>\n        ({\n          ...createPluginContextDefault(container),\n          get document(): FlowDocument {\n            return container.get<FlowDocument>(FlowDocument);\n          },\n          get operation(): FlowOperationService {\n            return container.get<FlowOperationService>(FlowOperationService);\n          },\n          get clipboard(): ClipboardService {\n            return container.get<ClipboardService>(ClipboardService);\n          },\n          get selection(): SelectionService {\n            return container.get<SelectionService>(SelectionService);\n          },\n          get history(): HistoryService {\n            return container.get<HistoryService>(HistoryService);\n          },\n          get tools(): FixedLayoutPluginTools {\n            return {\n              fitView: (easing?: boolean) => {\n                const playgroundConfig = container.get<Playground>(Playground).config;\n                const doc = container.get(FlowDocument);\n                return playgroundConfig.fitView(doc.root.bounds, easing, 30);\n              },\n            };\n          },\n        } as FixedLayoutPluginContext),\n      []\n    );\n    return (\n      <PlaygroundReactProvider\n        ref={ref}\n        plugins={preset}\n        customPluginContext={customPluginContext}\n        parentContainer={parentContainer}\n      >\n        {children}\n      </PlaygroundReactProvider>\n    );\n  }\n);\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/components/fixed-layout-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { forwardRef } from 'react';\n\nimport { EditorRenderer } from '@flowgram.ai/editor';\n\nimport { FixedLayoutPluginContext, FixedLayoutProps } from '../preset';\nimport { FixedLayoutEditorProvider } from './fixed-layout-editor-provider';\n\n/**\n * 固定布局编辑器\n * @param props\n * @constructor\n */\nexport const FixedLayoutEditor = forwardRef<FixedLayoutPluginContext, FixedLayoutProps>(\n  function FixedLayoutEditor(props: FixedLayoutProps, ref) {\n    const { children, ...otherProps } = props;\n    return (\n      <FixedLayoutEditorProvider ref={ref} {...otherProps}>\n        <EditorRenderer>{children}</EditorRenderer>\n      </FixedLayoutEditorProvider>\n    );\n  },\n);\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './fixed-layout-editor-provider';\nexport * from './fixed-layout-editor';\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/hooks/use-client-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService, PluginContext } from '@flowgram.ai/editor';\n\nimport { FixedLayoutPluginContext } from '../preset';\n\nexport function useClientContext(): FixedLayoutPluginContext {\n  return useService<FixedLayoutPluginContext>(PluginContext);\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/hooks/use-node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'react';\n\nimport { useObserve } from '@flowgram.ai/reactive';\nimport { useStartDragNode } from '@flowgram.ai/fixed-drag-plugin';\nimport {\n  usePlayground,\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeRenderData,\n  useService,\n  Disposable,\n  PlaygroundEntityContext,\n  NodeFormProps,\n  getNodeForm,\n} from '@flowgram.ai/editor';\n\nimport { FlowOperationService } from '../types';\n\nexport interface NodeRenderReturnType {\n  id: string;\n  type: string | number;\n  /**\n   * 节点 data 数据\n   */\n  data: any;\n  /**\n   * 更新节点 data 数据\n   */\n  updateData: (newData: any) => void;\n  /**\n   * BlockOrderIcon节点，一般用于分支的第一个占位节点\n   */\n  isBlockOrderIcon: boolean;\n  /**\n   * BlockIcon 节点，一般用于带有分支的占位节点\n   */\n  isBlockIcon: boolean;\n  /**\n   * 当前节点 (如果是 icon 则会返回它的父节点)\n   */\n  node: FlowNodeEntity;\n  /**\n   * 是否在拖拽中\n   */\n  dragging: boolean;\n  /**\n   * 节点是否激活\n   */\n  activated: boolean;\n  /**\n   * 节点是否展开\n   */\n  expanded: boolean;\n  /**\n   * 触发拖拽\n   * @param e\n   */\n  startDrag: (e: React.MouseEvent) => void;\n  /**\n   * 鼠标进入, 主要用于控制 activated 状态\n   */\n  onMouseEnter: (e: React.MouseEvent) => void;\n  /**\n   * 鼠标离开, 主要用于控制 activated 状态\n   */\n  onMouseLeave: (e: React.MouseEvent) => void;\n\n  /**\n   * 渲染表单，只有节点引擎开启才能使用\n   */\n  form: NodeFormProps<any> | undefined;\n\n  /**\n   * 获取节点的扩展数据\n   */\n  getExtInfo<T = any>(): T;\n\n  /**\n   * 更新节点的扩展数据\n   * @param extInfo\n   */\n  updateExtInfo<T = any>(extInfo: T, fullUpdate?: boolean): void;\n\n  /**\n   * 展开/收起节点\n   * @param expanded\n   */\n  toggleExpand(): void;\n\n  /**\n   * 删除节点\n   */\n  deleteNode: () => void;\n  /**\n   * 全局 readonly 状态\n   */\n  readonly: boolean;\n}\n\n/**\n * Provides methods related to node rendering\n * @param nodeFromProps\n */\nexport function useNodeRender(nodeFromProps?: FlowNodeEntity): NodeRenderReturnType {\n  const renderNode = nodeFromProps || useContext<FlowNodeEntity>(PlaygroundEntityContext);\n  const nodeCache = useRef<FlowNodeEntity | undefined>();\n  const renderData = renderNode.getData<FlowNodeRenderData>(FlowNodeRenderData)!;\n  const { expanded, dragging, activated } = renderData;\n  const { startDrag: startDragOrigin, dragOffset } = useStartDragNode();\n  const playground = usePlayground();\n  const isBlockOrderIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ORDER_ICON;\n  const isBlockIcon = renderNode.flowNodeType === FlowNodeBaseType.BLOCK_ICON;\n  const [formValueVersion, updateFormValueVersion] = useState<number>(0);\n  const formValueDependRef = useRef(false);\n  formValueDependRef.current = false;\n  // 在 BlockIcon 情况，如果在触发 fromJSON 时候更新表单数据导致刷新节点会存在 renderNode.parent 为 undefined，所以这里 nodeCache 进行缓存\n  const node =\n    (isBlockOrderIcon || isBlockIcon ? renderNode.parent! : renderNode) || nodeCache.current;\n  nodeCache.current = node;\n  const operationService = useService<FlowOperationService>(FlowOperationService);\n  const deleteNode = useCallback(() => {\n    operationService.deleteNode(node);\n  }, [node, operationService]);\n\n  const startDrag = useCallback(\n    (e: React.MouseEvent) => {\n      startDragOrigin(\n        e,\n        { dragStartEntity: renderNode },\n        { dragOffsetX: dragOffset.x, dragOffsetY: dragOffset.y }\n      );\n    },\n    [renderNode, startDragOrigin]\n  );\n\n  const onMouseEnter = useCallback(\n    (e: React.MouseEvent) => {\n      renderData.toggleMouseEnter();\n    },\n    [renderData]\n  );\n\n  const onMouseLeave = useCallback(\n    (e: React.MouseEvent) => {\n      renderData.toggleMouseLeave();\n    },\n    [renderData]\n  );\n\n  const toggleExpand = useCallback(() => {\n    renderData.toggleExpand();\n  }, [renderData]);\n\n  const getExtInfo = useCallback(() => node.getExtInfo() as any, [node]);\n  const updateExtInfo = useCallback(\n    (data: any, fullUpdate?: boolean) => {\n      node.updateExtInfo(data, fullUpdate);\n    },\n    [node]\n  );\n  const form = useMemo(() => getNodeForm(node), [node]);\n  // Listen FormState change\n  const formState = useObserve<any>(form?.state);\n\n  useEffect(() => {\n    let dispose: Disposable | undefined;\n    if (isBlockIcon || isBlockOrderIcon) {\n      // icon 的扩展数据是存在父节点上\n      dispose = renderNode.parent!.onExtInfoChange(() => renderNode.renderData.fireChange());\n    }\n    return () => dispose?.dispose();\n  }, [renderNode, isBlockIcon, isBlockOrderIcon]);\n\n  useEffect(() => {\n    const toDispose = form?.onFormValuesChange(() => {\n      if (formValueDependRef.current) {\n        updateFormValueVersion((v) => v + 1);\n      }\n    });\n    return () => toDispose?.dispose();\n  }, [form]);\n\n  const readonly = playground.config.readonly;\n\n  return useMemo(\n    () => ({\n      id: node.id,\n      type: node.flowNodeType,\n      get data() {\n        if (form) {\n          formValueDependRef.current = true;\n          return form.values;\n        }\n        return getExtInfo();\n      },\n      updateData(values: any) {\n        if (form) {\n          form.updateFormValues(values);\n        } else {\n          updateExtInfo(values, true);\n        }\n      },\n      node,\n      isBlockOrderIcon,\n      isBlockIcon,\n      activated,\n      readonly,\n      expanded,\n      dragging,\n      startDrag,\n      deleteNode,\n      onMouseEnter,\n      onMouseLeave,\n      getExtInfo,\n      updateExtInfo,\n      toggleExpand,\n      get form() {\n        if (!form) return undefined;\n        return {\n          ...form,\n          get values() {\n            formValueDependRef.current = true;\n            return form.values!;\n          },\n          get state() {\n            return formState;\n          },\n        };\n      },\n    }),\n    [\n      node,\n      isBlockOrderIcon,\n      isBlockIcon,\n      activated,\n      readonly,\n      expanded,\n      dragging,\n      startDrag,\n      deleteNode,\n      onMouseEnter,\n      onMouseLeave,\n      getExtInfo,\n      updateExtInfo,\n      toggleExpand,\n      form,\n      formState,\n      formValueVersion,\n    ]\n  );\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/hooks/use-playground-tools.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport { HistoryService } from '@flowgram.ai/history';\nimport {\n  FlowDocument,\n  FlowLayoutDefault,\n  FlowNodeRenderData,\n  PlaygroundInteractiveType,\n  EditorState,\n} from '@flowgram.ai/editor';\nimport { usePlayground, usePlaygroundContainer, useService } from '@flowgram.ai/editor';\n\nexport interface PlaygroundToolsPropsType {\n  /**\n   * 最大缩放比，默认 2\n   */\n  maxZoom?: number;\n  /**\n   * 最小缩放比，默认 0.25\n   */\n  minZoom?: number;\n}\n\nexport interface PlaygroundTools {\n  /**\n   * 缩放 zoom 大小比例\n   */\n  zoom: number;\n  /**\n   * 放大\n   */\n  zoomin: (easing?: boolean) => void;\n  /**\n   * 缩小\n   */\n  zoomout: (easing?: boolean) => void;\n  /**\n   * 设置缩放比例\n   * @param zoom\n   */\n  updateZoom: (newZoom: number, easing?: boolean, easingDuration?: number) => void;\n  /**\n   * 自适应视区\n   */\n  fitView: (easing?: boolean, easingDuration?: number, padding?: number) => Promise<void>;\n  /**\n   * 是否垂直布局\n   */\n  isVertical: boolean;\n  /**\n   * 切换布局, 如果不传入则直接切换\n   */\n  changeLayout: (layout?: FlowLayoutDefault) => void;\n\n  /** 交互模式：鼠标 or 触控板 */\n  interactiveType: PlaygroundInteractiveType;\n  setInteractiveType: (type: PlaygroundInteractiveType) => void;\n  /**\n   * 是否可 redo\n   */\n  canRedo: boolean;\n  /**\n   * 是否可 undo\n   */\n  canUndo: boolean;\n  /**\n   * redo\n   */\n  redo: () => void;\n  /**\n   * undo\n   */\n  undo: () => void;\n}\n\nexport function usePlaygroundTools(props?: PlaygroundToolsPropsType): PlaygroundTools {\n  const { maxZoom, minZoom } = props || {};\n  const playground = usePlayground();\n  const container = usePlaygroundContainer();\n  const historyService = container.isBound(HistoryService)\n    ? container.get(HistoryService)\n    : undefined;\n  const doc = useService<FlowDocument>(FlowDocument);\n  const [interactiveType, setInteractiveType] = useState<PlaygroundInteractiveType>('PAD');\n\n  const [zoom, setZoom] = useState(1);\n  const [currentLayout, updateLayout] = useState(doc.layout);\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n  // 获取合适视角\n  const handleFitView = useCallback(\n    (easing?: boolean, easingDuration?: number, padding?: number) => {\n      padding = padding || 30;\n      return playground.config.fitView(doc.root.bounds, easing, padding, easingDuration);\n    },\n    [doc, playground]\n  );\n  const changeLayout = useCallback(\n    (newLayout?: FlowLayoutDefault) => {\n      const allNodes = doc.getAllNodes();\n      newLayout =\n        newLayout ||\n        (doc.layout.name === FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT\n          ? FlowLayoutDefault.VERTICAL_FIXED_LAYOUT\n          : FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT);\n      allNodes.map((node) => {\n        const renderData = node.getData(FlowNodeRenderData);\n        renderData.node.classList.add('gedit-transition-ease');\n      });\n      setTimeout(() => {\n        handleFitView();\n      }, 10);\n      setTimeout(() => {\n        allNodes.map((node) => {\n          const renderData = node.getData(FlowNodeRenderData);\n          renderData.node.classList.remove('gedit-transition-ease');\n        });\n      }, 500);\n      doc.setLayout(newLayout);\n      updateLayout(doc.layout);\n    },\n    [doc, playground]\n  );\n\n  const handleZoomOut = useCallback(\n    (easing?: boolean) => {\n      playground?.config.zoomout(easing);\n    },\n    [zoom, playground]\n  );\n\n  const handleZoomIn = useCallback(\n    (easing?: boolean) => {\n      playground?.config.zoomin(easing);\n    },\n    [zoom, playground]\n  );\n\n  const handleUpdateZoom = useCallback(\n    (value: number, easing?: boolean, easingDuration?: number) => {\n      playground.config.updateZoom(value, easing, easingDuration);\n    },\n    [playground]\n  );\n\n  const handleUndo = useCallback(() => historyService?.undo(), [historyService]);\n  const handleRedo = useCallback(() => historyService?.redo(), [historyService]);\n\n  function handleUpdateInteractiveType(interactiveType: PlaygroundInteractiveType) {\n    if (interactiveType === 'MOUSE') {\n      playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n    } else if (interactiveType === 'PAD') {\n      playground.editorState.changeState(EditorState.STATE_SELECT.id);\n    }\n    setInteractiveType(interactiveType);\n  }\n\n  useEffect(() => {\n    const dispose = new DisposableCollection();\n    if (playground) {\n      dispose.push(playground.onZoom((z) => setZoom(z)));\n    }\n    if (historyService) {\n      dispose.push(\n        historyService.undoRedoService.onChange(() => {\n          setCanUndo(historyService.canUndo());\n          setCanRedo(historyService.canRedo());\n        })\n      );\n    }\n    return () => dispose.dispose();\n  }, [playground, historyService]);\n\n  useEffect(() => {\n    const config = playground.config.config;\n    playground.config.updateConfig({\n      maxZoom: maxZoom !== undefined ? maxZoom : config.maxZoom,\n      minZoom: minZoom !== undefined ? minZoom : config.minZoom,\n    });\n  }, [playground, maxZoom, minZoom]);\n\n  return {\n    zoomin: handleZoomIn,\n    zoomout: handleZoomOut,\n    fitView: handleFitView,\n    updateZoom: handleUpdateZoom,\n    zoom,\n    isVertical: currentLayout.name === FlowLayoutDefault.VERTICAL_FIXED_LAYOUT,\n    changeLayout,\n    canRedo,\n    canUndo,\n    undo: handleUndo,\n    redo: handleRedo,\n    interactiveType,\n    setInteractiveType: handleUpdateInteractiveType,\n  };\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n\n/* 核心模块导出 */\nexport * from '@flowgram.ai/editor';\n\n/**\n * 固定布局模块导出\n */\nexport * from '@flowgram.ai/fixed-layout-core';\nexport { useStartDragNode } from '@flowgram.ai/fixed-drag-plugin';\nexport * from './preset';\nexport * from './components';\nexport * from '@flowgram.ai/fixed-history-plugin';\nexport * from './hooks/use-node-render';\nexport * from './hooks/use-playground-tools';\nexport { useClientContext } from './hooks/use-client-context';\nexport * from './types';\nexport { createOperationPlugin } from './plugins/create-operation-plugin';\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/plugins/create-operation-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { FlowOperationService } from '../types';\nimport { HistoryOperationServiceImpl } from '../services/history-operation-service';\nimport { FlowOperationServiceImpl } from '../services/flow-operation-service';\nimport { FixedLayoutProps } from '../preset';\n\nexport const createOperationPlugin = definePluginCreator<FixedLayoutProps>({\n  onBind: ({ bind }, opts) => {\n    bind(FlowOperationService)\n      .to(opts?.history?.enable ? HistoryOperationServiceImpl : FlowOperationServiceImpl)\n      .inSingletonScope();\n  },\n  onDispose: ctx => {\n    const flowOperationService = ctx.container.get<FlowOperationService>(FlowOperationService);\n    flowOperationService.dispose();\n  },\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/preset/fixed-layout-preset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createSelectBoxPlugin } from '@flowgram.ai/select-box-plugin';\nimport { FixedLayoutContainerModule } from '@flowgram.ai/fixed-layout-core';\nimport { FixedHistoryService, createFixedHistoryPlugin } from '@flowgram.ai/fixed-history-plugin';\nimport { createFixedDragPlugin } from '@flowgram.ai/fixed-drag-plugin';\nimport {\n  PluginsProvider,\n  createDefaultPreset,\n  createVariablePlugin,\n  createPlaygroundPlugin,\n  createShortcutsPlugin,\n  SelectionService,\n  Command,\n  Plugin,\n  FlowDocument,\n  FlowNodeEntity,\n  FlowDocumentOptionsDefault,\n  FlowDocumentOptions,\n  FlowNodesContentLayer,\n  FlowNodesTransformLayer,\n  FlowScrollBarLayer,\n  FlowScrollLimitLayer,\n  createPlaygroundReactPreset,\n} from '@flowgram.ai/editor';\n\nimport { compose } from '../utils/compose';\nimport { FlowOperationService } from '../types';\nimport { createOperationPlugin } from '../plugins/create-operation-plugin';\nimport { fromNodeJSON, toNodeJSON } from './node-serialize';\nimport { FixedLayoutPluginContext, FixedLayoutProps } from './fixed-layout-props';\n\nexport function createFixedLayoutPreset(\n  opts: FixedLayoutProps\n): PluginsProvider<FixedLayoutPluginContext> {\n  return (ctx: FixedLayoutPluginContext) => {\n    opts = { ...FixedLayoutProps.DEFAULT, ...opts };\n    let plugins: Plugin[] = [createOperationPlugin(opts)];\n    /**\n     * 注册默认的快捷键\n     */\n    plugins.push(\n      createShortcutsPlugin({\n        registerShortcuts(registry) {\n          const selection = ctx.get<SelectionService>(SelectionService);\n          registry.addHandlers({\n            commandId: Command.Default.DELETE,\n            shortcuts: ['backspace', 'delete'],\n            isEnabled: () =>\n              selection.selection.length > 0 && !ctx.playground.config.readonlyOrDisabled,\n            execute: () => {\n              // TODO 这里要判断 CurrentEditor\n              const nodes = selection.selection.filter(\n                (entity) => entity instanceof FlowNodeEntity\n              ) as FlowNodeEntity[];\n\n              const flowOperationService = ctx.get<FlowOperationService>(FlowOperationService);\n              flowOperationService.deleteNodes(nodes);\n              selection.selection = selection.selection.filter((s) => !s.disposed);\n            },\n          });\n\n          if (opts?.history?.enable) {\n            const fixedHistoryService = ctx.get<FixedHistoryService>(FixedHistoryService);\n\n            if (!opts.history.disableShortcuts) {\n              registry.addHandlers({\n                commandId: Command.Default.UNDO,\n                shortcuts: ['meta z', 'ctrl z'],\n                isEnabled: () => true,\n                execute: () => {\n                  fixedHistoryService.undo();\n                },\n              });\n              registry.addHandlers({\n                commandId: Command.Default.REDO,\n                shortcuts: ['meta shift z', 'ctrl shift z'],\n                isEnabled: () => true,\n                execute: () => {\n                  fixedHistoryService.redo();\n                },\n              });\n            }\n          }\n        },\n      }),\n      /**\n       * 圈选逻辑实现\n       */\n      createSelectBoxPlugin({\n        canSelect: (e) =>\n          // 需满足以下条件：\n          // - 鼠标左键\n          e.button === 0 &&\n          !(ctx.get(FlowDocument) as FlowDocument).renderState.config.nodeHoveredId,\n        ...(opts.selectBox || {}),\n      }),\n      /**\n       * 固定布局拖拽逻辑实现\n       */\n      createFixedDragPlugin(opts.dragdrop || {})\n    );\n    /**\n     * 加载默认编辑器配置\n     */\n    plugins = createDefaultPreset(opts, plugins)(ctx);\n    /**\n     * 注册 变量系统\n     */\n    if (opts.variableEngine?.enable) {\n      plugins.push(\n        createVariablePlugin({\n          ...opts.variableEngine,\n          layout: 'fixed',\n        })\n      );\n    }\n    /**\n     * 注册 历史记录\n     */\n    if (opts.history?.enable) {\n      plugins.push(createFixedHistoryPlugin(opts.history));\n    }\n    /*\n     * 加载固定布局画布模块\n     * */\n    plugins.push(\n      createPlaygroundPlugin<FixedLayoutPluginContext>({\n        containerModules: [FixedLayoutContainerModule],\n        onBind(bindConfig) {\n          if (!bindConfig.isBound(FlowDocumentOptions)) {\n            bindConfig.bind(FlowDocumentOptions).toConstantValue({\n              ...FlowDocumentOptionsDefault,\n              defaultLayout: opts.defaultLayout,\n              toNodeJSON: (node) => toNodeJSON(opts, node),\n              fromNodeJSON: (node, json, isFirstCreate) =>\n                fromNodeJSON(opts, node, json, isFirstCreate),\n              allNodesDefaultExpanded: opts.allNodesDefaultExpanded,\n            } as FlowDocumentOptions);\n          }\n        },\n        onInit: (ctx) => {\n          ctx.playground.registerLayers(\n            FlowNodesContentLayer, // 节点内容渲染\n            FlowNodesTransformLayer // 节点位置偏移计算\n          );\n          // 劫持节点线条\n          if (opts.formatNodeLines) {\n            ctx.document.options.formatNodeLines = compose([\n              ctx.document.options.formatNodeLines,\n              opts.formatNodeLines,\n            ]);\n          }\n          // 劫持节点 label\n          if (opts.formatNodeLabels) {\n            ctx.document.options.formatNodeLabels = compose([\n              ctx.document.options.formatNodeLabels,\n              opts.formatNodeLabels,\n            ]);\n          }\n          if (opts.scroll?.enableScrollLimit) {\n            // 控制滚动范围\n            ctx.playground.registerLayer(FlowScrollLimitLayer);\n          }\n          if (!opts.scroll?.disableScrollBar) {\n            // 控制条\n            ctx.playground.registerLayer(FlowScrollBarLayer);\n          }\n          if (opts.scroll?.disableScroll) {\n            ctx.playground.config.scrollDisable = true;\n          }\n          if (opts.nodeRegistries) {\n            ctx.document.registerFlowNodes(...opts.nodeRegistries);\n          }\n        },\n      })\n    );\n\n    return createPlaygroundReactPreset(opts, plugins)(ctx);\n  };\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/preset/fixed-layout-props.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { SelectBoxPluginOptions } from '@flowgram.ai/select-box-plugin';\nimport { FixedHistoryPluginOptions, HistoryService } from '@flowgram.ai/fixed-history-plugin';\nimport { type FixDragPluginOptions } from '@flowgram.ai/fixed-drag-plugin';\nimport {\n  ClipboardService,\n  EditorPluginContext,\n  EditorProps,\n  FlowDocument,\n  FlowDocumentJSON,\n  FlowLayoutDefault,\n  SelectionService,\n  PluginContext,\n  FlowNodeEntity,\n  FlowTransitionLine,\n  FlowTransitionLabel,\n} from '@flowgram.ai/editor';\n\nimport { FlowOperationService } from '../types';\n\nexport const FixedLayoutPluginContext = PluginContext;\n\nexport interface FixedLayoutPluginTools {\n  fitView: (easing?: boolean) => Promise<void>;\n}\nexport interface FixedLayoutPluginContext extends EditorPluginContext {\n  document: FlowDocument;\n  /**\n   * 提供对画布节点相关操作方法, 并 支持 redo/undo\n   */\n  operation: FlowOperationService;\n  clipboard: ClipboardService;\n  selection: SelectionService;\n  history: HistoryService;\n  tools: FixedLayoutPluginTools;\n}\n\n/**\n * 固定布局配置\n */\nexport interface FixedLayoutProps extends EditorProps<FixedLayoutPluginContext, FlowDocumentJSON> {\n  /**\n   * SelectBox config\n   */\n  selectBox?: SelectBoxPluginOptions;\n  /**\n   * Drag/Drop config\n   */\n  dragdrop?: FixDragPluginOptions<FixedLayoutPluginContext>;\n  /**\n   * Redo/Undo enable\n   */\n  history?: FixedHistoryPluginOptions<FixedLayoutPluginContext> & { disableShortcuts?: boolean };\n  /**\n   * vertical or horizontal layout\n   */\n  defaultLayout?: FlowLayoutDefault | string; // 默认布局\n  /**\n   * Customize the node line\n   * 自定义节点线条\n   */\n  formatNodeLines?: (node: FlowNodeEntity, lines: FlowTransitionLine[]) => FlowTransitionLine[];\n  /**\n   * Custom node label\n   * 自定义节点 label\n   */\n  formatNodeLabels?: (node: FlowNodeEntity, lines: FlowTransitionLabel[]) => FlowTransitionLabel[];\n}\n\nexport namespace FixedLayoutProps {\n  /**\n   * 默认配置\n   */\n  export const DEFAULT: FixedLayoutProps = {\n    ...EditorProps.DEFAULT,\n    scroll: {\n      enableScrollLimit: true,\n    },\n  } as FixedLayoutProps;\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/preset/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './fixed-layout-props';\nexport * from './fixed-layout-preset';\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/preset/node-serialize.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, FlowNodeJSON, FlowNodeFormData } from '@flowgram.ai/editor';\n\nimport { FixedLayoutProps } from './fixed-layout-props';\n\nexport function fromNodeJSON(\n  opts: FixedLayoutProps,\n  node: FlowNodeEntity,\n  json: FlowNodeJSON,\n  isFirstCreate: boolean\n) {\n  json = opts.fromNodeJSON ? opts.fromNodeJSON(node, json, isFirstCreate) : json;\n  const formData = node.getData(FlowNodeFormData)!;\n  // 如果没有使用表单引擎，将 data 数据填入 extInfo\n  if (!formData) {\n    if (json.data) {\n      node.updateExtInfo(json.data, true);\n    }\n  } else {\n    const defaultFormMeta = opts.nodeEngine?.createDefaultFormMeta?.(node);\n\n    const formMeta = node.getNodeRegistry()?.formMeta || defaultFormMeta;\n\n    if (formMeta) {\n      if (isFirstCreate) {\n        formData.createForm(formMeta, json.data);\n      } else {\n        formData.updateFormValues(json.data);\n      }\n    }\n  }\n}\n\nexport function toNodeJSON(opts: FixedLayoutProps, node: FlowNodeEntity): FlowNodeJSON {\n  const nodesMap: Record<string, FlowNodeJSON> = {};\n  let startNodeJSON: FlowNodeJSON;\n  node.document.traverse((node) => {\n    const isSystemNode = node.id.startsWith('$');\n    if (isSystemNode) return;\n    const formData = node.getData(FlowNodeFormData);\n    let formJSON =\n      formData && formData.formModel && formData.formModel.initialized\n        ? formData.toJSON()\n        : undefined;\n    let nodeJSON: FlowNodeJSON = {\n      id: node.id,\n      type: node.flowNodeType,\n      data: formData ? formJSON : node.getExtInfo(),\n      blocks: [],\n    };\n    if (opts.toNodeJSON) {\n      nodeJSON = opts.toNodeJSON(node, nodeJSON);\n    }\n    if (!startNodeJSON) startNodeJSON = nodeJSON;\n    let { parent } = node;\n    if (parent && parent.id.startsWith('$')) {\n      parent = parent.originParent;\n    }\n    const parentJSON = parent ? nodesMap[parent.id] : undefined;\n    if (parentJSON) {\n      parentJSON.blocks?.push(nodeJSON);\n    }\n    nodesMap[node.id] = nodeJSON;\n  }, node);\n\n  // @ts-ignore\n  return startNodeJSON;\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/services/flow-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport {\n  FlowGroupService,\n  FlowNodeEntity,\n  FlowNodeEntityOrId,\n  FlowNodeFormData,\n  FlowOperationBaseServiceImpl,\n  FormModel,\n  FormModelV2,\n  isFormModelV2,\n} from '@flowgram.ai/editor';\n\nimport { FlowOperationService } from '../types';\n\n@injectable()\nexport class FlowOperationServiceImpl\n  extends FlowOperationBaseServiceImpl\n  implements FlowOperationService\n{\n  @inject(FlowGroupService)\n  protected groupService: FlowGroupService;\n\n  createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined {\n    return this.groupService.createGroup(nodes);\n  }\n\n  ungroup(groupNode: FlowNodeEntity): void {\n    return this.groupService.ungroup(groupNode);\n  }\n\n  setFormValue(nodeOrId: FlowNodeEntityOrId, path: string, value: unknown): void {\n    const node = this.toNodeEntity(nodeOrId);\n    const formModel = node?.getData(FlowNodeFormData)?.getFormModel<FormModel | FormModelV2>();\n\n    if (!formModel) {\n      return;\n    }\n\n    if (isFormModelV2(formModel)) {\n      (formModel as FormModelV2).setValueIn(path, value);\n    } else {\n      const formItem = (formModel as FormModel).getFormItemByPath(path);\n\n      if (!formItem) {\n        return;\n      }\n      formItem.value = value;\n    }\n  }\n\n  startTransaction(): void {}\n\n  endTransaction(): void {}\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/services/history-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, postConstruct } from 'inversify';\nimport { HistoryService } from '@flowgram.ai/history';\nimport { FixedHistoryService } from '@flowgram.ai/fixed-history-plugin';\nimport {\n  AddBlockConfig,\n  AddOrDeleteNodeValue,\n  FlowDocument,\n  FlowNodeEntity,\n  FlowNodeEntityOrId,\n  FlowNodeJSON,\n  FlowOperation,\n  MoveChildNodesOperationValue,\n  OnNodeAddEvent,\n  OperationType,\n} from '@flowgram.ai/editor';\n\nimport { FlowOperationService } from '../types';\nimport { FlowOperationServiceImpl } from './flow-operation-service';\n\n@injectable()\nexport class HistoryOperationServiceImpl\n  extends FlowOperationServiceImpl\n  implements FlowOperationService\n{\n  @inject(FixedHistoryService)\n  protected fixedHistoryService: FixedHistoryService;\n\n  @inject(HistoryService)\n  protected historyService: HistoryService;\n\n  @inject(FlowDocument)\n  protected document: FlowDocument;\n\n  @postConstruct()\n  protected init() {\n    this.toDispose.push(this.onNodeAdd(this.handleNodeAdd.bind(this)));\n  }\n\n  addFromNode(fromNode: FlowNodeEntityOrId, nodeJSON: FlowNodeJSON): FlowNodeEntity {\n    return this.fixedHistoryService.addFromNode(fromNode, nodeJSON);\n  }\n\n  addBlock(\n    target: FlowNodeEntityOrId,\n    blockJSON: FlowNodeJSON,\n    config: AddBlockConfig = {}\n  ): FlowNodeEntity {\n    const { parent, index } = config;\n    return this.fixedHistoryService.addBlock(target, blockJSON, parent, index);\n  }\n\n  deleteNode(nodeOrId: FlowNodeEntityOrId): void {\n    const node = this.toNodeEntity(nodeOrId);\n    if (!node) {\n      return;\n    }\n    this.fixedHistoryService.deleteNode(node);\n  }\n\n  deleteNodes(nodes: FlowNodeEntityOrId[]): void {\n    const nodesEntities = nodes.map((node) =>\n      typeof node === 'string' ? this.document.getNode(node) : node\n    ) as FlowNodeEntity[];\n    return this.fixedHistoryService.deleteNodes(nodesEntities);\n  }\n\n  startTransaction(): void {\n    this.historyService.startTransaction();\n  }\n\n  endTransaction(): void {\n    this.historyService.endTransaction();\n  }\n\n  apply(operation: FlowOperation) {\n    this.historyService.pushOperation(operation);\n  }\n\n  protected doMoveNode(node: FlowNodeEntity, newParent: FlowNodeEntity, index: number) {\n    const fromParentId = node.parent?.id;\n\n    if (!fromParentId) {\n      return;\n    }\n\n    const value: MoveChildNodesOperationValue = {\n      nodeIds: [this.toId(node)],\n      fromParentId: node.parent.id,\n      toParentId: this.toId(newParent),\n      fromIndex: this.getNodeIndex(node),\n      toIndex: index,\n    };\n\n    return this.historyService.pushOperation({\n      type: OperationType.moveChildNodes,\n      value,\n    });\n  }\n\n  protected handleNodeAdd({ data: addNodeData }: OnNodeAddEvent): FlowNodeEntity {\n    const { parent, index, hidden, originParent, ...nodeJSON } = addNodeData;\n    const value: AddOrDeleteNodeValue = {\n      data: nodeJSON,\n      parentId: parent?.id,\n      index,\n      hidden,\n    };\n\n    return this.historyService.pushOperation(\n      {\n        type: OperationType.addNode,\n        value: value,\n        uri: this.fixedHistoryService.config.getNodeURI(nodeJSON.id),\n      },\n      {\n        noApply: true,\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeEntity,\n  FlowNodeEntityOrId,\n  FlowOperationBaseService,\n} from '@flowgram.ai/editor';\n\nexport interface FlowOperationService extends FlowOperationBaseService {\n  /**\n   * 创建分组\n   * @param nodes 节点列表\n   */\n  createGroup(nodes: FlowNodeEntity[]): FlowNodeEntity | undefined;\n  /**\n   * 取消分组\n   * @param groupNode\n   */\n  ungroup(groupNode: FlowNodeEntity): void;\n  /**\n   * 开始事务\n   */\n  startTransaction(): void;\n  /**\n   * 结束事务\n   */\n  endTransaction(): void;\n  /**\n   * 修改表单数据\n   * @param node 节点\n   * @param path 属性路径\n   * @param value 值\n   */\n  setFormValue(node: FlowNodeEntityOrId, path: string, value: unknown): void;\n}\n\nexport const FlowOperationService = Symbol('FlowOperationService');\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/src/utils/compose.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/editor';\n\nexport type ComposeListItem<T> = (node: FlowNodeEntity, data: T[]) => T[];\n\nexport const compose =\n  <T>(fnList: (ComposeListItem<T> | undefined)[]) =>\n  (node: FlowNodeEntity, data: T[]): T[] => {\n    const list = fnList.filter(Boolean) as ComposeListItem<T>[];\n    if (!list.length) {\n      return data;\n    }\n    return list.reduce((acc: T[], fn) => fn(node, acc), data);\n  };\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [],\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/client/fixed-layout-editor/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/client/free-layout-editor/__mocks__/flow.mocks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-core';\n\nexport const mockJSON: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 381.75,\n        },\n      },\n      data: {\n        title: 'Start',\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 640,\n          y: 363.25,\n        },\n      },\n      data: {\n        title: 'Condition',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 2220,\n          y: 381.75,\n        },\n      },\n      data: {\n        title: 'End',\n      },\n    },\n    {\n      id: 'loop_H8M3U',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 1020,\n          y: 547.96875,\n        },\n      },\n      data: {\n        title: 'Loop_2',\n      },\n      blocks: [\n        {\n          id: 'llm_CBdCg',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 180,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_4',\n          },\n        },\n        {\n          id: 'llm_gZafu',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 640,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_5',\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'llm_CBdCg',\n          targetNodeID: 'llm_gZafu',\n        },\n      ],\n    },\n    {\n      id: '159623',\n      type: 'comment',\n      meta: {\n        position: {\n          x: 640,\n          y: 522.46875,\n        },\n      },\n      data: {\n        size: {\n          width: 240,\n          height: 150,\n        },\n        note: 'hi ~\\n\\nthis is a comment node\\n\\n- flowgram.ai',\n      },\n    },\n    {\n      id: 'group_V-_st',\n      type: 'group',\n      meta: {\n        position: {\n          x: 1020,\n          y: 96.25,\n        },\n      },\n      data: {\n        title: 'LLM_Group',\n        color: 'Violet',\n        parentID: 'root',\n        blockIDs: ['llm_0', 'llm_l_TcE'],\n      },\n    },\n    {\n      id: 'llm_0',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_0',\n      },\n    },\n    {\n      id: 'llm_l_TcE',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 180,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_l_TcE',\n      sourcePortID: 'if_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'loop_H8M3U',\n      sourcePortID: 'if_f0rOAt',\n    },\n    {\n      sourceNodeID: 'llm_0',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'loop_H8M3U',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_l_TcE',\n      targetNodeID: 'llm_0',\n    },\n  ],\n};\n\nexport const mockJSON2: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 0,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'Start changed',\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 235.74542284219706,\n          y: -157.7680906713165,\n        },\n      },\n      data: {\n        title: 'Condition changed',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 310.0959023539669,\n          y: 190.25,\n        },\n      },\n      data: {\n        title: 'End',\n      },\n    },\n    {\n      id: 'loop_H8M3U',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 1020,\n          y: 547.96875,\n        },\n      },\n      data: {\n        title: 'Loop_2 changed',\n      },\n      blocks: [\n        {\n          id: 'llm_CBdCg',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 180,\n              y: 0,\n            },\n          },\n          data: {\n            title: 'LLM_4 chnaged',\n          },\n        },\n        {\n          id: 'llm_gZafu',\n          type: 'llm changed',\n          meta: {\n            position: {\n              x: 9.626852659110725,\n              y: 121.49956408020925,\n            },\n          },\n          data: {\n            title: 'LLM_5',\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'llm_CBdCg',\n          targetNodeID: 'llm_gZafu',\n        },\n      ],\n    },\n    {\n      id: '159623',\n      type: 'comment',\n      meta: {\n        position: {\n          x: 300,\n          y: 486.2002234088928,\n        },\n      },\n      data: {\n        size: {\n          width: 240,\n          height: 150,\n        },\n        note: 'hi ~\\n\\nthis is a comment node changed\\n\\n- flowgram.ai',\n      },\n    },\n    {\n      id: 'group_V-_st',\n      type: 'group',\n      meta: {\n        position: {\n          x: 869.4856146469051,\n          y: 56.4254577157803,\n        },\n      },\n      data: {\n        title: 'LLM_Group changed',\n        color: 'Violet',\n        parentID: 'root',\n        blockIDs: ['llm_0', 'llm_l_TcE'],\n      },\n    },\n    {\n      id: 'llm_0',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_0 changed',\n      },\n    },\n    {\n      id: 'llm_l_TcE',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 180,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_l_TcE',\n      sourcePortID: 'if_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'loop_H8M3U',\n      sourcePortID: 'if_f0rOAt',\n    },\n    {\n      sourceNodeID: 'llm_0',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'loop_H8M3U',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_l_TcE',\n      targetNodeID: 'llm_0',\n    },\n  ],\n};\nexport const mockSimpleJSON: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: {\n        title: 'start',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: {\n        title: 'end',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n\nexport const mockSimpleJSON2: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 1, y: 1 },\n      },\n      data: {\n        title: 'start changed',\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 801, y: 1 },\n      },\n      data: {\n        title: 'end changed',\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/__snapshots__/free-layout-preset.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`free-layout-preset > custom fromNodeJSON and toNodeJSON 1`] = `\n{\n  \"edges\": [\n    {\n      \"sourceNodeID\": \"start_0\",\n      \"targetNodeID\": \"end_0\",\n    },\n  ],\n  \"nodes\": [\n    {\n      \"data\": {\n        \"isFirstCreate\": true,\n        \"runningTimes\": 1,\n        \"title\": \"start\",\n      },\n      \"id\": \"start_0\",\n      \"meta\": {\n        \"position\": {\n          \"x\": 0,\n          \"y\": 0,\n        },\n      },\n      \"type\": \"start\",\n    },\n    {\n      \"data\": {\n        \"isFirstCreate\": true,\n        \"runningTimes\": 1,\n        \"title\": \"end\",\n      },\n      \"id\": \"end_0\",\n      \"meta\": {\n        \"position\": {\n          \"x\": 800,\n          \"y\": 0,\n        },\n      },\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n\nexports[`free-layout-preset > custom fromNodeJSON and toNodeJSON 2`] = `\n{\n  \"edges\": [\n    {\n      \"sourceNodeID\": \"start_0\",\n      \"targetNodeID\": \"end_0\",\n    },\n  ],\n  \"nodes\": [\n    {\n      \"data\": {\n        \"isFirstCreate\": false,\n        \"runningTimes\": 1,\n        \"title\": \"start changed\",\n      },\n      \"id\": \"start_0\",\n      \"meta\": {\n        \"position\": {\n          \"x\": 1,\n          \"y\": 1,\n        },\n      },\n      \"type\": \"start\",\n    },\n    {\n      \"data\": {\n        \"isFirstCreate\": false,\n        \"runningTimes\": 1,\n        \"title\": \"end changed\",\n      },\n      \"id\": \"end_0\",\n      \"meta\": {\n        \"position\": {\n          \"x\": 801,\n          \"y\": 1,\n        },\n      },\n      \"type\": \"end\",\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/create-editor.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport {\n  createPlaygroundContainer,\n  Playground,\n  loadPlugins,\n  PluginContext,\n  createPluginContextDefault,\n  FlowDocument,\n} from '@flowgram.ai/editor';\n\nimport { FreeLayoutPluginContext, FreeLayoutProps, createFreeLayoutPreset } from '../src';\n\nexport function createEditor(opts: FreeLayoutProps): interfaces.Container {\n  const container = createPlaygroundContainer();\n\n  const playground = container.get(Playground);\n  const preset = createFreeLayoutPreset(opts);\n  const customPluginContext = (container: interfaces.Container) =>\n    ({\n      ...createPluginContextDefault(container),\n      get document(): FlowDocument {\n        return container.get<FlowDocument>(FlowDocument);\n      },\n    } as FreeLayoutPluginContext);\n\n  const ctx = customPluginContext(container);\n  container.rebind(PluginContext).toConstantValue(ctx);\n  loadPlugins(preset(ctx), container);\n  playground.init();\n  return container;\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/free-layout-preset.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { describe, it, expect } from 'vitest';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { FlowDocument, FlowNodeFormData } from '@flowgram.ai/editor';\n\nimport { WorkflowOperationService } from '../src/types';\nimport { mockJSON, mockJSON2, mockSimpleJSON, mockSimpleJSON2 } from '../__mocks__/flow.mocks';\nimport { createEditor } from './create-editor';\n\ndescribe('free-layout-preset', () => {\n  it('fromJSON and toJSON', () => {\n    const editor = createEditor({});\n    const document = editor.get(WorkflowDocument);\n    document.fromJSON(mockJSON);\n    expect(document.toJSON()).toEqual(mockJSON);\n    document.fromJSON(mockJSON2);\n    expect(document.toJSON()).toEqual(mockJSON2);\n  });\n  it('operation fromJSON', () => {\n    const editor = createEditor({\n      history: {\n        enable: true,\n      },\n    });\n    const operation = editor.get<WorkflowOperationService>(WorkflowOperationService);\n    const document = editor.get(WorkflowDocument);\n    operation.fromJSON(mockJSON);\n    expect(document.toJSON()).toEqual(mockJSON);\n    document.clear();\n    operation.fromJSON(mockJSON2);\n    expect(document.toJSON()).toEqual(mockJSON2);\n  });\n  it('custom fromNodeJSON and toNodeJSON', () => {\n    const container = createEditor({\n      fromNodeJSON: (node, json, isFirstCreate) => {\n        if (!json.data) {\n          json.data = {};\n        }\n        json.data = { ...json.data, isFirstCreate };\n        return json;\n      },\n      toNodeJSON(node, json) {\n        json.data!.runningTimes = (json.data!.runningTimes || 0) + 1;\n        return json;\n      },\n    });\n    container.get(FlowDocument).fromJSON(mockSimpleJSON);\n    expect(container.get(FlowDocument).toJSON()).toMatchSnapshot();\n    container.get(FlowDocument).fromJSON(mockSimpleJSON2);\n    expect(container.get(FlowDocument).toJSON()).toMatchSnapshot();\n  });\n  it('nodeEngine(v2) toJSON', async () => {\n    const container = createEditor({\n      nodeEngine: {},\n      nodeRegistries: [\n        {\n          type: 'start',\n          formMeta: {\n            render: () => React.createElement('div', { className: 'start-node' }),\n          },\n        },\n        {\n          type: 'end',\n          formMeta: {\n            render: () => React.createElement('div', { className: 'end-node' }),\n          },\n        },\n      ],\n    });\n    const flowDocument = container.get(FlowDocument);\n    flowDocument.fromJSON(mockSimpleJSON);\n    expect(flowDocument.toJSON()).toEqual(mockSimpleJSON);\n    flowDocument.fromJSON(mockSimpleJSON2);\n    expect(flowDocument.toJSON()).toEqual(mockSimpleJSON2);\n    const { formModel } = flowDocument.getNode('start_0')!.getData(FlowNodeFormData);\n    expect(formModel.getFormItemByPath('title')!.value).toEqual('start changed');\n    formModel.getFormItemByPath('title')!.value = 'start changed 2';\n    expect(formModel.toJSON()).toEqual({\n      title: 'start changed 2',\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/history.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\n\nimport { mockJSON } from '../__mocks__/flow.mocks';\nimport { createEditor } from './create-editor';\n\ndescribe('free-layout history', () => {\n  it('line-data-change', async () => {\n    const editor = createEditor({\n      history: {\n        enable: true,\n      },\n    });\n    const document = editor.get(WorkflowDocument);\n    const history = editor.get(HistoryService);\n    let historyEvent: any;\n    history.onApply((e) => {\n      historyEvent = e;\n    });\n    document.fromJSON(mockJSON);\n    const line = document.linesManager.getLine({\n      from: 'start_0',\n      to: 'condition_0',\n    });\n    line.lineData = { a: 33 };\n    expect(historyEvent.type).toEqual('changeLineData');\n    expect(historyEvent.value).toEqual({\n      id: 'start_0_-condition_0_',\n      oldValue: undefined,\n      newValue: { a: 33 },\n    });\n    await history.undo();\n    expect(historyEvent.value).toEqual({\n      id: 'start_0_-condition_0_',\n      oldValue: { a: 33 },\n      newValue: undefined,\n    });\n    expect(line.lineData).toEqual(undefined);\n    await history.redo();\n    expect(historyEvent.value).toEqual({\n      id: 'start_0_-condition_0_',\n      oldValue: undefined,\n      newValue: { a: 33 },\n    });\n    expect(line.lineData).toEqual({ a: 33 });\n    // change moreTimes\n    line.lineData = { a: 44 };\n    line.lineData = { a: 55 };\n    line.lineData = { a: 66 };\n    await history.undo();\n    expect(line.lineData).toEqual(undefined);\n    await history.redo();\n    expect(line.lineData).toEqual({ a: 66 });\n  });\n  it('enableChangeLineData to false', () => {\n    const editor = createEditor({\n      history: {\n        enable: true,\n        enableChangeLineData: false,\n      },\n    });\n    const document = editor.get(WorkflowDocument);\n    document.fromJSON(mockJSON);\n    const history = editor.get(HistoryService);\n    let historyEvent: any;\n    history.onApply((e) => {\n      historyEvent = e;\n    });\n    const line = document.linesManager.getLine({\n      from: 'start_0',\n      to: 'condition_0',\n    });\n    line.lineData = { a: 33 };\n    expect(historyEvent).toEqual(undefined);\n  });\n  it('changeNodeForm', async () => {\n    const editor = createEditor({\n      history: {\n        enable: true,\n      },\n      nodeEngine: {\n        enable: true,\n      },\n      getNodeDefaultRegistry: (type) => ({\n        type,\n        formMeta: {\n          render: () => null,\n        },\n      }),\n    });\n    let historyEvent: any;\n    const history = editor.get(HistoryService);\n    history.onApply((e) => {\n      historyEvent = e;\n    });\n    const flowDocument = editor.get(WorkflowDocument);\n    flowDocument.fromJSON(mockJSON);\n    const node = flowDocument.getNode('start_0');\n    const form = node.form;\n    form.setValueIn('title', 'title changed');\n    expect(historyEvent).toEqual({\n      type: 'changeFormValues',\n      value: {\n        id: 'start_0',\n        path: 'title',\n        value: 'title changed',\n        oldValue: 'Start',\n      },\n    });\n    await history.undo();\n    expect(form.getValueIn('title')).toEqual('Start');\n    await history.redo();\n    expect(form.getValueIn('title')).toEqual('title changed');\n  });\n});\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/use-playground-tools.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { interfaces } from 'inversify';\nimport { renderHook } from '@testing-library/react-hooks';\nimport { delay } from '@flowgram.ai/utils';\nimport {\n  WorkflowDocument,\n  WorkflowDocumentOptions,\n  InteractiveType,\n  EditorCursorState,\n  LineType,\n} from '@flowgram.ai/free-layout-core';\nimport { Playground, PositionData, FlowNodeBaseType } from '@flowgram.ai/editor';\n\nimport { PlaygroundTools, usePlaygroundTools } from '../src';\nimport { createDocument, createHookWrapper, nestJSON, createSubCanvasNodes } from './utils.mock';\n\ndescribe(\n  'use-playground-tools',\n  () => {\n    let toolsData: { current: PlaygroundTools };\n    let playground: Playground;\n    let container: interfaces.Container;\n    beforeEach(async () => {\n      container = (await createDocument()).container;\n      playground = container.get<Playground>(Playground);\n      // tools 工具要在 ready 之后才能生效\n      playground.ready();\n      const wrapper = createHookWrapper(container);\n      const { result } = renderHook(() => usePlaygroundTools(), {\n        wrapper,\n      });\n      toolsData = result;\n    });\n    it('zoomin', async () => {\n      expect(toolsData.current.zoom).toEqual(1);\n      toolsData.current.zoomin(false);\n      expect(toolsData.current.zoom).toEqual(1.1);\n    });\n    it('zoomout', async () => {\n      expect(toolsData.current.zoom).toEqual(1);\n      toolsData.current.zoomout(false);\n      expect(toolsData.current.zoom).toEqual(0.9);\n    });\n    it('fitview', async () => {\n      playground.config.updateConfig({\n        width: 1000,\n        height: 800,\n      });\n      await toolsData.current.fitView(false);\n      expect(playground.config.scrollData).toEqual({\n        scrollX: -30,\n        scrollY: -370,\n      });\n    });\n    it('autoLayout', async () => {\n      const doc = container.get(WorkflowDocument);\n      let startPos = doc.getNode('start_0')!.getData(PositionData)!;\n      const endPos = doc.getNode('end_0')!.getData(PositionData)!;\n      expect(endPos.x - startPos.x).toEqual(800);\n      const revert = await toolsData.current.autoLayout();\n      expect(endPos.x - startPos.x).toEqual(620);\n      revert(); // 回滚\n      expect(endPos.x - startPos.x).toEqual(800);\n    });\n    it('autoLayout with nested JSON', async () => {\n      const doc = container.get(WorkflowDocument);\n      doc.fromJSON(nestJSON);\n      await delay(10);\n      let startPos = doc.getNode('start_0')!.getData(PositionData)!;\n      const endPos = doc.getNode('end_0')!.getData(PositionData)!;\n      expect(endPos.x - startPos.x).toEqual(800);\n      const revert = await toolsData.current.autoLayout();\n      expect(endPos.x - startPos.x).toEqual(810);\n      revert(); // 回滚\n      expect(endPos.x - startPos.x).toEqual(800);\n    });\n    it.skip('autoLayout with verticalLine', async () => {\n      const document = container.get(WorkflowDocument);\n      // TODO\n      // const documentOptions = container.get<WorkflowDocumentOptions>(WorkflowDocumentOptions);\n      // documentOptions.isVerticalLine = (line) => {\n      //   if (\n      //     line.from?.flowNodeType === 'loop' &&\n      //     line.to?.flowNodeType === FlowNodeBaseType.SUB_CANVAS\n      //   ) {\n      //     return true;\n      //   }\n      //   return false;\n      // };\n      const { loopNode, subCanvasNode } = await createSubCanvasNodes(document);\n      const loopPos = loopNode.getData(PositionData)!;\n      const subCanvasPos = subCanvasNode.getData(PositionData)!;\n      await delay(10);\n      expect({\n        x: loopPos.x,\n        y: loopPos.y,\n      }).toEqual({\n        x: -100,\n        y: 0,\n      });\n      expect({\n        x: subCanvasPos.x,\n        y: subCanvasPos.y,\n      }).toEqual({\n        x: 100,\n        y: 0,\n      });\n      await toolsData.current.autoLayout();\n      await delay(10);\n      expect({\n        x: loopPos.x,\n        y: loopPos.y,\n      }).toEqual({\n        x: 140,\n        y: 130,\n      });\n      expect({\n        x: subCanvasPos.x,\n        y: subCanvasPos.y,\n      }).toEqual({\n        x: 0,\n        y: 290,\n      });\n    });\n    it('switchLineType', async () => {\n      expect(toolsData.current.lineType).toEqual(LineType.BEZIER);\n      toolsData.current.switchLineType();\n      expect(toolsData.current.lineType).toEqual(LineType.LINE_CHART);\n      toolsData.current.switchLineType();\n      expect(toolsData.current.lineType).toEqual(LineType.BEZIER);\n    });\n    it('setCursorState', async () => {\n      await toolsData.current.setCursorState(() => EditorCursorState.GRAB);\n      expect(toolsData.current.cursorState).toEqual('GRAB');\n\n      await toolsData.current.setCursorState(EditorCursorState.SELECT);\n      expect(toolsData.current.cursorState).toEqual('SELECT');\n    });\n    it('setInteractiveType', async () => {\n      await toolsData.current.setInteractiveType(InteractiveType.MOUSE);\n      expect(toolsData.current.interactiveType).toEqual(InteractiveType.MOUSE);\n      expect(toolsData.current.cursorState).toEqual('GRAB');\n\n      await toolsData.current.setInteractiveType(InteractiveType.PAD);\n      expect(toolsData.current.interactiveType).toEqual(InteractiveType.PAD);\n      expect(toolsData.current.cursorState).toEqual('SELECT');\n    });\n  },\n  {\n    timeout: 30000,\n  }\n);\n"
  },
  {
    "path": "packages/client/free-layout-editor/__tests__/utils.mock.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { ContainerModule, interfaces } from 'inversify';\nimport {\n  WorkflowBezierLineContribution,\n  WorkflowFoldLineContribution,\n} from '@flowgram.ai/free-lines-plugin';\nimport { AutoLayoutService } from '@flowgram.ai/free-auto-layout-plugin';\n\nimport { WorkflowAutoLayoutTool } from '../src/tools';\nimport {\n  FlowDocumentContainerModule,\n  FlowNodeBaseType,\n  FreeLayoutProps,\n  PlaygroundEntityContext,\n  PlaygroundMockTools,\n  PlaygroundReactProvider,\n  WorkflowDocument,\n  WorkflowDocumentContainerModule,\n  WorkflowJSON,\n  WorkflowLinesManager,\n} from '../src';\n\nconst MockContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {\n  bind(AutoLayoutService).toSelf().inSingletonScope();\n});\n\n/**\n * 创建基本的 Container\n */\nexport function createWorkflowContainer(opts: FreeLayoutProps): interfaces.Container {\n  const container = PlaygroundMockTools.createContainer([\n    FlowDocumentContainerModule,\n    WorkflowDocumentContainerModule,\n    MockContainerModule,\n  ]);\n  container.bind(WorkflowAutoLayoutTool).toSelf().inSingletonScope();\n  const linesManager = container.get(WorkflowLinesManager);\n  linesManager\n    .registerContribution(WorkflowBezierLineContribution)\n    .registerContribution(WorkflowFoldLineContribution);\n  return container;\n}\n\nexport const baseJSON: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n      data: undefined,\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: { x: 400, y: 0 },\n      },\n      data: undefined,\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n      },\n      data: undefined,\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'if',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'else',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n\nexport const nestJSON: WorkflowJSON = {\n  nodes: [\n    ...baseJSON.nodes,\n    {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: { x: 1200, y: 0 },\n      },\n      data: undefined,\n      blocks: [\n        {\n          id: 'break_0',\n          type: 'break',\n          meta: {\n            position: { x: 0, y: 0 },\n          },\n          data: undefined,\n        },\n        {\n          id: 'variable_0',\n          type: 'variable',\n          meta: {\n            position: { x: 400, y: 0 },\n          },\n          data: undefined,\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'break_0',\n          targetNodeID: 'variable_0',\n        },\n      ],\n    },\n  ],\n  edges: [...baseJSON.edges],\n};\n\nexport async function createDocument(params?: {\n  json: WorkflowJSON;\n  opts: FreeLayoutProps;\n}): Promise<{\n  document: WorkflowDocument;\n  container: interfaces.Container;\n}> {\n  const { json = baseJSON, opts = {} } = params || {};\n  const container = createWorkflowContainer(opts);\n  const document = container.get<WorkflowDocument>(WorkflowDocument);\n  await document.fromJSON(json);\n  return {\n    document,\n    container,\n  };\n}\n\nexport function createHookWrapper(\n  container: interfaces.Container,\n  entityId: string = 'start_0'\n): any {\n  // eslint-disable-next-line react/display-name\n  return ({ children }: any) => (\n    <PlaygroundReactProvider playgroundContainer={container}>\n      <PlaygroundEntityContext.Provider value={container.get(WorkflowDocument).getNode(entityId)}>\n        {children}\n      </PlaygroundEntityContext.Provider>\n    </PlaygroundReactProvider>\n  );\n}\n\nexport async function createSubCanvasNodes(document: WorkflowDocument) {\n  await document.fromJSON({ nodes: [], edges: [] });\n  const loopNode = await document.createWorkflowNode({\n    id: 'loop_0',\n    type: 'loop',\n    meta: {\n      position: { x: -100, y: 0 },\n      subCanvas: () => {\n        const parentNode = document.getNode('loop_0');\n        const canvasNode = document.getNode('subCanvas_0');\n        if (!parentNode || !canvasNode) {\n          return;\n        }\n        return {\n          isCanvas: false,\n          parentNode,\n          canvasNode,\n        };\n      },\n    },\n  });\n  const subCanvasNode = await document.createWorkflowNode({\n    id: 'subCanvas_0',\n    type: FlowNodeBaseType.SUB_CANVAS,\n    meta: {\n      position: { x: 100, y: 0 },\n      subCanvas: () => ({\n        isCanvas: true,\n        parentNode: document.getNode('loop_0')!,\n        canvasNode: document.getNode('subCanvas_0')!,\n      }),\n    },\n  });\n  document.linesManager.createLine({\n    from: loopNode.id,\n    to: subCanvasNode.id,\n  });\n  const variableNode = await document.createWorkflowNode(\n    {\n      id: 'variable_0',\n      type: 'variable',\n      meta: {\n        position: { x: 0, y: 0 },\n      },\n    },\n    false,\n    subCanvasNode.id\n  );\n  return {\n    loopNode,\n    subCanvasNode,\n    variableNode,\n  };\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/client/free-layout-editor/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  --g-selection-background: #4d53e8;\n  --g-editor-background: #f2f3f5;\n  --g-playground-select: var(--g-selection-background);\n  --g-playground-hover: var(--g-selection-background);\n  --g-playground-line: var(--g-selection-background);\n  --g-playground-blur: #999;\n  --g-playground-selectBox-outline: var(--g-selection-background);\n  --g-playground-selectBox-background: rgba(141, 144, 231, 0.1);\n  --g-playground-select-hover-background: rgba(77, 83, 232, 0.1);\n  --g-playground-select-control-size: 12px;\n}\n\n.gedit-playground {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n  z-index: 10;\n  overflow: hidden;\n  user-select: none;\n  outline: none;\n  box-sizing: border-box;\n  background-color: var(--g-editor-background);\n}\n\n.gedit-playground-scroll-right {\n  position: absolute;\n  right: 2px;\n  height: 100vh;\n  width: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-bottom {\n  position: absolute;\n  bottom: 2px;\n  width: 100vw;\n  height: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-right-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-right-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-bottom-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-bottom-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-hidden {\n  opacity: 0;\n}\n\n.gedit-playground * {\n  box-sizing: border-box;\n}\n\n.gedit-playground-loading {\n  position: absolute;\n  color: white;\n  left: 50%;\n  top: 50%;\n  z-index: 100;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  transition: opacity 0.8s;\n  flex-direction: column;\n  text-align: center;\n  opacity: 0.8;\n}\n\n.gedit-hidden {\n  display: none;\n}\n\n.gedit-playground-pipeline {\n  position: absolute;\n  overflow: visible;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-pipeline::before {\n  content: '';\n  position: absolute;\n  width: 1px;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-layer {\n  position: absolute;\n  overflow: visible;\n}\n\n.gedit-selector-box {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: var(--g-playground-selectBox-background);\n}\n\n.gedit-selector-box-block {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 9999;\n  display: none;\n  background-color: rgba(0, 0, 0, 0);\n}\n\n.gedit-selector-bounds-background {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: #f0f4ff;\n}\n\n.gedit-selector-bounds-foreground {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  background: rgba(255, 255, 255, 0);\n}\n\n.gedit-flow-activity-node {\n  position: absolute;\n}\n\n.gedit-grid-svg {\n  display: block;\n  position: absolute;\n  left: 20px;\n  top: 20px;\n  width: 0;\n  height: 0;\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-layout-editor\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"require\": \"./dist/index.js\",\n      \"import\": \"./dist/esm/index.js\"\n    },\n    \"./index.css\": {\n      \"import\": \"./index.css\",\n      \"require\": \"./index.css\"\n    }\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"index.css\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/editor\": \"workspace:*\",\n    \"@flowgram.ai/free-auto-layout-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-history-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-hover-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/free-lines-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-stack-plugin\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/select-box-plugin\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"clsx\": \"^1.1.1\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/components/free-layout-editor-provider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useCallback, forwardRef } from 'react';\n\nimport { interfaces } from 'inversify';\nimport { WorkflowDocument, fitView } from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\nimport {\n  PlaygroundReactProvider,\n  createPluginContextDefault,\n  ClipboardService,\n  SelectionService,\n  Playground,\n} from '@flowgram.ai/editor';\n\nimport { WorkflowOperationService } from '../types';\nimport { WorkflowAutoLayoutTool } from '../tools';\nimport {\n  createFreeLayoutPreset,\n  FreeLayoutPluginContext,\n  FreeLayoutPluginTools,\n  FreeLayoutProps,\n} from '../preset';\n\nexport const FreeLayoutEditorProvider = forwardRef<FreeLayoutPluginContext, FreeLayoutProps>(\n  function FreeLayoutEditorProvider(props: FreeLayoutProps, ref) {\n    const { children, ...others } = props;\n    const preset = useMemo(() => createFreeLayoutPreset(others), []);\n    const customPluginContext = useCallback(\n      (container: interfaces.Container) =>\n        ({\n          ...createPluginContextDefault(container),\n          get document(): WorkflowDocument {\n            return container.get<WorkflowDocument>(WorkflowDocument);\n          },\n          get clipboard(): ClipboardService {\n            return container.get<ClipboardService>(ClipboardService);\n          },\n          get selection(): SelectionService {\n            return container.get<SelectionService>(SelectionService);\n          },\n          get history(): HistoryService {\n            return container.get<HistoryService>(HistoryService);\n          },\n          get operation(): WorkflowOperationService {\n            return container.get<WorkflowOperationService>(WorkflowOperationService);\n          },\n          get tools(): FreeLayoutPluginTools {\n            const autoLayoutTool = container.get<WorkflowAutoLayoutTool>(WorkflowAutoLayoutTool);\n            return {\n              autoLayout: autoLayoutTool.handle.bind(autoLayoutTool),\n              fitView: (easing?: boolean) =>\n                fitView(\n                  container.get<WorkflowDocument>(WorkflowDocument),\n                  container.get<Playground>(Playground).config,\n                  easing\n                ),\n            };\n          },\n        } as FreeLayoutPluginContext),\n      []\n    );\n    return (\n      <PlaygroundReactProvider ref={ref} plugins={preset} customPluginContext={customPluginContext}>\n        {children}\n      </PlaygroundReactProvider>\n    );\n  }\n);\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/components/free-layout-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { forwardRef } from 'react';\n\nimport { EditorRenderer } from '@flowgram.ai/editor';\n\nimport { FreeLayoutPluginContext, FreeLayoutProps } from '../preset';\nimport { FreeLayoutEditorProvider } from './free-layout-editor-provider';\n\n/**\n * 自由布局编辑器\n * @param props\n * @constructor\n */\nexport const FreeLayoutEditor = forwardRef<FreeLayoutPluginContext, FreeLayoutProps>(\n  function FreeLayoutEditor(props: FreeLayoutProps, ref) {\n    const { children, ...otherProps } = props;\n    return (\n      <FreeLayoutEditorProvider ref={ref} {...otherProps}>\n        <EditorRenderer>{children}</EditorRenderer>\n      </FreeLayoutEditorProvider>\n    );\n  },\n);\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './free-layout-editor-provider';\nexport * from './workflow-node-renderer';\nexport * from './free-layout-editor';\nexport * from '@flowgram.ai/free-stack-plugin';\n\n// WARNING: 这里用 export * 会有问题！\nexport {\n  WorkflowPortRender,\n  type WorkflowPortRenderProps,\n} from '@flowgram.ai/free-lines-plugin';\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/components/workflow-node-renderer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport clx from 'clsx';\nimport { WorkflowPortRender } from '@flowgram.ai/free-lines-plugin';\nimport {\n  WorkflowNodeEntity,\n  useNodeRender,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-core';\n\nexport interface WorkflowNodeProps {\n  node: WorkflowNodeEntity;\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode | null;\n  portClassName?: string;\n  portStyle?: React.CSSProperties;\n  onPortClick?: (\n    port: WorkflowPortEntity,\n    e: React.MouseEvent<HTMLDivElement> | React.MouseEventHandler<HTMLDivElement>\n  ) => void;\n  /** 端口激活状态颜色 (linked/hovered) */\n  portPrimaryColor?: string;\n  /** 端口默认状态颜色 */\n  portSecondaryColor?: string;\n  /** 端口错误状态颜色 */\n  portErrorColor?: string;\n  /** 端口背景颜色 */\n  portBackgroundColor?: string;\n}\n\nexport const WorkflowNodeRenderer: React.FC<WorkflowNodeProps> = (props) => {\n  const { selected, activated, startDrag, ports, selectNode, nodeRef, onFocus, onBlur } =\n    useNodeRender();\n  const className = clx(props.className || '', {\n    activated,\n    selected,\n  });\n  return (\n    <>\n      <div\n        className={className}\n        style={props.style}\n        ref={nodeRef}\n        draggable\n        onDragStart={startDrag}\n        onClick={selectNode}\n        onFocus={onFocus}\n        onBlur={onBlur}\n        data-node-selected={String(selected)}\n      >\n        {props.children}\n      </div>\n      {ports.map((p) => (\n        <WorkflowPortRender\n          key={p.id}\n          entity={p}\n          onClick={props.onPortClick ? (e) => props.onPortClick!(p, e) : undefined}\n          className={props.portClassName}\n          style={props.portStyle}\n          primaryColor={props.portPrimaryColor}\n          secondaryColor={props.portSecondaryColor}\n          errorColor={props.portErrorColor}\n          backgroundColor={props.portBackgroundColor}\n        />\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useAutoLayout } from './use-auto-layout';\nexport { useClientContext } from './use-client-context';\nexport { PlaygroundTools, usePlaygroundTools } from './use-playground-tools';\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/hooks/use-auto-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService } from '@flowgram.ai/free-layout-core';\n\nimport { WorkflowAutoLayoutTool } from '../tools';\n\nexport const useAutoLayout = () => {\n  const autoLayoutTool = useService(WorkflowAutoLayoutTool);\n  return autoLayoutTool.handle.bind(autoLayoutTool);\n};\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/hooks/use-client-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService, PluginContext } from '@flowgram.ai/editor';\n\nimport { FreeLayoutPluginContext } from '../preset';\n\nexport function useClientContext(): FreeLayoutPluginContext {\n  return useService<FreeLayoutPluginContext>(PluginContext);\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/hooks/use-playground-tools.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable no-cond-assign */\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { type Disposable } from '@flowgram.ai/utils';\nimport {\n  EditorCursorState,\n  InteractiveType,\n  LineRenderType,\n  WorkflowDocument,\n  fitView,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/free-layout-core';\nimport { EditorState } from '@flowgram.ai/editor';\n\nimport { useAutoLayout } from './use-auto-layout';\nimport { FreeLayoutPluginTools } from '../preset';\n\ninterface SetCursorStateCallbackEvent {\n  isPressingSpaceBar: boolean;\n  cursorState: EditorCursorState;\n}\ntype SetCursorStateCallback = (e: SetCursorStateCallbackEvent) => EditorCursorState | undefined;\n\nexport interface PlaygroundTools {\n  zoomin: (easing?: boolean) => void;\n  zoomout: (easing?: boolean) => void;\n  fitView: (easing?: boolean) => void;\n  /**\n   * Auto layout tool - 自动布局工具\n   * https://flowgram.ai/guide/plugin/free-auto-layout-plugin.html\n   */\n  autoLayout: FreeLayoutPluginTools['autoLayout'];\n  /**\n   * 切换线条\n   */\n  switchLineType: (lineType?: LineRenderType) => LineRenderType;\n  lineType: LineRenderType;\n  zoom: number;\n  cursorState: EditorCursorState;\n  setCursorState: (stateId: EditorCursorState | SetCursorStateCallback) => void;\n\n  /** 交互模式：鼠标 or 触控板 */\n  interactiveType: InteractiveType;\n  setInteractiveType: (type: InteractiveType) => void;\n\n  /** 设置鼠标缩放 delta */\n  setMouseScrollDelta: (mouseScrollDelta: number | ((zoom: number) => number)) => void;\n}\n\nexport interface PlaygroundToolsPropsType {\n  /**\n   * 最大缩放比，默认 2\n   */\n  maxZoom?: number;\n  /**\n   * 最小缩放比，默认 0.25\n   */\n  minZoom?: number;\n}\n\nexport function usePlaygroundTools(props?: PlaygroundToolsPropsType): PlaygroundTools {\n  const { maxZoom, minZoom } = props || {};\n  const playground = usePlayground();\n  const doc = useService<WorkflowDocument>(WorkflowDocument);\n\n  const [zoom, setZoom] = useState(1);\n  const [lineType, setLineType] = useState(doc.linesManager.lineType);\n  const [cursorState, setCursorState] = useState<EditorCursorState>(EditorCursorState.SELECT);\n  const [interactiveType, setInteractiveType] = useState<InteractiveType>(InteractiveType.PAD);\n\n  const handleZoomOut = useCallback(\n    (easing?: boolean) => {\n      playground?.config.zoomout(easing);\n    },\n    [zoom, playground]\n  );\n\n  const handleZoomIn = useCallback(\n    (easing?: boolean) => {\n      playground?.config.zoomin(easing);\n    },\n    [zoom, playground]\n  );\n\n  // 切换线条类型\n  const handleLineTypeChange = useCallback(\n    (lineType?: LineRenderType) => {\n      const newLineType = doc.linesManager.switchLineType(lineType);\n      setLineType(newLineType);\n      return newLineType;\n    },\n    [doc]\n  );\n\n  // 获取合适视角\n  const handleFitView = useCallback(\n    (easing?: boolean) => {\n      fitView(doc, playground.config, easing);\n    },\n    [doc, playground]\n  );\n\n  const handleAutoLayout = useAutoLayout();\n\n  useEffect(() => {\n    let dispose: Disposable | null = null;\n    if (playground) {\n      dispose = playground.onZoom((z) => setZoom(z));\n    }\n    return () => {\n      if (dispose) {\n        dispose.dispose();\n      }\n    };\n  }, [playground]);\n\n  useEffect(() => {\n    const disposable = playground.editorState.onStateChange((e) => {\n      setCursorState(\n        e.state === EditorState.STATE_GRAB || e.state === EditorState.STATE_MOUSE_FRIENDLY_SELECT\n          ? EditorCursorState.GRAB\n          : EditorCursorState.SELECT\n      );\n\n      // 设置交互模式\n      setInteractiveType(\n        e.state === EditorState.STATE_MOUSE_FRIENDLY_SELECT\n          ? InteractiveType.MOUSE\n          : InteractiveType.PAD\n      );\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, [playground]);\n\n  function handleUpdateCursorState(stateId: EditorCursorState | SetCursorStateCallback) {\n    let finalStateId: EditorCursorState | undefined;\n\n    if (typeof stateId === 'function') {\n      finalStateId = stateId({\n        isPressingSpaceBar: playground.editorState.isPressingSpaceBar,\n        cursorState,\n      });\n    } else {\n      finalStateId = stateId;\n    }\n\n    if (typeof finalStateId === 'undefined') {\n      return;\n    }\n\n    if (finalStateId === EditorCursorState.GRAB) {\n      playground.editorState.changeState(EditorState.STATE_GRAB.id);\n      setCursorState(finalStateId);\n    } else if ((finalStateId = EditorCursorState.SELECT)) {\n      playground.editorState.changeState(EditorState.STATE_SELECT.id);\n      setCursorState(finalStateId);\n    }\n  }\n\n  function handleUpdateInteractiveType(interactiveType: InteractiveType) {\n    if (interactiveType === InteractiveType.MOUSE) {\n      // 鼠标优先交互模式：更新状态 & 设置小手\n      playground.editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n      setCursorState(EditorCursorState.GRAB);\n    } else if (interactiveType === InteractiveType.PAD) {\n      // 触控板优先交互模式：更新状态 & 设置箭头\n      playground.editorState.changeState(EditorState.STATE_SELECT.id);\n      setCursorState(EditorCursorState.SELECT);\n    }\n    setInteractiveType(interactiveType);\n    return;\n  }\n\n  function handleUpdateMouseScrollDelta(delta: number | ((zoom: number) => number)) {\n    playground.config.updateConfig({\n      mouseScrollDelta: delta,\n    });\n  }\n\n  useEffect(() => {\n    const config = playground.config.config;\n    playground.config.updateConfig({\n      maxZoom: maxZoom !== undefined ? maxZoom : config.maxZoom,\n      minZoom: minZoom !== undefined ? minZoom : config.minZoom,\n    });\n  }, [playground, maxZoom, minZoom]);\n\n  return {\n    zoomin: handleZoomIn,\n    zoomout: handleZoomOut,\n    fitView: handleFitView,\n    autoLayout: handleAutoLayout,\n    switchLineType: handleLineTypeChange,\n    zoom,\n    lineType,\n    cursorState,\n    setCursorState: handleUpdateCursorState,\n    interactiveType,\n    setInteractiveType: handleUpdateInteractiveType,\n    setMouseScrollDelta: handleUpdateMouseScrollDelta,\n  };\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n\n/* 核心 模块导出 */\nexport * from '@flowgram.ai/editor';\n\n/**\n * 自由布局模块导出\n */\nexport * from '@flowgram.ai/free-layout-core';\nexport * from './components';\nexport * from './preset';\nexport * from './hooks';\nexport * from './tools';\nexport * from '@flowgram.ai/free-history-plugin';\nexport { useClientContext } from './hooks/use-client-context';\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/plugins/create-operation-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/editor';\n\nimport { WorkflowOperationService } from '../types';\nimport { HistoryOperationServiceImpl } from '../services/history-operation-service';\nimport { WorkflowOperationServiceImpl } from '../services/flow-operation-service';\nimport { FreeLayoutProps } from '../preset';\n\nexport const createOperationPlugin = definePluginCreator<FreeLayoutProps>({\n  onBind: ({ bind }, opts) => {\n    bind(WorkflowOperationService)\n      .to(opts?.history?.enable ? HistoryOperationServiceImpl : WorkflowOperationServiceImpl)\n      .inSingletonScope();\n  },\n  onDispose: (ctx) => {\n    const flowOperationService =\n      ctx.container.get<WorkflowOperationService>(WorkflowOperationService);\n    flowOperationService.dispose();\n  },\n});\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/preset/free-layout-preset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createSelectBoxPlugin } from '@flowgram.ai/select-box-plugin';\nimport { createFreeStackPlugin, StackingContextManager } from '@flowgram.ai/free-stack-plugin';\nimport { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';\nimport {\n  WorkflowCommands,\n  WorkflowNodeEntity,\n  WorkflowLineEntity,\n  WorkflowDocumentContainerModule,\n  WorkflowHoverService,\n  WorkflowDocumentOptions,\n  WorkflowDocumentOptionsDefault,\n  WorkflowNodeMeta,\n} from '@flowgram.ai/free-layout-core';\nimport { createFreeHoverPlugin } from '@flowgram.ai/free-hover-plugin';\nimport { HistoryService, createFreeHistoryPlugin } from '@flowgram.ai/free-history-plugin';\nimport { createFreeAutoLayoutPlugin } from '@flowgram.ai/free-auto-layout-plugin';\nimport {\n  PluginsProvider,\n  Plugin,\n  createDefaultPreset,\n  SelectionService,\n  createShortcutsPlugin,\n  EditorProps,\n  createVariablePlugin,\n  createPlaygroundPlugin,\n  Command,\n  PluginContext,\n  FlowNodesContentLayer,\n  FlowNodesTransformLayer,\n  FlowScrollBarLayer,\n  FlowScrollLimitLayer,\n  createPlaygroundReactPreset,\n} from '@flowgram.ai/editor';\n\nimport { WorkflowAutoLayoutTool } from '../tools';\nimport { createOperationPlugin } from '../plugins/create-operation-plugin';\nimport { fromNodeJSON, toNodeJSON } from './node-serialize';\nimport { FreeLayoutProps, FreeLayoutPluginContext } from './free-layout-props';\n\nconst renderElement = (ctx: PluginContext) => {\n  const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);\n  if (stackingContextManager.node) {\n    return stackingContextManager.node;\n  }\n};\n\nexport function createFreeLayoutPreset(\n  opts: FreeLayoutProps\n): PluginsProvider<FreeLayoutPluginContext> {\n  return (ctx: FreeLayoutPluginContext) => {\n    opts = {\n      ...FreeLayoutProps.DEFAULT,\n      ...opts,\n      playground: {\n        ...opts.playground,\n        // 这里要把自由布局的 hoverService 注入进去\n        get hoverService() {\n          return ctx.get<WorkflowHoverService>(WorkflowHoverService);\n        },\n      },\n    };\n\n    let plugins: Plugin[] = [];\n    /**\n     * 注册默认的快捷键\n     */\n    plugins.push(\n      createShortcutsPlugin({\n        registerShortcuts(registry) {\n          const selection = ctx.get<SelectionService>(SelectionService);\n          registry.addHandlers({\n            commandId: WorkflowCommands.DELETE_NODES,\n            shortcuts: ['backspace', 'delete'],\n            isEnabled: () =>\n              selection.selection.length > 0 && !ctx.playground.config.readonlyOrDisabled,\n            execute: () => {\n              selection.selection.forEach((entity) => {\n                if (entity instanceof WorkflowNodeEntity) {\n                  if (!ctx.document.canRemove(entity)) {\n                    return;\n                  }\n                  const nodeMeta = entity.getNodeMeta<WorkflowNodeMeta>();\n                  const subCanvas = nodeMeta.subCanvas?.(entity);\n                  if (subCanvas?.isCanvas) {\n                    subCanvas.parentNode.dispose();\n                    return;\n                  }\n                  entity.dispose();\n                } else if (entity instanceof WorkflowLineEntity) {\n                  if (!ctx.document.linesManager.canRemove(entity)) {\n                    return;\n                  }\n                  entity.dispose();\n                }\n              });\n              selection.selection = selection.selection.filter((s) => !s.disposed);\n            },\n          });\n\n          if (opts?.history?.enable) {\n            const fixedHistoryService = ctx.get<HistoryService>(HistoryService);\n            if (!opts.history.disableShortcuts) {\n              registry.addHandlers({\n                commandId: Command.Default.UNDO,\n                shortcuts: ['meta z', 'ctrl z'],\n                isEnabled: () => true,\n                execute: () => {\n                  fixedHistoryService.undo();\n                },\n              });\n              registry.addHandlers({\n                commandId: Command.Default.REDO,\n                shortcuts: ['meta shift z', 'ctrl shift z'],\n                isEnabled: () => true,\n                execute: () => {\n                  fixedHistoryService.redo();\n                },\n              });\n            }\n          }\n        },\n      })\n    );\n    /**\n     * 加载默认编辑器配置\n     */\n    plugins = createDefaultPreset(opts as EditorProps, plugins)(ctx);\n    /**\n     * 注册变量系统\n     */\n    if (opts.variableEngine?.enable) {\n      plugins.push(\n        createVariablePlugin({\n          ...opts.variableEngine,\n          layout: 'free',\n        })\n      );\n    }\n    if (opts.history?.enable) {\n      plugins.push(createFreeHistoryPlugin(opts.history as any));\n    }\n\n    /**\n     * 注册自由布局模块\n     */\n    plugins.push(\n      createPlaygroundPlugin<FreeLayoutPluginContext>({\n        onBind: (bindConfig) => {\n          bindConfig.bind(WorkflowAutoLayoutTool).toSelf().inSingletonScope();\n          bindConfig.rebind(WorkflowDocumentOptions).toConstantValue({\n            canAddLine: opts.canAddLine?.bind(null, ctx),\n            canDeleteLine: opts.canDeleteLine?.bind(null, ctx),\n            isErrorLine: opts.isErrorLine?.bind(null, ctx),\n            isErrorPort: opts.isErrorPort?.bind(null, ctx),\n            isDisabledPort: opts.isDisabledPort?.bind(null, ctx),\n            isReverseLine: opts.isReverseLine?.bind(null, ctx),\n            isHideArrowLine: opts.isHideArrowLine?.bind(null, ctx),\n            isFlowingLine: opts.isFlowingLine?.bind(null, ctx),\n            isDisabledLine: opts.isDisabledLine?.bind(null, ctx),\n            onDragLineEnd: opts.onDragLineEnd?.bind(null, ctx),\n            setLineRenderType: opts.setLineRenderType?.bind(null, ctx),\n            setLineClassName: opts.setLineClassName?.bind(null, ctx),\n            canDeleteNode: opts.canDeleteNode?.bind(null, ctx),\n            canResetLine: opts.canResetLine?.bind(null, ctx),\n            canDropToNode: opts.canDropToNode?.bind(null, ctx),\n            cursors: opts.cursors ?? WorkflowDocumentOptionsDefault.cursors,\n            lineColor: opts.lineColor ?? WorkflowDocumentOptionsDefault.lineColor,\n            allNodesDefaultExpanded: opts.allNodesDefaultExpanded,\n            twoWayConnection: opts.twoWayConnection ?? true,\n            enableReadonlyNodeDragging: opts.enableReadonlyNodeDragging ?? false,\n            toNodeJSON: (node) => toNodeJSON(opts, node),\n            fromNodeJSON: (node, json, isFirstCreate) =>\n              fromNodeJSON(opts, node, json, isFirstCreate),\n          } as WorkflowDocumentOptions);\n        },\n        onInit: (ctx) => {\n          // 节点内容渲染\n          ctx.playground.registerLayer(FlowNodesContentLayer);\n          // 节点位置偏移计算\n          ctx.playground.registerLayer(FlowNodesTransformLayer, {\n            renderElement: () => {\n              if (typeof renderElement === 'function') {\n                return renderElement(ctx);\n              } else {\n                return renderElement;\n              }\n            },\n          });\n          if (opts.scroll?.enableScrollLimit) {\n            // 控制滚动范围\n            ctx.playground.registerLayer(FlowScrollLimitLayer);\n          }\n          if (!opts.scroll?.disableScrollBar) {\n            // 控制条\n            ctx.playground.registerLayer(FlowScrollBarLayer);\n          }\n          if (opts.scroll?.disableScroll) {\n            ctx.playground.config.scrollDisable = true;\n          }\n          if (opts.onContentChange) {\n            ctx.document.onContentChange((event) => opts.onContentChange!(ctx, event));\n          }\n        },\n        containerModules: [WorkflowDocumentContainerModule],\n      }),\n      createOperationPlugin(opts),\n      /**\n       * 渲染层级管理\n       */\n      createFreeStackPlugin({}),\n      /**\n       * 线条渲染插件\n       */\n      createFreeLinesPlugin({}),\n      /**\n       * 节点 hover 插件\n       */\n      createFreeHoverPlugin({}),\n      /**\n       * 自动布局插件\n       */\n      createFreeAutoLayoutPlugin({}),\n      /**\n       * 选择框插件\n       */\n      createSelectBoxPlugin({\n        canSelect: (e) => {\n          // 需满足以下条件：\n          // 1. 鼠标左键\n          if (e.button !== 0) {\n            return false;\n          }\n          // 2. 如存在自定义配置，以配置为准\n          const element = e.target as Element;\n          if (element) {\n            if (element.classList.contains('gedit-flow-background-layer')) {\n              return true;\n            }\n            if (element.closest('[data-flow-editor-selectable=\"true\"]')) {\n              return true;\n            }\n            if (element.closest('[data-flow-editor-selectable=\"false\"]')) {\n              return false;\n            }\n          }\n          // 3. hover 到节点或者线条不能触发框选\n          const hoverService = ctx.get<WorkflowHoverService>(WorkflowHoverService);\n          if (hoverService.isSomeHovered()) {\n            return false;\n          }\n          return true;\n        },\n        ignoreOneSelect: true, // 自由布局不选择单个节点\n        ignoreChildrenLength: true, // 自由布局忽略子节点数量\n        ...(opts.selectBox || {}),\n      })\n    );\n\n    return createPlaygroundReactPreset(opts, plugins)(ctx);\n  };\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/preset/free-layout-props.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { SelectBoxPluginOptions } from '@flowgram.ai/select-box-plugin';\nimport { HistoryService } from '@flowgram.ai/history';\nimport {\n  LineColor,\n  LineRenderType,\n  onDragLineEndParams,\n  WorkflowContentChangeEvent,\n  WorkflowDocument,\n  WorkflowJSON,\n  WorkflowLineEntity,\n  WorkflowLinePortInfo,\n  type WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodeRegistry,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-core';\nimport { FreeHistoryPluginOptions } from '@flowgram.ai/free-history-plugin';\nimport {\n  ClipboardService,\n  EditorPluginContext,\n  EditorProps,\n  SelectionService,\n  PluginContext,\n  FlowNodeType,\n} from '@flowgram.ai/editor';\n\nimport { WorkflowOperationService } from '../types';\nimport { AutoLayoutResetFn, AutoLayoutToolOptions } from '../tools';\n\nexport const FreeLayoutPluginContext = PluginContext;\n\nexport interface FreeLayoutPluginTools {\n  autoLayout: (options?: AutoLayoutToolOptions) => Promise<AutoLayoutResetFn>;\n  fitView: (easing?: boolean) => Promise<void>;\n}\n\nexport interface FreeLayoutPluginContext extends EditorPluginContext {\n  /**\n   * 文档\n   */\n  document: WorkflowDocument;\n  clipboard: ClipboardService;\n  selection: SelectionService;\n  /**\n   * 提供对画布节点相关操作方法, 并 支持 redo/undo\n   */\n  operation: WorkflowOperationService;\n  history: HistoryService;\n  tools: FreeLayoutPluginTools;\n}\n\n/**\n * Free layout configuration\n * 自由布局配置\n */\nexport interface FreeLayoutProps extends EditorProps<FreeLayoutPluginContext, WorkflowJSON> {\n  /**\n   * SelectBox config\n   * 选择框定义\n   */\n  selectBox?: SelectBoxPluginOptions;\n  /**\n   * Node registries\n   * 节点注册\n   */\n  nodeRegistries?: WorkflowNodeRegistry[];\n  /**\n   * By default, all nodes are expanded\n   * 默认是否展开所有节点\n   */\n  allNodesDefaultExpanded?: boolean;\n  /*\n   * Cursor configuration, support svg\n   * 光标图片, 支持 svg\n   */\n  cursors?: {\n    grab?: string;\n    grabbing?: string;\n  };\n  /**\n   * Line support both-way connection (default true)\n   * 线条支持双向连接\n   */\n  twoWayConnection?: boolean;\n  /**\n   * Enable dragging of read-only nodes (default false)\n   * 允许拖拽只读节点\n   */\n  enableReadonlyNodeDragging?: boolean;\n  /**\n   * History configuration\n   */\n  history?: FreeHistoryPluginOptions<FreeLayoutPluginContext> & { disableShortcuts?: boolean };\n  /**\n   * Line color configuration\n   * 线条颜色\n   */\n  lineColor?: LineColor;\n  /**\n   * Listen for content change\n   * 监听画布内容更新\n   */\n  onContentChange?: (ctx: FreeLayoutPluginContext, event: WorkflowContentChangeEvent) => void;\n  /**\n   * Determine whether the line is marked as error\n   * 判断线条是否标红\n   * @param ctx\n   * @param fromPort\n   * @param toPort\n   * @param lines\n   */\n  isErrorLine?: (\n    ctx: FreeLayoutPluginContext,\n    fromPort: WorkflowPortEntity,\n    toPort: WorkflowPortEntity | undefined,\n    lines: WorkflowLinesManager\n  ) => boolean;\n  /**\n   * Determine whether the port is marked as error\n   * 判断端口是否标红\n   * @param ctx\n   * @param port\n   */\n  isErrorPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n  /**\n   * Determine if the port is disabled\n   * 判断端口是否禁用\n   * @param ctx\n   * @param port\n   */\n  isDisabledPort?: (ctx: FreeLayoutPluginContext, port: WorkflowPortEntity) => boolean;\n  /**\n   * Determine whether the line arrow is reversed\n   * 判断线条箭头是否反转\n   * @param ctx\n   * @param line\n   */\n  isReverseLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n  /**\n   * Determine if the line hides the arrow\n   * 判断线条是否隐藏箭头\n   * @param ctx\n   * @param line\n   */\n  isHideArrowLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n  /**\n   * Determine whether the line shows a flow effect\n   * 判断线条是否展示流动效果\n   * @param ctx\n   * @param line\n   */\n  isFlowingLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n  /**\n   * Determine if a line is disabled\n   * 判断线条是否禁用\n   * @param ctx\n   * @param line\n   */\n  isDisabledLine?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => boolean;\n  /**\n   * Listen for dragging the line to end\n   * 拖拽线条结束\n   * @param ctx\n   * @param params\n   */\n  onDragLineEnd?: (ctx: FreeLayoutPluginContext, params: onDragLineEndParams) => Promise<void>;\n  /**\n   * Set the line renderer type\n   * 设置线条渲染器类型\n   * @param ctx\n   * @param line\n   */\n  setLineRenderType?: (\n    ctx: FreeLayoutPluginContext,\n    line: WorkflowLineEntity\n  ) => LineRenderType | undefined;\n  /**\n   * Set the line className\n   * 设置线条样式\n   * @param ctx\n   * @param line\n   */\n  setLineClassName?: (ctx: FreeLayoutPluginContext, line: WorkflowLineEntity) => string | undefined;\n  /**\n   * Whether to create lines or not\n   * 是否允许创建线条\n   * @param ctx\n   * @param fromPort - Source port\n   * @param toPort - Target port\n   */\n  canAddLine?: (\n    ctx: FreeLayoutPluginContext,\n    fromPort: WorkflowPortEntity,\n    toPort: WorkflowPortEntity,\n    lines: WorkflowLinesManager,\n    silent?: boolean\n  ) => boolean;\n  /**\n   * Whether to allow the deletion of nodes\n   * 是否允许删除节点\n   * @param ctx\n   * @param node - 目标节点\n   * @param silent - 如果为false，可以加 toast 弹窗\n   */\n  canDeleteNode?: (\n    ctx: FreeLayoutPluginContext,\n    node: WorkflowNodeEntity,\n    silent?: boolean\n  ) => boolean;\n  /**\n   *\n   * Whether to delete lines or not\n   * 是否允许删除线条\n   * @param ctx\n   * @param line - target line\n   * @param newLineInfo - new line info\n   * @param silent - If false, you can add a toast pop-up\n   */\n  canDeleteLine?: (\n    ctx: FreeLayoutPluginContext,\n    line: WorkflowLineEntity,\n    newLineInfo?: Required<WorkflowLinePortInfo>,\n    silent?: boolean\n  ) => boolean;\n  /**\n   * Whether to allow lines to be reset\n   * 是否允许重置线条\n   * @param ctx\n   * @param oldLine - old line\n   * @param newLineInfo - new line info\n   * @param lines - lines manager\n   */\n  canResetLine?: (\n    ctx: FreeLayoutPluginContext,\n    oldLine: WorkflowLineEntity,\n    newLineInfo: Required<WorkflowLinePortInfo>,\n    lines: WorkflowLinesManager\n  ) => boolean;\n  /**\n   * Whether to allow dragging into the sub-canvas (loop or group)\n   * 是否允许拖入子画布 (loop or group)\n   * @param params\n   */\n  canDropToNode?: (\n    ctx: FreeLayoutPluginContext,\n    params: {\n      dragNodeType?: FlowNodeType;\n      dragNode?: WorkflowNodeEntity;\n      dropNode?: WorkflowNodeEntity;\n      dropNodeType?: FlowNodeType;\n    }\n  ) => boolean;\n}\n\nexport namespace FreeLayoutProps {\n  /**\n   * 默认配置\n   */\n  export const DEFAULT: FreeLayoutProps = {\n    ...EditorProps.DEFAULT,\n  } as FreeLayoutProps;\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/preset/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './free-layout-preset';\nexport * from './free-layout-props';\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/preset/node-serialize.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowContentChangeType,\n  WorkflowDocument,\n  WorkflowDocumentOptionsDefault,\n} from '@flowgram.ai/free-layout-core';\nimport {\n  FlowNodeBaseType,\n  FlowNodeEntity,\n  FlowNodeFormData,\n  type FlowNodeJSON,\n} from '@flowgram.ai/editor';\n\nimport { FreeLayoutProps } from './free-layout-props';\n\nexport function fromNodeJSON(\n  opts: FreeLayoutProps,\n  node: FlowNodeEntity,\n  json: FlowNodeJSON,\n  isFirstCreate: boolean\n) {\n  json = opts.fromNodeJSON ? opts.fromNodeJSON(node, json, isFirstCreate) : json;\n  const formData = node.getData(FlowNodeFormData)!;\n  // 如果没有使用表单引擎，将 data 数据填入 extInfo\n  if (!formData) {\n    if (json.data) {\n      node.updateExtInfo(json.data, true);\n    }\n    // extInfo 数据更新则触发内容更新\n    if (isFirstCreate) {\n      node.onExtInfoChange(() => {\n        (node.document as WorkflowDocument).fireContentChange({\n          type: WorkflowContentChangeType.NODE_DATA_CHANGE,\n          toJSON: () => node.getExtInfo(),\n          entity: node,\n        });\n      });\n    }\n    return;\n  }\n\n  return WorkflowDocumentOptionsDefault.fromNodeJSON!(node, json, isFirstCreate);\n}\n\nexport function toNodeJSON(opts: FreeLayoutProps, node: FlowNodeEntity): FlowNodeJSON {\n  const formData = node.getData(FlowNodeFormData)!;\n  const position = node.transform.position;\n  let json: FlowNodeJSON;\n  // 不使用节点引擎则采用 extInfo\n  if (!formData) {\n    json = {\n      id: node.id,\n      type: node.flowNodeType,\n      meta: {\n        position: { x: position.x, y: position.y },\n      },\n      data: node.getExtInfo(),\n    };\n  } else {\n    json = WorkflowDocumentOptionsDefault.toNodeJSON!(node);\n  }\n  // 处理分组节点\n  if (node.flowNodeType === FlowNodeBaseType.GROUP) {\n    const parentID = node.parent?.id ?? FlowNodeBaseType.ROOT;\n    const blockIDs = node.blocks.map((block) => block.id) ?? [];\n    json.data = {\n      ...json.data,\n      parentID,\n      blockIDs,\n    };\n  }\n  return opts.toNodeJSON ? opts.toNodeJSON(node, json) : json;\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/services/flow-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { WorkflowOperationBaseServiceImpl } from '@flowgram.ai/free-layout-core';\n\nimport { WorkflowOperationService } from '../types';\n\n@injectable()\nexport class WorkflowOperationServiceImpl\n  extends WorkflowOperationBaseServiceImpl\n  implements WorkflowOperationService\n{\n  startTransaction(): void {}\n\n  endTransaction(): void {}\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/services/history-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { HistoryService } from '@flowgram.ai/history';\nimport { WorkflowJSON } from '@flowgram.ai/free-layout-core';\n\nimport { WorkflowOperationServiceImpl } from './flow-operation-service';\nimport { WorkflowOperationService } from '../types';\n\n@injectable()\nexport class HistoryOperationServiceImpl\n  extends WorkflowOperationServiceImpl\n  implements WorkflowOperationService\n{\n  @inject(HistoryService)\n  protected historyService: HistoryService;\n\n  startTransaction(): void {\n    this.historyService.startTransaction();\n  }\n\n  endTransaction(): void {\n    this.historyService.endTransaction();\n  }\n\n  fromJSON(json: WorkflowJSON): void {\n    this.startTransaction();\n    try {\n      super.fromJSON(json);\n    } catch (e) {\n      // eslint-disable-next-line no-console\n      console.log('fromJSON error', e);\n    }\n    this.endTransaction();\n  }\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/tools/auto-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject, optional } from 'inversify';\nimport { PositionSchema } from '@flowgram.ai/utils';\nimport { HistoryService } from '@flowgram.ai/history';\nimport { WorkflowDocument, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FreeOperationType } from '@flowgram.ai/free-history-plugin';\nimport { AutoLayoutService, LayoutOptions } from '@flowgram.ai/free-auto-layout-plugin';\nimport { TransformData } from '@flowgram.ai/editor';\n\nexport type AutoLayoutResetFn = () => void;\n\nexport type AutoLayoutToolOptions = LayoutOptions;\n\n/**\n * Auto layout tool - 自动布局工具\n * https://flowgram.ai/guide/plugin/free-auto-layout-plugin.html\n */\n@injectable()\nexport class WorkflowAutoLayoutTool {\n  @inject(WorkflowDocument) private document: WorkflowDocument;\n\n  @inject(AutoLayoutService) private autoLayoutService: AutoLayoutService;\n\n  @inject(HistoryService) @optional() private historyService: HistoryService;\n\n  public async handle(options: AutoLayoutToolOptions = {}): Promise<AutoLayoutResetFn> {\n    const resetFn = await this.autoLayout(options);\n    return resetFn;\n  }\n\n  private async autoLayout(options?: LayoutOptions): Promise<AutoLayoutResetFn> {\n    const nodes = this.document.getAllNodes();\n    const startPositions = nodes.map(this.getNodePosition);\n    await this.autoLayoutService.layout(options);\n    const endPositions = nodes.map(this.getNodePosition);\n    this.updateHistory({\n      nodes,\n      startPositions,\n      endPositions,\n    });\n    return this.createResetFn({\n      nodes,\n      startPositions,\n    });\n  }\n\n  private getNodePosition(node: WorkflowNodeEntity): PositionSchema {\n    const transform = node.getData(TransformData);\n    return {\n      x: transform.position.x,\n      y: transform.position.y,\n    };\n  }\n\n  private createResetFn(params: {\n    nodes: WorkflowNodeEntity[];\n    startPositions: PositionSchema[];\n  }): AutoLayoutResetFn {\n    const { nodes, startPositions } = params;\n    return () => {\n      nodes.forEach((node, index) => {\n        const transform = node.getData(TransformData);\n        const position = startPositions[index];\n        transform.update({\n          position,\n        });\n      });\n    };\n  }\n\n  private updateHistory(params: {\n    nodes: WorkflowNodeEntity[];\n    startPositions: PositionSchema[];\n    endPositions: PositionSchema[];\n  }): void {\n    const { nodes, startPositions: oldValue, endPositions: value } = params;\n    const ids = nodes.map((node) => node.id);\n    this.historyService?.pushOperation(\n      {\n        type: FreeOperationType.dragNodes,\n        value: {\n          ids,\n          value,\n          oldValue,\n        },\n      },\n      {\n        noApply: true,\n      }\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/tools/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { AutoLayoutToolOptions, AutoLayoutResetFn, WorkflowAutoLayoutTool } from './auto-layout';\n"
  },
  {
    "path": "packages/client/free-layout-editor/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON, WorkflowOperationBaseService } from '@flowgram.ai/free-layout-core';\n\nexport interface WorkflowOperationService extends WorkflowOperationBaseService {\n  /**\n   * 开始事务\n   */\n  startTransaction(): void;\n  /**\n   * 结束事务\n   */\n  endTransaction(): void;\n\n  fromJSON(json: WorkflowJSON): void;\n}\n\nexport const WorkflowOperationService = Symbol('WorkflowOperationService');\n"
  },
  {
    "path": "packages/client/free-layout-editor/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [],\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/client/free-layout-editor/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/client/free-layout-editor/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/client/playground-react/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/client/playground-react/index.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n:root {\n  --g-selection-background: #4d53e8;\n  --g-editor-background: #f2f3f5;\n  --g-playground-select: var(--g-selection-background);\n  --g-playground-hover: var(--g-selection-background);\n  --g-playground-line: var(--g-selection-background);\n  --g-playground-blur: #999;\n  --g-playground-selectBox-outline: var(--g-selection-background);\n  --g-playground-selectBox-background: rgba(141, 144, 231, 0.1);\n  --g-playground-select-hover-background: rgba(77, 83, 232, 0.1);\n  --g-playground-select-control-size: 12px;\n}\n\n.gedit-playground {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n  z-index: 10;\n  overflow: hidden;\n  user-select: none;\n  outline: none;\n  box-sizing: border-box;\n  background-color: var(--g-editor-background);\n}\n\n.gedit-playground-scroll-right {\n  position: absolute;\n  right: 2px;\n  height: 100vh;\n  width: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-bottom {\n  position: absolute;\n  bottom: 2px;\n  width: 100vw;\n  height: 7px;\n  z-index: 10;\n}\n\n.gedit-playground-scroll-right-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-right-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-bottom-block {\n  position: absolute;\n  opacity: 0.3;\n  border-radius: 3.5px;\n}\n\n.gedit-playground-scroll-bottom-block:hover {\n  opacity: 0.6;\n}\n\n.gedit-playground-scroll-hidden {\n  opacity: 0;\n}\n\n.gedit-playground * {\n  box-sizing: border-box;\n}\n\n.gedit-playground-loading {\n  position: absolute;\n  color: white;\n  left: 50%;\n  top: 50%;\n  z-index: 100;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  transition: opacity 0.8s;\n  flex-direction: column;\n  text-align: center;\n  opacity: 0.8;\n}\n\n.gedit-hidden {\n  display: none;\n}\n\n.gedit-playground-pipeline {\n  position: absolute;\n  overflow: visible;\n  width: 100%;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-pipeline::before {\n  content: '';\n  position: absolute;\n  width: 1px;\n  height: 100%;\n  left: 0;\n  top: 0;\n}\n\n.gedit-playground-layer {\n  position: absolute;\n  overflow: visible;\n}\n\n.gedit-selector-box {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: var(--g-playground-selectBox-background);\n}\n\n.gedit-selector-box-block {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 9999;\n  display: none;\n  background-color: rgba(0, 0, 0, 0);\n}\n\n.gedit-selector-bounds-background {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  outline: 1px solid var(--g-playground-selectBox-outline);\n  background-color: #f0f4ff;\n}\n\n.gedit-selector-bounds-foreground {\n  position: absolute;\n  left: 0;\n  top: 0;\n  width: 0;\n  height: 0;\n  z-index: 33;\n  background: rgba(255, 255, 255, 0);\n}\n\n.gedit-grid-svg {\n  display: block;\n  position: absolute;\n  left: 20px;\n  top: 20px;\n  width: 0;\n  height: 0;\n}\n"
  },
  {
    "path": "packages/client/playground-react/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/playground-react\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"require\": \"./dist/index.js\",\n      \"import\": \"./dist/esm/index.js\"\n    },\n    \"./index.css\": {\n      \"import\": \"./index.css\",\n      \"require\": \"./index.css\"\n    }\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"index.css\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/background-plugin\": \"workspace:*\",\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/shortcuts-plugin\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/client/playground-react/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PlaygroundReact, PlaygroundRef } from './playground-react';\nexport { PlaygroundReactContent, PlaygroundReactContentProps } from './playground-react-content';\n"
  },
  {
    "path": "packages/client/playground-react/src/components/playground-react-content.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { usePlayground } from '@flowgram.ai/core';\n\nimport {\n  PlaygroundContentLayer,\n  PlaygroundReactContentProps,\n} from '../layers/playground-content-layer';\n\nexport { PlaygroundReactContentProps };\n\nexport const PlaygroundReactContent: React.FC<PlaygroundReactContentProps> = props => {\n  const playground = usePlayground();\n  useMemo(() => {\n    const layer = playground.getLayer(PlaygroundContentLayer)!;\n    layer.updateOptions(props);\n  }, [props]);\n  return <></>;\n};\n"
  },
  {
    "path": "packages/client/playground-react/src/components/playground-react.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, forwardRef } from 'react';\n\nimport {\n  createPlaygroundPlugin,\n  PlaygroundReactProvider,\n  PlaygroundReactRenderer,\n  PluginContext,\n} from '@flowgram.ai/core';\n\nimport { PlaygroundReactProps, createPlaygroundReactPreset } from '../preset';\nimport { PlaygroundContentLayer } from '../layers/playground-content-layer';\n\nexport type PlaygroundRef = PluginContext;\n\nexport const PlaygroundReact = forwardRef<PlaygroundRef, PlaygroundReactProps>(\n  function PlaygroundReact(props, ref) {\n    const { parentContainer, children, ...others } = props;\n    const contentLoadPlugin = useMemo(\n      () =>\n        createPlaygroundPlugin({\n          onInit(ctx) {\n            ctx.playground.registerLayer(PlaygroundContentLayer);\n          },\n        }),\n      [],\n    );\n    const preset = useMemo(() => createPlaygroundReactPreset(others, [contentLoadPlugin]), []);\n    return (\n      <PlaygroundReactProvider ref={ref} plugins={preset} parentContainer={parentContainer}>\n        <PlaygroundReactRenderer>{children}</PlaygroundReactRenderer>\n      </PlaygroundReactProvider>\n    );\n  },\n);\n"
  },
  {
    "path": "packages/client/playground-react/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { usePlaygroundTools } from './use-playground-tools';\n"
  },
  {
    "path": "packages/client/playground-react/src/hooks/use-playground-tools.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport {\n  EditorState,\n  EditorStateConfigEntity,\n  PlaygroundInteractiveType,\n  useConfigEntity,\n  usePlayground,\n} from '@flowgram.ai/core';\n\nexport interface PlaygroundToolsPropsType {\n  /**\n   * 最大缩放比，默认 2\n   */\n  maxZoom?: number;\n  /**\n   * 最小缩放比，默认 0.25\n   */\n  minZoom?: number;\n}\n\nexport interface PlaygroundTools {\n  /**\n   * 缩放 zoom 大小比例\n   */\n  zoom: number;\n  /**\n   * 放大\n   */\n  zoomin: (easing?: boolean) => void;\n  /**\n   * 缩小\n   */\n  zoomout: (easing?: boolean) => void;\n  /**\n   * 设置缩放比例\n   * @param zoom\n   */\n  updateZoom: (newZoom: number, easing?: boolean, easingDuration?: number) => void;\n  /**\n   * 当前的交互模式, 鼠标友好模式 和 触摸板模式\n   */\n  interactiveType: PlaygroundInteractiveType;\n  /**\n   * 切换交互模式\n   */\n  toggleIneractiveType: () => void;\n}\n\nexport function usePlaygroundTools(props?: PlaygroundToolsPropsType): PlaygroundTools {\n  const { maxZoom, minZoom } = props || {};\n  const playground = usePlayground();\n  const editorState = useConfigEntity(EditorStateConfigEntity, true);\n\n  const [zoom, setZoom] = useState(1);\n\n  const handleZoomOut = useCallback(\n    (easing?: boolean) => {\n      playground.config.zoomout(easing);\n    },\n    [playground]\n  );\n\n  const handleZoomIn = useCallback(\n    (easing?: boolean) => {\n      playground.config.zoomin(easing);\n    },\n    [playground]\n  );\n\n  const handleUpdateZoom = useCallback(\n    (value: number, easing?: boolean, easingDuration?: number) => {\n      playground.config.updateZoom(value, easing, easingDuration);\n    },\n    [playground]\n  );\n\n  const handleToggleIneractiveType = useCallback(() => {\n    if (editorState.isMouseFriendlyMode()) {\n      editorState.changeState(EditorState.STATE_SELECT.id);\n    } else {\n      editorState.changeState(EditorState.STATE_MOUSE_FRIENDLY_SELECT.id);\n    }\n  }, [editorState]);\n\n  useEffect(() => {\n    const dispose = new DisposableCollection();\n    dispose.push(playground.onZoom((z) => setZoom(z)));\n    return () => dispose.dispose();\n  }, [playground]);\n\n  useEffect(() => {\n    const config = playground.config.config;\n    playground.config.updateConfig({\n      maxZoom: maxZoom !== undefined ? maxZoom : config.maxZoom,\n      minZoom: minZoom !== undefined ? minZoom : config.minZoom,\n    });\n  }, [playground, maxZoom, minZoom]);\n\n  return {\n    zoomin: handleZoomIn,\n    zoomout: handleZoomOut,\n    updateZoom: handleUpdateZoom,\n    zoom,\n    interactiveType: editorState.isMouseFriendlyMode() ? 'MOUSE' : 'PAD',\n    toggleIneractiveType: handleToggleIneractiveType,\n  };\n}\n"
  },
  {
    "path": "packages/client/playground-react/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n\n/* 核心 模块导出 */\nexport { useRefresh, Emitter, Event, Disposable } from '@flowgram.ai/utils';\nexport * from '@flowgram.ai/core';\n\nexport { usePlaygroundTools } from './hooks';\nexport {\n  PlaygroundReact,\n  PlaygroundReactContent,\n  PlaygroundReactContentProps,\n  PlaygroundRef,\n} from './components';\nexport { PlaygroundReactProps, createPlaygroundReactPreset } from './preset';\n"
  },
  {
    "path": "packages/client/playground-react/src/layers/playground-content-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { injectable } from 'inversify';\nimport { Layer } from '@flowgram.ai/core';\nimport { domUtils } from '@flowgram.ai/utils';\n\nexport interface PlaygroundReactContentProps {\n  className?: string;\n  style?: React.CSSProperties;\n  children?: React.ReactNode;\n}\n\n@injectable()\nexport class PlaygroundContentLayer extends Layer<PlaygroundReactContentProps> {\n  static type = 'PlaygroundContentLayer';\n\n  readonly node = domUtils.createDivWithClass(\n    'gedit-playground-layer gedit-playground-content-layer',\n  );\n\n  onZoom(scale: number): void {\n    this.node.style.transform = `scale(${scale})`;\n  }\n\n  onReady() {\n    this.node.style.left = '0px';\n    this.node.style.top = '0px';\n  }\n\n  updateOptions(opts: PlaygroundReactContentProps) {\n    this.options = opts;\n    this.render();\n  }\n\n  render(): JSX.Element {\n    return (\n      <div\n        className={this.options.className}\n        style={{ position: 'absolute', ...this.options.style }}\n      >\n        {this.options.children}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client/playground-react/src/preset/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PlaygroundReactProps } from './playground-react-props';\nexport { createPlaygroundReactPreset } from './playground-react-preset';\n"
  },
  {
    "path": "packages/client/playground-react/src/preset/playground-react-preset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createShortcutsPlugin } from '@flowgram.ai/shortcuts-plugin';\nimport {\n  PluginContext,\n  PluginsProvider,\n  Plugin,\n  createPlaygroundPlugin,\n  PlaygroundConfig,\n  PlaygroundLayer,\n} from '@flowgram.ai/core';\nimport { createBackgroundPlugin } from '@flowgram.ai/background-plugin';\n\nimport { PlaygroundReactProps } from './playground-react-props';\n\nexport function createPlaygroundReactPreset<CTX extends PluginContext = PluginContext>(\n  opts: PlaygroundReactProps<CTX>,\n  plugins: Plugin[] = []\n): PluginsProvider<CTX> {\n  return (ctx: CTX) => {\n    plugins = plugins.slice();\n    /**\n     * 注册背景 (放前面插入), 默认打开\n     */\n    if (opts.background || opts.background === undefined) {\n      const backgroundOptions = typeof opts.background === 'object' ? opts.background : {};\n      plugins.push(createBackgroundPlugin(backgroundOptions));\n    }\n    /**\n     * 注册快捷键\n     */\n    if (opts.shortcuts) {\n      plugins.push(\n        createShortcutsPlugin({\n          registerShortcuts: (registry) => opts.shortcuts!(registry, ctx),\n        })\n      );\n    }\n    /**\n     * 注册三方插件\n     */\n    if (opts.plugins) {\n      plugins.push(...opts.plugins(ctx));\n    }\n    /**\n     * 画布生命周期注册\n     */\n    plugins.push(\n      createPlaygroundPlugin<CTX>({\n        onBind: (bindConfig) => {\n          opts.onBind?.(bindConfig);\n        },\n        onInit: (ctx) => {\n          const playgroundConfig = ctx.get<PlaygroundConfig>(PlaygroundConfig);\n          if (opts.playground) {\n            if (opts.playground.autoFocus !== undefined) {\n              playgroundConfig.autoFocus = opts.playground.autoFocus;\n            }\n            if (opts.playground.autoResize !== undefined) {\n              playgroundConfig.autoResize = opts.playground.autoResize;\n            }\n          }\n          playgroundConfig.autoFocus = false;\n          ctx.playground.registerLayer(PlaygroundLayer, opts.playground);\n          if (opts.layers) {\n            ctx.playground.registerLayers(...opts.layers);\n          }\n          if (opts.onInit) opts.onInit(ctx);\n        },\n        onReady(ctx) {\n          if (opts.onReady) opts.onReady(ctx);\n        },\n        onAllLayersRendered() {\n          if (opts.onAllLayersRendered) opts.onAllLayersRendered(ctx);\n        },\n        onDispose() {\n          if (opts.onDispose) opts.onDispose(ctx);\n        },\n        containerModules: opts.containerModules || [],\n      })\n    );\n    return plugins;\n  };\n}\n"
  },
  {
    "path": "packages/client/playground-react/src/preset/playground-react-props.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { interfaces } from 'inversify';\nimport { ShortcutsRegistry } from '@flowgram.ai/shortcuts-plugin';\nimport {\n  PlaygroundLayerOptions,\n  Plugin,\n  PluginBindConfig,\n  PluginContext,\n  LayerRegistry,\n} from '@flowgram.ai/core';\nimport { BackgroundLayerOptions } from '@flowgram.ai/background-plugin';\n\n/**\n * 画布配置配置\n */\nexport interface PlaygroundReactProps<CTX extends PluginContext = PluginContext> {\n  /**\n   * 背景开关，默认打开\n   */\n  background?: BackgroundLayerOptions | boolean;\n  /**\n   * 画布相关配置\n   */\n  playground?: PlaygroundLayerOptions & {\n    autoFocus?: boolean; // 默认是否聚焦\n    autoResize?: boolean; // 是否自动 resize 画布\n  };\n  /**\n   * 注册快捷键\n   */\n  shortcuts?: (shortcutsRegistry: ShortcutsRegistry, ctx: CTX) => void;\n  /**\n   * 插件 IOC 注册，等价于 containerModule\n   */\n  onBind?: (bindConfig: PluginBindConfig) => void;\n  /**\n   * 画布模块注册阶段\n   */\n  onInit?: (ctx: CTX) => void;\n  /**\n   * 画布事件注册阶段 (一般用于注册 dom 事件)\n   */\n  onReady?: (ctx: CTX) => void;\n  /**\n   * 画布所有 layer 第一次渲染完成后触发\n   */\n  onAllLayersRendered?: (ctx: CTX) => void;\n  /**\n   * 画布销毁阶段\n   */\n  onDispose?: (ctx: CTX) => void;\n  /**\n   * 插件扩展\n   * @param ctx\n   */\n  plugins?: (ctx: CTX) => Plugin[];\n  /**\n   * 注册 layer\n   */\n  layers?: LayerRegistry[];\n  /**\n   * IOC 模块，用于更底层的插件扩展\n   */\n  containerModules?: interfaces.ContainerModule[];\n\n  children?: React.ReactNode;\n  /**\n   * 父 IOC 容器\n   */\n  parentContainer?: interfaces.Container;\n}\n"
  },
  {
    "path": "packages/client/playground-react/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [],\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/client/playground-react/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/client/playground-react/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/common/command/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-restricted-syntax': [\n      'error',\n      {\n        selector: 'ExportAllDeclaration',\n        message:\n          'Do not re-export everything from another modules, you should explicitly specify the members to be exported.',\n      },\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/common/command/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/command\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/command/src/command-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\nimport { bindContributionProvider } from '@flowgram.ai/utils';\n\nimport { CommandService } from './command-service';\nimport { CommandRegistry, CommandRegistryFactory, CommandContribution } from './command';\n\nexport const CommandContainerModule = new ContainerModule(bind => {\n  bindContributionProvider(bind, CommandContribution);\n  bind(CommandRegistry).toSelf().inSingletonScope();\n  bind(CommandService).toService(CommandRegistry);\n  bind(CommandRegistryFactory).toFactory(ctx => () => ctx.container.get(CommandRegistry));\n});\n"
  },
  {
    "path": "packages/common/command/src/command-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Disposable, type Event } from '@flowgram.ai/utils';\n\nimport { type CommandEvent } from './command';\n\nexport const CommandService = Symbol('CommandService');\n\n/**\n * command service 执行接口\n */\nexport interface CommandService extends Disposable {\n  /**\n   * command 事件执行前触发事件\n   */\n  readonly onWillExecuteCommand: Event<CommandEvent>;\n  /**\n   * command 事件执行完成后触发\n   */\n  readonly onDidExecuteCommand: Event<CommandEvent>;\n\n  /**\n   * 执行 command\n   */\n  executeCommand<T>(command: string, ...args: any[]): Promise<T | undefined>;\n}\n"
  },
  {
    "path": "packages/common/command/src/command.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, multiInject, optional } from 'inversify';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { type CommandService } from './command-service';\n\nexport interface Command {\n  /**\n   * id，唯一 key\n   */\n  id: string;\n  /**\n   * 展示用 label\n   */\n  label?: string;\n  /**\n   * 在一些明确的场景下，部分只展示简短的 label 即可\n   */\n  shortLabel?: string;\n\n  /**\n   * 展示用 command icon\n   */\n  icon?: string | React.ReactNode | ((props: any) => React.ReactNode);\n  /**\n   * 暂不使用\n   */\n  category?: string;\n}\nexport namespace Command {\n  export enum Default {\n    ZOOM_IN = 'ZOOM_IN',\n    ZOOM_OUT = 'ZOOM_OUT',\n    DELETE = 'DELETE',\n    COPY = 'COPY',\n    PASTE = 'PASTE',\n    UNDO = 'UNDO',\n    REDO = 'REDO',\n    VIEW_CLOSE_ALL_WIDGET = 'view.closeAllWidget',\n    VIEW_CLOSE_CURRENT_WIDGET = 'view.closeCurrentWidget',\n    VIEW_REOPEN_LAST_WIDGET = 'view.reopenLastWidget',\n    VIEW_CLOSE_OTHER_WIDGET = 'view.closeOtherWidget',\n    VIEW_CLOSE_BOTTOM_PANEL = 'view.closeBottomPannel',\n    VIEW_OPEN_NEXT_TAB = 'view.openNextTab',\n    VIEW_OEPN_LAST_TAB = 'view.openLastTab',\n    VIEW_FULL_SCREEN = 'view.fullScreen',\n    VIEW_SAVING_WIDGET_CLOSE_CONFIRM = 'view.savingWidgetCloseConfirm',\n    VIEW_SHORTCUTS = 'view.shortcuts',\n    VIEW_PREFERENCES = 'view.preferences',\n    VIEW_LOG = 'view.log',\n    VIEW_PROBLEMS = 'view.problems',\n  }\n\n  /**\n   * 判断是否是 command\n   */\n  export function is(arg: Command | any): arg is Command {\n    return !!arg && arg === Object(arg) && 'id' in arg;\n  }\n}\n\nexport interface CommandHandler {\n  /**\n   * handler 执行函数\n   */\n  execute(...args: any[]): any;\n\n  /**\n   * 该 handler 是否可以执行\n   */\n  isEnabled?(...args: any[]): boolean;\n\n  /**\n   * 预留 contextMenu 用，该 handler 是否可见\n   */\n  isVisible?(...args: any[]): boolean;\n\n  /**\n   * 预留 contextMenu 用，该 handler 是否可以触发\n   */\n  isToggled?(...args: any[]): boolean;\n}\n\nexport interface CommandEvent {\n  /**\n   * commandId\n   */\n  commandId: string;\n  /**\n   * 参数\n   */\n  args: any[];\n}\nexport const CommandContribution = Symbol('CommandContribution');\n\nexport interface CommandContribution {\n  /**\n   * 注册 command\n   */\n  registerCommands(commands: CommandService): void;\n}\n\n/**\n * 当前正在运行的 command\n */\ninterface CommandExecuting {\n  /**\n   * commandid\n   */\n  id: string;\n  /**\n   * 参数\n   */\n  args: any[];\n  /**\n   * 正在进行的 promise\n   */\n  promise?: Promise<any>;\n}\n\nnamespace CommandExecuting {\n  /**\n   * 获取正在运行的 command 单个实例\n   */\n  export function findSimple(\n    arrs: Set<CommandExecuting>,\n    newCmd: CommandExecuting,\n  ): CommandExecuting | undefined {\n    for (const item of arrs.values()) {\n      if (\n        item.id === newCmd.id &&\n        item.args.length === newCmd.args.length &&\n        item.args.every((arg, index) => (newCmd as any)[index] === arg)\n      ) {\n        return item;\n      }\n    }\n  }\n}\n\nexport const CommandRegistryFactory = 'CommandRegistryFactory';\n\n@injectable()\nexport class CommandRegistry implements CommandService {\n  protected readonly _handlers: { [id: string]: CommandHandler[] } = {};\n\n  protected readonly _commands: { [id: string]: Command } = {};\n\n  protected readonly _commandExecutings = new Set<CommandExecuting>();\n\n  protected readonly toUnregisterCommands = new Map<string, Disposable>();\n\n  protected readonly onDidExecuteCommandEmitter = new Emitter<CommandEvent>();\n\n  readonly onDidExecuteCommand = this.onDidExecuteCommandEmitter.event;\n\n  protected readonly onWillExecuteCommandEmitter = new Emitter<CommandEvent>();\n\n  readonly onWillExecuteCommand = this.onWillExecuteCommandEmitter.event;\n\n  @multiInject(CommandContribution)\n  @optional()\n  protected readonly contributions: CommandContribution[];\n\n  init() {\n    for (const contrib of this.contributions) {\n      contrib.registerCommands(this);\n    }\n  }\n\n  /**\n   * 当前所有 command\n   */\n  get commands(): Command[] {\n    const commands: Command[] = [];\n    for (const id of this.commandIds) {\n      const cmd = this.getCommand(id);\n      if (cmd) {\n        commands.push(cmd);\n      }\n    }\n    return commands;\n  }\n\n  /**\n   * 当前所有 commandid\n   */\n  get commandIds(): string[] {\n    return Object.keys(this._commands);\n  }\n\n  /**\n   * registerCommand\n   */\n  registerCommand(id: string, handler?: CommandHandler): Disposable;\n\n  registerCommand(command: Command, handler?: CommandHandler): Disposable;\n\n  registerCommand(commandOrId: string | Command, handler?: CommandHandler): Disposable {\n    const command: Command = typeof commandOrId === 'string' ? { id: commandOrId } : commandOrId;\n\n    if (this._commands[command.id]) {\n      console.warn(`A command ${command.id} is already registered.`);\n      return Disposable.NULL;\n    }\n    const toDispose = new DisposableCollection(this.doRegisterCommand(command));\n    if (handler) {\n      toDispose.push(this.registerHandler(command.id, handler));\n    }\n    this.toUnregisterCommands.set(command.id, toDispose);\n    toDispose.push(Disposable.create(() => this.toUnregisterCommands.delete(command.id)));\n    return toDispose;\n  }\n\n  /**\n   * unregisterCommand\n   */\n  unregisterCommand(command: Command): void;\n\n  unregisterCommand(id: string): void;\n\n  unregisterCommand(commandOrId: Command | string): void {\n    const id = Command.is(commandOrId) ? commandOrId.id : commandOrId;\n    const toUnregister = this.toUnregisterCommands.get(id);\n    if (toUnregister) {\n      toUnregister.dispose();\n    }\n  }\n\n  /**\n   * 注册 handler\n   */\n  registerHandler(commandId: string, handler: CommandHandler): Disposable {\n    let handlers = this._handlers[commandId];\n    if (!handlers) {\n      this._handlers[commandId] = handlers = [];\n    }\n    handlers.unshift(handler);\n    return {\n      dispose: () => {\n        const idx = handlers.indexOf(handler);\n        if (idx >= 0) {\n          handlers.splice(idx, 1);\n        }\n      },\n    };\n  }\n\n  /**\n   * 预留 contextMenu 用，该 handler 是否可见\n   */\n  isVisible(command: string, ...args: any[]): boolean {\n    return typeof this.getVisibleHandler(command, ...args) !== 'undefined';\n  }\n\n  /**\n   * command 是否可用\n   */\n  isEnabled(command: string, ...args: any[]): boolean {\n    return typeof this.getActiveHandler(command, ...args) !== 'undefined';\n  }\n\n  /**\n   * 预留 contextMenu 用，该 handler 是否可以触发\n   */\n  isToggled(command: string, ...args: any[]): boolean {\n    return typeof this.getToggledHandler(command, ...args) !== 'undefined';\n  }\n\n  /**\n   * 执行 command，会先判断是否可以执行，不会重复执行\n   */\n  async executeCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {\n    const handler = this.getActiveHandler(commandId, ...args);\n    const execInfo: CommandExecuting = { id: commandId, args };\n    const simpleExecInfo = CommandExecuting.findSimple(this._commandExecutings, execInfo);\n    if (simpleExecInfo) {\n      return execInfo.promise;\n    }\n    if (handler) {\n      try {\n        this._commandExecutings.add(execInfo);\n        this.onWillExecuteCommandEmitter.fire({ commandId, args });\n        const promise = handler.execute(...args);\n        execInfo.promise = promise;\n        const result = await promise;\n        this.onDidExecuteCommandEmitter.fire({ commandId, args });\n        return result;\n      } finally {\n        this._commandExecutings.delete(execInfo);\n      }\n    }\n  }\n\n  getVisibleHandler(commandId: string, ...args: any[]): CommandHandler | undefined {\n    const handlers = this._handlers[commandId];\n    if (handlers) {\n      for (const handler of handlers) {\n        try {\n          if (!handler.isVisible || handler.isVisible(...args)) {\n            return handler;\n          }\n        } catch (error) {\n          console.error(error);\n        }\n      }\n    }\n    return undefined;\n  }\n\n  getActiveHandler(commandId: string, ...args: any[]): CommandHandler | undefined {\n    const handlers = this._handlers[commandId];\n    if (handlers) {\n      for (const handler of handlers) {\n        try {\n          if (!handler.isEnabled || handler.isEnabled(...args)) {\n            return handler;\n          }\n        } catch (error) {\n          console.error(error);\n        }\n      }\n    }\n    return undefined;\n  }\n\n  /**\n   * 获取 command 对应的所有 handler\n   */\n  getAllHandlers(commandId: string): CommandHandler[] {\n    const handlers = this._handlers[commandId];\n    return handlers ? handlers.slice() : [];\n  }\n\n  getToggledHandler(commandId: string, ...args: any[]): CommandHandler | undefined {\n    const handlers = this._handlers[commandId];\n    if (handlers) {\n      for (const handler of handlers) {\n        try {\n          if (handler.isToggled && handler.isToggled(...args)) {\n            return handler;\n          }\n        } catch (error) {\n          console.error(error);\n        }\n      }\n    }\n    return undefined;\n  }\n\n  /**\n   * 获取 command\n   */\n  getCommand(id: string): Command | undefined {\n    return this._commands[id];\n  }\n\n  protected doRegisterCommand(command: Command): Disposable {\n    this._commands[command.id] = command;\n    return {\n      dispose: () => {\n        delete this._commands[command.id];\n      },\n    };\n  }\n\n  /**\n   * 更新 command\n   */\n  public updateCommand(id: string, command: Partial<Omit<Command, 'id'>>) {\n    if (this._commands[id]) {\n      this._commands[id] = {\n        ...this._commands[id],\n        ...command,\n      };\n    }\n  }\n\n  dispose() {\n    this.onWillExecuteCommandEmitter.dispose();\n    this.onDidExecuteCommandEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/common/command/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  CommandContribution,\n  type CommandHandler,\n  CommandRegistry,\n  Command,\n  CommandRegistryFactory,\n} from './command';\nexport { CommandService } from './command-service';\nexport { CommandContainerModule } from './command-container-module';\n"
  },
  {
    "path": "packages/common/command/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n}\n"
  },
  {
    "path": "packages/common/command/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/common/command/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/common/history/__mocks__/editor.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { decorate, inject, injectable, postConstruct } from 'inversify';\n\nimport {\n  HistoryService,\n  Operation,\n  OperationContribution,\n  OperationMeta,\n  OperationRegistry,\n  StackOperation,\n} from '../src';\n\ninterface Node {\n  id: number;\n  data: any;\n  children: Node[];\n}\n\ninterface NodeOperationValue {\n  parentId: number;\n  index: number;\n  node: Node;\n}\n\ninterface TextOperationValue {\n  index: number;\n  text: string;\n}\n\nenum OperationType {\n  insertNode = 'insert-node',\n  deleteNode = 'delete-node',\n  insertText = 'insert-text',\n  deleteText = 'delete-text',\n  selection = 'selection',\n  mergeByTime = 'mergeByTime',\n}\n\nexport function defaultRoot() {\n  return {\n    id: 1,\n    data: 'test',\n    children: [],\n  }\n}\n\nexport const MOCK_URI = 'file:///mock URI'\n\n@injectable()\nexport class Editor {\n  @inject(HistoryService)\n  private historyService: HistoryService;\n\n  @postConstruct()\n  init() {\n    this.historyService.context.source = this;\n  }\n\n  public node: Node = defaultRoot();\n\n  public text: string = '';\n\n  reset() {\n    this.node = defaultRoot()\n  }\n\n  async undo() {\n    await this.historyService.undo()\n  }\n\n  async redo() {\n    await this.historyService.redo()\n  }\n\n  canRedo() {\n    return this.historyService.canRedo()\n  }\n\n  canUndo() {\n    return this.historyService.canUndo()\n  }\n\n  getHistoryOperations() {\n    return this.historyService.getHistoryOperations()\n  }\n\n  handleSelection() {\n    this.historyService.pushOperation({ type: OperationType.selection, value: {} })\n  }\n\n  handleInsert(value: NodeOperationValue) {\n    this.historyService.pushOperation({ type: OperationType.insertNode, value })\n  }\n\n  handleInsertText(value: TextOperationValue, uri?: string, noApply?: boolean) {\n    this.historyService.pushOperation({ type: OperationType.insertText, value, uri }, { noApply })\n  }\n\n  handleDeleteText(value: TextOperationValue) {\n    this.historyService.pushOperation({ type: OperationType.deleteText, value }, { noApply: true})\n  }\n\n  handleMultiOperation() {\n    this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 1} })\n    this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 2} })\n    this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 3} })\n    this.historyService.pushOperation({ type: OperationType.mergeByTime, value: { test: 4} })\n  }\n\n  insertNode(value: NodeOperationValue) {\n    const { parentId, index, node } = value;\n    const parent = this.findNodeById(parentId);\n\n    if (!parent) {\n      return\n    }\n    parent.children.splice(index, 0, node);\n  }\n\n  deleteNode(value: NodeOperationValue) {\n    const { parentId, index } = value;\n    const parent = this.findNodeById(parentId);\n\n    if (!parent) {\n      return\n    }\n    parent.children.splice(index, 1);\n  }\n\n  insertText(value: TextOperationValue) {\n    const { index, text } = value;\n    this.text = this.text.slice(0, index) + text + this.text.slice(index);\n  }\n\n  deleteText(value: TextOperationValue) {\n    const { index, text } = value;\n    this.text = this.text.slice(0, index) + this.text.slice(index + text.length);\n  }\n\n  findNodeById(id: number): Node | null {\n    const nodes = [this.node];\n    while (nodes.length) {\n      const node = nodes.shift() as Node;\n      if (node.id === id) return node\n      nodes.push(...node.children);\n    }\n    return null;\n  }\n\n  testTransact() {\n    this.historyService.transact(() => {\n      this.handleInsertText({ index: 0, text: 'test' })\n      this.handleInsertText({ index: 4, text: 'test' })\n    })\n  }\n}\n\nexport const insertNodeOperationMeta: OperationMeta = {\n  type: OperationType.insertNode,\n  inverse: (op: Operation) => ({ type: OperationType.deleteNode, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n    source.insertNode(op.value as NodeOperationValue)\n  },\n  getLabel: (op: Operation) => {\n    const value = op.value as NodeOperationValue;\n    return `插入节点${value?.node?.id}`\n  }\n};\n\nexport const deleteNodeOperationMeta: OperationMeta = {\n  type: OperationType.deleteNode,\n  inverse: (op: Operation) => ({ type: OperationType.insertNode, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n    source.deleteNode(op.value as NodeOperationValue)\n  },\n};\n\nexport const insertTextOperationMeta: OperationMeta = {\n  type: OperationType.insertText,\n  inverse: (op: Operation) => ({ type: OperationType.deleteText, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n    source.insertText(op.value as TextOperationValue)\n  },\n  shouldMerge: (op: Operation, prev: Operation | undefined) => true,\n  getURI: () => MOCK_URI,\n};\n\nexport const deleteTextOperationMeta: OperationMeta = {\n  type: OperationType.deleteText,\n  inverse: (op: Operation) => ({ type: OperationType.deleteText, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n    source.deleteText(op.value as TextOperationValue)\n  },\n  shouldMerge: (op: Operation, prev: Operation | undefined) => op,\n};\n\nexport const selectionOperationMeta: OperationMeta = {\n  type: OperationType.selection,\n  inverse: (op: Operation) => ({ type: OperationType.selection, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n  },\n  shouldSave: (op: Operation) => false,\n};\n\nexport const mergeByTimeOperationMeta: OperationMeta = {\n  type: OperationType.mergeByTime,\n  inverse: (op: Operation) => ({ type: OperationType.mergeByTime, value: op.value }),\n  apply: (op: Operation, source: Editor) => {\n  },\n  shouldMerge: (op: Operation, prev: Operation | undefined, stackItem: StackOperation) => {\n    if (Date.now() - stackItem.getTimestamp() < 100) {\n      return true\n    }\n    return false\n  },\n};\n\n\nexport class EditorRegister implements OperationContribution {\n  registerOperationMeta(operationRegistry: OperationRegistry): void {\n    operationRegistry.registerOperationMeta(insertNodeOperationMeta);\n    operationRegistry.registerOperationMeta(deleteNodeOperationMeta);\n    operationRegistry.registerOperationMeta(insertTextOperationMeta);\n    operationRegistry.registerOperationMeta(deleteTextOperationMeta);\n    operationRegistry.registerOperationMeta(selectionOperationMeta);\n    operationRegistry.registerOperationMeta(mergeByTimeOperationMeta);\n  }\n}\ndecorate(injectable(), EditorRegister);\n"
  },
  {
    "path": "packages/common/history/__mocks__/history-container.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container, ContainerModule, type interfaces } from 'inversify';\nimport { bindContributions } from '@flowgram.ai/utils';\nimport { EditorRegister, Editor } from './editor.mock'\n\nimport {\n  OperationContribution,\n  HistoryContainerModule,\n} from '../src';\n\nconst TestContainerModule = new ContainerModule(bind => {\n  bind(Editor).toSelf().inSingletonScope();\n  bindContributions(bind, EditorRegister, [OperationContribution]);\n});\n\nexport function createHistoryContainer(name?: string, parent?: interfaces.Container): interfaces.Container {\n  const container = new Container();\n  if (parent) {\n    container.parent = parent;\n  }\n  if (name) {\n    (container as any).name = name\n  }\n  container.load(HistoryContainerModule);\n  container.load(TestContainerModule);\n  return container;\n}\n"
  },
  {
    "path": "packages/common/history/__tests__/__snapshots__/history-manager.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`history-manager > merge operation should update history stack 1`] = `\n[\n  {\n    \"operations\": [\n      {\n        \"type\": \"insert-text\",\n        \"uri\": \"file:///mock URI\",\n        \"value\": {\n          \"index\": 0,\n          \"text\": \"test\",\n        },\n      },\n      {\n        \"type\": \"insert-text\",\n        \"uri\": \"file:///mock URI\",\n        \"value\": {\n          \"index\": 4,\n          \"text\": \"test\",\n        },\n      },\n    ],\n    \"type\": \"push\",\n    \"uri\": \"file:///editor2\",\n  },\n  {\n    \"operations\": [\n      {\n        \"type\": \"insert-text\",\n        \"uri\": \"file:///mock URI\",\n        \"value\": {\n          \"index\": 0,\n          \"text\": \"test\",\n        },\n      },\n      {\n        \"type\": \"insert-text\",\n        \"uri\": \"file:///mock URI\",\n        \"value\": {\n          \"index\": 4,\n          \"text\": \"test\",\n        },\n      },\n    ],\n    \"type\": \"push\",\n    \"uri\": \"file:///editor1\",\n  },\n]\n`;\n"
  },
  {
    "path": "packages/common/history/__tests__/__snapshots__/history-service.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`history-service > merge by time 1`] = `\n[\n  [\n    {\n      \"type\": \"mergeByTime\",\n      \"value\": {\n        \"test\": 1,\n      },\n    },\n    {\n      \"type\": \"mergeByTime\",\n      \"value\": {\n        \"test\": 2,\n      },\n    },\n    {\n      \"type\": \"mergeByTime\",\n      \"value\": {\n        \"test\": 3,\n      },\n    },\n    {\n      \"type\": \"mergeByTime\",\n      \"value\": {\n        \"test\": 4,\n      },\n    },\n  ],\n]\n`;\n\nexports[`history-service > push operation should return correct history options 1`] = `\n[\n  {\n    \"label\": \"插入节点2\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test2\",\n        \"id\": 2,\n      },\n      \"parentId\": 1,\n    },\n  },\n  {\n    \"label\": \"插入节点3\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test3\",\n        \"id\": 3,\n      },\n      \"parentId\": 2,\n    },\n  },\n]\n`;\n\nexports[`history-service > push operation when limited should not increase the length 1`] = `\n[\n  [\n    {\n      \"type\": \"insert-node\",\n      \"value\": {\n        \"index\": 0,\n        \"node\": {\n          \"children\": [],\n          \"data\": \"test3\",\n          \"id\": 3,\n        },\n        \"parentId\": 2,\n      },\n    },\n  ],\n  [\n    {\n      \"type\": \"insert-node\",\n      \"value\": {\n        \"index\": 0,\n        \"node\": {\n          \"children\": [],\n          \"data\": \"test4\",\n          \"id\": 4,\n        },\n        \"parentId\": 2,\n      },\n    },\n  ],\n]\n`;\n\nexports[`history-service > push undo redo multi times should get correct tree and history options 1`] = `\n{\n  \"children\": [\n    {\n      \"children\": [\n        {\n          \"children\": [],\n          \"data\": \"test4\",\n          \"id\": 4,\n        },\n        {\n          \"children\": [],\n          \"data\": \"test3\",\n          \"id\": 3,\n        },\n      ],\n      \"data\": \"test2\",\n      \"id\": 2,\n    },\n  ],\n  \"data\": \"test\",\n  \"id\": 1,\n}\n`;\n\nexports[`history-service > push undo redo multi times should get correct tree and history options 2`] = `\n[\n  {\n    \"label\": \"插入节点2\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test2\",\n        \"id\": 2,\n      },\n      \"parentId\": 1,\n    },\n  },\n  {\n    \"label\": \"插入节点3\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test3\",\n        \"id\": 3,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点4\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点5\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test5\",\n        \"id\": 5,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test5\",\n        \"id\": 5,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点4\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点4\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n]\n`;\n\nexports[`history-service > transact 1`] = `\n[\n  {\n    \"label\": \"插入节点2\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test2\",\n        \"id\": 2,\n      },\n      \"parentId\": 1,\n    },\n  },\n  {\n    \"label\": \"插入节点3\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test3\",\n        \"id\": 3,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点4\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"插入节点5\",\n    \"type\": \"insert-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test5\",\n        \"id\": 5,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test5\",\n        \"id\": 5,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test4\",\n        \"id\": 4,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test3\",\n        \"id\": 3,\n      },\n      \"parentId\": 2,\n    },\n  },\n  {\n    \"label\": \"delete-node\",\n    \"type\": \"delete-node\",\n    \"value\": {\n      \"index\": 0,\n      \"node\": {\n        \"children\": [],\n        \"data\": \"test2\",\n        \"id\": 2,\n      },\n      \"parentId\": 1,\n    },\n  },\n]\n`;\n"
  },
  {
    "path": "packages/common/history/__tests__/__snapshots__/undo-redo-service.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`operation-registry > change event 1`] = `\n[\n  \"push\",\n  \"undo\",\n  \"redo\",\n]\n`;\n"
  },
  {
    "path": "packages/common/history/__tests__/history-manager.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { Container, interfaces } from 'inversify';\n\nimport { HistoryManager } from '../src/history/history-manager';\nimport { HistoryContainerModule, HistoryService, HistoryStack } from '../src';\nimport { createHistoryContainer } from '../__mocks__/history-container.mock';\nimport { Editor } from '../__mocks__/editor.mock';\n\nfunction getStackSnapshot(historyStack: HistoryStack) {\n  return historyStack.items.map((item) => ({\n    operations: item.operations.map((op) => ({\n      type: op.type,\n      value: op.value,\n      uri: op.uri?.toString(),\n    })),\n    type: item.type,\n    uri: item.uri?.toString(),\n  }));\n}\n\ndescribe('history-manager', () => {\n  let ide_container: interfaces.Container;\n  let containers: interfaces.Container[];\n  let editor1: Editor;\n  let historyService1: HistoryService;\n  let editor2: Editor;\n  let historyService2: HistoryService;\n  let historyManager: HistoryManager;\n\n  beforeEach(() => {\n    ide_container = new Container();\n    (ide_container as any).name = 'ide_container';\n    ide_container.load(HistoryContainerModule);\n    const container1 = createHistoryContainer('container1', ide_container);\n    const container2 = createHistoryContainer('container2', ide_container);\n    containers = [container1, container2];\n    editor1 = container1.get(Editor);\n    editor2 = container2.get(Editor);\n    historyService1 = container1.get(HistoryService);\n    historyService1.context.uri = 'file:///editor1';\n    historyService2 = container2.get(HistoryService);\n    historyService2.context.uri = 'file:///editor2';\n    historyManager = ide_container.get(HistoryManager);\n  });\n\n  it('different container instances should not be the same', () => {\n    expect(editor1 === editor2).toBeFalsy();\n    expect(historyService1 === historyService2).toBeFalsy();\n  });\n\n  it('different editor operations should not be the same', () => {\n    editor1.handleInsertText({ index: 0, text: 'test' });\n    expect(editor1.canUndo()).toBeTruthy();\n    expect(editor2.canUndo()).toBeFalsy();\n  });\n\n  it('different editor operations should all be captured in history manager', async () => {\n    const fn = vi.fn();\n    historyManager.historyStack.onChange(fn);\n    editor1.handleInsertText({ index: 0, text: 'test' });\n    editor2.handleInsertText({ index: 0, text: 'test' });\n\n    await editor1.undo();\n    await editor2.undo();\n    await editor1.redo();\n    await editor2.redo();\n    expect(historyManager.historyStack.items).toHaveLength(6);\n    expect(fn).toBeCalledTimes(6);\n  });\n\n  it('dispose', () => {\n    historyService1.dispose();\n    expect((historyManager as any)._historyServices).toHaveLength(1);\n    historyManager.dispose();\n    expect(historyManager.historyStack.items).toHaveLength(0);\n    expect((historyManager as any).historyStack._toDispose.disposed).toBeTruthy();\n  });\n\n  it('unrRegisterHistoryService', () => {\n    historyManager.unregisterHistoryService(historyService1);\n    const services = Array.from((historyManager as any)._historyServices.keys());\n    expect(services).toHaveLength(1);\n    expect(services[0]).toEqual(historyService2);\n  });\n\n  it('limit', async () => {\n    historyManager.historyStack.limit = 2;\n    editor1.handleInsertText({ index: 0, text: 'test' });\n    editor2.handleInsertText({ index: 0, text: 'test' });\n\n    await editor1.undo();\n    await editor2.undo();\n    await editor2.redo();\n    expect(historyManager.historyStack.items.map((s) => s.type)).toEqual(['redo', 'undo']);\n  });\n\n  it('merge operation should update history stack', () => {\n    editor1.testTransact();\n    editor2.testTransact();\n    expect(getStackSnapshot(historyManager.historyStack)).toMatchSnapshot();\n  });\n\n  it('clear should not be recorded', async () => {\n    editor1.handleInsertText({ index: 0, text: 'test' });\n    await editor1.undo();\n    historyService1.clear();\n    expect(historyManager.historyStack.items).toHaveLength(2);\n  });\n});\n"
  },
  {
    "path": "packages/common/history/__tests__/history-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\nimport { omit } from 'lodash-es';\n\nimport { HistoryService, UndoRedoService } from '../src';\nimport { createHistoryContainer } from '../__mocks__/history-container.mock';\nimport { Editor, MOCK_URI, defaultRoot } from '../__mocks__/editor.mock';\n\ndescribe('history-service', () => {\n  let container;\n  let editor: Editor;\n  let undoRedoService: UndoRedoService;\n  let historyService: HistoryService;\n\n  beforeEach(() => {\n    container = createHistoryContainer();\n    editor = container.get(Editor);\n    undoRedoService = container.get(UndoRedoService);\n    historyService = container.get(HistoryService);\n  });\n\n  it('push insert operation should apply editor insert', () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    expect(editor.node.children[0]).toEqual(node1);\n  });\n\n  it('undo insert operation should delete node', async () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    await editor.undo();\n    expect(editor.node.children).toEqual([]);\n  });\n\n  it('undo operation then push operation should clear redo stack', async () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    await editor.undo();\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    expect(editor.canRedo()).toBeFalsy();\n  });\n\n  it('push twice undo once should remain the first insert node', async () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    const node2 = { id: 3, data: 'test3', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node2 });\n    await editor.undo();\n    expect(editor.node.children).toEqual([node1]);\n  });\n\n  it('push save disabled operation should push no operation', async () => {\n    editor.handleSelection();\n    expect(editor.canUndo()).toBeFalsy();\n  });\n\n  it('push operation with merge should be merged', async () => {\n    editor.handleInsertText({ index: 0, text: 'test' });\n    editor.handleInsertText({ index: 4, text: 'aaa' });\n    expect(undoRedoService.getUndoStack().length).toEqual(1);\n    expect(undoRedoService.getLastElement().getOperations().length).toEqual(2);\n    expect(editor.text).toEqual('testaaa');\n    await editor.undo();\n    expect(editor.text).toEqual('');\n  });\n\n  it('push operation with merge operation should be merged', async () => {\n    editor.handleDeleteText({ index: 0, text: 'test' });\n    editor.handleDeleteText({ index: 4, text: 'aaa' });\n    expect(undoRedoService.getUndoStack().length).toEqual(1);\n    expect(undoRedoService.getLastElement().getOperations().length).toEqual(1);\n    expect(editor.text).toEqual('');\n    await editor.undo();\n    expect(editor.text).toEqual('');\n  });\n\n  it('push no registered operation should throw error', () => {\n    expect(() => historyService.pushOperation({ type: 'test', value: {} })).toThrowError(\n      'Operation meta test has not registered.'\n    );\n  });\n\n  it('push operation should return correct history options', async () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    const node2 = { id: 3, data: 'test3', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node2 });\n    expect(editor.getHistoryOperations()).toMatchSnapshot();\n  });\n\n  it('push undo redo multi times should get correct tree and history options', async () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    const node2 = { id: 3, data: 'test3', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node2 });\n    const node3 = { id: 4, data: 'test4', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node3 });\n    const node4 = { id: 5, data: 'test5', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node4 });\n    await editor.undo();\n    await editor.undo();\n    await editor.redo();\n    await editor.undo();\n    await editor.redo();\n    expect(editor.node).toMatchSnapshot();\n    expect(editor.getHistoryOperations()).toMatchSnapshot();\n  });\n\n  it('transact', async () => {\n    editor.reset();\n    historyService.transact(() => {\n      const node1 = { id: 2, data: 'test2', children: [] };\n      editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n      const node2 = { id: 3, data: 'test3', children: [] };\n      editor.handleInsert({ parentId: 2, index: 0, node: node2 });\n      const node3 = { id: 4, data: 'test4', children: [] };\n      editor.handleInsert({ parentId: 2, index: 0, node: node3 });\n      const node4 = { id: 5, data: 'test5', children: [] };\n      editor.handleInsert({ parentId: 2, index: 0, node: node4 });\n\n      historyService.transact(() => {\n        const node5 = { id: 6, data: 'test6', children: [] };\n        editor.handleInsert({ parentId: 2, index: 0, node: node5 });\n      });\n    });\n    await editor.undo();\n    expect(editor.node).toEqual(defaultRoot());\n    expect(editor.getHistoryOperations()).toMatchSnapshot();\n  });\n\n  it('transact no operation', async () => {\n    historyService.transact(() => {});\n    expect(historyService.canUndo()).toEqual(false);\n  });\n\n  it('clear should clear all', () => {\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    expect(historyService.canUndo()).toEqual(true);\n    expect(historyService.getHistoryOperations().length).toEqual(1);\n    historyService.clear();\n    expect(historyService.canUndo()).toEqual(false);\n  });\n\n  it('operation should not be recorded when stopped', () => {\n    historyService.clear();\n    const node1 = { id: 2, data: 'test2', children: [] };\n    historyService.stop();\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    expect(historyService.canUndo()).toEqual(false);\n    historyService.start();\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    expect(historyService.canUndo()).toEqual(true);\n  });\n\n  it('push operation when limited should not increase the length', () => {\n    historyService.limit(2);\n    const node1 = { id: 2, data: 'test2', children: [] };\n    editor.handleInsert({ parentId: 1, index: 0, node: node1 });\n    const node2 = { id: 3, data: 'test3', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node2 });\n    const node3 = { id: 4, data: 'test4', children: [] };\n    editor.handleInsert({ parentId: 2, index: 0, node: node3 });\n    const undoStack = (historyService as any).undoRedoService.getUndoStack();\n    expect(undoStack.length).toEqual(2);\n    expect(\n      undoStack.map((item) => item.getOperations().map((op) => omit(op, 'id')))\n    ).toMatchSnapshot();\n  });\n\n  it('merge by time', () => {\n    editor.handleMultiOperation();\n    const undoStack = (historyService as any).undoRedoService.getUndoStack();\n    expect(undoStack.length).toEqual(1);\n    expect(\n      undoStack.map((item) => item.getOperations().map((op) => omit(op, 'id')))\n    ).toMatchSnapshot();\n  });\n\n  it('push with uri', () => {\n    let uri = 'test';\n    editor.handleInsertText({ index: 0, text: 'test' }, uri);\n    expect(\n      historyService.undoRedoService.getLastElement().getLastOperation().uri === uri\n    ).toBeTruthy();\n    editor.handleInsertText({ index: 4, text: 'aaa' });\n    expect(\n      historyService.undoRedoService.getLastElement().getLastOperation().uri === MOCK_URI\n    ).toBeTruthy();\n  });\n\n  it('push operation with noApply', async () => {\n    const text = editor.text;\n    editor.handleInsertText({ index: 0, text: 'test' }, undefined, true);\n    expect(editor.text).toEqual(text);\n    editor.handleInsertText({ index: 0, text: 'test' }, undefined, false);\n    expect(editor.text).not.toEqual(text);\n  });\n\n  it('dispose', async () => {\n    const disposables = (historyService as any)._toDispose;\n    historyService.dispose();\n    expect(disposables.disposed).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/common/history/__tests__/operation-registry.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach } from 'vitest';\n\nimport { OperationRegistry, Operation } from '../src';\nimport { createHistoryContainer } from '../__mocks__/history-container.mock';\nimport { insertNodeOperationMeta } from '../__mocks__/editor.mock';\n\ndescribe('operation-registry', () => {\n  let operationRegistry: OperationRegistry;\n  let container;\n  beforeEach(() => {\n    container = createHistoryContainer();\n    operationRegistry = container.get(OperationRegistry);\n  });\n\n  it('registerOperationMeta success should return correct operationMeta', () => {\n    const operationMeta = {\n      type: 'test',\n      inverse: (op: Operation) => op,\n      label: 'test',\n      description: 'test',\n      apply: () => {},\n    };\n    operationRegistry.registerOperationMeta(operationMeta);\n    expect(operationRegistry.getOperationMeta(operationMeta.type)).toEqual(operationMeta);\n  });\n\n  it('register by contribution success should return correct operationMeta', () => {\n    expect(operationRegistry.getOperationMeta(insertNodeOperationMeta.type)).toEqual(\n      insertNodeOperationMeta,\n    );\n  });\n});\n"
  },
  {
    "path": "packages/common/history/__tests__/operation-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\n\nimport { OperationRegistry, Operation, OperationService } from '../src';\nimport { createHistoryContainer } from '../__mocks__/history-container.mock';\n\nconst fn = vi.fn();\n\ndescribe('operation-service', () => {\n  let operationRegistry: OperationRegistry;\n  let operationService: OperationService;\n  let container;\n  beforeEach(() => {\n    container = createHistoryContainer();\n    operationService = container.get(OperationService);\n    operationRegistry = container.get(OperationRegistry);\n    const operationMeta = {\n      type: 'test',\n      inverse: (op: Operation) => op,\n      description: 'test',\n      apply: fn,\n      getLabel: () => 'test1',\n    };\n    operationRegistry.registerOperationMeta(operationMeta);\n  });\n\n  it('get operation label should return correct label', () => {\n    expect(operationService.getOperationLabel({ type: 'test' } as any)).toEqual('test1');\n  });\n\n  it('no apply', () => {\n    operationService.applyOperation({ type: 'test' } as any, { noApply: true });\n    expect(fn).toBeCalledTimes(0);\n    operationService.applyOperation({ type: 'test' } as any, { noApply: false });\n    expect(fn).toBeCalledTimes(1);\n    operationService.applyOperation({ type: 'test' } as any);\n    expect(fn).toBeCalledTimes(2);\n  });\n\n  it('on apply', () => {\n    const handleApply = vi.fn();\n    operationService.onApply(handleApply);\n    operationService.applyOperation({ type: 'test' } as any);\n    expect(handleApply).toBeCalledTimes(1);\n    operationService.applyOperation({ type: 'test' } as any, { noApply: true });\n    expect(handleApply).toBeCalledTimes(2);\n  });\n\n  it('apply with origin', () => {\n    const handleApply = vi.fn();\n    operationService.onApply(handleApply);\n    const op = { type: 'test', origin: Symbol('origin'), value: {} };\n    operationService.applyOperation(op);\n    expect(handleApply).toBeCalledWith(op);\n  });\n});\n"
  },
  {
    "path": "packages/common/history/__tests__/undo-redo-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect, beforeEach, vi } from 'vitest';\n\nimport {\n  OperationService,\n  StackOperation,\n  UndoRedoChangeEvent,\n  UndoRedoChangeType,\n  UndoRedoService,\n} from '../src';\nimport { createHistoryContainer } from '../__mocks__/history-container.mock';\n\ndescribe('operation-registry', () => {\n  let undoRedoService: UndoRedoService;\n  let container;\n  let createStackOperation = (operations = []) =>\n    new StackOperation(container.get(OperationService), operations);\n  beforeEach(() => {\n    container = createHistoryContainer();\n    undoRedoService = container.get(UndoRedoService);\n  });\n\n  it('pushElement', () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    expect(undoRedoService.getUndoStack()).toEqual([element]);\n  });\n\n  it('getUndoStack', () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    expect(undoRedoService.getUndoStack()).toEqual([element]);\n  });\n\n  it('getRedoStack', () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    expect(undoRedoService.getRedoStack()).toEqual([]);\n  });\n\n  it('clearRedoStack', () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    undoRedoService.clearRedoStack();\n    expect(undoRedoService.getRedoStack()).toEqual([]);\n  });\n\n  it('undo', async () => {\n    const element = createStackOperation();\n    await undoRedoService.undo();\n    undoRedoService.pushElement(element);\n    await undoRedoService.undo();\n    expect(undoRedoService.getUndoStack()).toEqual([]);\n  });\n\n  it('undo twice will only revert once', async () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    undoRedoService.pushElement(element);\n    undoRedoService.undo();\n    undoRedoService.undo();\n    expect(undoRedoService.getUndoStack()).toEqual([element]);\n  });\n\n  it('redo', async () => {\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    await undoRedoService.undo();\n    await undoRedoService.redo();\n    expect(undoRedoService.getUndoStack()).toEqual([element]);\n  });\n\n  it('canUndo', () => {\n    const element = createStackOperation();\n    expect(undoRedoService.canUndo()).toEqual(false);\n    undoRedoService.pushElement(element);\n    expect(undoRedoService.canUndo()).toEqual(true);\n  });\n\n  it('canRedo', async () => {\n    const element = createStackOperation();\n    expect(undoRedoService.canRedo()).toEqual(false);\n    undoRedoService.pushElement(element);\n    expect(undoRedoService.canRedo()).toEqual(false);\n    await undoRedoService.undo();\n    expect(undoRedoService.canRedo()).toEqual(true);\n  });\n\n  it('change event', async () => {\n    const element = createStackOperation();\n    const events: UndoRedoChangeEvent[] = [];\n    undoRedoService.onChange(e => events.push(e));\n    undoRedoService.pushElement(element);\n    await undoRedoService.undo();\n    await undoRedoService.redo();\n    expect(events.map(e => e.type)).toMatchSnapshot();\n  });\n\n  it('clear', () => {\n    const fn = vi.fn();\n    const element = createStackOperation();\n    undoRedoService.pushElement(element);\n    undoRedoService.pushElement(element);\n    undoRedoService.onChange(event => {\n      if (event.type === UndoRedoChangeType.CLEAR) {\n        fn();\n      }\n    });\n    undoRedoService.undo();\n    undoRedoService.clear();\n    expect(undoRedoService.getUndoStack()).toEqual([]);\n    expect(undoRedoService.getRedoStack()).toEqual([]);\n    expect(fn).toBeCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/common/history/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/common/history/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/history\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"test:update\": \"vitest run --update\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/create-history-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { Operation, OperationService } from './operation';\nimport { HistoryContainerModule } from './history-container-module';\n\nexport interface HistoryPluginOptions<T = PluginContext> {\n  enable?: boolean;\n  enableChangeNode?: boolean;\n  onApply?: (ctx: T, operation: Operation) => void;\n}\n\nexport const createHistoryPlugin = definePluginCreator<HistoryPluginOptions>({\n  onInit: (ctx, opts) => {\n    if (opts.onApply) {\n      ctx.get(OperationService).onApply(opts.onApply.bind(null, ctx));\n    }\n  },\n  containerModules: [HistoryContainerModule],\n});\n"
  },
  {
    "path": "packages/common/history/src/history/history-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { type Disposable, DisposableCollection } from '@flowgram.ai/utils';\n\nimport { OperationWithId } from '../operation';\nimport { HistoryConfig } from '../history-config';\nimport { type UndoRedoChangeEvent } from './types';\nimport {\n  type IHistoryManager,\n  type HistoryStackItem,\n  UndoRedoChangeType,\n  HistoryMergeEventType,\n  HistoryMergeEvent,\n} from './types';\nimport { StackOperation } from './stack-operation';\nimport { HistoryStack } from './history-stack';\nimport { type HistoryService } from './history-service';\n\n@injectable()\nexport class HistoryManager implements IHistoryManager {\n  @inject(HistoryStack) readonly historyStack: HistoryStack;\n\n  @inject(HistoryConfig) readonly historyConfig: HistoryConfig;\n\n  private _historyServices = new Map<HistoryService, Disposable>();\n\n  private _toDispose = new DisposableCollection();\n\n  registerHistoryService(service: HistoryService): void {\n    const toDispose = new DisposableCollection();\n    toDispose.pushAll([\n      service.undoRedoService.onChange((event: UndoRedoChangeEvent) => {\n        if (event.type === UndoRedoChangeType.CLEAR) {\n          return;\n        }\n\n        const { type, element } = event;\n        const operations = element.getChangeOperations(type);\n        const historyStackItem: HistoryStackItem = {\n          id:\n            type === UndoRedoChangeType.PUSH\n              ? (element as StackOperation).id\n              : this.historyConfig.generateId(),\n          type,\n          uri: service.context.uri,\n          operations,\n          timestamp: Date.now(),\n        };\n        this.historyStack.add(service, historyStackItem);\n      }),\n      service.onMerge((event) => {\n        this._handleMerge(service, event);\n      }),\n    ]);\n    this._historyServices.set(service, toDispose);\n\n    this._toDispose.push(\n      service.onWillDispose(() => {\n        this.unregisterHistoryService(service);\n      })\n    );\n  }\n\n  unregisterHistoryService(service: HistoryService): void {\n    const disposable = this._historyServices.get(service);\n    if (!disposable) {\n      return;\n    }\n    disposable.dispose();\n    this._historyServices.delete(service);\n  }\n\n  getHistoryServiceByURI(uri: string) {\n    for (const service of this._historyServices.keys()) {\n      if (service.context.uri === uri) {\n        return service;\n      }\n    }\n  }\n\n  getFirstHistoryService() {\n    for (const service of this._historyServices.keys()) {\n      return service;\n    }\n  }\n\n  dispose(): void {\n    this._toDispose.dispose();\n    this.historyStack.dispose();\n\n    this._historyServices.forEach((service) => service.dispose());\n    this._historyServices.clear();\n  }\n\n  _handleMerge(service: HistoryService, event: HistoryMergeEvent) {\n    const { element, operation } = event.value;\n\n    const find = this.historyStack.findById((element as StackOperation).id);\n\n    if (!find) {\n      return;\n    }\n\n    if (!(operation as OperationWithId).id) {\n      console.warn('no operation id found');\n      return;\n    }\n\n    if (event.type === HistoryMergeEventType.UPDATE) {\n      this.historyStack.updateOperation(\n        service,\n        (element as StackOperation).id,\n        operation as OperationWithId\n      );\n    }\n\n    if (event.type === HistoryMergeEventType.ADD) {\n      this.historyStack.addOperation(\n        service,\n        (element as StackOperation).id,\n        operation as OperationWithId\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/history/history-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { pick } from 'lodash-es';\nimport { injectable, inject, postConstruct } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { OperationService } from '../operation/operation-service';\nimport { OperationMeta, OperationRegistry, PushOperationOptions } from '../operation';\nimport { Operation } from '../operation';\nimport { HistoryContext } from '../history-context';\nimport { HistoryConfig } from '../history-config';\nimport { UndoRedoService } from './undo-redo-service';\nimport {\n  HistoryMergeEvent,\n  HistoryMergeEventType,\n  HistoryRecord,\n  IHistoryService,\n  IUndoRedoElement,\n} from './types';\nimport { StackOperation } from './stack-operation';\nimport { HistoryManager } from './history-manager';\n\n@injectable()\nexport class HistoryService implements IHistoryService {\n  @inject(UndoRedoService)\n  readonly undoRedoService: UndoRedoService;\n\n  @inject(OperationRegistry)\n  readonly operationRegistry: OperationRegistry;\n\n  @inject(OperationService)\n  readonly operationService: OperationService;\n\n  @inject(HistoryContext)\n  readonly context: HistoryContext;\n\n  @inject(HistoryConfig)\n  readonly config: HistoryConfig;\n\n  @inject(HistoryManager)\n  historyManager: HistoryManager;\n\n  private _toDispose = new DisposableCollection();\n\n  private _transacting: boolean = false;\n\n  private _transactOperation: StackOperation | null = null;\n\n  private _locked: boolean = false;\n\n  private _willDisposeEmitter = new Emitter<HistoryService>();\n\n  private _mergeEmitter = new Emitter<HistoryMergeEvent>();\n\n  onWillDispose = this._willDisposeEmitter.event;\n\n  onMerge = this._mergeEmitter.event;\n\n  get onApply() {\n    return this.operationService.onApply;\n  }\n\n  @postConstruct()\n  init() {\n    this._toDispose.push(this._willDisposeEmitter);\n    this._toDispose.push(this._mergeEmitter);\n  }\n\n  start() {\n    this._locked = false;\n  }\n\n  stop() {\n    this._locked = true;\n  }\n\n  limit(num: number) {\n    this.undoRedoService.setLimit(num);\n  }\n\n  startTransaction() {\n    if (this._transacting) {\n      return;\n    }\n\n    this._transacting = true;\n    const stackOperation = new StackOperation(this.operationService, []);\n    this._transactOperation = stackOperation;\n  }\n\n  endTransaction() {\n    const stackOperation = this._transactOperation;\n    if (!stackOperation) {\n      return;\n    }\n    if (stackOperation.getOperations().length !== 0) {\n      this._pushStackOperation(stackOperation);\n    }\n\n    this._transactOperation = null;\n    this._transacting = false;\n  }\n\n  transact(transaction: () => void) {\n    if (this._transacting) {\n      return;\n    }\n    this.startTransaction();\n    transaction();\n    this.endTransaction();\n  }\n\n  pushOperation(operation: Operation, options?: PushOperationOptions): any {\n    if (!this._canPush()) {\n      return;\n    }\n\n    const prev = this._transactOperation || this.undoRedoService.getLastElement();\n    const operationMeta = this.operationRegistry.getOperationMeta(operation.type) as OperationMeta;\n\n    if (!operationMeta) {\n      throw new Error(`Operation meta ${operation.type} has not registered.`);\n    }\n\n    if (operationMeta.shouldSave && !operationMeta.shouldSave(operation)) {\n      return operationMeta.apply(operation, this.context.source);\n    }\n\n    const res = this.operationService.applyOperation(operation, { noApply: options?.noApply });\n\n    if (operationMeta.getURI && !operation.uri) {\n      operation.uri = operationMeta.getURI(operation, this.context.source);\n    }\n\n    const shouldMerge = this._shouldMerge(operation, prev, operationMeta);\n\n    if (shouldMerge) {\n      if (typeof shouldMerge === 'object') {\n        const operation = prev.getLastOperation();\n        operation.value = shouldMerge.value;\n        this._mergeEmitter.fire({\n          type: HistoryMergeEventType.UPDATE,\n          value: {\n            element: prev,\n            operation: operation,\n            value: shouldMerge.value,\n          },\n        });\n      } else {\n        const op = prev.pushOperation(operation);\n        this._mergeEmitter.fire({\n          type: HistoryMergeEventType.ADD,\n          value: {\n            element: prev,\n            operation: op,\n          },\n        });\n      }\n    } else {\n      const stackOperation = new StackOperation(this.operationService, [operation]);\n      this._pushStackOperation(stackOperation);\n    }\n\n    return res;\n  }\n\n  getHistoryOperations(): Operation<unknown>[] {\n    return this.historyManager.historyStack.items\n      .reverse()\n      .map((item) =>\n        item.operations.map((o) => ({\n          ...pick(o, ['type', 'value']),\n          label: o.label || o.type,\n        }))\n      )\n      .flat();\n  }\n\n  async undo(): Promise<void> {\n    await this.undoRedoService.undo();\n  }\n\n  async redo(): Promise<void> {\n    await this.undoRedoService.redo();\n  }\n\n  canUndo(): boolean {\n    return this.undoRedoService.canUndo();\n  }\n\n  canRedo(): boolean {\n    return this.undoRedoService.canRedo();\n  }\n\n  getSnapshot(): unknown {\n    return this.config.getSnapshot();\n  }\n\n  getRecords(): Promise<HistoryRecord[]> {\n    throw new Error('Method not implemented.');\n  }\n\n  restore(historyRecord: HistoryRecord): Promise<void> {\n    throw new Error('Method not implemented.');\n  }\n\n  clear() {\n    this.undoRedoService.clear();\n  }\n\n  dispose(): void {\n    this._willDisposeEmitter.fire(this);\n    this._toDispose.dispose();\n  }\n\n  private _canPush() {\n    if (this._locked) {\n      return false;\n    }\n    return this.undoRedoService.canPush();\n  }\n\n  private _pushStackOperation(stackOperation: StackOperation) {\n    this.undoRedoService.pushElement(stackOperation);\n    this.undoRedoService.clearRedoStack();\n  }\n\n  private _shouldMerge(operation: Operation, prev: IUndoRedoElement, operationMeta: OperationMeta) {\n    if (!prev) {\n      return false;\n    }\n\n    if (this._transacting) {\n      return true;\n    }\n    return (\n      operationMeta.shouldMerge &&\n      operationMeta.shouldMerge(operation, prev.getLastOperation(), prev as StackOperation)\n    );\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/history/history-stack.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { cloneDeep } from 'lodash-es';\nimport { injectable, inject } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { HistoryOperation, Operation, OperationWithId } from '../operation';\nimport { HistoryConfig } from '../history-config';\nimport {\n  type HistoryItem,\n  HistoryStackChangeType,\n  type HistoryStackItem,\n  HistoryStackChangeEvent,\n  UndoRedoChangeType,\n} from './types';\nimport { type HistoryService } from './history-service';\n\n/**\n * 历史栈，聚合所有历史操作\n */\n@injectable()\nexport class HistoryStack {\n  @inject(HistoryConfig)\n  historyConfig: HistoryConfig;\n\n  private _items: HistoryItem[] = [];\n\n  readonly onChangeEmitter = new Emitter<HistoryStackChangeEvent>();\n\n  readonly onChange = this.onChangeEmitter.event;\n\n  private _toDispose: DisposableCollection = new DisposableCollection();\n\n  limit = 100;\n\n  constructor() {\n    this._toDispose.push(this.onChangeEmitter);\n  }\n\n  get items(): HistoryItem[] {\n    return this._items;\n  }\n\n  add(service: HistoryService, item: HistoryStackItem) {\n    const historyItem = this._getHistoryItem(service, item);\n    this._items.unshift(historyItem);\n    if (this._items.length > this.limit) {\n      this._items.pop();\n    }\n    this.onChangeEmitter.fire({\n      type: HistoryStackChangeType.ADD,\n      value: historyItem,\n      service,\n    });\n    return historyItem;\n  }\n\n  findById(id: string): HistoryItem | undefined {\n    return this._items.find((item) => item.id === id);\n  }\n\n  changeByIndex(index: number, service: HistoryService, item: HistoryStackItem) {\n    const historyItem = this._getHistoryItem(service, item);\n    this._items[index] = historyItem;\n    this.onChangeEmitter.fire({\n      type: HistoryStackChangeType.UPDATE,\n      value: historyItem,\n      service,\n    });\n  }\n\n  addOperation(service: HistoryService, id: string, op: OperationWithId) {\n    const historyItem = this._items.find((item) => item.id === id);\n    if (!historyItem) {\n      console.warn('no history item found');\n      return;\n    }\n\n    const newOperatopn = this._getHistoryOperation(service, op);\n    historyItem.operations.push(newOperatopn);\n\n    this.onChangeEmitter.fire({\n      type: HistoryStackChangeType.ADD_OPERATION,\n      value: {\n        historyItem,\n        operation: newOperatopn,\n      },\n      service,\n    });\n  }\n\n  updateOperation(service: HistoryService, id: string, op: OperationWithId) {\n    const historyItem = this._items.find((item) => item.id === id);\n    if (!historyItem) {\n      console.warn('no history item found');\n      return;\n    }\n    const index = historyItem.operations.findIndex((op) => op.id === op.id);\n    if (index < 0) {\n      console.warn('no operation found');\n      return;\n    }\n    const newOperatopn = this._getHistoryOperation(service, op);\n    historyItem.operations.splice(index, 1, newOperatopn);\n    this.onChangeEmitter.fire({\n      type: HistoryStackChangeType.UPDATE_OPERATION,\n      value: {\n        historyItem,\n        operation: newOperatopn,\n      },\n      service,\n    });\n  }\n\n  clear() {\n    this._items = [];\n  }\n\n  dispose() {\n    this._items = [];\n    this._toDispose.dispose();\n  }\n\n  private _getHistoryItem(service: HistoryService, item: HistoryStackItem): HistoryItem {\n    return {\n      ...item,\n      uri: service.context.uri,\n      time: HistoryStack.dateFormat(item.timestamp),\n      operations: item.operations.map((op) =>\n        this._getHistoryOperation(service, op, item.type !== UndoRedoChangeType.PUSH)\n      ),\n    };\n  }\n\n  private _getHistoryOperation(\n    service: HistoryService,\n    op: Operation,\n    generateId: boolean = false\n  ): HistoryOperation {\n    let id;\n    if (generateId) {\n      id = this.historyConfig.generateId();\n    } else {\n      const oldId = (op as OperationWithId).id;\n      if (!oldId) {\n        throw new Error('no operation id found');\n      }\n      id = oldId;\n    }\n\n    return {\n      ...cloneDeep(op),\n      id,\n      label: service.operationService.getOperationLabel(op),\n      description: service.operationService.getOperationDescription(op),\n      timestamp: Date.now(),\n    };\n  }\n\n  static dateFormat(timestamp: number) {\n    return new Date(timestamp).toLocaleString();\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/history/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './undo-redo-service';\nexport * from './types';\nexport * from './history-service';\nexport * from './stack-operation';\nexport * from './history-manager';\nexport * from './history-stack';\nexport * from '../history-config';\n"
  },
  {
    "path": "packages/common/history/src/history/stack-operation.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { cloneDeep } from 'lodash-es';\nimport { DisposableCollection } from '@flowgram.ai/utils';\n\nimport { OperationService } from '../operation/operation-service';\nimport { Operation, OperationWithId } from '../operation';\nimport { IUndoRedoElement, UndoRedoChangeType } from './types';\n\nexport class StackOperation implements IUndoRedoElement {\n  label?: string | undefined;\n\n  description?: string | undefined;\n\n  private _operations: OperationWithId[];\n\n  private _toDispose = new DisposableCollection();\n\n  private _timestamp: number = Date.now();\n\n  private _operationService: OperationService;\n\n  private _id: string;\n\n  get id() {\n    return this._id;\n  }\n\n  constructor(operationService: OperationService, operations: Operation[] = []) {\n    this._operationService = operationService;\n    this._operations = operations.map((op) => this._operation(op));\n    this._id = operationService.config.generateId();\n  }\n\n  getTimestamp(): number {\n    return this._timestamp;\n  }\n\n  pushOperation(operation: Operation): OperationWithId {\n    const op = this._operation(operation);\n    this._operations.push(op);\n    return op;\n  }\n\n  getOperations(): Operation[] {\n    return this._operations;\n  }\n\n  getChangeOperations(type: UndoRedoChangeType): Operation[] {\n    if (type === UndoRedoChangeType.UNDO) {\n      return this._operationService.inverseOperations(this._operations);\n    }\n    return this._operations;\n  }\n\n  getFirstOperation(): Operation {\n    return this._operations[0];\n  }\n\n  getLastOperation(): Operation<unknown> {\n    return this._operations[this._operations.length - 1];\n  }\n\n  async undo(): Promise<void> {\n    const inverseOps = this._operationService.inverseOperations(this._operations);\n\n    for (const op of inverseOps) {\n      await this._apply(op);\n    }\n  }\n\n  async redo(): Promise<void> {\n    for (const op of this._operations) {\n      await this._apply(op);\n    }\n  }\n\n  revert(type: UndoRedoChangeType): void | Promise<void> {\n    let operations: Operation[] = this._operations;\n\n    if (type !== UndoRedoChangeType.UNDO) {\n      operations = this._operations.map((op) => this._inverse(op)).reverse();\n    }\n\n    for (const op of operations) {\n      this._apply(op);\n    }\n  }\n\n  private _inverse(op: Operation): Operation {\n    return this._operationService.inverseOperation(op);\n  }\n\n  private async _apply(op: Operation) {\n    await this._operationService.applyOperation(op);\n  }\n\n  private _operation(op: Operation) {\n    return {\n      ...op,\n      value: cloneDeep(op.value),\n      id: this._operationService.config.generateId(),\n    };\n  }\n\n  dispose(): void {\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/history/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { HistoryOperation, Operation } from '../operation';\nimport { HistoryService } from './history-service';\n\nexport interface HistoryRecord {\n  snapshot: any;\n  stack: any[];\n}\n\nexport interface HistoryItem extends HistoryStackItem {\n  id: string;\n  time: string;\n  operations: HistoryOperation[];\n}\n\n/**\n * 历史服务管理\n */\nexport interface IHistoryManager {\n  /**\n   * 注册历史服务\n   * @param service 历史服务示例\n   */\n  registerHistoryService(service: IHistoryService): void;\n  /**\n   * 取消注册历史服务\n   * @param service 历史服务示例\n   */\n  unregisterHistoryService(service: HistoryService): void;\n}\n\n/**\n * 历史服务\n */\nexport interface IHistoryService extends Disposable {\n  /**\n   * 添加操作\n   * @param operation 操作\n   */\n  pushOperation(operation: Operation): void | Promise<void>;\n  /**\n   * 获取所有历史操作\n   */\n  getHistoryOperations(): Operation[];\n  /**\n   * 撤回\n   */\n  undo(): void | Promise<void>;\n  /**\n   * 重做\n   */\n  redo(): void | Promise<void>;\n  /**\n   * 是否有可撤销的操作\n   */\n  canUndo(): boolean;\n  /**\n   * 是否有可重做的操作\n   */\n  canRedo(): boolean;\n  /**\n   * 获取历史记录\n   */\n  getRecords(): Promise<HistoryRecord[]>;\n  /**\n   * 根据历史版本重新存储历史记录\n   * @param historyRecord 历史记录\n   */\n  restore(historyRecord: HistoryRecord): Promise<void>;\n  /**\n   * 清空undo/redo\n   */\n  clear(): void;\n  /**\n   * 最大数量限制\n   * @param num 数量\n   */\n  limit(num: number): void;\n  /**\n   * 返回快照\n   */\n  getSnapshot(): unknown;\n}\n\nexport interface IOperationService {\n  pushOperation(operation: Operation): void;\n}\n\n/**\n * UndoRedo服务\n */\nexport interface IUndoRedoService extends Disposable {\n  /**\n   * 添加一个undo/redo元素\n   * @param element 可undo/redo的元素\n   */\n  pushElement(element: IUndoRedoElement): void;\n  /**\n   * 获取最后一个可undo的元素\n   */\n  getLastElement(): IUndoRedoElement;\n  /**\n   * 获取undo栈\n   */\n  getUndoStack(): IUndoRedoElement[];\n  /**\n   * 获取redo栈\n   */\n  getRedoStack(): IUndoRedoElement[];\n  /**\n   * 清空redo栈\n   */\n  clearRedoStack(): void;\n  /**\n   * 是否可undo\n   */\n  canUndo(): boolean;\n  /**\n   * 执行undo\n   */\n  undo(): Promise<void> | void;\n  /**\n   * 是否可redo\n   */\n  canRedo(): boolean;\n  /**\n   * 执行redo\n   */\n  redo(): Promise<void> | void;\n  /**\n   * 清空 undo和redo栈\n   */\n  clear(): void;\n}\n\n/**\n * UndoRedo元素\n */\nexport interface IUndoRedoElement extends Disposable {\n  /**\n   * 操作标题\n   */\n  readonly label?: string;\n  /**\n   * 操作描述\n   */\n  readonly description?: string;\n  /**\n   * 撤销\n   */\n  undo(): Promise<void> | void;\n  /**\n   * 重做\n   */\n  redo(): Promise<void> | void;\n  /**\n   * 添加一个操作\n   * @param operation 操作\n   */\n  pushOperation(operation: Operation): Operation;\n  /**\n   * 获取所有操作\n   */\n  getOperations(): Operation[];\n  /**\n   * 获取第一个操作\n   */\n  getFirstOperation(): Operation;\n  /**\n   * 获取最后一个操作\n   */\n  getLastOperation(): Operation;\n  /**\n   * 获取修改的操作\n   */\n  getChangeOperations(type: UndoRedoChangeType): Operation[];\n}\n\n/**\n * 操作注册\n */\nexport interface IOperationRegistry {\n  register(type: string, factory: IUndoRedoElementFactory<unknown>): void;\n}\n\n/**\n * 操作工厂\n */\nexport type IUndoRedoElementFactory<OperationValue> = (\n  operation: Operation<OperationValue>\n) => IUndoRedoElement;\n\n/**\n * undo redo 类型\n */\nexport enum UndoRedoChangeType {\n  UNDO = 'undo',\n  REDO = 'redo',\n  PUSH = 'push',\n  CLEAR = 'clear',\n}\n\n/**\n * 带element的事件\n */\nexport interface UndoRedoChangeElementEvent {\n  type: UndoRedoChangeType.PUSH | UndoRedoChangeType.UNDO | UndoRedoChangeType.REDO;\n  element: IUndoRedoElement;\n}\n/**\n * 清空事件\n */\nexport interface UndoRedoClearEvent {\n  type: UndoRedoChangeType.CLEAR;\n}\n/**\n * undo redo变化事件\n */\nexport type UndoRedoChangeEvent = UndoRedoChangeElementEvent | UndoRedoClearEvent;\n\nexport interface HistoryStackItem {\n  id: string;\n  type: UndoRedoChangeType;\n  timestamp: number;\n  operations: Operation[];\n  uri?: string;\n}\n\n/**\n * 历史栈变化类型\n */\nexport enum HistoryStackChangeType {\n  ADD = 'add',\n  UPDATE = 'update',\n  CLEAR = 'clear',\n  ADD_OPERATION = 'add_operation',\n  UPDATE_OPERATION = 'update_operation',\n}\n\n/**\n * 历史栈变化事件基础\n */\nexport interface HistoryStackBaseEvent {\n  type: HistoryStackChangeType;\n  value?: any;\n  service: HistoryService;\n}\n\n/**\n * 添加历史事件\n */\nexport interface HistoryStackAddEvent extends HistoryStackBaseEvent {\n  type: HistoryStackChangeType.ADD;\n  value: HistoryItem;\n}\n\n/**\n * 更新历史事件\n */\nexport interface HistoryStackUpdateEvent extends HistoryStackBaseEvent {\n  type: HistoryStackChangeType.UPDATE;\n  value: HistoryItem;\n}\n\n/**\n * 添加操作事件\n */\nexport interface HistoryStackAddOperationEvent extends HistoryStackBaseEvent {\n  type: HistoryStackChangeType.ADD_OPERATION;\n  value: {\n    historyItem: HistoryItem;\n    operation: HistoryOperation;\n  };\n}\n\n/**\n * 更新操作事件\n */\nexport interface HistoryStackUpdateOperationEvent extends HistoryStackBaseEvent {\n  type: HistoryStackChangeType.UPDATE_OPERATION;\n  value: {\n    historyItem: HistoryItem;\n    operation: HistoryOperation;\n  };\n}\n\n/**\n * 历史记录变化事件\n */\nexport type HistoryStackChangeEvent =\n  | HistoryStackAddEvent\n  | HistoryStackUpdateEvent\n  | HistoryStackAddOperationEvent\n  | HistoryStackUpdateOperationEvent;\n\nexport enum HistoryMergeEventType {\n  ADD = 'ADD',\n  UPDATE = 'UPDATE',\n}\n\n/**\n * 历史合并事件\n */\nexport type HistoryMergeEvent =\n  | {\n      type: HistoryMergeEventType.ADD;\n      value: {\n        element: IUndoRedoElement;\n        operation: Operation;\n      };\n    }\n  | {\n      type: HistoryMergeEventType.UPDATE;\n      value: {\n        element: IUndoRedoElement;\n        operation: Operation;\n        value: any;\n      };\n    };\n"
  },
  {
    "path": "packages/common/history/src/history/undo-redo-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Emitter, DisposableCollection } from '@flowgram.ai/utils';\n\nimport {\n  IUndoRedoElement,\n  IUndoRedoService,\n  UndoRedoChangeType,\n  UndoRedoChangeEvent,\n  UndoRedoClearEvent,\n} from './types';\n\n@injectable()\nexport class UndoRedoService implements IUndoRedoService {\n  private _undoStack: IUndoRedoElement[];\n\n  private _redoStack: IUndoRedoElement[];\n\n  private _undoing: boolean = false;\n\n  private _redoing: boolean = false;\n\n  private _limit: number = 100;\n\n  protected onChangeEmitter = new Emitter<UndoRedoChangeEvent>();\n\n  readonly onChange = this.onChangeEmitter.event;\n\n  readonly _toDispose = new DisposableCollection();\n\n  constructor() {\n    this._undoStack = [];\n    this._redoStack = [];\n    this._toDispose.push(this.onChangeEmitter);\n  }\n\n  setLimit(limit: number) {\n    this._limit = limit;\n  }\n\n  pushElement(element: IUndoRedoElement): void {\n    this._redoStack = [];\n    this._stackPush(this._undoStack, element);\n    this._toDispose.push(element);\n    this._emitChange(UndoRedoChangeType.PUSH, element);\n  }\n\n  getUndoStack() {\n    return this._undoStack;\n  }\n\n  getRedoStack() {\n    return this._redoStack;\n  }\n\n  getLastElement() {\n    return this._undoStack[this._undoStack.length - 1];\n  }\n\n  /**\n   * 执行undo\n   * @returns void\n   */\n  async undo(): Promise<void> {\n    if (!this.canUndo()) {\n      return;\n    }\n\n    if (this._undoing) {\n      return;\n    }\n    this._undoing = true;\n\n    const item = this._undoStack.pop() as IUndoRedoElement;\n\n    try {\n      await item.undo();\n    } finally {\n      this._stackPush(this._redoStack, item);\n      this._emitChange(UndoRedoChangeType.UNDO, item);\n      this._undoing = false;\n    }\n  }\n\n  /**\n   * 执行redo\n   * @returns void\n   */\n  async redo(): Promise<void> {\n    if (!this.canRedo()) {\n      return;\n    }\n\n    if (this._redoing) {\n      return;\n    }\n    this._redoing = true;\n\n    const item = this._redoStack.pop() as IUndoRedoElement;\n\n    try {\n      await item.redo();\n    } finally {\n      this._stackPush(this._undoStack, item);\n      this._emitChange(UndoRedoChangeType.REDO, item);\n      this._redoing = false;\n    }\n  }\n\n  /**\n   * 是否可undo\n   * @returns true代表可以，false代表不可以\n   */\n  canUndo(): boolean {\n    return this._undoStack.length > 0;\n  }\n\n  /**\n   * 是否可redo\n   * @returns true代表可以，false代表不可以\n   */\n  canRedo(): boolean {\n    return this._redoStack.length > 0;\n  }\n\n  /**\n   * 是否可以push\n   * @returns true代表可以，false代表不可以\n   */\n  canPush(): boolean {\n    return !this._redoing && !this._undoing;\n  }\n\n  /**\n   * 清空\n   */\n  clear() {\n    this.clearRedoStack();\n    this.clearUndoStack();\n    this._emitChange(UndoRedoChangeType.CLEAR);\n  }\n\n  /**\n   * 清空redo栈\n   */\n  clearRedoStack(): void {\n    this._redoStack.forEach(element => {\n      element.dispose();\n    });\n    this._redoStack = [];\n  }\n\n  /**\n   * 清空undo栈\n   */\n  clearUndoStack(): void {\n    this._undoStack.forEach(element => {\n      element.dispose();\n    });\n    this._undoStack = [];\n  }\n\n  /**\n   * 销毁\n   */\n  dispose(): void {\n    this.clear();\n    this._toDispose.dispose();\n  }\n\n  private _stackPush(stack: IUndoRedoElement[], element: IUndoRedoElement) {\n    stack.push(element);\n    if (stack.length > this._limit) {\n      stack.shift();\n    }\n  }\n\n  private _emitChange(type: UndoRedoChangeType, element?: IUndoRedoElement) {\n    if (element) {\n      this.onChangeEmitter.fire({ type, element });\n    } else {\n      this.onChangeEmitter.fire({ type } as UndoRedoClearEvent);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/history-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { injectable } from 'inversify';\n\n@injectable()\nexport class HistoryConfig {\n  generateId: () => string = () => nanoid();\n\n  getSnapshot: () => unknown = () => '';\n}\n"
  },
  {
    "path": "packages/common/history/src/history-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { OperationRegistry, OperationService } from './operation';\nimport { HistoryContext } from './history-context';\nimport {\n  HistoryService,\n  HistoryStack,\n  HistoryManager,\n  UndoRedoService,\n  HistoryConfig,\n} from './history';\n\nexport const HistoryContainerModule = new ContainerModule(\n  (bind, _unbind, _isBound, _rebind, _unbindAsync, onActivation, _onDeactivation) => {\n    bind(OperationRegistry).toSelf().inSingletonScope();\n    bind(OperationService).toSelf().inSingletonScope();\n    bind(UndoRedoService).toSelf().inSingletonScope();\n    bind(HistoryService).toSelf().inSingletonScope();\n    bind(HistoryContext).toSelf().inSingletonScope();\n    bind(HistoryManager).toSelf().inSingletonScope();\n    bind(HistoryStack).toSelf().inSingletonScope();\n    bind(HistoryConfig).toSelf().inSingletonScope();\n\n    onActivation(HistoryService, (ctx, historyService) => {\n      let historyManager;\n\n      if (ctx.container?.parent?.isBound(HistoryManager)) {\n        historyManager = ctx.container?.parent?.get(HistoryManager);\n      } else {\n        historyManager = ctx.container.get(HistoryManager);\n      }\n\n      if (!historyManager) {\n        return historyService;\n      }\n\n      historyService.historyManager = historyManager;\n      historyManager.registerHistoryService(historyService);\n      return historyService;\n    });\n  }\n);\n"
  },
  {
    "path": "packages/common/history/src/history-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\n@injectable()\nexport class HistoryContext {\n  /**\n   * 所属uri\n   */\n  uri?: string;\n\n  /**\n   * 操作触发的源对象，如编辑器对象\n   */\n  source?: unknown;\n}\n"
  },
  {
    "path": "packages/common/history/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './operation';\nexport * from './history';\nexport * from './history-container-module';\nexport * from './create-history-plugin';\nexport * from './history-context';\n"
  },
  {
    "path": "packages/common/history/src/operation/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './operation-contribution';\nexport * from './operation-registry';\nexport * from './operation-service';\nexport * from './types';\n"
  },
  {
    "path": "packages/common/history/src/operation/operation-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OperationRegistry } from './operation-registry';\n\nexport const OperationContribution = Symbol('OperationContribution');\n\nexport interface OperationContribution {\n  registerOperationMeta?(operationRegistry: OperationRegistry): void;\n}\n"
  },
  {
    "path": "packages/common/history/src/operation/operation-registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, multiInject, optional, postConstruct } from 'inversify';\nimport { Disposable, DisposableCollection } from '@flowgram.ai/utils';\n\nimport { OperationMeta } from './types';\nimport { OperationContribution } from './operation-contribution';\n\n@injectable()\nexport class OperationRegistry {\n  private readonly _operationMetas: Map<string, OperationMeta> = new Map();\n\n  @multiInject(OperationContribution)\n  @optional()\n  protected readonly contributions: OperationContribution[] = [];\n\n  @postConstruct()\n  protected init() {\n    for (const contrib of this.contributions) {\n      contrib.registerOperationMeta?.(this);\n    }\n  }\n\n  /**\n   * 注册操作的元数据\n   * @param operationMeta 操作的元数据\n   * @returns 销毁函数\n   */\n  registerOperationMeta(operationMeta: OperationMeta): Disposable {\n    if (this._operationMetas.has(operationMeta.type)) {\n      console.warn(`A operation meta ${operationMeta.type} is already registered.`);\n      return Disposable.NULL;\n    }\n    const toDispose = new DisposableCollection(this._doRegisterOperationMetaMeta(operationMeta));\n    return toDispose;\n  }\n\n  /**\n   * 获取操作的元数据\n   * @param type 操作类型\n   * @returns 操作的元数据\n   */\n  getOperationMeta(type: string): OperationMeta | undefined {\n    return this._operationMetas.get(type);\n  }\n\n  private _doRegisterOperationMetaMeta(operationMeta: OperationMeta): Disposable {\n    this._operationMetas.set(operationMeta.type, operationMeta);\n    return {\n      dispose: () => {\n        this._operationMetas.delete(operationMeta.type);\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/operation/operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject, postConstruct } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { HistoryContext } from '../history-context';\nimport { HistoryConfig } from '../history-config';\nimport { Operation } from './types';\nimport { OperationRegistry } from './operation-registry';\n\n@injectable()\nexport class OperationService {\n  @inject(OperationRegistry)\n  readonly operationRegistry: OperationRegistry;\n\n  @inject(HistoryContext)\n  readonly context: HistoryContext;\n\n  @inject(HistoryConfig)\n  config: HistoryConfig;\n\n  readonly applyEmitter = new Emitter<Operation>();\n\n  readonly onApply = this.applyEmitter.event;\n\n  private _toDispose = new DisposableCollection();\n\n  @postConstruct()\n  init() {\n    this._toDispose.push(this.applyEmitter);\n  }\n\n  /**\n   * 执行操作\n   * @param op\n   * @returns\n   */\n  applyOperation(op: Operation, options?: { noApply?: boolean }): any {\n    const meta = this.operationRegistry.getOperationMeta(op.type);\n\n    if (!meta) {\n      throw new Error(`Operation meta ${op.type} has not registered.`);\n    }\n\n    let res;\n    if (!options?.noApply) {\n      res = meta.apply(op, this.context.source);\n    }\n\n    this.applyEmitter.fire(op);\n\n    return res;\n  }\n\n  /**\n   * 根据操作类型获取操作的label\n   * @param operation 操作\n   * @returns\n   */\n  getOperationLabel(operation: Operation): string | undefined {\n    const operationMeta = this.operationRegistry.getOperationMeta(operation.type);\n\n    if (operationMeta && operationMeta.getLabel) {\n      return operationMeta.getLabel(operation, this.context.source);\n    }\n  }\n\n  /**\n   * 根据操作类型获取操作的description\n   * @param operation 操作\n   * @returns\n   */\n  getOperationDescription(operation: Operation): string | undefined {\n    const operationMeta = this.operationRegistry.getOperationMeta(operation.type);\n\n    if (operationMeta && operationMeta.getDescription) {\n      return operationMeta.getDescription(operation, this.context.source);\n    }\n  }\n\n  /**\n   * 操作取反\n   * @param operations\n   * @returns\n   */\n  inverseOperations(operations: Operation[]) {\n    return operations.map(op => this.inverseOperation(op)).reverse();\n  }\n\n  inverseOperation(op: Operation): Operation {\n    const meta = this.operationRegistry.getOperationMeta(op.type);\n\n    if (!meta) {\n      throw new Error(`Operation meta ${op.type} has not registered.`);\n    }\n    return meta.inverse(op);\n  }\n\n  dispose() {\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/common/history/src/operation/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { StackOperation } from '../history';\n\n/**\n * 操作\n */\nexport interface Operation<OperationValue = unknown> {\n  /**\n   * 操作的类型 如insert_node, move_node等\n   */\n  type: string;\n  /**\n   * 操作的值 外部自定义\n   */\n  value: OperationValue;\n  /**\n   * 资源唯一标志\n   */\n  uri?: string;\n  /**\n   * 操作触发源头\n   */\n  origin?: string | Symbol;\n}\n\nexport type OperationWithId = Operation & { id: string };\n\n/**\n * push操作配置\n */\nexport interface PushOperationOptions {\n  noApply?: boolean;\n}\n\n/**\n * 操作历史\n */\nexport interface HistoryOperation extends Operation {\n  /**\n   * 唯一id\n   */\n  id: string;\n  /**\n   * 显示名称\n   */\n  label?: string;\n  /**\n   * 描述\n   */\n  description?: string;\n  /**\n   * 时间戳\n   */\n  timestamp: number;\n}\n\n/**\n * 操作元数据\n */\nexport interface OperationMeta<OperationValue = any, Source = any, ApplyResult = any> {\n  /**\n   * 操作类型 需要唯一\n   */\n  type: string;\n  /**\n   * 将一个操作转换成另一个逆操作， 如insert转成delete\n   * @param op 操作\n   * @returns 逆操作\n   */\n  inverse: (op: Operation<OperationValue>) => Operation<OperationValue>;\n  /**\n   * 判断是否可以合并\n   * @param op 操作\n   * @param prev 上一个操作\n   * @returns true表示可以合并 返回一个操作表示直接用新操作替换之前的操作\n   */\n  shouldMerge?: (\n    op: Operation<OperationValue>,\n    prev: Operation<OperationValue> | undefined,\n    stackItem: StackOperation\n  ) => boolean | Operation;\n  /**\n   * 判断是否需要保存，如选中等操作可以不保存\n   * @param op 操作\n   * @returns true表示可以保存\n   */\n  shouldSave?: (op: Operation<OperationValue>) => boolean;\n  /**\n   * 执行操作\n   * @param operation 操作\n   */\n  apply(operation: Operation<OperationValue>, source: Source): ApplyResult | Promise<ApplyResult>;\n  /**\n   * 获取标签\n   */\n  getLabel?: (operation: Operation<OperationValue>, source: Source) => string;\n  /**\n   * 获取描述\n   */\n  getDescription?: (operation: Operation<OperationValue>, source: Source) => string;\n  /**\n   * 获取uri\n   */\n  getURI?: (operation: Operation<OperationValue>, source: Source) => string | undefined;\n}\n"
  },
  {
    "path": "packages/common/history/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"]\n  },\n  \"include\": [\"./src\", \"./__mocks__\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/common/history/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    coverage: {\n      exclude: ['setup/**', '**/*.mock.*'],\n    },\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/common/history/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/common/history-storage/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/common/history-storage/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/history-storage\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"test:update\": \"vitest run --update\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"dexie\": \"4.0.4\",\n    \"dexie-react-hooks\": \"1.1.7\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"fake-indexeddb\": \"5.0.2\",\n    \"jsdom\": \"^26.1.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/history-storage/src/__mocks__/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const MOCK_RESOURCE_URI1 = 'resource-uri1'\nexport const MOCK_RESOURCE_URI2 = 'resource-uri2'\n\nexport const MOCK_HISTORY1 = {\n  resourceURI: MOCK_RESOURCE_URI1,\n  uuid: 'history1',\n  timestamp: 111,\n  type: 'push',\n  resourceJSON: 'resourceJSON',\n}\n\nexport const MOCK_HISTORY2 = {\n  resourceURI: MOCK_RESOURCE_URI2,\n  uuid: 'history2',\n  timestamp: 111,\n  type: 'push',\n  resourceJSON: 'resourceJSON',\n}\n\nexport const MOCK_OPERATION1 = {\n  historyId: 'history1',\n  uri: 'test-1',\n  uuid: 'operation1',\n  type: 'addFromNode',\n  value: 'value1',\n  resourceURI: MOCK_RESOURCE_URI1,\n  label: 'operation1-label',\n  description: 'operation1-description',\n  timestamp: 1,\n}\n\nexport const MOCK_OPERATION2 = {\n  historyId: 'history1',\n  uri: 'test-2',\n  uuid: 'operation2',\n  type: 'deleteFromNode',\n  value: 'value2',\n  resourceURI: MOCK_RESOURCE_URI1,\n  label: 'operation2-label',\n  description: 'operation2-description',\n  timestamp: 2,\n}\n\nexport const MOCK_OPERATION3 = {\n  historyId: 'history1',\n  uri: 'test-3',\n  uuid: 'operation3',\n  type: 'addText',\n  value: 'value3',\n  resourceURI: MOCK_RESOURCE_URI1,\n  label: 'operation3-label',\n  description: 'operation3-description',\n  timestamp: 3,\n}"
  },
  {
    "path": "packages/common/history-storage/src/__tests__/history-database.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, beforeEach } from 'vitest';\nimport { cloneDeep, omit } from 'lodash-es';\n\nimport { HistoryOperationRecord, HistoryRecord } from '../types';\nimport { HistoryDatabase } from '../history-database';\nimport {\n  MOCK_HISTORY1,\n  MOCK_HISTORY2,\n  MOCK_OPERATION1,\n  MOCK_OPERATION2,\n  MOCK_OPERATION3,\n  MOCK_RESOURCE_URI1,\n} from '../__mocks__';\n\ndescribe('history-database', () => {\n  let db: HistoryDatabase;\n  let history1: HistoryRecord;\n  let history2: HistoryRecord;\n  let operation1: HistoryOperationRecord;\n  let operation2: HistoryOperationRecord;\n\n  beforeEach(async () => {\n    db = new HistoryDatabase();\n    await db.reset();\n    history1 = cloneDeep(MOCK_HISTORY1);\n    history2 = cloneDeep(MOCK_HISTORY2);\n    operation1 = cloneDeep(MOCK_OPERATION1);\n    operation2 = cloneDeep(MOCK_OPERATION2);\n  });\n\n  it('addHistoryRecord allHistoryByResourceURI allOperationByResourceURI', async () => {\n    const operations = [operation1, operation2];\n    const res = await db.addHistoryRecord(history1, operations);\n    await db.addHistoryRecord(history2, []);\n    expect(res.length).toEqual(2);\n    const [dbHistory] = await db.allHistoryByResourceURI(MOCK_RESOURCE_URI1);\n    expect(MOCK_HISTORY1).toEqual(omit(dbHistory, ['id']));\n    const dbOperations = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    expect(operations).toEqual(dbOperations.map((o) => omit(o, ['id'])));\n\n    const operation3 = cloneDeep(MOCK_OPERATION3);\n\n    await db.addOperationRecord(operation3);\n\n    const dbOperations3 = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    expect([MOCK_OPERATION1, MOCK_OPERATION2, MOCK_OPERATION3]).toEqual(\n      dbOperations3.map((o) => omit(o, ['id']))\n    );\n  });\n\n  it('getHistoryByUUID', async () => {\n    await db.addHistoryRecord(history1, []);\n    const res = await db.getHistoryByUUID(history1.uuid);\n    expect(omit(res, ['id'])).toEqual(MOCK_HISTORY1);\n  });\n\n  it('updateHistoryByUUID', async () => {\n    await db.addHistoryRecord(history1, []);\n    const dbHistory = await db.getHistoryByUUID(history1.uuid);\n    if (!dbHistory) {\n      throw new Error('no dbHistory');\n    }\n    const resourceJSON = 'newResourceJSON';\n    await db.updateHistoryByUUID(dbHistory.uuid, {\n      resourceJSON,\n    });\n    const [dbHistory1] = await db.allHistoryByResourceURI(MOCK_RESOURCE_URI1);\n    expect(dbHistory1.resourceJSON).toEqual(resourceJSON);\n  });\n\n  it('addOperationRecord', async () => {\n    await db.addOperationRecord(operation1);\n    await db.addOperationRecord(operation2);\n    const dbOperations = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    expect([MOCK_OPERATION1, MOCK_OPERATION2]).toEqual(dbOperations.map((o) => omit(o, ['id'])));\n  });\n\n  it('updateOperationRecord', async () => {\n    await db.addOperationRecord(operation1);\n    await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n\n    await db.updateOperationRecord({ ...MOCK_OPERATION2, uuid: MOCK_OPERATION1.uuid });\n\n    const [dbUpdatedOperation1] = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    expect(omit(MOCK_OPERATION2, ['uuid'])).toEqual(omit(dbUpdatedOperation1, ['id', 'uuid']));\n  });\n\n  it('reset', async () => {\n    await db.addHistoryRecord(history1, [operation1, operation2]);\n    await db.reset();\n    const dbOperation = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    const dbHistory = await db.allHistoryByResourceURI(MOCK_RESOURCE_URI1);\n    expect(dbOperation.length).toEqual(0);\n    expect(dbHistory.length).toEqual(0);\n  });\n\n  it('resetByResourceURI', async () => {\n    await db.addHistoryRecord(history1, [operation1, operation2]);\n    await db.resetByResourceURI(MOCK_RESOURCE_URI1);\n    const dbOperation = await db.allOperationByResourceURI(MOCK_RESOURCE_URI1);\n    const dbHistory = await db.allHistoryByResourceURI(MOCK_RESOURCE_URI1);\n    expect(dbOperation.length).toEqual(0);\n    expect(dbHistory.length).toEqual(0);\n  });\n\n  it('resourceStorageLimit', async () => {\n    db.resourceStorageLimit = 1;\n    await db.addHistoryRecord(history1, []);\n    await db.addHistoryRecord(history2, []);\n    const res = await db.allHistoryByResourceURI(MOCK_RESOURCE_URI1);\n    expect(res.length).toEqual(1);\n  });\n});\n"
  },
  {
    "path": "packages/common/history-storage/src/create-history-storage-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { HistoryStoragePluginOptions } from './types';\nimport { HistoryStorageManager } from './history-storage-manager';\nimport { HistoryStorageContainerModule } from './history-storage-container-module';\n\nexport const createHistoryStoragePlugin = definePluginCreator<HistoryStoragePluginOptions>({\n  onBind: ({ bind, rebind }) => {},\n  onInit(ctx, opts): void {\n    const historyStorageManager = ctx.get<HistoryStorageManager>(HistoryStorageManager);\n    historyStorageManager.onInit(ctx, opts);\n  },\n  onDispose(ctx) {\n    const historyStorageManager = ctx.get<HistoryStorageManager>(HistoryStorageManager);\n    historyStorageManager.dispose();\n  },\n  containerModules: [HistoryStorageContainerModule],\n});\n"
  },
  {
    "path": "packages/common/history-storage/src/history-database.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport Dexie, { type Table } from 'dexie';\n\nimport { HistoryOperationRecord, HistoryRecord } from './types';\n\n/**\n * 历史数据库\n */\nexport class HistoryDatabase extends Dexie {\n  readonly history: Table<HistoryRecord>;\n\n  readonly operation: Table<HistoryOperationRecord>;\n\n  resourceStorageLimit: number = 100;\n\n  constructor(databaseName: string = 'ide-history-storage') {\n    super(databaseName);\n    this.version(1).stores({\n      history: '++id, &uuid, resourceURI',\n      operation: '++id, &uuid, historyId, uri, resourceURI',\n    });\n  }\n\n  /**\n   * 某个uri下所有的history记录\n   * @param resourceURI 资源uri\n   * @returns\n   */\n  allHistoryByResourceURI(resourceURI: string) {\n    return this.history.where({ resourceURI }).toArray();\n  }\n\n  /**\n   * 根据uuid获取历史\n   * @param uuid\n   * @returns\n   */\n  getHistoryByUUID(uuid: string) {\n    return this.history.get({ uuid });\n  }\n\n  /**\n   * 某个uri下所有的operation记录\n   * @param resourceURI 资源uri\n   * @returns\n   */\n  allOperationByResourceURI(resourceURI: string) {\n    return this.operation.where({ resourceURI }).toArray();\n  }\n\n  /**\n   * 添加历史记录\n   * @param history 历史记录\n   * @param operations 操作记录\n   * @returns\n   */\n  addHistoryRecord(history: HistoryRecord, operations: HistoryOperationRecord[]) {\n    return this.transaction('rw', this.history, this.operation, async () => {\n      const count = await this.history.where({ resourceURI: history.resourceURI }).count();\n      if (count >= this.resourceStorageLimit) {\n        const limit = count - this.resourceStorageLimit;\n        const items = await this.history\n          .where({ resourceURI: history.resourceURI })\n          .limit(limit)\n          .toArray();\n        const ids = items.map(i => i.id);\n        const uuid = items.map(i => i.uuid);\n        await Promise.all([\n          this.history.bulkDelete(ids),\n          ...uuid.map(async uuid => {\n            await this.operation.where({ historyId: uuid }).delete();\n          }),\n        ]);\n      }\n\n      return Promise.all([this.history.add(history), this.operation.bulkAdd(operations)]);\n    });\n  }\n\n  /**\n   * 更新历史记录\n   * @param historyRecord\n   * @returns\n   */\n  async updateHistoryByUUID(uuid: string, historyRecord: Partial<HistoryRecord>) {\n    const history = await this.getHistoryByUUID(uuid);\n    if (!history) {\n      console.warn('no history record found');\n      return;\n    }\n    return this.history.update(history.id, historyRecord);\n  }\n\n  /**\n   * 添加操作记录\n   * @param record 操作记录\n   * @returns\n   */\n  addOperationRecord(record: HistoryOperationRecord) {\n    return this.operation.add(record);\n  }\n\n  /**\n   * 更新操作记录\n   * @param record 操作记录\n   * @returns\n   */\n  async updateOperationRecord(record: HistoryOperationRecord) {\n    const op = await this.operation.where({ uuid: record.uuid }).first();\n    if (!op) {\n      console.warn('no operation record found');\n      return;\n    }\n    return this.operation.put({\n      id: op.id,\n      ...record,\n    });\n  }\n\n  /**\n   * 重置数据库\n   * @returns\n   */\n  reset() {\n    return this.transaction('rw', this.history, this.operation, async () => {\n      await Promise.all(this.tables.map(table => table.clear()));\n    });\n  }\n\n  /**\n   * 清空某个资源下所有的数据\n   * @param resourceURI\n   * @returns\n   */\n  resetByResourceURI(resourceURI: string) {\n    return this.transaction('rw', this.history, this.operation, async () => {\n      await Promise.all(this.tables.map(table => table.where({ resourceURI }).delete()));\n    });\n  }\n}\n"
  },
  {
    "path": "packages/common/history-storage/src/history-storage-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { HistoryStorageManager } from './history-storage-manager';\n\nexport const HistoryStorageContainerModule = new ContainerModule(bind => {\n  bind(HistoryStorageManager).toSelf().inSingletonScope();\n});\n"
  },
  {
    "path": "packages/common/history-storage/src/history-storage-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport {\n  HistoryItem,\n  HistoryManager,\n  HistoryOperation,\n  HistoryStackChangeType,\n  HistoryService,\n  HistoryStackAddOperationEvent,\n  HistoryStackUpdateOperationEvent,\n} from '@flowgram.ai/history';\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport { HistoryOperationRecord, HistoryRecord, HistoryStoragePluginOptions } from './types';\nimport { HistoryDatabase } from './history-database';\n\n/**\n * 历史存储管理\n */\n@injectable()\nexport class HistoryStorageManager {\n  private _toDispose = new DisposableCollection();\n\n  db: HistoryDatabase;\n\n  @inject(HistoryManager)\n  protected historyManager: HistoryManager;\n\n  /**\n   * 初始化\n   * @param ctx\n   */\n  onInit(_ctx: PluginContext, opts: HistoryStoragePluginOptions) {\n    this.db = new HistoryDatabase(opts?.databaseName);\n\n    if (opts?.resourceStorageLimit) {\n      this.db.resourceStorageLimit = opts.resourceStorageLimit;\n    }\n\n    this._toDispose.push(\n      this.historyManager.historyStack.onChange(event => {\n        if (event.type === HistoryStackChangeType.ADD) {\n          const [history, operations] = this.historyItemToRecord(event.service, event.value);\n          this.db.addHistoryRecord(history, operations).catch(console.error);\n        }\n\n        // operation merge的时候需要更新snapshot\n        if (\n          [HistoryStackChangeType.ADD_OPERATION, HistoryStackChangeType.UPDATE_OPERATION].includes(\n            event.type,\n          )\n        ) {\n          const {\n            service,\n            value: { historyItem },\n          } = event as HistoryStackAddOperationEvent | HistoryStackUpdateOperationEvent;\n          // 更新快照\n          this.db\n            .updateHistoryByUUID(historyItem.id, {\n              resourceJSON: service.getSnapshot() || '',\n            })\n            .catch(console.error);\n        }\n\n        if (event.type === HistoryStackChangeType.ADD_OPERATION) {\n          const operationRecord: HistoryOperationRecord = this.historyOperationToRecord(\n            event.value.historyItem,\n            event.value.operation,\n          );\n          this.db.addOperationRecord(operationRecord).catch(console.error);\n        }\n        if (event.type === HistoryStackChangeType.UPDATE_OPERATION) {\n          const operationRecord: HistoryOperationRecord = this.historyOperationToRecord(\n            event.value.historyItem,\n            event.value.operation,\n          );\n          this.db.updateOperationRecord(operationRecord).catch(console.error);\n        }\n      }),\n    );\n  }\n\n  /**\n   * 内存历史转数据表记录\n   * @param historyItem\n   * @returns\n   */\n  historyItemToRecord(\n    historyService: HistoryService,\n    historyItem: HistoryItem,\n  ): [HistoryRecord, HistoryOperationRecord[]] {\n    const operations = historyItem.operations.map(op =>\n      this.historyOperationToRecord(historyItem, op),\n    );\n\n    return [\n      {\n        uuid: historyItem.id,\n        timestamp: historyItem.timestamp,\n        type: historyItem.type,\n        resourceURI: historyItem.uri?.toString() || '',\n        resourceJSON: historyService.getSnapshot() || '',\n      },\n      operations,\n    ];\n  }\n\n  /**\n   * 内存操作转数据表操作\n   * @param historyItem\n   * @param op\n   * @returns\n   */\n  historyOperationToRecord(historyItem: HistoryItem, op: HistoryOperation): HistoryOperationRecord {\n    return {\n      uuid: op.id,\n      type: op.type,\n      timestamp: op.timestamp,\n      label: op.label || '',\n      uri: op?.uri?.toString() || '',\n      resourceURI: historyItem.uri?.toString() || '',\n      description: op.description || '',\n      value: JSON.stringify(op.value),\n      historyId: historyItem.id,\n    };\n  }\n\n  /**\n   * 销毁\n   */\n  dispose() {\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/common/history-storage/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-history-storage-plugin';\nexport * from './use-storage-hisotry-items';\nexport * from './types';\nexport * from './history-database';\nexport * from './history-storage-container-module';\nexport * from './history-storage-manager';\n"
  },
  {
    "path": "packages/common/history-storage/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface HistoryRecord {\n  /**\n   * 自增id\n   */\n  id?: number;\n  /**\n   * 唯一标识\n   */\n  uuid: string;\n  /**\n   * 类型 如 push undo redo\n   */\n  type: string;\n  /**\n   * 时间戳\n   */\n  timestamp: number;\n  /**\n   * 资源uri\n   */\n  resourceURI: string;\n  /**\n   * 资源json\n   */\n  resourceJSON: unknown;\n}\n\nexport interface HistoryOperationRecord {\n  /**\n   * 自增id\n   */\n  id?: number;\n  /**\n   * 唯一标识\n   */\n  uuid: string;\n  /**\n   * 历史记录唯一标志，记录的uuid\n   */\n  historyId: string;\n  /**\n   * 类型，如 addFromNode deleteFromNode\n   */\n  type: string;\n  /**\n   * 操作值，不同类型不同，json字符串\n   */\n  value: string;\n  /**\n   * uri操作对象uri，如某个node的uri\n   */\n  uri: string;\n  /**\n   * 操作资源uri，如某个流程的uri\n   */\n  resourceURI: string;\n  /**\n   * 操作显示标题\n   */\n  label: string;\n  /**\n   * 操作显示描述\n   */\n  description: string;\n  /**\n   * 时间戳\n   */\n  timestamp: number;\n}\n\n/**\n * 插件配置\n */\nexport interface HistoryStoragePluginOptions {\n  /**\n   * 数据库名称\n   */\n  databaseName?: string;\n  /**\n   * 每个资源最大历史记录数量\n   */\n  resourceStorageLimit?: number;\n}\n"
  },
  {
    "path": "packages/common/history-storage/src/use-storage-hisotry-items.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { groupBy } from 'lodash-es';\nimport { useLiveQuery } from 'dexie-react-hooks';\nimport { HistoryItem, HistoryOperation, HistoryStack } from '@flowgram.ai/history';\n\nimport { HistoryStorageManager } from './history-storage-manager';\nexport function useStorageHistoryItems(\n  historyStorageManager: HistoryStorageManager,\n  resourceURI: string\n): {\n  items: HistoryItem[];\n} {\n  const items: HistoryItem[] =\n    useLiveQuery(async () => {\n      const [historyItems, operations] = await Promise.all([\n        historyStorageManager.db.allHistoryByResourceURI(resourceURI),\n        historyStorageManager.db.allOperationByResourceURI(resourceURI),\n      ]);\n\n      const grouped = groupBy<HistoryOperation>(\n        operations.map((o) => ({\n          id: o.uuid,\n          timestamp: o.timestamp,\n          type: o.type,\n          label: o.label,\n          description: o.description,\n          value: o.value ? JSON.parse(o.value) : undefined,\n          uri: o.uri,\n          historyId: o.historyId,\n        })),\n        'historyId'\n      );\n      return historyItems\n        .sort((a, b) => (b.id as number) - (a.id as number))\n        .map(\n          (historyItem) =>\n            ({\n              id: historyItem.uuid,\n              type: historyItem.type,\n              timestamp: historyItem.timestamp,\n              operations: grouped[historyItem.uuid] || [],\n              time: HistoryStack.dateFormat(historyItem.timestamp),\n              uri: historyItem.resourceURI,\n            } as HistoryItem)\n        );\n    }, [resourceURI]) || [];\n\n  return {\n    items,\n  };\n}\n"
  },
  {
    "path": "packages/common/history-storage/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"]\n  },\n  \"include\": [\"./src\", \"./__mocks__\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/common/history-storage/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    coverage: {\n      exclude: ['setup/**', '**/*.mock.*'],\n    },\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/common/history-storage/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\nimport 'fake-indexeddb/auto';\n"
  },
  {
    "path": "packages/common/i18n/__tests__/i18n.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\n\nimport { I18n } from '../src';\n\ndescribe('i18n', () => {\n  it('default', () => {\n    expect(I18n.locale).toBe('en-US');\n  });\n  it('setLocal', () => {\n    let changeTimes = 0;\n    let dispose = I18n.onLanguageChange((langId) => {\n      changeTimes++;\n    });\n    I18n.locale = 'en-US';\n    expect(changeTimes).toEqual(0);\n    I18n.locale = 'zh-CN';\n    expect(changeTimes).toEqual(1);\n    dispose.dispose();\n    I18n.locale = 'en-US';\n    expect(changeTimes).toEqual(1);\n  });\n  it('translation', () => {\n    expect(I18n.t('Yes')).toEqual('Yes');\n    I18n.locale = 'zh-CN';\n    expect(I18n.t('Yes')).toEqual('是');\n    expect(I18n.t('Unknown')).toEqual('Unknown');\n    expect(I18n.t('Unknown', { defaultValue: '' })).toEqual('');\n    I18n.addLanguage({\n      languageId: 'zh-CN',\n      contents: {\n        Unknown: '未知',\n      },\n    });\n    expect(I18n.t('Unknown')).toEqual('未知');\n    expect(I18n.t('Unknown', { defaultValue: '' })).toEqual('未知');\n  });\n  it('missingStrictMode', () => {\n    I18n.locale = 'en-US';\n    I18n.missingStrictMode = true;\n    expect(I18n.t('Unknown')).toEqual('[missing \"en-US.Unknown\" translation]');\n    I18n.missingStrictMode = false;\n    expect(I18n.t('Unknown')).toEqual('Unknown');\n  });\n});\n"
  },
  {
    "path": "packages/common/i18n/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/common/i18n/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/i18n\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"i18n-js\": \"^4.5.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/i18n/src/i18n/en-US.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport default {\n  languageId: 'en-US',\n  languageName: 'English',\n  localizedLanguageName: 'English',\n  contents: {\n    Yes: 'Yes',\n    No: 'No',\n  },\n};\n"
  },
  {
    "path": "packages/common/i18n/src/i18n/zh-CN.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport default {\n  languageId: 'zh-CN',\n  languageName: 'Chinese',\n  localizedLanguageName: '中文(中国)',\n  contents: {\n    Yes: '是',\n    No: '否',\n  },\n};\n"
  },
  {
    "path": "packages/common/i18n/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { I18n as I18nStore } from 'i18n-js';\nimport { Emitter } from '@flowgram.ai/utils';\n\ntype Scope = Readonly<string | string[]>;\n\ninterface TranslateOptions {\n  defaultValue?: any;\n  [key: string]: any;\n}\n\ninterface I18nLanguage {\n  languageId: string;\n  languageName?: string;\n  localizedLanguageName?: string;\n  contents: Record<string, string | string[]>;\n}\n\nimport zhCNLanguageDefault from './i18n/zh-CN';\nimport enUSLanguageDefault from './i18n/en-US';\n\nfunction getDefaultLanugage(): string {\n  if (typeof navigator !== 'object') return 'en-US';\n  const defaultLanguage = navigator.language;\n  if (defaultLanguage === 'en' || defaultLanguage === 'en-US') {\n    return 'en-US';\n  }\n  if (defaultLanguage === 'zh' || defaultLanguage === 'zh-CN') {\n    return 'zh-CN';\n  }\n  return defaultLanguage;\n}\nclass I18nImpl {\n  public i18n = new I18nStore();\n\n  private _onLanguageChangeEmitter = new Emitter<string>();\n\n  readonly onLanguageChange = this._onLanguageChangeEmitter.event;\n\n  constructor(languages: I18nLanguage[]) {\n    this.addLanguages(languages);\n    this.locale = getDefaultLanugage();\n    this.i18n.onChange(() => {\n      this._onLanguageChangeEmitter.fire(this.i18n.locale);\n    });\n  }\n\n  /**\n   * missing check\n   */\n  missingStrictMode = false;\n\n  /**\n   * @param key\n   * @param options\n   */\n  t(key: Scope, options?: TranslateOptions): string {\n    return this.i18n.t(key, {\n      defaultValue: this.missingStrictMode ? undefined : key,\n      ...options,\n    });\n  }\n\n  get locale(): string {\n    return this.i18n.locale;\n  }\n\n  set locale(locale: string) {\n    this.i18n.locale = locale;\n  }\n\n  addLanguages(newLanguage: I18nLanguage[]): void {\n    this.i18n.store(\n      newLanguage.reduce(\n        (dict, lang) =>\n          Object.assign(dict, {\n            [lang.languageId]: {\n              languageName: lang.languageName,\n              localizedLanguageName: lang.localizedLanguageName,\n              ...lang.contents,\n            },\n          }),\n        {}\n      )\n    );\n  }\n\n  addLanguage(language: I18nLanguage) {\n    this.addLanguages([language]);\n  }\n}\n\nconst I18n = new I18nImpl([enUSLanguageDefault, zhCNLanguageDefault]);\n\nexport { I18n, I18nLanguage };\n"
  },
  {
    "path": "packages/common/i18n/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"],\n  },\n}\n"
  },
  {
    "path": "packages/common/i18n/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n  },\n});\n"
  },
  {
    "path": "packages/common/reactive/README.md",
    "content": "# Reactive\n\n## Usage\n\n### 创建响应式数据并做依赖追踪\n\n```typescript\n\nimport { ReactiveState, Tracker } from '@flowgram.ai/reactive'\n\n// 创建 数据\nconst reactiveState = new ReactiveState<{ a: number, b: number }>({ a: 0, b: 0 })\n\n// 监听函数\nconst result = Tracker.autorun(() => {\n  console.log('run: ', reactiveState.value, reactiveState.value.a)\n})\n\n// 更新字典数据 a 会自动执行上边的 autorun\nreactiveState.value.a = 1\n\n// 更新数据 b 则不会执行，因为 autorun 函数里没有依赖\nreactiveState.value.b = 1\n```\n\n\n### react 中使用\n\n```typescript jsx\n\nimport { useReactiveState, observe } from '@flowgram.ai/reactive'\n\nconst SomeComp = ({ state }) => {\n  return <div>{state.a}</div>\n}\n\nfunction App() {\n  const state = useReactiveState<{ a: number, b: number }>({ a: 0, b: 0 });\n  useEffect(() => {\n    // 触发 SompeComp 更新\n    state.value.a = 1\n    // 不触发 SompeComp 更新\n    state.value.b = 1\n  })\n  return <SomeComp state={{state}} />\n}\n\n```\n"
  },
  {
    "path": "packages/common/reactive/__tests__/hooks.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { describe, it, expect, afterEach } from 'vitest';\nimport { render, cleanup } from '@testing-library/react';\n\nimport { useReactiveState, useReadonlyReactiveState, Tracker, ReactiveState } from '../src';\n\ndescribe('hooks', () => {\n  afterEach(() => cleanup());\n  it('useReactiveState update more times', () => {\n    let renderTimes = 0;\n    const Comp = () => {\n      renderTimes++;\n      const value = useReactiveState({ a: 0, b: 0 });\n      React.useEffect(() => {\n        value.a = 1;\n        value.a = 2;\n        value.b = 1;\n        value.b = 2;\n      }, []);\n      return (\n        <div>\n          {value.a} - {value.b}\n        </div>\n      );\n    };\n    const result = render(<Comp />);\n    Tracker.flush();\n    expect(renderTimes).toEqual(2); // batch update\n    expect(result.asFragment().textContent).toEqual('2 - 2');\n  });\n  it('useReactiveState sub component', () => {\n    let comp1RenderTimes = 0;\n    let comp2RenderTimes = 0;\n    const Comp1 = ({ value }: any) => {\n      comp1RenderTimes++;\n      React.useEffect(() => {\n        value.a = 2;\n      }, []);\n      return <div>{value.a}</div>;\n    };\n    const Comp2 = () => {\n      comp2RenderTimes++;\n      const value = useReactiveState({ a: 0 });\n      return <Comp1 value={value} />;\n    };\n    const result = render(<Comp2 />);\n    function checkTimes(a: number, b: number) {\n      expect(comp1RenderTimes).toEqual(a);\n      expect(comp2RenderTimes).toEqual(b);\n    }\n    checkTimes(1, 1);\n    Tracker.flush();\n    checkTimes(2, 2);\n    expect(result.asFragment().textContent).toEqual('2');\n  });\n  it('useReactiveState from outside', () => {\n    let comp1RenderTimes = 0;\n    let comp2RenderTimes = 0;\n    const state = new ReactiveState({ a: 0, b: 0 });\n    const Comp1 = ({ value }: any) => {\n      comp1RenderTimes++;\n      return <div>{comp1RenderTimes >= 3 ? '-' : value.a}</div>;\n    };\n    const Comp2 = () => {\n      comp2RenderTimes++;\n      const value = useReactiveState(state);\n      return <Comp1 value={value} />;\n    };\n    const result = render(<Comp2 />);\n    function checkTimes(a: number, b: number) {\n      expect(comp1RenderTimes).toEqual(a);\n      expect(comp2RenderTimes).toEqual(b);\n    }\n    checkTimes(1, 1);\n    state.value.b = 1;\n    Tracker.flush();\n    checkTimes(1, 1); // b 没有依赖所有不更新\n    state.value.a = 1;\n    Tracker.flush();\n    checkTimes(2, 2);\n    state.value.a = 2;\n    Tracker.flush();\n    checkTimes(3, 3);\n    expect(result.asFragment().textContent).toEqual('-');\n    state.value.a = 3;\n    Tracker.flush();\n    checkTimes(3, 3); // a 不再依赖所以不更新\n  });\n  it('useReactiveState nested', () => {\n    const state = new ReactiveState({ a: 0 });\n    let comp1RenderTimes = 0;\n    let comp2RenderTimes = 0;\n    const Comp1 = () => {\n      comp1RenderTimes++;\n      const value = useReactiveState(state);\n      React.useEffect(() => {\n        value.a = 1;\n      }, []);\n      return <div>{value.a}</div>;\n    };\n    const Comp2 = () => {\n      comp2RenderTimes++;\n      const value = useReactiveState(state);\n      React.useEffect(() => {\n        value.a = 2;\n      }, []);\n      return (\n        <div>\n          <Comp1 /> - {value.a}\n        </div>\n      );\n    };\n    function checkTimes(a: number, b: number) {\n      expect(comp1RenderTimes).toEqual(a);\n      expect(comp2RenderTimes).toEqual(b);\n    }\n    const result = render(<Comp2 />);\n    Tracker.flush();\n    checkTimes(2, 2);\n    expect(result.asFragment().textContent).toEqual('2 - 2');\n  });\n  it('useReadonlyReactiveState', () => {\n    const state = new ReactiveState({ a: 0 });\n    const Comp = () => {\n      const v = useReadonlyReactiveState(state);\n      expect(() => {\n        (v as any).a = 3;\n      }).toThrowError(/readonly/);\n      return <div>{v.a}</div>;\n    };\n    render(<Comp />);\n  });\n});\n"
  },
  {
    "path": "packages/common/reactive/__tests__/observe.test.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { describe, it, expect, afterEach } from 'vitest';\nimport { render, cleanup } from '@testing-library/react';\n\nimport { observe, Tracker, ReactiveBaseState } from '../src';\n\nfunction nextTick(v = 0): Promise<void> {\n  return new Promise(res => setTimeout(res, v));\n}\nfunction createComp(name: string): {\n  Comp: any;\n  renderTimes: number;\n  name: string;\n  fireRender: () => void;\n} {\n  let renderTimes = 0;\n  let state = new ReactiveBaseState(0);\n  // let refresh: any;\n  const Comp = observe((props: any) => {\n    // refresh = useRefresh()\n    renderTimes++;\n    return (\n      <span>\n        {state.value}\n        {typeof props.children === 'function' ? props.children() : props.children}\n      </span>\n    );\n  });\n  return {\n    Comp,\n    name,\n    get renderTimes(): number {\n      return renderTimes;\n    },\n    fireRender(): void {\n      state.value += 1;\n      // refresh()\n    },\n  };\n}\n\ndescribe('observe', () => {\n  afterEach(() => cleanup());\n  it('base', async () => {\n    const comp = createComp('comp1');\n    const result = render(<comp.Comp />);\n    expect(comp.renderTimes).toEqual(1);\n    expect(result.asFragment().textContent).toEqual('0');\n    comp.fireRender();\n    Tracker.flush();\n    expect(comp.renderTimes).toEqual(2);\n    expect(result.asFragment().textContent).toEqual('1');\n    comp.fireRender();\n    Tracker.flush();\n    expect(comp.renderTimes).toEqual(3);\n    expect(result.asFragment().textContent).toEqual('2');\n    comp.fireRender();\n    // use next tick to wait update\n    await nextTick();\n    expect(comp.renderTimes).toEqual(4);\n    expect(result.asFragment().textContent).toEqual('3');\n  });\n  it('render nested', () => {\n    const comp1 = createComp('comp1');\n    const comp2 = createComp('comp2');\n    const checkTimes = (v1: number, v2: number) => {\n      // console.log(comp1.renderTimes, comp2.renderTimes)\n      expect(comp1.renderTimes).toEqual(v1);\n      expect(comp2.renderTimes).toEqual(v2);\n    };\n    render(\n      <comp1.Comp>\n        <comp2.Comp />\n      </comp1.Comp>,\n    );\n    checkTimes(1, 1);\n    comp1.fireRender();\n    Tracker.flush();\n    checkTimes(2, 1);\n    comp2.fireRender();\n    Tracker.flush();\n    checkTimes(2, 2);\n    comp1.fireRender();\n    comp2.fireRender();\n    Tracker.flush();\n    checkTimes(3, 3);\n  });\n  it('render nested with renderProps', () => {\n    const comp1 = createComp('comp1');\n    const comp2 = createComp('comp2');\n    const checkTimes = (v1: number, v2: number) => {\n      // console.log(comp1.renderTimes, comp2.renderTimes)\n      expect(comp1.renderTimes).toEqual(v1);\n      expect(comp2.renderTimes).toEqual(v2);\n    };\n    render(<comp1.Comp>{() => <comp2.Comp />}</comp1.Comp>);\n    checkTimes(1, 1);\n    comp1.fireRender();\n    Tracker.flush();\n    checkTimes(2, 2);\n    comp2.fireRender();\n    Tracker.flush();\n    checkTimes(2, 3);\n    comp1.fireRender();\n    comp2.fireRender();\n    Tracker.flush();\n    checkTimes(3, 4);\n  });\n});\n"
  },
  {
    "path": "packages/common/reactive/__tests__/reactive-base-state.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\n\nimport { ReactiveBaseState, Tracker } from '../src';\n\ndescribe('reactive-base-state', () => {\n  it('base', () => {\n    const state = new ReactiveBaseState(0);\n    let autorunTimes = -1;\n    const compute = Tracker.autorun<number>(() => {\n      autorunTimes++;\n      return autorunTimes <= 2 ? state.value : -1;\n    });\n    expect(state.hasDependents()).toEqual(true);\n    expect(autorunTimes).toEqual(0);\n    expect(compute.result).toEqual(0);\n    state.value = 1;\n    expect(compute.result).toEqual(0);\n    expect(autorunTimes).toEqual(0);\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    Tracker.flush();\n    // Still 1!\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    state.value = 1;\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    state.value = 2;\n    Tracker.flush();\n    expect(compute.result).toEqual(2);\n    expect(autorunTimes).toEqual(2);\n    state.value = 3;\n    Tracker.flush();\n    expect(compute.result).toEqual(-1);\n    expect(autorunTimes).toEqual(3);\n    state.value = 4;\n    Tracker.flush();\n    // Still 1!\n    expect(compute.result).toEqual(-1);\n    expect(autorunTimes).toEqual(3);\n  });\n  it('custom isEqual', () => {\n    const state = new ReactiveBaseState(0, {\n      isEqual: () => false,\n    });\n    let autorunTimes = 0;\n    Tracker.autorun<number>(() => {\n      autorunTimes++;\n      return state.value;\n    });\n    // isEqual 不再判断是否相等, 所以会触发刷新\n    state.value = 0;\n    Tracker.flush();\n    expect(autorunTimes).toEqual(2);\n  });\n});\n"
  },
  {
    "path": "packages/common/reactive/__tests__/reactive-state.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\n\nimport { ReactiveState, Tracker } from '../src';\n\ndescribe('reactive-state', () => {\n  it('base', () => {\n    const state = new ReactiveState({ a: 0, b: 0 });\n    const { value } = state;\n    let autorunTimes = -1;\n    const compute = Tracker.autorun<number>(() => {\n      autorunTimes++;\n      return value.a;\n    });\n    expect(state.hasDependents()).toEqual(true);\n    expect(autorunTimes).toEqual(0);\n    expect(compute.result).toEqual(0);\n    state.value.a = 1;\n    expect(compute.result).toEqual(0);\n    expect(autorunTimes).toEqual(0);\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    Tracker.flush();\n    // Still 1!\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    state.value.a = 1;\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n    state.value.b = 1;\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n    expect(autorunTimes).toEqual(1);\n  });\n  it('keys', () => {\n    const state = new ReactiveState<{ a: number; b: number }>({ a: 0, b: 0 });\n    expect(state.keys()).toEqual(['a', 'b']);\n    expect(Object.keys(state.value)).toEqual(['a', 'b']);\n    expect(Object.keys(state.readonlyValue)).toEqual(['a', 'b']);\n  });\n  it('hasDependents', () => {\n    const state = new ReactiveState<{ a: number; b: number }>({ a: 0, b: 0 });\n    expect(state.hasDependents()).toEqual(false);\n    const compute = Tracker.autorun<number>(() => state.value.a);\n    expect(state.hasDependents()).toEqual(true);\n    compute.stop();\n    expect(state.hasDependents()).toEqual(false);\n  });\n  it('set all value', () => {\n    const state = new ReactiveState<{ a: number; b: number }>({ a: 0, b: 0 });\n    const compute = Tracker.autorun<number>(() => state.value.a);\n    state.value = { a: 1, b: 1 };\n    Tracker.flush();\n    expect(compute.result).toEqual(1);\n  });\n  it('dict state iterator (use Proxy)', () => {\n    const { value } = new ReactiveState<{ a: number; b: number }>({ a: 0, b: 0 });\n    let autorunTimes = -1;\n    const compute = Tracker.autorun<{ a: number; b: number }>(() => {\n      autorunTimes++;\n      const result = {};\n      for (let key in value) {\n        result[key] = value[key];\n      }\n      return result as any;\n    });\n    expect(autorunTimes).toEqual(0);\n    expect(compute.result).toEqual({ a: 0, b: 0 });\n    value.a = 1;\n    value.b = 1;\n    Tracker.flush();\n    expect(autorunTimes).toEqual(1);\n    expect(compute.result).toEqual({ a: 1, b: 1 });\n  });\n  it('dict state iterator (use defineProperty)', () => {\n    global.__ignoreProxy = true;\n    const { value } = new ReactiveState<{ a: number; b: number }>({ a: 0, b: 0 });\n    let autorunTimes = -1;\n    const compute = Tracker.autorun<{ a: number; b: number }>(() => {\n      autorunTimes++;\n      const result = {};\n      for (let key in value) {\n        result[key] = value[key];\n      }\n      return result as any;\n    });\n    expect(Object.keys(value)).toEqual(['a', 'b']);\n    expect(autorunTimes).toEqual(0);\n    expect(compute.result).toEqual({ a: 0, b: 0 });\n    value.a = 1;\n    value.b = 1;\n    Tracker.flush();\n    expect(autorunTimes).toEqual(1);\n    expect(compute.result).toEqual({ a: 1, b: 1 });\n    global.__ignoreProxy = false;\n  });\n  it('set unknown field', () => {\n    const { value } = new ReactiveState<Record<string, any>>({});\n    let runTimes = 0;\n    const compute = Tracker.autorun(() => {\n      runTimes++;\n      return value.a;\n    });\n    value.a = 'new field';\n    Tracker.flush();\n    expect(runTimes).toEqual(2);\n    expect(compute.result).toEqual('new field');\n    expect(Object.keys(value)).toEqual(['a']);\n    delete value.a;\n    expect(Object.keys(value)).toEqual([]);\n  });\n  it('readonly value (use Proxy)', () => {\n    const originState = new ReactiveState({ a: 0, b: 0 });\n    const readonlyValue = originState.readonlyValue;\n    expect(() => {\n      (readonlyValue as any).a = 1;\n    }).toThrow(/readonly field/);\n  });\n  it('readonly value (use define property)', () => {\n    global.__ignoreProxy = true;\n    const originState = new ReactiveState({ a: 0, b: 0 });\n    const readonlyValue = originState.readonlyValue;\n    expect(() => {\n      (readonlyValue as any).a = 1;\n    }).toThrow(/readonly field/);\n    global.__ignoreProxy = false;\n  });\n});\n"
  },
  {
    "path": "packages/common/reactive/__tests__/tracker.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect, describe } from 'vitest';\n\nimport { Tracker } from '../src';\n\nfunction expectTrue(value: any): void {\n  expect(value).toEqual(true);\n}\n\nfunction expectFalse(value: any): void {\n  expect(value).toEqual(false);\n}\n\nfunction expectEqual(v1: any, v2: any, msg?: string) {\n  expect(v1).toEqual(v2);\n}\n\nfunction createPromiseDelegate(): { promise: Promise<void>; complete: () => void } {\n  let complete: () => void;\n  const promise = new Promise<void>(res => {\n    complete = res;\n  });\n  return {\n    promise,\n    complete,\n  };\n}\n\nfunction nextTick(v = 0): Promise<void> {\n  return new Promise(res => setTimeout(res, v));\n}\n\n/**\n * fork from: https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker_tests.js\n */\ndescribe('Tracker', () => {\n  test('tracker - run', function () {\n    var d = new Tracker.Dependency();\n    var x = 0;\n    var handle = Tracker.autorun(function () {\n      d.depend();\n      ++x;\n    });\n    // 默认会先执行一次\n    expect(x).toEqual(1);\n    Tracker.flush();\n    expect(x).toEqual(1);\n    d.changed();\n    expect(x).toEqual(1);\n    Tracker.flush();\n    expect(x).toEqual(2);\n    d.changed();\n    expect(x).toEqual(2);\n    Tracker.flush();\n    expect(x).toEqual(3);\n    d.changed();\n    // Prevent the function from running further.\n    handle.stop();\n    Tracker.flush();\n    expect(x).toEqual(3);\n    d.changed();\n    Tracker.flush();\n    expect(x).toEqual(3);\n\n    Tracker.autorun(function (internalHandle) {\n      d.depend();\n      ++x;\n      if (x == 6) internalHandle.stop();\n    });\n    expect(x).toEqual(4);\n    d.changed();\n    Tracker.flush();\n    expect(x).toEqual(5);\n    d.changed();\n    // Increment to 6 and stop.\n    Tracker.flush();\n    expect(x).toEqual(6);\n    d.changed();\n    Tracker.flush();\n    // Still 6!\n    expect(x).toEqual(6);\n  });\n\n  test('tracker - nested run', function () {\n    var a = new Tracker.Dependency();\n    var b = new Tracker.Dependency();\n    var c = new Tracker.Dependency();\n    var d = new Tracker.Dependency();\n    var e = new Tracker.Dependency();\n    var f = new Tracker.Dependency();\n\n    var buf = '';\n\n    Tracker.autorun(function () {\n      a.depend();\n      buf += 'a';\n      Tracker.autorun(function () {\n        b.depend();\n        buf += 'b';\n        Tracker.autorun(function () {\n          c.depend();\n          buf += 'c';\n          var c2 = Tracker.autorun(function () {\n            d.depend();\n            buf += 'd';\n            Tracker.autorun(function () {\n              e.depend();\n              buf += 'e';\n              Tracker.autorun(function () {\n                f.depend();\n                buf += 'f';\n              });\n            });\n            Tracker.onInvalidate(function () {\n              // only run once\n              c2.stop();\n            });\n          });\n        });\n      });\n      Tracker.onInvalidate(function (c1) {\n        c1.stop();\n      });\n    });\n\n    const expectAndClear = function (str: string) {\n      expect(buf).toEqual(str);\n      buf = '';\n    };\n\n    expectAndClear('abcdef');\n\n    expect(a.hasDependents()).toEqual(true);\n    expect(b.hasDependents()).toEqual(true);\n    expect(c.hasDependents()).toEqual(true);\n    expect(d.hasDependents()).toEqual(true);\n    expect(e.hasDependents()).toEqual(true);\n    expect(f.hasDependents()).toEqual(true);\n\n    b.changed();\n    expectAndClear(''); // didn't flush yet\n    Tracker.flush();\n    expectAndClear('bcdef');\n\n    c.changed();\n    Tracker.flush();\n    expectAndClear('cdef');\n\n    var changeAndExpect = function (v, str) {\n      v.changed();\n      Tracker.flush();\n      expectAndClear(str);\n    };\n\n    // should cause running\n    changeAndExpect(e, 'ef');\n    changeAndExpect(f, 'f');\n    // invalidate inner context\n    changeAndExpect(d, '');\n    // no more running!\n    changeAndExpect(e, '');\n    changeAndExpect(f, '');\n\n    expectTrue(a.hasDependents());\n    expectTrue(b.hasDependents());\n    expectTrue(c.hasDependents());\n    expectFalse(d.hasDependents());\n    expectFalse(e.hasDependents());\n    expectFalse(f.hasDependents());\n\n    // rerun C\n    changeAndExpect(c, 'cdef');\n    changeAndExpect(e, 'ef');\n    changeAndExpect(f, 'f');\n    // rerun B\n    changeAndExpect(b, 'bcdef');\n    changeAndExpect(e, 'ef');\n    changeAndExpect(f, 'f');\n\n    expectTrue(a.hasDependents());\n    expectTrue(b.hasDependents());\n    expectTrue(c.hasDependents());\n    expectTrue(d.hasDependents());\n    expectTrue(e.hasDependents());\n    expectTrue(f.hasDependents());\n\n    // kill A\n    a.changed();\n    changeAndExpect(f, '');\n    changeAndExpect(e, '');\n    changeAndExpect(d, '');\n    changeAndExpect(c, '');\n    changeAndExpect(b, '');\n    changeAndExpect(a, '');\n\n    expectFalse(a.hasDependents());\n    expectFalse(b.hasDependents());\n    expectFalse(c.hasDependents());\n    expectFalse(d.hasDependents());\n    expectFalse(e.hasDependents());\n    expectFalse(f.hasDependents());\n  });\n\n  test('tracker - flush', function () {\n    var buf = '';\n\n    var c1 = Tracker.autorun(function (c) {\n      buf += 'a';\n      // invalidate first time\n      if (c.firstRun) c.invalidate();\n    });\n\n    expectEqual(buf, 'a');\n    Tracker.flush();\n    expectEqual(buf, 'aa');\n    Tracker.flush();\n    expectEqual(buf, 'aa');\n    c1.stop();\n    Tracker.flush();\n    expectEqual(buf, 'aa');\n\n    //////\n\n    buf = '';\n\n    var c2 = Tracker.autorun(function (c) {\n      buf += 'a';\n      // invalidate first time\n      if (c.firstRun) c.invalidate();\n\n      Tracker.onInvalidate(function () {\n        buf += '*';\n      });\n    });\n\n    expectEqual(buf, 'a*');\n    Tracker.flush();\n    expectEqual(buf, 'a*a');\n    c2.stop();\n    expectEqual(buf, 'a*a*');\n    Tracker.flush();\n    expectEqual(buf, 'a*a*');\n\n    /////\n    // Can flush a different run from a run;\n    // no current computation in afterFlush\n\n    buf = '';\n\n    var c3 = Tracker.autorun(function (c) {\n      buf += 'a';\n      // invalidate first time\n      if (c.firstRun) c.invalidate();\n      Tracker.afterFlush(function () {\n        buf += Tracker.isActive() ? '1' : '0';\n      });\n    });\n\n    Tracker.afterFlush(function () {\n      buf += 'c';\n    });\n\n    var c4 = Tracker.autorun(function (c) {\n      c4 = c;\n      buf += 'b';\n    });\n\n    Tracker.flush();\n    expectEqual(buf, 'aba0c0');\n    c3.stop();\n    c4.stop();\n    Tracker.flush();\n\n    // cases where flush throws\n\n    var ran = false;\n    Tracker.afterFlush(function (arg) {\n      ran = true;\n      expectEqual(typeof arg, 'undefined');\n      expect(function () {\n        Tracker.flush(); // illegal nested flush\n      }).toThrowError();\n    });\n\n    Tracker.flush();\n    expectTrue(ran);\n\n    expect(function () {\n      Tracker.autorun(function () {\n        Tracker.flush(); // illegal to flush from a computation\n      });\n    }).toThrowError();\n\n    expect(function () {\n      Tracker.autorun(function () {\n        Tracker.autorun(function () {});\n        Tracker.flush();\n      });\n    }).toThrowError();\n  });\n\n  test('tracker - lifecycle', function () {\n    expectFalse(Tracker.isActive());\n    expectEqual(undefined, Tracker.getCurrentComputation());\n\n    var runCount = 0;\n    var firstRun = true;\n    var buf = [];\n    var cbId = 1;\n    var makeCb = function () {\n      var id = cbId++;\n      return function () {\n        buf.push(id);\n      };\n    };\n\n    var shouldStop = false;\n\n    var c1 = Tracker.autorun(function (c) {\n      expectTrue(Tracker.isActive());\n      expectEqual(c, Tracker.getCurrentComputation());\n      expectEqual(c.stopped, false);\n      expectEqual(c.invalidated, false);\n      expectEqual(c.firstRun, firstRun);\n\n      Tracker.onInvalidate(makeCb()); // 1, 6, ...\n      Tracker.afterFlush(makeCb()); // 2, 7, ...\n\n      Tracker.autorun(function (x) {\n        x.stop();\n        c.onInvalidate(makeCb()); // 3, 8, ...\n\n        Tracker.onInvalidate(makeCb()); // 4, 9, ...\n        Tracker.afterFlush(makeCb()); // 5, 10, ...\n      });\n      runCount++;\n\n      if (shouldStop) c.stop();\n    });\n\n    firstRun = false;\n\n    expectEqual(runCount, 1);\n\n    expectEqual(buf, [4]);\n    c1.invalidate();\n    expectEqual(runCount, 1);\n    expectEqual(c1.invalidated, true);\n    expectEqual(c1.stopped, false);\n    expectEqual(buf, [4, 1, 3]);\n\n    Tracker.flush();\n\n    expectEqual(runCount, 2);\n    expectEqual(c1.invalidated, false);\n    expectEqual(buf, [4, 1, 3, 9, 2, 5, 7, 10]);\n\n    // test self-stop\n    buf.length = 0;\n    shouldStop = true;\n    c1.invalidate();\n    expectEqual(buf, [6, 8]);\n    Tracker.flush();\n    expectEqual(buf, [6, 8, 14, 11, 13, 12, 15]);\n  });\n\n  test('tracker - onInvalidate', function () {\n    var buf = '';\n\n    var c1 = Tracker.autorun(function () {\n      buf += '*';\n    });\n\n    var append = function (\n      x,\n      expectedComputation?: Tracker.Computation,\n    ): Tracker.IComputationCallback {\n      return function (givenComputation) {\n        expectFalse(Tracker.isActive());\n        expectEqual(givenComputation, expectedComputation || c1);\n        buf += x;\n      };\n    };\n\n    c1.onStop(append('s'));\n\n    c1.onInvalidate(append('a'));\n    c1.onInvalidate(append('b'));\n    expectEqual(buf, '*');\n    Tracker.autorun(function (me) {\n      Tracker.onInvalidate(append('z', me));\n      me.stop();\n      expectEqual(buf, '*z');\n      c1.invalidate();\n    });\n    expectEqual(buf, '*zab');\n    c1.onInvalidate(append('c'));\n    c1.onInvalidate(append('d'));\n    expectEqual(buf, '*zabcd');\n    Tracker.flush();\n    expectEqual(buf, '*zabcd*');\n\n    // afterFlush ordering\n    buf = '';\n    c1.onInvalidate(append('a'));\n    c1.onInvalidate(append('b'));\n    Tracker.afterFlush(function () {\n      append('x')(c1);\n      c1.onInvalidate(append('c'));\n      c1.invalidate();\n      Tracker.afterFlush(function () {\n        append('y')(c1);\n        c1.onInvalidate(append('d'));\n        c1.invalidate();\n      });\n    });\n    Tracker.afterFlush(function () {\n      append('z')(c1);\n      c1.onInvalidate(append('e'));\n      c1.invalidate();\n    });\n\n    expectEqual(buf, '');\n    Tracker.flush();\n    expectEqual(buf, 'xabc*ze*yd*');\n\n    buf = '';\n    c1.onInvalidate(append('m'));\n    Tracker.flush();\n    expectEqual(buf, '');\n    c1.stop();\n    expectEqual(buf, 'ms'); // s is from onStop\n    Tracker.flush();\n    expectEqual(buf, 'ms');\n    c1.onStop(append('S'));\n    expectEqual(buf, 'msS');\n  });\n\n  test('tracker - invalidate at flush time', function () {\n    // Test this sentence of the docs: Functions are guaranteed to be\n    // called at a time when there are no invalidated computations that\n    // need rerunning.\n\n    var buf = [];\n\n    Tracker.afterFlush(function () {\n      buf.push('C');\n    });\n\n    // When c1 is invalidated, it invalidates c2, then stops.\n    var c1 = Tracker.autorun(function (c) {\n      if (!c.firstRun) {\n        buf.push('A');\n        c2.invalidate();\n        c.stop();\n      }\n    });\n\n    var c2 = Tracker.autorun(function (c) {\n      if (!c.firstRun) {\n        buf.push('B');\n        c.stop();\n      }\n    });\n\n    // Invalidate c1.  If all goes well, the re-running of\n    // c2 should happen before the afterFlush.\n    c1.invalidate();\n    Tracker.flush();\n\n    expectEqual(buf.join(''), 'ABC');\n  });\n\n  test('tracker - throwFirstError', function (test) {\n    var d = new Tracker.Dependency();\n    Tracker.autorun(function (c) {\n      d.depend();\n\n      if (!c.firstRun) throw new Error('foo');\n    });\n\n    d.changed();\n    Tracker.flush();\n\n    d.changed();\n    expect(function () {\n      Tracker.flush({ throwFirstError: true });\n    }).toThrowError(/foo/);\n  });\n\n  test('tracker - no infinite recomputation', async function () {\n    var reran = false;\n    var c = Tracker.autorun(function (c) {\n      if (!c.firstRun) reran = true;\n      c.invalidate();\n    });\n    expectFalse(reran);\n    await new Promise(res => {\n      setTimeout(function () {\n        c.stop();\n        Tracker.afterFlush(function () {\n          expectTrue(reran);\n          expectTrue(c.stopped);\n          res(null);\n        });\n      }, 100);\n    });\n  });\n\n  test('tracker - Tracker.flush finishes', function () {\n    // Currently, _runFlush will \"yield\" every 1000 computations... unless run in\n    // Tracker.flush. So this test validates that Tracker.flush is capable of\n    // running 2000 computations. Which isn't quite the same as infinity, but it's\n    // getting there.\n    var n = 0;\n    var c = Tracker.autorun(function (c) {\n      if (++n < 2000) {\n        c.invalidate();\n      }\n    });\n    expectEqual(n, 1);\n    Tracker.flush();\n    expectEqual(n, 2000);\n  });\n  //\n  test('tracker - Tracker.autorun, onError option', async function (ctx) {\n    var d = new Tracker.Dependency();\n    const promiseDelegate = createPromiseDelegate();\n    var c = Tracker.autorun(\n      function (c) {\n        d.depend();\n\n        if (!c.firstRun) throw new Error('foo');\n      },\n      {\n        onError: function (err) {\n          expectEqual(err.message, 'foo');\n          promiseDelegate.complete();\n        },\n      },\n    );\n\n    d.changed();\n    Tracker.flush();\n    await promiseDelegate.promise;\n  });\n\n  test('tracker - async function - basics', async function () {\n    const promiseDelegate = createPromiseDelegate();\n    const computation = Tracker.autorun(async function (computation) {\n      expectEqual(computation.firstRun, true, 'before (firstRun)');\n      expectEqual(Tracker.getCurrentComputation(), computation, 'before');\n      const x = await Promise.resolve().then(() =>\n        Tracker.withComputation(computation, () => {\n          // The `firstRun` is `false` as soon as the first `await` happens.\n          expectEqual(computation.firstRun, false, 'inside (firstRun)');\n          expectEqual(Tracker.getCurrentComputation(), computation, 'inside');\n          return 123;\n        }),\n      );\n      expectEqual(x, 123, 'await (value)');\n      expectEqual(computation.firstRun, false, 'await (firstRun)');\n      Tracker.withComputation(computation, () => {\n        expectEqual(Tracker.getCurrentComputation(), computation, 'await');\n      });\n      await new Promise(resolve => setTimeout(resolve, 10));\n      Tracker.withComputation(computation, () => {\n        expectEqual(computation.firstRun, false, 'sleep (firstRun)');\n        expectEqual(Tracker.getCurrentComputation(), computation, 'sleep');\n      });\n      try {\n        await Promise.reject('example');\n      } catch (error) {\n        Tracker.withComputation(computation, () => {\n          expectEqual(error, 'example', 'catch (error)');\n          expectEqual(computation.firstRun, false, 'catch (firstRun)');\n          expectEqual(Tracker.getCurrentComputation(), computation, 'catch');\n        });\n      }\n      promiseDelegate.complete();\n    });\n\n    expectEqual(Tracker.getCurrentComputation(), undefined, 'outside (computation)');\n    // test.instanceOf(computation, Tracker.Computation, 'outside (result)');\n    await promiseDelegate.promise;\n  });\n\n  test('tracker - async function - interleaved', async function () {\n    let count = 0;\n    const limit = 100;\n    for (let index = 0; index < limit; ++index) {\n      Tracker.autorun(async function (computation) {\n        expectEqual(Tracker.getCurrentComputation(), computation, `before (${index})`);\n        await new Promise(resolve => setTimeout(resolve, Math.random() * limit));\n        count++;\n        Tracker.withComputation(computation, () => {\n          expectEqual(Tracker.getCurrentComputation(), computation, `after (${index})`);\n        });\n      });\n    }\n\n    expectEqual(count, 0, 'before resolve');\n    await new Promise(resolve => setTimeout(resolve, limit));\n    expectEqual(count, limit, 'after resolve');\n  });\n\n  test('tracker - async function - parallel', async function () {\n    let resolvePromise;\n    const promise = new Promise(resolve => {\n      resolvePromise = resolve;\n    });\n\n    let count = 0;\n    const limit = 100;\n    const dependency = new Tracker.Dependency();\n    for (let index = 0; index < limit; ++index) {\n      Tracker.autorun(async function (computation) {\n        count++;\n        Tracker.withComputation(computation, () => {\n          dependency.depend();\n        });\n        await promise;\n        count--;\n      });\n    }\n\n    expectEqual(count, limit, 'before');\n    dependency.changed();\n    await nextTick();\n    expectEqual(count, limit * 2, 'changed');\n    resolvePromise();\n    await nextTick();\n    expectEqual(count, 0, 'after');\n  });\n\n  test('tracker - async function - stepped', async function () {\n    let resolvePromise;\n    const promise = new Promise(resolve => {\n      resolvePromise = resolve;\n    });\n\n    let count = 0;\n    const limit = 100;\n    for (let index = 0; index < limit; ++index) {\n      Tracker.autorun(async function (computation) {\n        expectEqual(Tracker.getCurrentComputation(), computation, `before (${index})`);\n        await promise;\n        count++;\n        Tracker.withComputation(computation, () => {\n          expectEqual(Tracker.getCurrentComputation(), computation, `after (${index})`);\n        });\n      });\n    }\n\n    expectEqual(count, 0, 'before resolve');\n    resolvePromise();\n    await nextTick();\n    expectEqual(count, limit, 'after resolve');\n  });\n\n  test('tracker - async function - synchronize - firstRunPromise', async test => {\n    let counter = 0;\n    await Tracker.autorun(async () => {\n      expectEqual(counter, 0);\n      counter += 1;\n      expectEqual(counter, 1);\n      await new Promise(resolve => setTimeout(resolve));\n      expectEqual(counter, 1);\n      counter *= 2;\n      expectEqual(counter, 2);\n    }).result;\n\n    await Tracker.autorun(async () => {\n      expectEqual(counter, 2);\n      counter += 1;\n      expectEqual(counter, 3);\n      await new Promise(resolve => setTimeout(resolve));\n      expectEqual(counter, 3);\n      counter *= 2;\n      expectEqual(counter, 6);\n    }).result;\n  });\n\n  test('computation - #flush', function () {\n    var i = 0,\n      j = 0,\n      d = new Tracker.Dependency();\n    var c1 = Tracker.autorun(function () {\n      d.depend();\n      i = i + 1;\n    });\n    var c2 = Tracker.autorun(function () {\n      d.depend();\n      j = j + 1;\n    });\n    expectEqual(i, 1);\n    expectEqual(j, 1);\n\n    d.changed();\n    c1.flush();\n    expectEqual(i, 2);\n    expectEqual(j, 1);\n\n    Tracker.flush();\n    expectEqual(i, 2);\n    expectEqual(j, 2);\n  });\n  test('computation - #run', function () {\n    var i = 0,\n      d = new Tracker.Dependency(),\n      d2 = new Tracker.Dependency();\n    var computation = Tracker.autorun(function (c) {\n      d.depend();\n      i = i + 1;\n      //when #run() is called, this dependency should be picked up\n      if (i >= 2 && i < 4) {\n        d2.depend();\n      }\n    });\n    expectEqual(i, 1);\n    computation.run();\n    expectEqual(i, 2);\n\n    d.changed();\n    Tracker.flush();\n    expectEqual(i, 3);\n\n    //we expect to depend on d2 at this point\n    d2.changed();\n    Tracker.flush();\n    expectEqual(i, 4);\n\n    //we no longer depend on d2, only d\n    d2.changed();\n    Tracker.flush();\n    expectEqual(i, 4);\n    d.changed();\n    Tracker.flush();\n    expectEqual(i, 5);\n  });\n});\n"
  },
  {
    "path": "packages/common/reactive/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/common/reactive/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/reactive\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/utils\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/reactive/src/core/reactive-base-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Tracker } from './tracker';\n\ntype IStateEqual = (a: any, b: any) => boolean;\n\nexport class ReactiveBaseState<V> {\n  protected _dep = new Tracker.Dependency();\n\n  protected _value: V;\n\n  protected _isEqual: IStateEqual = (a: any, b: any) => a == b;\n\n  protected _addDepend(dep: Tracker.Dependency): void {\n    if (Tracker.isActive()) {\n      dep.depend();\n    }\n  }\n\n  constructor(initialValue: V, opts?: { isEqual?: IStateEqual }) {\n    this._value = initialValue;\n    if (opts?.isEqual) {\n      this._isEqual = opts.isEqual;\n    }\n  }\n\n  hasDependents(): boolean {\n    return this._dep.hasDependents();\n  }\n\n  get value(): V {\n    this._addDepend(this._dep);\n    return this._value;\n  }\n\n  set value(newValue: V) {\n    if (!this._isEqual(this._value, newValue)) {\n      this._value = newValue;\n      this._dep.changed();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/common/reactive/src/core/reactive-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Tracker } from './tracker';\n\nimport Dependency = Tracker.Dependency;\n\nimport { ReactiveBaseState } from './reactive-base-state';\nimport { createProxy } from '../utils/create-proxy';\n\nexport class ReactiveState<V extends Record<string, any>> extends ReactiveBaseState<V> {\n  private _keyDeps: Map<string, Dependency> = new Map();\n\n  set<K extends keyof V & string>(key: K, value: V[K]): boolean {\n    this._ensureKey(key);\n    const oldValue = this._value[key];\n    if (!this._isEqual(oldValue, value)) {\n      this._value[key] = value;\n      this._keyDeps.get(key)!.changed();\n      return true;\n    }\n    return false;\n  }\n\n  get<K extends keyof V & string>(key: K): V[K] {\n    this._ensureKey(key);\n    this._addDepend(this._keyDeps.get(key)!);\n    return this._value[key];\n  }\n\n  protected _ensureKey(key: keyof V & string) {\n    if (!this._keyDeps.has(key)) {\n      this._keyDeps.set(key, new Dependency());\n    }\n  }\n\n  hasDependents(): boolean {\n    if (this._dep.hasDependents()) return true;\n    for (const dep of this._keyDeps.values()) {\n      if (dep.hasDependents()) return true;\n    }\n    return false;\n  }\n\n  keys(): string[] {\n    return Object.keys(this._value);\n  }\n\n  set value(newValue: V) {\n    if (!this._isEqual(this._value, newValue)) {\n      this._value = newValue;\n      this._keyDeps.clear();\n      this._dep.changed();\n    }\n  }\n\n  private _proxyValue: V;\n\n  get value(): V {\n    this._addDepend(this._dep);\n    if (!this._proxyValue) {\n      this._proxyValue = createProxy<V>(this._value, {\n        get: (target, key: string) => this.get(key),\n        set: (target, key: string, newValue) => {\n          this.set(key, newValue);\n          return true;\n        },\n      });\n    }\n    return this._proxyValue;\n  }\n\n  private _proxyReadonlyValue: V;\n\n  get readonlyValue(): Readonly<V> {\n    this._addDepend(this._dep);\n    if (!this._proxyReadonlyValue) {\n      this._proxyReadonlyValue = createProxy(this._value, {\n        get: (target, key: string) => this.get(key),\n        set: (newValue, key: string) => {\n          throw new Error(`[ReactiveState] Cannnot set readonly field \"${key}\"`);\n        },\n      });\n    }\n    return this._proxyReadonlyValue;\n  }\n}\n"
  },
  {
    "path": "packages/common/reactive/src/core/tracker.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Fork from: https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js\n */\ntype ICallback<ARG = void, RET = void> = (arg: ARG) => RET;\n\n/**\n * Tracker 是一套 响应式依赖追踪 库，来源于 Meteor.Tracker\n * https://docs.meteor.com/api/Tracker.html#tracker-autorun-and-async-callbacks\n * https://github.com/meteor/meteor/blob/devel/packages/tracker/tracker.js\n *\n * 相关论文：https://dl.acm.org/doi/fullHtml/10.1145/3184558.3185978\n */\nexport namespace Tracker {\n  const _pendingComputations: Computation[] = [];\n  const _afterFlushCallbacks: ICallback[] = [];\n  // `true` if a Tracker.flush is scheduled, or if we are in Tracker.flush now\n  let _willFlush = false;\n  // `true` if we are in Tracker.flush now\n  let _inFlush = false;\n  // `true` if we are computing a computation now, either first time\n  // or recompute.  This matches Tracker.active unless we are inside\n  // Tracker.nonreactive, which nullfies currentComputation even though\n  // an enclosing computation may still be running.\n  let _inCompute = false;\n  let _currentComputation: Computation | undefined = undefined;\n  // `true` if the `_throwFirstError` option was passed in to the call\n  // to Tracker.flush that we are in. When set, throw rather than log the\n  // first error encountered while flushing. Before throwing the error,\n  // finish flushing (from a finally block), logging any subsequent\n  // errors.\n  let _throwFirstError = false;\n\n  export interface FlushOptions {\n    finishSynchronously?: boolean;\n    throwFirstError?: boolean;\n  }\n\n  function _throwOrLog(msg: string, e: any) {\n    if (_throwFirstError) {\n      throw e;\n    } else {\n      console.error(`[Tracker error] ${msg}`, e);\n    }\n  }\n\n  // Run all pending computations and afterFlush callbacks.  If we were not called\n  // directly via Tracker.flush, this may return before they're all done to allow\n  // the event loop to run a little before continuing.\n  function _runFlush(options?: FlushOptions) {\n    // Nested flush could plausibly happen if, say, a flush causes\n    // DOM mutation, which causes a \"blur\" event, which runs an\n    // app event handler that calls Tracker.flush.  At the moment\n    // Spark blocks event handlers during DOM mutation anyway,\n    // because the LiveRange tree isn't valid.  And we don't have\n    // any useful notion of a nested flush.\n    if (inFlush()) throw new Error(\"Can't call Tracker.flush while flushing\");\n\n    if (_inCompute) throw new Error(\"Can't flush inside Tracker.autorun\");\n\n    options = options || {};\n\n    _inFlush = true;\n    _willFlush = true;\n    _throwFirstError = !!options.throwFirstError;\n\n    var recomputedCount = 0;\n    var finishedTry = false;\n    try {\n      while (_pendingComputations.length || _afterFlushCallbacks.length) {\n        // recompute all pending computations\n        while (_pendingComputations.length) {\n          var comp = _pendingComputations.shift()!;\n          comp._recompute();\n          if (comp._needsRecompute()) {\n            _pendingComputations.unshift(comp);\n          }\n\n          if (!options.finishSynchronously && ++recomputedCount > 100) {\n            finishedTry = true;\n            return;\n          }\n        }\n\n        if (_afterFlushCallbacks.length) {\n          // call one afterFlush callback, which may\n          // invalidate more computations\n          var func = _afterFlushCallbacks.shift()!;\n          try {\n            func();\n          } catch (e: any) {\n            _throwOrLog('afterFlush', e);\n          }\n        }\n      }\n      finishedTry = true;\n    } finally {\n      if (!finishedTry) {\n        // we're erroring due to throwFirstError being true.\n        _inFlush = false; // needed before calling `Tracker.flush()` again\n        // finish flushing\n        _runFlush({\n          finishSynchronously: options.finishSynchronously,\n          throwFirstError: false,\n        });\n      }\n      _willFlush = false;\n      _inFlush = false;\n      if (_pendingComputations.length || _afterFlushCallbacks.length) {\n        // We're yielding because we ran a bunch of computations and we aren't\n        // required to finish synchronously, so we'd like to give the event loop a\n        // chance. We should flush again soon.\n        if (options.finishSynchronously) {\n          throw new Error('still have more to do?'); // shouldn't happen\n        }\n        setTimeout(_requireFlush, 10);\n      }\n    }\n  }\n\n  function _requireFlush() {\n    if (!_willFlush) {\n      setTimeout(_runFlush, 0);\n      _willFlush = true;\n    }\n  }\n\n  /******************************** Tracker Base API ******************************************/\n\n  /**\n   * 函数在响应式模块中执行\n   * @param computation\n   * @param f\n   */\n  export function withComputation<T = any>(\n    computation: Computation,\n    f: ICallback<Computation, T>,\n  ): T {\n    let previousComputation = _currentComputation;\n    _currentComputation = computation;\n    try {\n      return f.call(null, computation);\n    } finally {\n      _currentComputation = previousComputation;\n    }\n  }\n\n  /**\n   * 函数在非响应式模块中执行\n   */\n  export function withoutComputation<T = any>(f: ICallback<undefined, T>): T {\n    let previousComputation = _currentComputation;\n    _currentComputation = undefined;\n    try {\n      return f(undefined);\n    } finally {\n      _currentComputation = previousComputation;\n    }\n  }\n\n  export function isActive(): boolean {\n    return !!_currentComputation;\n  }\n\n  export function getCurrentComputation(): Computation | undefined {\n    return _currentComputation;\n  }\n\n  /**\n   * Run a function now and rerun it later whenever its dependencies\n   * change. Returns a Computation object that can be used to stop or observe the\n   * rerunning.\n   */\n  export function autorun<T = any>(\n    f: IComputationCallback<T>,\n    options?: { onError: ICallback<Error> },\n  ): Computation<T> {\n    var c = new Computation<T>(f, _currentComputation, options?.onError);\n\n    if (isActive())\n      Tracker.onInvalidate(function () {\n        c.stop();\n      });\n\n    return c;\n  }\n\n  export function onInvalidate(f: ICallback<Computation | undefined>) {\n    if (!_currentComputation) {\n      throw new Error('Tracker.onInvalidate requires a currentComputation');\n    }\n    _currentComputation.onInvalidate(f);\n  }\n\n  /**\n   * True if we are computing a computation now, either first time or recompute.  This matches Tracker.active unless we are inside Tracker.nonreactive, which nullfies currentComputation even though an enclosing computation may still be running.\n   */\n  export function inFlush(): boolean {\n    return _inFlush;\n  }\n\n  /**\n   * Process all reactive updates immediately and ensure that all invalidated computations are rerun.\n   */\n  export function flush(options?: Omit<FlushOptions, 'finishSynchronously'>) {\n    _runFlush({\n      finishSynchronously: true,\n      throwFirstError: options && options.throwFirstError,\n    });\n  }\n\n  /**\n   * Schedules a function to be called during the next flush, or later in the current flush if one is in progress, after all invalidated computations have been rerun.  The function will be run once and not on subsequent flushes unless `afterFlush` is called again.\n   */\n  export function afterFlush(f: ICallback) {\n    _afterFlushCallbacks.push(f);\n    _requireFlush();\n  }\n\n  /********************************************************************************************/\n\n  export type IComputationCallback<V = any> = ICallback<Computation, V>;\n\n  /**\n   * A Computation object represents code that is repeatedly rerun\n   * in response to\n   * reactive data changes. Computations don't have return values; they just\n   * perform actions, such as rerendering a template on the screen. Computations\n   * are created using Tracker.autorun. Use stop to prevent further rerunning of a\n   * computation.\n   */\n  export class Computation<V = any> {\n    private _onInvalidateCallbacks: IComputationCallback[] = [];\n\n    private _onStopCallbacks: IComputationCallback[] = [];\n\n    private _recomputing = false;\n\n    private _result: V;\n\n    /**\n     * 是否停止\n     */\n    public stopped = false;\n\n    /**\n     * 未开始执行则返回 false\n     */\n    public invalidated = false;\n\n    /**\n     * 是否第一次执行\n     */\n    public firstRun = true;\n\n    constructor(\n      private _fn: IComputationCallback<V>,\n      public readonly parent?: Computation,\n      private readonly _onError?: ICallback<Error>,\n    ) {\n      let hasError = true;\n      try {\n        this._compute();\n        hasError = false;\n      } finally {\n        this.firstRun = false;\n        if (hasError) {\n          this.stop();\n        }\n      }\n    }\n\n    onInvalidate(f: IComputationCallback): void {\n      if (this.invalidated) {\n        withoutComputation(f.bind(null, this));\n      } else {\n        this._onInvalidateCallbacks.push(f);\n      }\n    }\n\n    /**\n     * @summary Invalidates this computation so that it will be rerun.\n     */\n    invalidate() {\n      if (!this.invalidated) {\n        // if we're currently in _recompute(), don't enqueue\n        // ourselves, since we'll rerun immediately anyway.\n        if (!this._recomputing && !this.stopped) {\n          _requireFlush();\n          _pendingComputations.push(this);\n        }\n\n        this.invalidated = true;\n\n        // callbacks can't add callbacks, because\n        // this.invalidated === true.\n        for (var i = 0, f: IComputationCallback; (f = this._onInvalidateCallbacks[i]); i++) {\n          withoutComputation(f.bind(null, this));\n        }\n        this._onInvalidateCallbacks = [];\n      }\n    }\n\n    /**\n     * @summary Prevents this computation from rerunning.\n     * @locus Client\n     */\n    stop() {\n      if (!this.stopped) {\n        this.stopped = true;\n        this.invalidate();\n        for (let i = 0, f: IComputationCallback; (f = this._onStopCallbacks[i]); i++) {\n          withoutComputation(f.bind(null, this));\n        }\n        this._onStopCallbacks = [];\n      }\n    }\n\n    onStop(f: IComputationCallback): void {\n      if (this.stopped) {\n        withoutComputation(f.bind(null, this));\n      } else {\n        this._onStopCallbacks.push(f);\n      }\n    }\n\n    private _compute(): void {\n      this.invalidated = false;\n\n      var previousInCompute = _inCompute;\n      _inCompute = true;\n      try {\n        this._result = Tracker.withComputation<V>(this, this._fn);\n      } finally {\n        _inCompute = previousInCompute;\n      }\n    }\n\n    _needsRecompute() {\n      return this.invalidated && !this.stopped;\n    }\n\n    _recompute() {\n      this._recomputing = true;\n      try {\n        if (this._needsRecompute()) {\n          try {\n            this._compute();\n          } catch (e: any) {\n            if (this._onError) {\n              this._onError(e);\n            } else {\n              _throwOrLog('recompute', e);\n            }\n          }\n        }\n      } finally {\n        this._recomputing = false;\n      }\n    }\n\n    /**\n     * @summary Process the reactive updates for this computation immediately\n     * and ensure that the computation is rerun. The computation is rerun only\n     * if it is invalidated.\n     */\n    flush() {\n      if (this._recomputing) return;\n\n      this._recompute();\n    }\n\n    /**\n     * @summary Causes the function inside this computation to run and\n     * synchronously process all reactive updtes.\n     * @locus Client\n     */\n    run() {\n      this.invalidate();\n      this.flush();\n    }\n\n    get result(): V {\n      return this._result;\n    }\n  }\n\n  /**\n   * A Dependency represents an atomic unit of reactive data that a\n   * computation might depend on. Reactive data sources such as Session or\n   * Minimongo internally create different Dependency objects for different\n   * pieces of data, each of which may be depended on by multiple computations.\n   * When the data changes, the computations are invalidated.\n   */\n  export class Dependency {\n    private _dependents: Set<Computation> = new Set<Computation>();\n\n    /**\n     * Declares that the current computation (or `fromComputation` if given) depends on `dependency`.  The computation will be invalidated the next time `dependency` changes.\n     * If there is no current computation and `depend()` is called with no arguments, it does nothing and returns false.\n     * Returns true if the computation is a new dependent of `dependency` rather than an existing one.\n     */\n    depend(computation?: Computation): boolean {\n      if (!computation) {\n        if (!isActive()) {\n          return false;\n        }\n        computation = _currentComputation;\n      }\n      if (!this._dependents.has(computation!)) {\n        this._dependents.add(computation!);\n        computation!.onInvalidate(() => {\n          this._dependents.delete(computation!);\n        });\n        return true;\n      }\n      return false;\n    }\n\n    /**\n     * Invalidate all dependent computations immediately and remove them as dependents.\n     */\n    changed() {\n      for (const dep of this._dependents) {\n        dep.invalidate();\n      }\n    }\n\n    /**\n     * True if this Dependency has one or more dependent Computations, which would be invalidated if this Dependency were to change.\n     */\n    hasDependents() {\n      return this._dependents.size !== 0;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/common/reactive/src/hooks/use-observe.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useMemo } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/utils';\n\nimport { createProxy } from '../utils/create-proxy';\nimport { Tracker } from '../core/tracker';\n\nimport Computation = Tracker.Computation;\n\nexport function useObserve<T extends Record<string, any>>(value: T | undefined): T {\n  const refresh = useRefresh();\n  const computationMap = useMemo<Map<string, Computation>>(() => new Map(), []);\n  const clear = useCallback(() => {\n    computationMap.forEach((comp) => comp.stop());\n    computationMap.clear();\n  }, []);\n  useEffect(() => clear, []);\n  // 重新渲染需要清空依赖\n  clear();\n  return useMemo(() => {\n    if (value === undefined) return {} as T;\n    return createProxy(value, {\n      get(target, key: string) {\n        let computation = computationMap.get(key);\n        if (!computation) {\n          computation = new Tracker.Computation((c) => {\n            if (!c.firstRun) {\n              refresh();\n              return;\n            }\n            return value[key];\n          });\n          computationMap.set(key, computation);\n        }\n        return value[key];\n      },\n    });\n  }, [value]);\n}\n"
  },
  {
    "path": "packages/common/reactive/src/hooks/use-reactive-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { ReactiveState } from '../core/reactive-state';\nimport { useObserve } from './use-observe';\n\nexport function useReactiveState<T extends Record<string, any>>(v: ReactiveState<T> | T): T {\n  const state = useMemo<ReactiveState<T>>(\n    () => (v instanceof ReactiveState ? v : new ReactiveState(v)),\n    [],\n  );\n  return useObserve<T>(state.value);\n}\n"
  },
  {
    "path": "packages/common/reactive/src/hooks/use-readonly-reactive-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ReactiveState } from '../core/reactive-state';\nimport { useObserve } from './use-observe';\n\nexport function useReadonlyReactiveState<T extends Record<string, any>>(\n  state: ReactiveState<T>,\n): Readonly<T> {\n  return useObserve<T>(state.readonlyValue);\n}\n"
  },
  {
    "path": "packages/common/reactive/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Tracker } from './core/tracker';\n\nexport { Tracker } from './core/tracker';\nexport { ReactiveState } from './core/reactive-state';\nexport { ReactiveBaseState } from './core/reactive-base-state';\nexport { useReactiveState } from './hooks/use-reactive-state';\nexport { useReadonlyReactiveState } from './hooks/use-readonly-reactive-state';\nexport { useObserve } from './hooks/use-observe';\nexport { observe } from './react/observe';\nexport const { Dependency, Computation } = Tracker;\n"
  },
  {
    "path": "packages/common/reactive/src/react/observe.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useRef, useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/utils';\n\nimport { Tracker } from '../core/tracker';\n\nimport Computation = Tracker.Computation;\n\nexport function observe<T = any>(fc: React.FC<T>): React.FC<T> {\n  return function ReactiveObserver(props: T) {\n    const childrenRef = useRef<React.ReactNode | null>();\n    const computationRef = useRef<Computation | undefined>();\n    const refresh = useRefresh();\n    computationRef.current?.stop();\n    computationRef.current = new Tracker.Computation((c) => {\n      if (c.firstRun) {\n        childrenRef.current = fc(props);\n      } else {\n        refresh();\n      }\n    });\n    useEffect(\n      () => () => {\n        computationRef.current?.stop();\n      },\n      []\n    );\n    return childrenRef.current!;\n  };\n}\n"
  },
  {
    "path": "packages/common/reactive/src/utils/create-proxy.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ninterface ProxyOptions<V> {\n  get?: (target: V, key: string) => any;\n  set?: (target: V, key: string, newValue: any) => boolean;\n}\n\nexport function createProxy<V extends Record<string, any>>(target: V, opts: ProxyOptions<V>): V {\n  let useProxy = 'Proxy' in window;\n  if (process.env.NODE_ENV === 'test') {\n    if ((global as any).__ignoreProxy) {\n      useProxy = false;\n    }\n  }\n  if (useProxy) {\n    return new Proxy<V>(target, opts);\n  }\n  const result: V = {} as V;\n  for (const key in target) {\n    Object.defineProperty(result, key, {\n      enumerable: true,\n      get: opts.get ? () => opts.get!(target, key) : undefined,\n      set: opts.set ? (newValue: any) => opts.set!(target, key, newValue) : undefined,\n    });\n  }\n  return result;\n}\n"
  },
  {
    "path": "packages/common/reactive/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"],\n    \"lib\": [\n      \"dom\",\n      \"es5\",\n      \"scripthost\",\n      \"es2015.collection\"\n    ]\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/common/reactive/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/common/utils/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/common/utils/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/utils\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"clsx\": \"^1.1.1\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"jsdom\": \"^26.1.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/add-event-listener.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable } from './disposable';\ntype EventListener<K extends keyof HTMLElementEventMap> = (\n  this: HTMLElement,\n  event: HTMLElementEventMap[K],\n) => any;\ntype EventListenerOrEventListenerObject<K extends keyof HTMLElementEventMap> = EventListener<K>;\nexport function addEventListener<K extends keyof HTMLElementEventMap>(\n  element: HTMLElement,\n  type: K,\n  listener: EventListenerOrEventListenerObject<K>,\n  useCapture?: boolean,\n): Disposable {\n  element.addEventListener(type, listener, useCapture);\n  return Disposable.create(() => element.removeEventListener(type, listener, useCapture));\n}\n"
  },
  {
    "path": "packages/common/utils/src/array.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { arrayToSet, arrayUnion, iterToArray } from './array';\n\ndescribe('array', () => {\n  test('arrayToSet', async () => {\n    expect([...arrayToSet([])]).toEqual([]);\n    expect([...arrayToSet([1])]).toEqual([1]);\n    expect([...arrayToSet([1, 2])]).toEqual([1, 2]);\n    expect([...arrayToSet([1, undefined, 3])]).toEqual([1, undefined, 3]);\n\n    expect(arrayToSet([1, 2]).has(2)).toBeTruthy();\n  });\n\n  test('iterToArray', async () => {\n    expect(iterToArray(arrayToSet([]).values())).toEqual([]);\n    expect(iterToArray(arrayToSet([1]).values())).toEqual([1]);\n    expect(iterToArray(arrayToSet([1, 2]).values())).toEqual([1, 2]);\n    expect(iterToArray(arrayToSet([1, undefined, 3]).values())).toEqual([1, undefined, 3]);\n  });\n\n  test('arrayUnion', async () => {\n    expect(arrayUnion([])).toEqual([]);\n\n    expect(arrayUnion([1])).toEqual([1]);\n    expect(arrayUnion([1, 2])).toEqual([1, 2]);\n    expect(arrayUnion([1, 2, 1])).toEqual([1, 2]);\n\n    expect(arrayUnion([''])).toEqual(['']);\n    expect(arrayUnion(['1'])).toEqual(['1']);\n    expect(arrayUnion(['1', '2'])).toEqual(['1', '2']);\n    expect(arrayUnion(['1', '2', '1'])).toEqual(['1', '2']);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/array.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function iterToArray<T = any>(iter: IterableIterator<T>): T[] {\n  const result = [];\n  for (const v of iter) {\n    result.push(v);\n  }\n  return result;\n}\n\nexport function arrayToSet(arr: any[]): Set<any> {\n  const set = new Set();\n  for (let i = 0, len = arr.length; i < len; i++) {\n    set.add(arr[i]);\n  }\n  return set;\n}\n\n/**\n * @see https://stackoverflow.com/a/9229821\n *  export function arrayUnion(arr: any[]): any[] {\n *     return [...new Set(arr)]\n *  }\n */\nexport function arrayUnion(arr: any[]): any[] {\n  const result: any[] = [];\n  for (let i = 0, len = arr.length; i < len; i++) {\n    if (!result.includes(arr[i])) result.push(arr[i]);\n  }\n  return result;\n}\n"
  },
  {
    "path": "packages/common/utils/src/cache.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * @jest-environment jsdom\n */\nimport { describe, beforeEach, test, expect } from 'vitest';\n\nimport { delay } from './promise-util';\nimport { Cache, type CacheOriginItem } from './cache';\n\ninterface Item extends CacheOriginItem {\n  key: string | number;\n}\n\nlet _uid = 0;\nfunction itemFactory(): Cache<Item> {\n  const item: Item = {\n    key: `${_uid++}`,\n  };\n  return item;\n}\n\nfunction dispose() {}\n\nfunction itemWithDisposeFactory(): Cache<Item> {\n  const item: Item = {\n    key: `${_uid++}`,\n  };\n  Cache.assign(item, { dispose });\n  return item;\n}\n\ndescribe('cache', () => {\n  beforeEach(() => {\n    _uid = 0;\n  });\n\n  test('Cache/getFromCache', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    expect(cache.getFromCache()).toEqual([]);\n  });\n\n  test('Cache/get', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    expect(cache.get()).toEqual({ key: '0' });\n    expect(cache.get()).toEqual({ key: '0' });\n  });\n\n  test('Cache/getMore', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    expect(cache.get()).toEqual({ key: '0' });\n    expect(cache.getMore(1)).toEqual([{ key: '0' }]);\n    expect(cache.getMore(2)).toEqual([{ key: '0' }, { key: '1' }]);\n    expect(cache.getMore(1)).toEqual([{ key: '0' }]);\n    expect(cache.getMore(2)).toEqual([{ key: '0' }, { key: '2' }]);\n    expect(cache.getMore(1, false)).toEqual([{ key: '0' }]);\n    expect(cache.getMore(3)).toEqual([{ key: '0' }, { key: '2' }, { key: '3' }]);\n  });\n\n  test('Cache/getMore/deleteLimit', async () => {\n    const cache = Cache.create<Item>(itemFactory, { deleteLimit: 2 });\n    expect(cache.getMore(4)).toEqual([{ key: '0' }, { key: '1' }, { key: '2' }, { key: '3' }]);\n    expect(cache.getMore(1)).toEqual([{ key: '0' }]);\n    expect(cache.getMore(2)).toEqual([{ key: '0' }, { key: '4' }]);\n  });\n\n  test('Cache/getFromCacheByKey', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    expect(cache.get()).toEqual({ key: '0' });\n    expect(cache.getFromCacheByKey('0')).toEqual({ key: '0' });\n    expect(cache.getFromCacheByKey('1')).toEqual(undefined);\n  });\n\n  test('Cache/getMoreByItemKeys', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    const items = cache.getMoreByItemKeys([{ key: '0' }, { key: '1' }]);\n    expect(items).toEqual([{ key: '0' }, { key: '1' }]);\n    // cache.clear()\n    items[0].key = undefined as any;\n    (items[0] as any).dispose = dispose;\n    expect(cache.getMoreByItemKeys([{ key: '1' }])).toEqual([{ key: '1' }]);\n  });\n\n  test('Cache/getMoreByItems', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    const item1 = { key: '1' };\n    const items = cache.getMoreByItems([{ key: '0' }, item1]);\n    expect(items).toEqual([{ key: { key: '0' } }, { key: { key: '1' } }]);\n    expect(cache.getMoreByItems([item1])).toEqual([{ key: { key: '1' } }]);\n\n    expect(cache.getMoreByItems([{ key: '1' }])).toEqual([{ key: { key: '1' } }]);\n    // // cache.clear()\n    items[0].key = undefined as any;\n    (items[0] as any).dispose = dispose;\n    expect(cache.getMoreByItems([{ key: '1' }])).toEqual([{ key: { key: '1' } }]);\n    // expect(cache.getMoreByItems([{ key: '2' }])).toEqual([{ key: { key: '2' } }]);\n  });\n\n  test('Cache/clear', async () => {\n    const cache = Cache.create<Item>(itemFactory);\n    expect(cache.getMore(2)).toEqual([{ key: '0' }, { key: '1' }]);\n    expect(cache.get()).toEqual({ key: '0' });\n    cache.clear();\n    expect(cache.get()).toEqual({ key: '2' });\n  });\n\n  test('Cache/disposes', async () => {\n    const cache = Cache.create<Item>(itemWithDisposeFactory);\n    expect(cache.getMore(2)).toEqual([\n      { key: '0', dispose },\n      { key: '1', dispose },\n    ]);\n    expect(cache.getMore(1)).toEqual([{ key: '0', dispose }]);\n\n    cache.dispose();\n    expect(cache.getMore(2)).toEqual([\n      { key: '2', dispose },\n      { key: '3', dispose },\n    ]);\n    expect(cache.getMoreByItemKeys([{ key: '2' }])).toEqual([{ key: '2', dispose }]);\n  });\n\n  test('createShortCache', async () => {\n    const cache = Cache.createShortCache(10);\n    let id = 0;\n    const getValue = () => ++id;\n    expect(cache.get(getValue)).toEqual(1);\n    expect(cache.get(getValue)).toEqual(1);\n    await delay(20);\n    expect(cache.get(getValue)).toEqual(2);\n\n    const cache1 = Cache.createShortCache();\n    expect(cache1.get(getValue)).toEqual(3);\n  });\n\n  test('createWeakCache', async () => {\n    const cache = Cache.createWeakCache();\n    const el = document.createElement('div');\n    cache.save(el, 1);\n    expect(cache.get(el)).toEqual(1);\n    expect(cache.isChanged(el, 1)).toEqual(false);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/cache.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Disposable } from './disposable';\nimport { Compare } from './compare';\n\nexport interface CacheManager<T, ITEM extends CacheOriginItem = CacheOriginItem>\n  extends Disposable {\n  get(): T;\n  getMore(count: number, autoDelete?: boolean): T[];\n  getMoreByItemKeys(item: ITEM[]): T[];\n  getMoreByItems(item: ITEM[]): T[];\n  /**\n   * 从缓存中获取\n   * @param key\n   */\n  getFromCacheByKey(key: string): T | undefined;\n  /**\n   * 获取所有缓存\n   */\n  getFromCache(): Cache<T>[];\n  /**\n   * 清空缓存数据\n   */\n  clear(): void;\n}\n\nexport interface ShortCache<T> {\n  get(fn: () => T): T;\n}\n\nexport interface WeakCache {\n  get(key: any): any;\n  save(key: any, value: any): void;\n  isChanged(key: any, value: any): boolean;\n}\n\nexport type Cache<T> = {\n  [P in keyof T]: T[P];\n} & { dispose?: () => void; key?: any };\n\nexport interface CacheOpts {\n  deleteLimit?: number; // 限制数目，只有超过这个数目，才会自动删除\n}\n\nexport interface CacheOriginItem {\n  key?: any;\n}\n\n/**\n * 缓存工具：\n *  1. 可延迟按需创建，提升性能\n *  2. 可支持多个或单个，有些动态创建多个的场景可以共享已有的实例，提升性能\n *  3. 自动删除，超过一定的数目会自动做清空回收\n *\n * @example\n *  function htmlFactory<HTMLElement>(): Cache<HTMLElement> {\n *    const el = document.createElement('div')\n *    return Cache.assign(el, { dispose: () => el.remove() })\n *  }\n *  const htmlCache = Cache.create<HTMLElement>(htmlFactory)\n *  console.log(htmlCache.get() === htmlCache.get()) // true\n *  console.log(htmlCache.getMore(3)) // [HTMLElement, HTMLElement, HTMLElement]\n *  console.log(htmlCache.getMore(2)) // [HTMLElement, HTMLElement] 自动删除第三个\n */\nexport namespace Cache {\n  export function create<T, ITEM extends CacheOriginItem = CacheOriginItem>(\n    cacheFactory: (item?: ITEM) => Cache<T>,\n    opts: CacheOpts = {},\n  ): CacheManager<T, ITEM> {\n    let cache: Cache<T>[] = [];\n    return {\n      getFromCache(): Cache<T>[] {\n        return cache;\n      },\n      getMore(count: number, autoDelete = true): T[] {\n        if (count === cache.length) {\n          // 强调互斥，统一 return cache.slice()\n        } else if (count > cache.length) {\n          let added = count - cache.length;\n          while (added > 0) {\n            cache.push(cacheFactory());\n            added--;\n          }\n        } else if (autoDelete) {\n          const deleteLimit = opts.deleteLimit ?? 0;\n          // 只有剩余个数超过 deleteLimit，才会自动删除\n          if (cache.length - count > deleteLimit) {\n            const deleted = cache.splice(count);\n            deleted.forEach(el => el.dispose && el.dispose());\n          }\n        }\n\n        return cache.slice(0, count);\n      },\n      /**\n       * 通过 key 去创建缓存\n       * @param items\n       */\n      getMoreByItemKeys(items: ITEM[]): T[] {\n        const newCache: Cache<T>[] = [];\n        const findedMap: Map<any, any> = new Map();\n        cache.forEach(item => {\n          const finded = items.find(i => i.key === item.key);\n          if (finded) {\n            findedMap.set(item.key, item);\n          } else {\n            item.dispose?.();\n          }\n        });\n        items.forEach(item => {\n          if (!item.key) throw new Error('getMoreByItemKeys need a key');\n          const finded = findedMap.get(item.key);\n          if (finded) {\n            newCache.push(finded);\n          } else {\n            newCache.push(cacheFactory(item));\n          }\n        });\n        cache = newCache;\n        return cache;\n      },\n      /**\n       * 通过 item 引用取拿缓存数据\n       */\n      getMoreByItems(items: any[]): T[] {\n        const newCache: Cache<T>[] = [];\n        const findedMap: Map<any, any> = new Map();\n        cache.forEach(cacheItem => {\n          // 这里 key 存的是 item 的引用\n          const finded = items.find(ref => ref === cacheItem.key);\n          if (finded) {\n            findedMap.set(cacheItem.key, cacheItem);\n          } else {\n            cacheItem.dispose?.();\n          }\n        });\n        items.forEach(item => {\n          const finded = findedMap.get(item);\n          if (finded) {\n            newCache.push(finded);\n          } else {\n            newCache.push({\n              ...cacheFactory(item),\n              key: item,\n            });\n          }\n        });\n        cache = newCache;\n        return cache;\n      },\n      get(): T {\n        if (cache.length > 0) return cache[0];\n        cache.push(cacheFactory());\n        return cache[0];\n      },\n      getFromCacheByKey(key: string): T | undefined {\n        return cache.find(item => item.key === key);\n      },\n      dispose(): void {\n        cache.forEach(item => item.dispose && item.dispose());\n        cache.length = 0;\n      },\n      clear(): void {\n        this.dispose();\n      },\n    };\n  }\n\n  export function assign<T = any>(target: T, fn: Disposable): Cache<T> {\n    return Object.assign(target as any, fn) as any;\n  }\n\n  /**\n   * 短存储\n   * @param timeout\n   */\n  export function createShortCache<T>(timeout = 1000): ShortCache<T> {\n    let cache: T | undefined;\n    let timeoutId: number | undefined;\n\n    function updateTimeout(): void {\n      if (timeoutId) clearTimeout(timeoutId);\n      timeoutId = setTimeout(() => {\n        timeoutId = undefined;\n        cache = undefined;\n        // 这里加 any 是因为在 nodejs 场景 setTimeout 返回的格式定义的不是 number, yarn dev 会报错\n      }, timeout) as any;\n    }\n\n    return {\n      get(getValue: () => T): T {\n        if (cache) {\n          updateTimeout();\n          return cache;\n        }\n        cache = getValue();\n        updateTimeout();\n        return cache;\n      },\n    };\n  }\n\n  export function createWeakCache(): WeakCache {\n    const weakCache: WeakMap<any, any> = new WeakMap();\n    return {\n      get: key => weakCache.get(key),\n      save: (key: any, value: any) => weakCache.set(key, value),\n      isChanged: (key: any, value: any) => Compare.isChanged(weakCache.get(key), value),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/cancellation.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport {\n  CancellationToken,\n  CancellationTokenSource,\n  type MutableToken,\n  cancelled,\n  checkCancelled,\n  isCancelled,\n} from './cancellation';\n\ndescribe('cancellation', () => {\n  test('CancellationTokenSource', async () => {\n    const tokenSource = new CancellationTokenSource();\n    tokenSource.dispose();\n    expect(tokenSource.token.isCancellationRequested).toBeTruthy();\n  });\n\n  test('CancellationTokenSource', async () => {\n    const tokenSource = new CancellationTokenSource();\n    expect(tokenSource.token).toBeDefined();\n    tokenSource.dispose();\n    expect(tokenSource.token.isCancellationRequested).toBeTruthy();\n  });\n\n  test('CancellationTokenSource', async () => {\n    const tokenSource = new CancellationTokenSource();\n\n    const arr: number[] = [];\n    const listener = (): void => {\n      arr.push(1);\n    };\n    tokenSource.token.onCancellationRequested(listener);\n    const mutableToken = tokenSource.token as MutableToken;\n    expect(mutableToken.isCancellationRequested).toBeFalsy();\n    mutableToken.cancel();\n    expect(mutableToken.isCancellationRequested).toBeTruthy();\n\n    const shortcutEventDisposable = tokenSource.token.onCancellationRequested(listener);\n    shortcutEventDisposable.dispose();\n    expect(mutableToken.isCancellationRequested).toBeTruthy();\n  });\n\n  test('cancelled()', async () => {\n    expect(cancelled().message).toEqual('Cancelled');\n  });\n\n  test('isCancelled()', async () => {\n    expect(isCancelled(cancelled())).toBeTruthy();\n  });\n\n  test('checkCancelled()', async () => {\n    expect(checkCancelled(CancellationToken.None)).toBeUndefined();\n    expect(() => checkCancelled(CancellationToken.Cancelled)).toThrowError(/Cancelled/);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/cancellation.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/*---------------------------------------------------------------------------------------------\n *  Copyright (c) Microsoft Corporation and others. All rights reserved.\n *  Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.\n *\n * Fork: https://github.com/Microsoft/vscode/blob/main/src/vs/base/common/cancellation.ts\n *--------------------------------------------------------------------------------------------*/\n\nimport { Emitter, Event } from './event';\nimport { type Disposable } from './disposable';\n\nexport interface CancellationToken {\n  /**\n   * A flag signalling is cancellation has been requested.\n   */\n  readonly isCancellationRequested: boolean;\n  /**\n   * An event which fires when cancellation is requested. This event\n   * only ever fires `once` as cancellation can only happen once. Listeners\n   * that are registered after cancellation will be called (next event loop run),\n   * but also only once.\n   * @event\n   */\n  readonly onCancellationRequested: Event<void>;\n}\n\nconst shortcutEvent: Event<any> = Object.freeze(function (callback, context?): Disposable {\n  const handle = setTimeout(callback.bind(context), 0);\n  return {\n    dispose() {\n      clearTimeout(handle);\n    },\n  };\n});\n\nexport namespace CancellationToken {\n  export function isCancellationToken(thing: unknown): thing is CancellationToken {\n    if (thing === CancellationToken.None || thing === CancellationToken.Cancelled) {\n      return true;\n    }\n    if (thing instanceof MutableToken) {\n      return true;\n    }\n    if (!thing || typeof thing !== 'object') {\n      return false;\n    }\n    return (\n      typeof (thing as CancellationToken).isCancellationRequested === 'boolean' &&\n      typeof (thing as CancellationToken).onCancellationRequested === 'function'\n    );\n  }\n  export const None = Object.freeze<CancellationToken>({\n    isCancellationRequested: false,\n    onCancellationRequested: Event.None,\n  });\n\n  export const Cancelled = Object.freeze<CancellationToken>({\n    isCancellationRequested: true,\n    onCancellationRequested: shortcutEvent,\n  });\n}\n\nexport class MutableToken implements CancellationToken {\n  private _isCancelled = false;\n\n  private _emitter?: Emitter<void>;\n\n  public cancel(): void {\n    if (!this._isCancelled) {\n      this._isCancelled = true;\n      if (this._emitter) {\n        this._emitter.fire(undefined);\n        this.dispose();\n      }\n    }\n  }\n\n  get isCancellationRequested(): boolean {\n    return this._isCancelled;\n  }\n\n  get onCancellationRequested(): Event<void> {\n    if (this._isCancelled) {\n      return shortcutEvent;\n    }\n    if (!this._emitter) {\n      this._emitter = new Emitter<void>();\n    }\n    return this._emitter.event;\n  }\n\n  public dispose(): void {\n    if (this._emitter) {\n      this._emitter.dispose();\n      this._emitter = undefined;\n    }\n  }\n}\n\nexport class CancellationTokenSource {\n  private _token: CancellationToken | undefined;\n\n  get token(): CancellationToken {\n    if (!this._token) {\n      // be lazy and create the token only when\n      // actually needed\n      this._token = new MutableToken();\n    }\n    return this._token;\n  }\n\n  cancel(): void {\n    if (!this._token) {\n      // save an object by returning the default\n      // cancelled token when cancellation happens\n      // before someone asks for the token\n      this._token = CancellationToken.Cancelled;\n    } else if (this._token !== CancellationToken.Cancelled) {\n      (<MutableToken>this._token).cancel();\n    }\n  }\n\n  dispose(): void {\n    this.cancel();\n  }\n}\n\nconst cancelledMessage = 'Cancelled';\n\nexport function cancelled(): Error {\n  return new Error(cancelledMessage);\n}\n\nexport function isCancelled(err: Error | undefined): boolean {\n  return !!err && err.message === cancelledMessage;\n}\n\nexport function checkCancelled(token?: CancellationToken): void {\n  if (!!token && token.isCancellationRequested) {\n    throw cancelled();\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/compare.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Compare } from './compare';\n\nconst { isChanged, isDeepChanged, isArrayShallowChanged } = Compare;\n\ndescribe('Compare', () => {\n  test('isChanged', async () => {\n    // base - types\n    expect(isChanged({}, {})).toBeFalsy();\n    expect(isChanged(1, 1)).toBeFalsy();\n    expect(isChanged('1', '1')).toBeFalsy();\n    expect(isChanged(true, true)).toBeFalsy();\n    expect(isChanged(false, false)).toBeFalsy();\n    const obj = { a: 1, b: 2 };\n    expect(isChanged(obj, obj)).toBeFalsy();\n    const arr = [1, 2];\n    expect(isChanged(arr, arr)).toBeFalsy();\n\n    // base\n    expect(isChanged({ a: 1 }, { a: 2 })).toBeTruthy();\n    const node = { v: 1, l: null, r: null };\n    node.v = 2;\n    expect(isChanged({ a: node }, { a: node })).toBeFalsy();\n    expect(isChanged({ a: node }, { a: { ...node } })).toBeTruthy();\n    const node1 = { v: 1, l: null, r: null };\n    expect(isChanged({ a: node }, { a: node1 })).toBeTruthy();\n\n    // depth\n    expect(isChanged({ a: 1 }, { a: 1 }, 0)).toBeTruthy();\n    expect(isChanged({ a: 1 }, { a: 1 }, 1)).toBeFalsy();\n    expect(isChanged({ a: 1 }, { a: 1 }, 2)).toBeFalsy();\n    expect(isChanged({ a: { b: 1 } }, { a: { b: 1 } }, 0)).toBeTruthy();\n    expect(isChanged({ a: { b: 1 } }, { a: { b: 1 } }, 1)).toBeTruthy();\n    expect(isChanged({ a: { b: 1 } }, { a: { b: 1 } }, 2)).toBeFalsy();\n\n    // partial\n    expect(isChanged({ a: 1 }, { a: 1, b: 2 }, 1)).toBeTruthy();\n    expect(isChanged({ a: 1 }, { a: 1, b: 2 }, 1, false)).toBeTruthy();\n  });\n\n  test('isDeepChanged', async () => {\n    expect(isDeepChanged({ a: 1 }, { a: 2 })).toBeTruthy();\n    const node = { v: 1, l: null, r: null };\n    expect(isDeepChanged({ a: node }, { a: node })).toBeFalsy();\n    expect(isDeepChanged({ a: node }, { a: { ...node, v: 2 } })).toBeTruthy();\n    const node1 = { v: 1, l: null, r: null };\n    expect(isDeepChanged({ a: node }, { a: node1 })).toBeFalsy();\n  });\n\n  test('isArrayShallowChanged', async () => {\n    expect(isArrayShallowChanged([], [1])).toBeTruthy();\n    expect(isArrayShallowChanged([1], [])).toBeTruthy();\n    expect(isArrayShallowChanged([1], [1, 2])).toBeTruthy();\n    expect(isArrayShallowChanged([1, 2], [1, 3])).toBeTruthy();\n    expect(isArrayShallowChanged([{}], [{}])).toBeTruthy();\n    expect(isArrayShallowChanged([{ a: 1 }], [{ a: 1 }])).toBeTruthy();\n\n    expect(isArrayShallowChanged([], [])).toBeFalsy();\n    expect(isArrayShallowChanged([1], [1])).toBeFalsy();\n    expect(isArrayShallowChanged([1, null, 3], [1, null, 3])).toBeFalsy();\n    const obj = {};\n    expect(isArrayShallowChanged([obj], [obj])).toBeFalsy();\n    const obj1 = { a: 1, b: 2 };\n    expect(isArrayShallowChanged([obj1], [obj1])).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/compare.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport namespace Compare {\n  /**\n   * 比较，默认浅比较\n   * @param oldProps\n   * @param newProps\n   * @param depth - 比较的深度，默认是 1\n   * @param partial - 比较对象的局部，默认 true\n   */\n  export function isChanged(oldProps: any, newProps: any, depth = 1, partial = true): boolean {\n    if (oldProps === newProps) return false;\n    if (depth === 0 || typeof oldProps !== 'object' || typeof newProps !== 'object') {\n      return oldProps !== newProps;\n    }\n    const keys = Object.keys(newProps);\n    if (!partial) {\n      const oldKeys = Object.keys(oldProps);\n      if (keys.length !== oldKeys.length) return true;\n    }\n    for (let i = 0, len = keys.length; i < len; i++) {\n      const key = keys[i];\n      if (isChanged(oldProps[key], newProps[key], depth - 1, partial)) return true;\n    }\n    return false;\n  }\n  /**\n   * 深度比较\n   * @param oldProps\n   * @param newProps\n   * @param partial - 比较对象的局部，默认 true\n   */\n  export function isDeepChanged(oldProps: any, newProps: any, partial?: boolean): boolean {\n    return isChanged(oldProps, newProps, Infinity, partial);\n  }\n  export function isArrayShallowChanged(arr1: any[], arr2: any[]): boolean {\n    if (arr1.length !== arr2.length) return true;\n    for (let i = 0, len = arr1.length; i < len; i++) {\n      if (arr1[i] !== arr2[i]) {\n        return true;\n      }\n    }\n    return false;\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/compose.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MaybePromise } from './';\n\ntype FuncMaybePromise<D> = (d: D, ...others: any[]) => MaybePromise<D>;\ntype FuncPromise<D> = (d: D, ...others: any[]) => Promise<D>;\ntype Func<D> = (d: D, ...others: any[]) => D;\n\nexport function composeAsync<D>(...fns: FuncMaybePromise<D>[]): FuncPromise<D> {\n  return async (data: D, ...others: any[]) => {\n    let index = 0;\n    while (fns[index]) {\n      data = await fns[index](data, ...others);\n      index += 1;\n    }\n    return data;\n  };\n}\n\nexport function compose<D>(...fns: Func<D>[]): Func<D> {\n  return (data: D, ...others: any[]) => {\n    let index = 0;\n    while (fns[index]) {\n      data = fns[index](data, ...others);\n      index += 1;\n    }\n    return data;\n  };\n}\n"
  },
  {
    "path": "packages/common/utils/src/contribution-provider.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type interfaces } from 'inversify';\n\nexport const ContributionProvider = Symbol('ContributionProvider');\n\nexport interface ContributionProvider<T extends object> {\n  getContributions(): T[];\n\n  forEach(fn: (v: T) => void): void;\n}\n\nclass ContainerContributionProviderImpl<T extends object> implements ContributionProvider<T> {\n  protected services: T[] | undefined;\n\n  constructor(\n    protected readonly container: interfaces.Container,\n    protected readonly identifier: interfaces.ServiceIdentifier<T>\n  ) {}\n\n  forEach(fn: (v: T) => void): void {\n    this.getContributions().forEach(fn);\n  }\n\n  getContributions(): T[] {\n    if (!this.services) {\n      const currentServices: T[] = [];\n      let { container } = this;\n      if (container.isBound(this.identifier)) {\n        try {\n          currentServices.push(...container.getAll(this.identifier));\n        } catch (error: any) {\n          console.error(error);\n        }\n      }\n\n      this.services = currentServices;\n    }\n    return this.services;\n  }\n}\n\nexport function bindContributionProvider(bind: interfaces.Bind, id: symbol): void {\n  bind(ContributionProvider)\n    .toDynamicValue((ctx) => new ContainerContributionProviderImpl(ctx.container, id))\n    .inSingletonScope()\n    .whenTargetNamed(id);\n}\n"
  },
  {
    "path": "packages/common/utils/src/decoration-style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nfunction createStyleElement(\n  styleId: string,\n  container: HTMLElement = document.head,\n): HTMLStyleElement {\n  const style = document.createElement('style');\n  style.id = styleId;\n  style.type = 'text/css';\n  style.media = 'screen';\n  style.appendChild(document.createTextNode('')); // trick for webkit\n  container.appendChild(style);\n  return style;\n}\n\nexport const DecorationStyle = {\n  createStyleElement,\n};\n"
  },
  {
    "path": "packages/common/utils/src/disposable-collection.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter, Event } from './event';\nimport { Disposable } from './disposable';\n\nexport class DisposableImpl implements Disposable {\n  readonly toDispose = new DisposableCollection();\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  get onDispose(): Event<void> {\n    return this.toDispose.onDispose;\n  }\n}\n\nexport class DisposableCollection implements Disposable {\n  protected readonly disposables: Disposable[] = [];\n\n  protected readonly onDisposeEmitter = new Emitter<void>();\n\n  private _disposed = false;\n\n  constructor(...toDispose: Disposable[]) {\n    toDispose.forEach((d) => this.push(d));\n  }\n\n  get length() {\n    return this.disposables.length;\n  }\n\n  get onDispose(): Event<void> {\n    return this.onDisposeEmitter.event;\n  }\n\n  get disposed(): boolean {\n    return this._disposed;\n  }\n\n  dispose(): void {\n    if (this.disposed) {\n      return;\n    }\n    this._disposed = true;\n    this.disposables\n      .slice()\n      .reverse()\n      .forEach((disposable) => {\n        try {\n          disposable.dispose();\n        } catch (e) {\n          console.error(e);\n        }\n      });\n    this.onDisposeEmitter.fire(undefined);\n    this.onDisposeEmitter.dispose();\n  }\n\n  push(disposable: Disposable): Disposable {\n    if (this.disposed) return Disposable.NULL;\n    if (disposable === Disposable.NULL) {\n      return Disposable.NULL;\n    }\n    const { disposables } = this;\n    if (disposables.find((d) => d === disposable)) {\n      return Disposable.NULL;\n    }\n    const originalDispose = disposable.dispose;\n    const toRemove = Disposable.create(() => {\n      const index = disposables.indexOf(disposable);\n      if (index !== -1) {\n        disposables.splice(index, 1);\n      }\n      disposable.dispose = originalDispose;\n    });\n    disposable.dispose = () => {\n      toRemove.dispose();\n      disposable.dispose();\n    };\n    disposables.push(disposable);\n    return toRemove;\n  }\n\n  pushAll(disposables: Disposable[]): Disposable[] {\n    return disposables.map((disposable) => this.push(disposable));\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/disposable.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { DisposableCollection, DisposableImpl } from './disposable-collection';\nimport { Disposable } from './disposable';\n\ndescribe('disposable', () => {\n  test('Disposable', async () => {\n    const disposable: Disposable = {\n      dispose() {},\n    };\n    expect(disposable.dispose()).toBeUndefined();\n\n    expect(Disposable.is(disposable)).toBeTruthy();\n    expect(Disposable.NULL.dispose()).toBeUndefined();\n    expect(Disposable.create(() => {}).dispose()).toBeUndefined();\n  });\n\n  test('DisposableCollection', async () => {\n    let dispose1Times = 0;\n    let dispose3Times = 0;\n    let disposeAllTimes = 0;\n    const execSort: string[] = [];\n    const disposable1: Disposable = {\n      dispose() {\n        dispose1Times += 1;\n        execSort.push('1');\n      },\n    };\n    const disposable2: Disposable = {\n      dispose() {\n        execSort.push('2');\n        throw new Error('[ignore] disposable2 error');\n      },\n    };\n    const disposable3: Disposable = {\n      dispose() {\n        dispose3Times += 1;\n        execSort.push('3');\n      },\n    };\n    const dc = new DisposableCollection(disposable1);\n    dc.onDispose(() => {\n      disposeAllTimes += 1;\n      execSort.push('all');\n    });\n    dc.pushAll([disposable1, disposable2]); // disposable1 add twice;\n    const dispose3Remove = dc.push(disposable3);\n    dispose3Remove.dispose(); // remove;\n\n    dc.dispose();\n    expect(dispose1Times).toEqual(1);\n    expect(dispose3Times).toEqual(0);\n    expect(disposeAllTimes).toEqual(1);\n    expect(dc.disposed).toBeTruthy();\n\n    // readd\n    dc.push(disposable1);\n\n    // dupilicate dispose\n    dc.dispose();\n    expect(dispose1Times).toEqual(1);\n    expect(dispose3Times).toEqual(0);\n    expect(disposeAllTimes).toEqual(1);\n    expect(dc.disposed).toBeTruthy();\n    expect(execSort).toEqual(['2', '1', 'all']);\n  });\n  test('DisposableCololection dispose inside', () => {\n    let dispose1Times = 0;\n    let disposeAllTimes = 0;\n    const dc = new DisposableCollection();\n    const disposable1: Disposable = {\n      dispose() {\n        dc.dispose(); // dispose inside\n        dispose1Times += 1;\n      },\n    };\n    dc.onDispose(() => {\n      dc.dispose(); // dispose inside\n      disposeAllTimes += 1;\n    });\n    dc.push(disposable1);\n\n    dc.dispose();\n    expect(dispose1Times).toEqual(1);\n    expect(disposeAllTimes).toEqual(1);\n    expect(dc.disposed).toBeTruthy();\n  });\n\n  test('DisposableImpl', async () => {\n    const di = new DisposableImpl();\n    const disposedRet: number[] = [];\n    let isDisposed = false;\n    di.onDispose(() => {\n      isDisposed = true;\n    });\n    di.toDispose.pushAll([\n      {\n        dispose() {\n          disposedRet.push(1);\n        },\n      },\n    ]);\n    di.dispose();\n    expect(di.disposed).toBeTruthy();\n    expect(disposedRet).toEqual([1]);\n    expect(isDisposed).toBeTruthy();\n  });\n  test('DisposableCollection auto remove', () => {\n    const dc = new DisposableCollection();\n    const dc2 = new DisposableCollection();\n    const disposable1: Disposable = {\n      dispose() {},\n    };\n    const originalDispose = disposable1.dispose;\n    dc.push(disposable1);\n    dc2.push(disposable1);\n    expect(originalDispose === disposable1.dispose).toBeFalsy();\n    disposable1.dispose();\n    expect(dc.length).toEqual(0);\n    expect(dc2.length).toEqual(0);\n    expect(originalDispose === disposable1.dispose).toBeTruthy();\n  });\n  test('DisposableCollection push cancel', () => {\n    const dc = new DisposableCollection();\n    const disposable1: Disposable = {\n      dispose() {},\n    };\n    const originalDispose = disposable1.dispose;\n    const cancel = dc.push(disposable1);\n    expect(originalDispose === disposable1.dispose).toBeFalsy();\n    cancel.dispose();\n    expect(originalDispose === disposable1.dispose).toBeTruthy();\n  });\n  test('DisposableCollection auto remove nested', () => {\n    const dc1 = new DisposableCollection();\n    const dc2 = new DisposableCollection();\n    const disposable1: Disposable = {\n      dispose() {},\n    };\n    dc1.push(disposable1);\n    dc2.push(dc1);\n    dc2.push(disposable1);\n    dc1.dispose();\n    expect(dc2.length).toEqual(0);\n  });\n  test('DisposableCollection push Disposbale.NULL', () => {\n    const dc = new DisposableCollection();\n    dc.push(Disposable.NULL);\n    expect(dc.length).toEqual(0);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/disposable.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * An object that performs a cleanup operation when `.dispose()` is called.\n *\n * Some examples of how disposables are used:\n *\n * - An event listener that removes itself when `.dispose()` is called.\n * - The return value from registering a provider. When `.dispose()` is called, the provider is unregistered.\n */\nexport interface Disposable {\n  dispose(): void;\n}\n\nexport namespace Disposable {\n  export function is(thing: any): thing is Disposable {\n    return (\n      typeof thing === 'object' &&\n      thing !== null &&\n      typeof (<Disposable>(<any>thing)).dispose === 'function'\n    );\n  }\n\n  export function create(func: () => void): Disposable {\n    return {\n      dispose: func,\n    };\n  }\n\n  export const NULL = Object.freeze(create(() => {}));\n}\n"
  },
  {
    "path": "packages/common/utils/src/dom-utils.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { domUtils as u } from './dom-utils';\n\ndescribe('dom-utils', () => {\n  test('toPixel', async () => {\n    expect(u.toPixel(0)).toEqual('0px');\n    expect(u.toPixel(1)).toEqual('1px');\n    expect(u.toPixel(1.1)).toEqual('1.1px');\n    expect(u.toPixel(+0)).toEqual('0px');\n    expect(u.toPixel(-0)).toEqual('0px');\n    expect(u.toPixel(-0.1)).toEqual('-0.1px');\n  });\n\n  test('fromPercent', async () => {\n    expect(u.fromPercent('0%')).toEqual(0);\n    expect(u.fromPercent('1%')).toEqual(1);\n    expect(u.fromPercent('1.1%')).toEqual(1.1);\n    expect(u.fromPercent('-0.1%')).toEqual(-0.1);\n  });\n\n  test('toPercent', async () => {\n    expect(u.toPercent(0)).toEqual('0%');\n    expect(u.toPercent(1)).toEqual('1%');\n    expect(u.toPercent(1.1)).toEqual('1.1%');\n    expect(u.toPercent(+0)).toEqual('0%');\n    expect(u.toPercent(-0)).toEqual('0%');\n    expect(u.toPercent(-0.1)).toEqual('-0.1%');\n  });\n\n  test('enableEvent', async () => {\n    const el = document.createElement('div');\n    expect(el.style.pointerEvents).toEqual('');\n    u.enableEvent(el);\n    expect(el.style.pointerEvents).toEqual('all');\n  });\n\n  test('disableEvent', async () => {\n    const el = document.createElement('div');\n    expect(el.style.pointerEvents).toEqual('');\n    u.disableEvent(el);\n    expect(el.style.pointerEvents).toEqual('none');\n\n    u.enableEvent(el);\n    expect(el.style.pointerEvents).toEqual('all');\n  });\n\n  test('createElement', async () => {\n    expect(u.createElement('div', 'a', 'b').className).toEqual('a b');\n  });\n\n  test('createDivWithClass', async () => {\n    expect(u.createDivWithClass('a', 'b').className).toEqual('a b');\n    expect(u.createDivWithClass('a', 'b').tagName).toEqual('DIV');\n  });\n\n  test('addClass', async () => {\n    const el = document.createElement('div');\n    u.addClass(el, 'a', 'b');\n    expect(el.className).toEqual('a b');\n\n    el.className = 'c d';\n    u.addClass(el, 'a', 'b');\n    expect(el.className).toEqual('a b c d');\n  });\n\n  test('delClass', async () => {\n    const el = document.createElement('div');\n    u.addClass(el, 'a', 'b');\n    u.delClass(el, 'a');\n    expect(el.className).toEqual('b');\n    u.delClass(el, 'b');\n    expect(el.className).toEqual('');\n\n    u.delClass(el, 'a', 'b');\n    expect(el.className).toEqual('');\n  });\n\n  test('coverClass', async () => {\n    const el = document.createElement('div');\n    u.coverClass(el, 'a', 'b');\n    expect(el.className).toEqual('a b');\n\n    u.coverClass(el, 'a', 'c', 'd');\n    expect(el.className).toEqual('a c d');\n  });\n\n  test('clearChildren', async () => {\n    const el = document.createElement('div');\n    el.innerHTML = '<a>link</a>';\n    u.clearChildren(el);\n    expect(el.innerHTML).toEqual('');\n\n    const el1 = document.createElement('div');\n    el1.appendChild(document.createElement('a'));\n    expect(el1.innerHTML).toEqual('<a></a>');\n    u.clearChildren(el1);\n    expect(el.innerHTML).toEqual('');\n  });\n\n  test('translatePercent', async () => {\n    const el = document.createElement('div');\n    u.translatePercent(el, 0, 1);\n    expect(el.style.transform).toEqual('translate(0%, 1%)');\n  });\n\n  test('translateXPercent', async () => {\n    const el = document.createElement('div');\n    u.translateXPercent(el, 0);\n    expect(el.style.transform).toEqual('translateX(0%)');\n  });\n\n  test('translateYPercent', async () => {\n    const el = document.createElement('div');\n    u.translateYPercent(el, 1);\n    expect(el.style.transform).toEqual('translateY(1%)');\n  });\n\n  test('setStyle', async () => {\n    const el = document.createElement('div');\n    u.setStyle(el, { width: 10, position: 'fixed', margin: '0 1px' });\n    expect(el.style.width).toEqual('10px');\n    expect(el.style.position).toEqual('fixed');\n    expect(el.style.margin).toEqual('0px 1px');\n\n    const el1 = document.createElement('div');\n    u.setStyle(el1, { width: undefined });\n    expect(el1.style.width).toEqual('');\n\n    const el2 = document.createElement('div');\n    u.setStyle(el2, { Width: 1, paddingTop: 1 } as any);\n    expect(el2.style.width).toEqual('');\n    expect(el2.style['padding-top' as any]).toEqual('1px');\n    expect(el2.style['-width' as any]).toEqual(undefined);\n  });\n\n  test('classNameWithPrefix', async () => {\n    expect(u.classNameWithPrefix('pre')('a b')).toEqual('pre-a pre-b');\n    expect(u.classNameWithPrefix('pre-')('a b')).toEqual('pre--a pre--b');\n  });\n\n  test('addStandardDisposableListener', async () => {\n    const el = document.createElement('div');\n    let called = false;\n    const disposable = u.addStandardDisposableListener(el, 'click', () => {\n      called = true;\n    });\n    const event = document.createEvent('Event');\n    event.initEvent('click');\n\n    el.dispatchEvent(event);\n    expect(called).toEqual(true);\n\n    called = false;\n    disposable.dispose();\n    el.dispatchEvent(event);\n    expect(called).toEqual(false);\n  });\n\n  test('createDOMCache', async () => {\n    const parent = document.createElement('div');\n\n    const cache1 = u.createDOMCache(parent, 'c1');\n    expect(cache1.get()).toEqual(u.createDivWithClass('c1'));\n    const el1 = cache1.get();\n    el1.setStyle({ width: 1 });\n    expect(el1.style.width).toEqual('1px');\n    cache1.dispose();\n\n    const cache2 = u.createDOMCache(parent, () => u.createDivWithClass('c2'), '<div />');\n    const el2 = u.createDivWithClass('c2');\n    el2.innerHTML = '<div />';\n    expect(cache2.get()).toEqual(el2);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/dom-utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport clx from 'clsx';\n\nimport { each } from './objects';\nimport { Disposable } from './disposable';\nimport { Cache, type CacheManager } from './cache';\n\nconst toStyleKey = (key: string) => key.replace(/([A-Z])/, (k) => `-${k.toLowerCase()}`);\n\nexport type CSSStyle = {\n  [P in keyof CSSStyleDeclaration]?: string | number | undefined;\n};\n\nexport interface DOMCache extends HTMLElement, Disposable {\n  setStyle(style: CSSStyle): void;\n  key?: string | number;\n}\n\nexport namespace domUtils {\n  export function toPixel(num: number): string {\n    return `${num}px`;\n  }\n\n  // export function fromPixel(pixel: string): number {\n  //   return parseInt(pixel.substring(0, pixel.length - 2));\n  // }\n\n  export function fromPercent(percent: string): number {\n    return parseFloat(percent.substring(0, percent.length - 1));\n  }\n\n  export function toPercent(percent: number): string {\n    return `${percent}%`;\n  }\n\n  export function enableEvent(element: HTMLDivElement): void {\n    element.style.pointerEvents = 'all';\n  }\n\n  export function disableEvent(element: HTMLDivElement): void {\n    element.style.pointerEvents = 'none';\n  }\n\n  export function createElement<T extends HTMLElement>(ele: string, ...classNames: string[]): T {\n    const element = document.createElement(ele);\n    if (classNames.length > 0) {\n      element.className = clx(classNames);\n    }\n    return element as T;\n  }\n\n  export function createDivWithClass(...classNames: string[]): HTMLDivElement {\n    return createElement('div', ...classNames) as HTMLDivElement;\n  }\n\n  export function addClass(element: Element, ...classNames: string[]): void {\n    element.className = clx(classNames.concat(element.className.split(' ')));\n  }\n\n  export function delClass(element: Element, ...classNames: string[]): void {\n    classNames.forEach((name) => {\n      element.classList.remove(name);\n    });\n    element.className = element.classList.toString();\n  }\n\n  export function coverClass(element: Element, ...classNames: string[]): void {\n    element.className = clx(classNames);\n  }\n\n  export function clearChildren(container: HTMLDivElement): void {\n    container.innerHTML = '';\n  }\n\n  export function translatePercent(node: HTMLDivElement, x: number, y: number): void {\n    node.style.transform = `translate(${x}%, ${y}%)`;\n  }\n\n  export function translateXPercent(node: HTMLDivElement, x: number): void {\n    node.style.transform = `translateX(${x}%)`;\n  }\n\n  export function translateYPercent(node: HTMLDivElement, y: number): void {\n    node.style.transform = `translateY(${y}%)`;\n  }\n\n  export function setStyle(node: HTMLElement, styles: CSSStyle): void {\n    const styleStrs: string[] = [];\n    each(styles, (value, key) => {\n      if (value === undefined) return;\n      if (typeof value === 'number' && key !== 'opacity' && key !== 'zIndex' && key !== 'scale') {\n        value = toPixel(value);\n      }\n      styleStrs.push(`${toStyleKey(key)}:${value}`);\n    });\n    const oldStyle = node.getAttribute('style');\n    const newStyle = styleStrs.join(';');\n    if (oldStyle !== newStyle) {\n      node.setAttribute('style', newStyle);\n    }\n  }\n\n  export function classNameWithPrefix(prefix: string): (key: string, opts?: any) => string {\n    return (key: string, opts?: any) =>\n      clx(\n        key\n          .split(/\\s+/)\n          .map((s) => `${prefix}-${s}`)\n          .join(' '),\n        opts\n      );\n  }\n\n  export function addStandardDisposableListener(\n    dom: HTMLElement | HTMLDocument,\n    type: string,\n    listener: EventListenerOrEventListenerObject | any,\n    options?: boolean | any\n  ): Disposable {\n    dom.addEventListener(type, listener, options);\n    return Disposable.create(() => {\n      dom.removeEventListener(type, listener);\n    });\n  }\n\n  /**\n   * dom 缓存\n   * @param parent\n   * @param className\n   */\n  export function createDOMCache<T extends DOMCache = DOMCache>(\n    parent: HTMLElement,\n    className: string | (() => HTMLElement),\n    children?: string\n  ): CacheManager<T> {\n    return Cache.create<T>((/* item */) => {\n      // item 悬空了？\n      const dom =\n        typeof className === 'string' ? domUtils.createDivWithClass(className) : className();\n      if (children) {\n        dom.innerHTML = children;\n      }\n      parent.appendChild(dom);\n      return Object.assign(dom, {\n        // key: item ? item.key : undefined,\n        dispose: () => {\n          const { parentNode } = dom;\n          if (parentNode) {\n            parentNode.removeChild(dom);\n          }\n        },\n        setStyle: (style: CSSStyle) => {\n          domUtils.setStyle(dom, style);\n        },\n      }) as T;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/event.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Emitter, Event } from './event';\n\ndescribe('event', () => {\n  test('emitter base', () => {\n    const emitter = new Emitter<number>();\n    expect(emitter.disposed).toBe(false);\n    const doResult: number[] = [];\n    const doResult2: number[] = [];\n    const doContext = {};\n    function listener1(num: number) {\n      doResult.push(num);\n    }\n    function listener2(num: number) {\n      // @ts-ignore\n      expect(this).toEqual(doContext);\n      doResult2.push(num);\n    }\n    const dispose1 = emitter.event(listener1);\n    emitter.event(listener2, doContext);\n    emitter.fire(1);\n    expect(doResult).toEqual([1]);\n    expect(doResult2).toEqual([1]);\n    emitter.fire(2);\n    expect(doResult).toEqual([1, 2]);\n    expect(doResult2).toEqual([1, 2]);\n    dispose1.dispose();\n    emitter.fire(3);\n    expect(doResult).toEqual([1, 2]);\n    expect(doResult2).toEqual([1, 2, 3]);\n    emitter.dispose(); // dispose the event;\n    expect(emitter.disposed).toBe(true);\n    emitter.fire(4);\n    expect(doResult).toEqual([1, 2]);\n    expect(doResult2).toEqual([1, 2, 3]);\n  });\n  test('emitter with dispose', () => {\n    const emitter = new Emitter<number>();\n    emitter.dispose(); // dispose the event;\n    const doResult: number[] = [];\n    function listener1(num: number) {\n      doResult.push(num);\n    }\n    const dispose1 = emitter.event(listener1);\n    expect(Event.None(() => {}) === dispose1).toBeTruthy();\n    emitter.fire(1); // do nothing\n    expect(doResult).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/event.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NOOP } from './objects';\nimport { Disposable } from './disposable';\n\nexport interface EventListener<T> {\n  (args: T): void;\n}\n\nexport interface Event<T> {\n  (listener: EventListener<T>, thisArgs?: any): Disposable;\n}\n\nexport namespace Event {\n  export const None: Event<any> = () => Disposable.NULL;\n}\n\nexport class Emitter<T = any> {\n  private _event?: Event<T>;\n\n  private _listeners?: EventListener<T>[];\n\n  private _disposed = false;\n\n  get event(): Event<T> {\n    if (!this._event) {\n      this._event = (listener: EventListener<T>, thisArgs?: any) => {\n        if (this._disposed) {\n          return Disposable.NULL;\n        }\n        if (!this._listeners) {\n          this._listeners = [];\n        }\n        const finalListener = thisArgs ? listener.bind(thisArgs) : listener;\n        this._listeners.push(finalListener);\n\n        const eventDisposable: Disposable = {\n          dispose: () => {\n            eventDisposable.dispose = NOOP;\n            if (!this._disposed) {\n              const index = this._listeners!.indexOf(finalListener);\n              if (index !== -1) {\n                this._listeners!.splice(index, 1);\n              }\n            }\n          },\n        };\n\n        return eventDisposable;\n      };\n    }\n    return this._event;\n  }\n\n  fire(event: T): void {\n    if (this._listeners) {\n      this._listeners.forEach((listener) => listener(event));\n    }\n  }\n\n  get disposed(): boolean {\n    return this._disposed;\n  }\n\n  dispose(): void {\n    if (this._listeners) {\n      this._listeners = undefined;\n    }\n    this._disposed = true;\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/hooks/use-refresh.spec.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { render } from '@testing-library/react';\n\nimport { useRefresh } from './use-refresh';\n\nit('refresh nested', () => {\n  let comp1RenderTimes = 0;\n  let comp2RenderTimes = 0;\n  const Comp1 = () => {\n    comp1RenderTimes++;\n    const refresh = useRefresh();\n    React.useEffect(() => {\n      refresh();\n    }, []);\n    return <div></div>;\n  };\n  const Comp2 = () => {\n    comp2RenderTimes++;\n    const refresh = useRefresh();\n    React.useEffect(() => {\n      refresh();\n    }, []);\n    return (\n      <div>\n        <Comp1 />\n      </div>\n    );\n  };\n  render(<Comp2 />);\n  expect(comp1RenderTimes).toEqual(2);\n  expect(comp2RenderTimes).toEqual(2);\n});\n"
  },
  {
    "path": "packages/common/utils/src/hooks/use-refresh.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useState } from 'react';\n\nexport function useRefresh(defaultValue?: any): (v?: any) => void {\n  const [, update] = useState<any>(defaultValue);\n  return useCallback((v?: any) => update(v !== undefined ? v : {}), []);\n}\n"
  },
  {
    "path": "packages/common/utils/src/id.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { generateLocalId, _setIdx } from './id';\n\ndescribe('id', () => {\n  test('generateLocalId', async () => {\n    expect(generateLocalId()).toBe(0);\n    expect(generateLocalId()).toBe(1);\n    expect(generateLocalId()).toBe(2);\n    expect(generateLocalId()).toBeGreaterThan(2);\n  });\n\n  test('_setIdx', async () => {\n    _setIdx(Number.MAX_SAFE_INTEGER - 1);\n    expect(generateLocalId()).toBe(Number.MAX_SAFE_INTEGER - 1);\n    expect(generateLocalId()).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/id.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nlet _idx = 0;\n\nexport type LocalId = number;\nexport function generateLocalId(): LocalId {\n  // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER\n  if (_idx === Number.MAX_SAFE_INTEGER) {\n    _idx = 0;\n  }\n  return _idx++;\n}\n\nexport function _setIdx(idx: number): void {\n  _idx = idx;\n}\n"
  },
  {
    "path": "packages/common/utils/src/index.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Point } from './index';\n\ndescribe('utils', () => {\n  test('Point', () => {\n    expect(Point).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './math/index';\nexport * from './objects';\nexport * from './types';\nexport * from './event';\nexport * from './disposable';\nexport * from './disposable-collection';\nexport * from './cancellation';\nexport * from './promise-util';\nexport * from './cache';\nexport * from './compare';\nexport * from './schema/index';\nexport * from './dom-utils';\nexport * from './id';\nexport * from './array';\nexport { bindContributions } from './inversify-utils';\nexport * from './request-with-memo';\nexport * from './compose';\nexport { ContributionProvider, bindContributionProvider } from './contribution-provider';\nexport * from './add-event-listener';\nexport * from './logger';\nexport { DecorationStyle } from './decoration-style';\nexport { useRefresh } from './hooks/use-refresh';\n"
  },
  {
    "path": "packages/common/utils/src/inversify-utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type interfaces } from 'inversify';\n\nexport function bindContributions(bind: interfaces.Bind, target: any, contribs: any[]) {\n  bind(target).toSelf().inSingletonScope();\n  contribs.forEach(contrib => bind(contrib).toService(target));\n}\n"
  },
  {
    "path": "packages/common/utils/src/logger.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { logger } from './logger';\n\ndescribe('logger', () => {\n  const consoleLogMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);\n  const consoleInfoMock = vi.spyOn(console, 'info').mockImplementation(() => undefined);\n  const consoleWarnMock = vi.spyOn(console, 'warn').mockImplementation(() => undefined);\n  const consoleErrorMock = vi.spyOn(console, 'error').mockImplementation(() => undefined);\n\n  afterAll(() => {\n    consoleLogMock.mockReset();\n    consoleInfoMock.mockReset();\n    consoleWarnMock.mockReset();\n    consoleErrorMock.mockReset();\n  });\n\n  test('log', () => {\n    logger.log('log');\n    expect(consoleLogMock).not.toHaveBeenCalledOnce();\n  });\n  test('info', () => {\n    logger.info('info');\n    expect(consoleInfoMock).not.toHaveBeenCalledOnce();\n  });\n  test('error', () => {\n    logger.error('error');\n    expect(consoleErrorMock).toHaveBeenCalledOnce();\n  });\n  test('warn', () => {\n    logger.warn('warn');\n    expect(consoleWarnMock).toHaveBeenCalledOnce();\n  });\n\n  test('develop', () => {\n    vi.stubEnv('NODE_ENV', 'production');\n    expect(logger.isDevEnv()).toEqual(false);\n  });\n  test('develop', () => {\n    vi.stubEnv('NODE_ENV', 'development');\n    expect(logger.isDevEnv()).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/logger.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nclass Logger {\n  isDevEnv() {\n    return process.env.NODE_ENV === 'development';\n  }\n\n  info(...props: any) {\n    if (!this.isDevEnv()) return;\n    // eslint-disable-next-line no-console\n    return console.info(props);\n  }\n\n  log(...props: any) {\n    if (!this.isDevEnv()) return;\n    // eslint-disable-next-line no-console\n    return console.log(...props);\n  }\n\n  error(...props: any) {\n    return console.error(...props);\n  }\n\n  warn(...props: any) {\n    return console.warn(...props);\n  }\n}\n\nexport const logger = new Logger();\n"
  },
  {
    "path": "packages/common/utils/src/math/IPoint.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import { type IPoint } from './IPoint'\nimport { describe, test } from 'vitest';\n\ndescribe('IPoint', () => {\n  test('type', () => {\n    // expectTypeOf({ x: 1, y: 1 }).toEqualTypeOf<IPoint>()\n    // expectTypeOf({ x: 1 }).not.toEqualTypeOf<IPoint>()\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/IPoint.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Common interface for points. Both Point and ObservablePoint implement it\n */\nexport interface IPoint {\n  /**\n   * X coord\n   */\n  x: number;\n  /**\n   * Y coord\n   */\n  y: number;\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/Matrix.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// nolint: cyclo_complexity,method_line\nimport { describe, test, expect, it } from 'vitest';\n\nimport { Transform } from './Transform';\nimport { Matrix as M, Matrix } from './Matrix';\nimport { PI } from './const';\n\ndescribe('Matrix', () => {\n  test('Matrix', async () => {\n    expect(new M()).toEqual(M.IDENTITY);\n    expect(new M()).toEqual(M.TEMP_MATRIX);\n  });\n\n  test('fromArray', async () => {\n    expect(new M().fromArray([])).toEqual(M.IDENTITY);\n    expect(new M().fromArray([1, 2, 3])).toEqual(M.IDENTITY);\n    expect(new M().fromArray([0, 1, 2, 3, 4, 5])).toEqual(new M(0, 1, 3, 4, 2, 5));\n    expect(new M().fromArray([0, 1, 2, 3, 4, 5, 6])).toEqual(new M(0, 1, 3, 4, 2, 5));\n  });\n\n  test('set', async () => {\n    expect(new M().set(0, 1, 2, 3, 4, 5)).toEqual(new M(0, 1, 2, 3, 4, 5));\n  });\n\n  test('toArray', async () => {\n    expect(new M(0, 1, 2, 3, 4, 5).toArray(true)).toEqual(\n      Float32Array.from([0, 1, 0, 2, 3, 0, 4, 5, 1]),\n    );\n    expect(new M(0, 1, 2, 3, 4, 5).toArray(false)).toEqual(\n      Float32Array.from([0, 2, 4, 1, 3, 5, 0, 0, 1]),\n    );\n\n    const arr = new Float32Array(9);\n    new M(0, 1, 2, 3, 4, 5).toArray(false, arr);\n    expect(arr).toEqual(Float32Array.from([0, 2, 4, 1, 3, 5, 0, 0, 1]));\n  });\n\n  test('apply', async () => {\n    expect(M.IDENTITY.apply({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 });\n    // translate only\n    expect(new M(1, 0, 0, 1, 1, 1).apply({ x: 1, y: 2 })).toEqual({\n      x: 2,\n      y: 3,\n    });\n    // scale only\n    expect(new M(2, 0, 0, 2).apply({ x: 1, y: 2 })).toEqual({ x: 2, y: 4 });\n    // skew only\n    expect(new M(1, 1, 1, 1).apply({ x: 1, y: 2 })).toEqual({ x: 3, y: 3 });\n    expect(new M(1, 1, -1, 1).apply({ x: 1, y: 2 })).toEqual({ x: -1, y: 3 });\n  });\n\n  test('applyInverse', async () => {\n    expect(M.IDENTITY.applyInverse({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 });\n    // translate only\n    expect(new M(1, 0, 0, 1, 1, 1).applyInverse({ x: 1, y: 2 })).toEqual({\n      x: 0,\n      y: 1,\n    });\n    // scale only\n    expect(new M(2, 0, 0, 2).applyInverse({ x: 1, y: 2 })).toEqual({\n      x: 0.5,\n      y: 1,\n    });\n    // skew only\n    expect(new M(1, 1, -1, 1).applyInverse({ x: 1, y: 2 })).toEqual({\n      x: 1.5,\n      y: 0.5,\n    });\n  });\n\n  test('translate', async () => {\n    expect(M.IDENTITY.translate(1, -2).apply({ x: 0, y: 0 })).toEqual({\n      x: 1,\n      y: -2,\n    });\n  });\n\n  test('scale', async () => {\n    expect(M.IDENTITY.scale(1, -2).apply({ x: 1, y: 2 })).toEqual({\n      x: 1,\n      y: -4,\n    });\n    expect(M.IDENTITY.scale(0, 0).apply({ x: 1, y: 2 })).toEqual({\n      x: 0,\n      y: 0,\n    });\n  });\n\n  test('rotate', async () => {\n    const r1 = M.IDENTITY.rotate(PI / 2).apply({ x: 1, y: 2 });\n    expect(r1.x).toBeCloseTo(-2);\n    expect(r1.y).toBeCloseTo(1);\n    expect(M.IDENTITY.rotate(PI / 2).apply({ x: 0, y: 0 })).toEqual({\n      x: 0,\n      y: 0,\n    });\n  });\n\n  test('append', async () => {\n    expect(M.IDENTITY.append(M.IDENTITY)).toEqual(M.IDENTITY);\n    expect(M.IDENTITY.append(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));\n    expect(new M(0, 1, 2, 3, 4, 5).append(M.IDENTITY)).toEqual(new M(0, 1, 2, 3, 4, 5));\n    expect(new M(0, 1, 2, 3, 4, 5).append(new M(0, 1, 2, 3, 4, 5))).toEqual(\n      new M(2, 3, 6, 11, 14, 24),\n    );\n  });\n\n  test('prepend', async () => {\n    expect(M.IDENTITY.prepend(M.IDENTITY)).toEqual(M.IDENTITY);\n    expect(M.IDENTITY.prepend(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));\n    expect(new M(0, 1, 2, 3, 4, 5).prepend(M.IDENTITY)).toEqual(new M(0, 1, 2, 3, 4, 5));\n    expect(new M(0, 1, 2, 3, 4, 5).prepend(new M(0, 1, 2, 3, 1, 2))).toEqual(\n      new M(2, 3, 6, 11, 11, 21),\n    );\n  });\n\n  test('identity', async () => {\n    expect(new M(0, 1, 2, 3, 4, 5).identity()).toEqual(M.IDENTITY);\n  });\n\n  test('invert', async () => {\n    expect(new M(0, 1, 2, 3, 4, 5).invert()).toEqual(new M(-1.5, 0.5, 1, -0, 1, -2));\n    expect(new M(-1.5, 0.5, 1, -0, 1, -2).invert()).toEqual(new M(0, 1, 2, 3, 4, 5));\n    // expect(M.IDENTITY.invert()).toEqual(M.IDENTITY)\n  });\n\n  test('copyTo', async () => {\n    expect(new M(0, 1, 2, 3, 4, 5).copyTo(M.TEMP_MATRIX)).toEqual(new M(0, 1, 2, 3, 4, 5));\n  });\n\n  test('copyFrom', async () => {\n    expect(M.TEMP_MATRIX.copyFrom(new M(0, 1, 2, 3, 4, 5))).toEqual(new M(0, 1, 2, 3, 4, 5));\n  });\n\n  test('isSimple', async () => {\n    expect(new M(1, 0, 0, 1, 0, 0).isSimple()).toBeTruthy();\n    expect(new M(0, 1, 2, 3, 4, 5).isSimple()).toBeFalsy();\n  });\n\n  /**\n   * @see https://github.com/pixijs/pixijs/blob/dev/packages/math/test/Matrix.tests.ts\n   */\n  it('should create a new matrix', () => {\n    const matrix = new Matrix();\n\n    expect(matrix.a).toEqual(1);\n    expect(matrix.b).toEqual(0);\n    expect(matrix.c).toEqual(0);\n    expect(matrix.d).toEqual(1);\n    expect(matrix.tx).toEqual(0);\n    expect(matrix.ty).toEqual(0);\n\n    const input = [0, 1, 2, 3, 4, 5];\n\n    matrix.fromArray(input);\n\n    expect(matrix.a).toEqual(0);\n    expect(matrix.b).toEqual(1);\n    expect(matrix.c).toEqual(3);\n    expect(matrix.d).toEqual(4);\n    expect(matrix.tx).toEqual(2);\n    expect(matrix.ty).toEqual(5);\n\n    let output = matrix.toArray(true);\n\n    expect(output.length).toEqual(9);\n    expect(output[0]).toEqual(0);\n    expect(output[1]).toEqual(1);\n    expect(output[3]).toEqual(3);\n    expect(output[4]).toEqual(4);\n    expect(output[6]).toEqual(2);\n    expect(output[7]).toEqual(5);\n\n    output = matrix.toArray(false);\n\n    expect(output.length).toEqual(9);\n    expect(output[0]).toEqual(0);\n    expect(output[1]).toEqual(3);\n    expect(output[2]).toEqual(2);\n    expect(output[3]).toEqual(1);\n    expect(output[4]).toEqual(4);\n    expect(output[5]).toEqual(5);\n  });\n\n  it('should apply different transforms', () => {\n    const matrix = new Matrix();\n\n    matrix.translate(10, 20);\n    matrix.translate(1, 2);\n    expect(matrix.tx).toEqual(11);\n    expect(matrix.ty).toEqual(22);\n\n    matrix.scale(2, 4);\n    expect(matrix.a).toEqual(2);\n    expect(matrix.b).toEqual(0);\n    expect(matrix.c).toEqual(0);\n    expect(matrix.d).toEqual(4);\n    expect(matrix.tx).toEqual(22);\n    expect(matrix.ty).toEqual(88);\n\n    const m2 = matrix.clone();\n\n    expect(m2).not.toBe(matrix);\n    expect(m2.a).toEqual(2);\n    expect(m2.b).toEqual(0);\n    expect(m2.c).toEqual(0);\n    expect(m2.d).toEqual(4);\n    expect(m2.tx).toEqual(22);\n    expect(m2.ty).toEqual(88);\n\n    matrix.setTransform(14, 15, 0, 0, 4, 2, 0, 0, 0);\n    expect(matrix.a).toEqual(4);\n    expect(matrix.b).toEqual(0);\n    // Object.is cant distinguish between 0 and -0\n    expect(Math.abs(matrix.c)).toEqual(0);\n    expect(matrix.d).toEqual(2);\n    expect(matrix.tx).toEqual(14);\n    expect(matrix.ty).toEqual(15);\n  });\n\n  it('should allow rotatation', () => {\n    const matrix = new Matrix();\n\n    matrix.rotate(Math.PI);\n\n    expect(matrix.a).toEqual(-1);\n    expect(matrix.b).toEqual(Math.sin(Math.PI));\n    expect(matrix.c).toEqual(-Math.sin(Math.PI));\n    expect(matrix.d).toEqual(-1);\n  });\n\n  it('should append matrix', () => {\n    const m1 = new Matrix();\n    const m2 = new Matrix();\n\n    m2.tx = 100;\n    m2.ty = 200;\n\n    m1.append(m2);\n\n    expect(m1.tx).toEqual(m2.tx);\n    expect(m1.ty).toEqual(m2.ty);\n  });\n\n  it('should prepend matrix', () => {\n    const m1 = new Matrix();\n    const m2 = new Matrix();\n\n    m2.set(2, 3, 4, 5, 100, 200);\n    m1.prepend(m2);\n\n    expect(m1.a).toEqual(m2.a);\n    expect(m1.b).toEqual(m2.b);\n    expect(m1.c).toEqual(m2.c);\n    expect(m1.d).toEqual(m2.d);\n    expect(m1.tx).toEqual(m2.tx);\n    expect(m1.ty).toEqual(m2.ty);\n\n    const m3 = new Matrix();\n    const m4 = new Matrix();\n\n    m3.prepend(m4);\n\n    expect(m3.a).toEqual(m4.a);\n    expect(m3.b).toEqual(m4.b);\n    expect(m3.c).toEqual(m4.c);\n    expect(m3.d).toEqual(m4.d);\n    expect(m3.tx).toEqual(m4.tx);\n    expect(m3.ty).toEqual(m4.ty);\n  });\n\n  it('should get IDENTITY and TEMP_MATRIX', () => {\n    expect(Matrix.IDENTITY instanceof Matrix).toBe(true);\n    expect(Matrix.TEMP_MATRIX instanceof Matrix).toBe(true);\n  });\n\n  it('should reset matrix to default when identity() is called', () => {\n    const matrix = new Matrix();\n\n    matrix.set(2, 3, 4, 5, 100, 200);\n\n    expect(matrix.a).toEqual(2);\n    expect(matrix.b).toEqual(3);\n    expect(matrix.c).toEqual(4);\n    expect(matrix.d).toEqual(5);\n    expect(matrix.tx).toEqual(100);\n    expect(matrix.ty).toEqual(200);\n\n    matrix.identity();\n\n    expect(matrix.a).toEqual(1);\n    expect(matrix.b).toEqual(0);\n    expect(matrix.c).toEqual(0);\n    expect(matrix.d).toEqual(1);\n    expect(matrix.tx).toEqual(0);\n    expect(matrix.ty).toEqual(0);\n  });\n\n  it('should have the same transform after decompose', () => {\n    const matrix = new Matrix();\n    const transformInitial = new Transform();\n    const transformDecomposed = new Transform();\n\n    for (let x = 0; x < 50; ++x) {\n      transformInitial.position.x = Math.random() * 1000 - 2000;\n      transformInitial.position.y = Math.random() * 1000 - 2000;\n      transformInitial.scale.x = Math.random() * 5 - 10;\n      transformInitial.scale.y = Math.random() * 5 - 10;\n      transformInitial.rotation = (Math.random() - 2) * Math.PI;\n      transformInitial.skew.x = (Math.random() - 2) * Math.PI;\n      transformInitial.skew.y = (Math.random() - 2) * Math.PI;\n\n      matrix.setTransform(\n        transformInitial.position.x,\n        transformInitial.position.y,\n        0,\n        0,\n        transformInitial.scale.x,\n        transformInitial.scale.y,\n        transformInitial.rotation,\n        transformInitial.skew.x,\n        transformInitial.skew.y,\n      );\n      matrix.decompose(transformDecomposed);\n\n      transformInitial.updateLocalTransform();\n      transformDecomposed.updateLocalTransform();\n\n      expect(transformInitial.localTransform.a).toBeCloseTo(\n        transformDecomposed.localTransform.a,\n        0.0001,\n      );\n      expect(transformInitial.localTransform.b).toBeCloseTo(\n        transformDecomposed.localTransform.b,\n        0.0001,\n      );\n      expect(transformInitial.localTransform.c).toBeCloseTo(\n        transformDecomposed.localTransform.c,\n        0.0001,\n      );\n      expect(transformInitial.localTransform.d).toBeCloseTo(\n        transformDecomposed.localTransform.d,\n        0.0001,\n      );\n      expect(transformInitial.localTransform.tx).toBeCloseTo(\n        transformDecomposed.localTransform.tx,\n        0.0001,\n      );\n      expect(transformInitial.localTransform.ty).toBeCloseTo(\n        transformDecomposed.localTransform.ty,\n        0.0001,\n      );\n    }\n  });\n\n  it('should decompose corner case', () => {\n    const matrix = new Matrix();\n    const transform = new Transform();\n    const result = transform.localTransform;\n\n    matrix.a = -0.00001;\n    matrix.b = -1;\n    matrix.c = 1;\n    matrix.d = 0;\n    matrix.decompose(transform);\n    transform.updateLocalTransform();\n\n    expect(result.a).toBeCloseTo(matrix.a, 0.001);\n    expect(result.b).toBeCloseTo(matrix.b, 0.001);\n    expect(result.c).toBeCloseTo(matrix.c, 0.001);\n    expect(result.d).toBeCloseTo(matrix.d, 0.001);\n  });\n\n  describe('decompose', () => {\n    it('should be the inverse of updateLocalTransform even when pivot is set', () => {\n      const matrix = new Matrix(0.01, 0.04, 0.04, 0.1, 2, 2);\n      const transform = new Transform();\n\n      transform.pivot.set(40, 40);\n\n      matrix.decompose(transform);\n      transform.updateLocalTransform();\n\n      const { localTransform } = transform;\n\n      expect(localTransform.a).toBeCloseTo(matrix.a, 0.001);\n      expect(localTransform.b).toBeCloseTo(matrix.b, 0.001);\n      expect(localTransform.c).toBeCloseTo(matrix.c, 0.001);\n      expect(localTransform.d).toBeCloseTo(matrix.d, 0.001);\n      // FIXME expect(localTransform.tx).toBeCloseTo(matrix.tx, 0.001)\n      // FIXME expect(localTransform.ty).toBeCloseTo(matrix.ty, 0.001)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/Matrix.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable prefer-destructuring */\nimport type { Transform } from './Transform';\nimport type { IPoint } from './IPoint';\nimport { PI_2 } from './const';\n\n/**\n * The PIXIJS Matrix as a class makes it a lot faster.\n *\n * Here is a representation of it:\n * ```js\n * | a | c | tx|\n * | b | d | ty|\n * | 0 | 0 | 1 |\n * // default:\n * | 1 | 0 | 0 |\n * | 0 | 1 | 0 |\n * | 0 | 0 | 1 |\n * ```\n */\nexport class Matrix {\n  public array: Float32Array | null = null;\n\n  /**\n   * @param [a] x scale\n   * @param [b] x skew\n   * @param [c] y skew\n   * @param [d] y scale\n   * @param [tx] x translation\n   * @param [ty] y translation\n   */\n  constructor(\n    public a = 1,\n    public b = 0,\n    public c = 0,\n    public d = 1,\n    public tx = 0,\n    public ty = 0,\n  ) {}\n\n  /**\n   * A default (identity) matrix\n   */\n  static get IDENTITY(): Matrix {\n    return new Matrix();\n  }\n\n  /**\n   * A temp matrix\n   */\n  static get TEMP_MATRIX(): Matrix {\n    return new Matrix();\n  }\n\n  /**\n   * Creates a Matrix object based on the given array. The Element to Matrix mapping order is as follows:\n   *\n   * @param array The array that the matrix will be populated from.\n   */\n  fromArray(array: number[]): this {\n    if (array.length < 6) return this;\n\n    this.a = array[0];\n    this.b = array[1];\n    this.c = array[3];\n    this.d = array[4];\n    this.tx = array[2];\n    this.ty = array[5];\n    return this;\n  }\n\n  /**\n   * sets the matrix properties\n   *\n   * @param a Matrix component\n   * @param b Matrix component\n   * @param c Matrix component\n   * @param d Matrix component\n   * @param tx Matrix component\n   * @param ty Matrix component\n   */\n  set(a: number, b: number, c: number, d: number, tx: number, ty: number): this {\n    this.a = a;\n    this.b = b;\n    this.c = c;\n    this.d = d;\n    this.tx = tx;\n    this.ty = ty;\n\n    return this;\n  }\n\n  /**\n   * Creates an array from the current Matrix object.\n   *\n   * @param transpose Whether we need to transpose the matrix or not\n   * @param [out=new Float32Array(9)] If provided the array will be assigned to out\n   * @return the newly created array which contains the matrix\n   */\n  toArray(transpose: boolean, out?: Float32Array): Float32Array {\n    if (!this.array) {\n      this.array = new Float32Array(9);\n    }\n\n    const array = out || this.array;\n\n    if (transpose) {\n      array[0] = this.a;\n      array[1] = this.b;\n      array[2] = 0;\n      array[3] = this.c;\n      array[4] = this.d;\n      array[5] = 0;\n      array[6] = this.tx;\n      array[7] = this.ty;\n      array[8] = 1;\n    } else {\n      array[0] = this.a;\n      array[1] = this.c;\n      array[2] = this.tx;\n      array[3] = this.b;\n      array[4] = this.d;\n      array[5] = this.ty;\n      array[6] = 0;\n      array[7] = 0;\n      array[8] = 1;\n    }\n\n    return array;\n  }\n\n  /**\n   * Get a new position with the current transformation applied.\n   * Can be used to go from a child's coordinate space to the world coordinate space. (e.g. rendering)\n   *\n   * @param pos The origin\n   * @param [newPos] The point that the new position is assigned to (allowed to be same as input)\n   * @return The new point, transformed through this matrix\n   */\n  apply(pos: IPoint, newPos?: IPoint): IPoint {\n    newPos = newPos || { x: 0, y: 0 };\n\n    const { x, y } = pos;\n\n    newPos.x = this.a * x + this.c * y + this.tx;\n    newPos.y = this.b * x + this.d * y + this.ty;\n\n    return newPos;\n  }\n\n  /**\n   * Get a new position with the inverse of the current transformation applied.\n   * Can be used to go from the world coordinate space to a child's coordinate space. (e.g. input)\n   *\n   * @param pos The origin\n   * @param [newPos] The point that the new position is assigned to (allowed to be same as input)\n   * @return The new point, inverse-transformed through this matrix\n   */\n  applyInverse(pos: IPoint, newPos?: IPoint): IPoint {\n    newPos = newPos || { x: 0, y: 0 };\n\n    const id = 1 / (this.a * this.d + this.c * -this.b);\n\n    const { x } = pos;\n    const { y } = pos;\n\n    newPos.x = this.d * id * x + -this.c * id * y + (this.ty * this.c - this.tx * this.d) * id;\n    newPos.y = this.a * id * y + -this.b * id * x + (-this.ty * this.a + this.tx * this.b) * id;\n\n    return newPos;\n  }\n\n  /**\n   * Translates the matrix on the x and y.\n   *\n   * @param x How much to translate x by\n   * @param y How much to translate y by\n   */\n  translate(x: number, y: number): this {\n    this.tx += x;\n    this.ty += y;\n\n    return this;\n  }\n\n  /**\n   * Applies a scale transformation to the matrix.\n   *\n   * @param x The amount to scale horizontally\n   * @param y The amount to scale vertically\n   */\n  scale(x: number, y: number): this {\n    this.a *= x;\n    this.d *= y;\n    this.c *= x;\n    this.b *= y;\n    this.tx *= x;\n    this.ty *= y;\n\n    return this;\n  }\n\n  /**\n   * Applies a rotation transformation to the matrix.\n   *\n   * @param angle The angle in radians.\n   */\n  rotate(angle: number): this {\n    const cos = Math.cos(angle);\n    const sin = Math.sin(angle);\n\n    const a1 = this.a;\n    const c1 = this.c;\n    const tx1 = this.tx;\n\n    this.a = a1 * cos - this.b * sin;\n    this.b = a1 * sin + this.b * cos;\n    this.c = c1 * cos - this.d * sin;\n    this.d = c1 * sin + this.d * cos;\n    this.tx = tx1 * cos - this.ty * sin;\n    this.ty = tx1 * sin + this.ty * cos;\n\n    return this;\n  }\n\n  /**\n   * 矩阵乘法，当前矩阵 * matrix\n   * Appends the given Matrix to this Matrix.\n   */\n  append(matrix: Matrix): this {\n    const a1 = this.a;\n    const b1 = this.b;\n    const c1 = this.c;\n    const d1 = this.d;\n\n    this.a = matrix.a * a1 + matrix.b * c1;\n    this.b = matrix.a * b1 + matrix.b * d1;\n    this.c = matrix.c * a1 + matrix.d * c1;\n    this.d = matrix.c * b1 + matrix.d * d1;\n\n    this.tx = matrix.tx * a1 + matrix.ty * c1 + this.tx;\n    this.ty = matrix.tx * b1 + matrix.ty * d1 + this.ty;\n\n    return this;\n  }\n\n  /**\n   * Sets the matrix based on all the available properties\n   *\n   * @param x Position on the x axis\n   * @param y Position on the y axis\n   * @param pivotX Pivot on the x axis\n   * @param pivotY Pivot on the y axis\n   * @param scaleX Scale on the x axis\n   * @param scaleY Scale on the y axis\n   * @param rotation Rotation in radians\n   * @param skewX Skew on the x axis\n   * @param skewY Skew on the y axis\n   */\n  setTransform(\n    x: number,\n    y: number,\n    pivotX: number,\n    pivotY: number,\n    scaleX: number,\n    scaleY: number,\n    rotation: number,\n    skewX: number,\n    skewY: number,\n  ): this {\n    this.a = Math.cos(rotation + skewY) * scaleX;\n    this.b = Math.sin(rotation + skewY) * scaleX;\n    this.c = -Math.sin(rotation - skewX) * scaleY;\n    this.d = Math.cos(rotation - skewX) * scaleY;\n\n    this.tx = x - (pivotX * this.a + pivotY * this.c);\n    this.ty = y - (pivotX * this.b + pivotY * this.d);\n\n    return this;\n  }\n\n  /**\n   * 矩阵乘法，matrix * 当前矩阵\n   * Prepends the given Matrix to this Matrix.\n   */\n  prepend(matrix: Matrix): this {\n    const tx1 = this.tx;\n\n    if (matrix.a !== 1 || matrix.b !== 0 || matrix.c !== 0 || matrix.d !== 1) {\n      const a1 = this.a;\n      const c1 = this.c;\n\n      this.a = a1 * matrix.a + this.b * matrix.c;\n      this.b = a1 * matrix.b + this.b * matrix.d;\n      this.c = c1 * matrix.a + this.d * matrix.c;\n      this.d = c1 * matrix.b + this.d * matrix.d;\n    }\n\n    this.tx = tx1 * matrix.a + this.ty * matrix.c + matrix.tx;\n    this.ty = tx1 * matrix.b + this.ty * matrix.d + matrix.ty;\n\n    return this;\n  }\n\n  /**\n   * Decomposes the matrix (x, y, scaleX, scaleY, and rotation) and sets the properties on to a transform.\n   *\n   * @param transform The transform to apply the properties to.\n   * @return The transform with the newly applied properties\n   */\n  decompose(transform: Transform): Transform {\n    // sort out rotation / skew..\n    const { a } = this;\n    const { b } = this;\n    const { c } = this;\n    const { d } = this;\n\n    const skewX = -Math.atan2(-c, d);\n    const skewY = Math.atan2(b, a);\n\n    const delta = Math.abs(skewX + skewY);\n\n    if (delta < 0.00001 || Math.abs(PI_2 - delta) < 0.00001) {\n      transform.rotation = skewY;\n      transform.skew.x = 0;\n      transform.skew.y = 0;\n    } else {\n      transform.rotation = 0;\n      transform.skew.x = skewX;\n      transform.skew.y = skewY;\n    }\n\n    // next set scale\n    transform.scale.x = Math.sqrt(a * a + b * b);\n    transform.scale.y = Math.sqrt(c * c + d * d);\n\n    // next set position\n    transform.position.x = this.tx;\n    transform.position.y = this.ty;\n\n    return transform;\n  }\n\n  /**\n   * Inverts this matrix\n   */\n  invert(): this {\n    const a1 = this.a;\n    const b1 = this.b;\n    const c1 = this.c;\n    const d1 = this.d;\n    const tx1 = this.tx;\n    const n = a1 * d1 - b1 * c1;\n\n    this.a = d1 / n;\n    this.b = -b1 / n;\n    this.c = -c1 / n;\n    this.d = a1 / n;\n    this.tx = (c1 * this.ty - d1 * tx1) / n;\n    this.ty = -(a1 * this.ty - b1 * tx1) / n;\n\n    return this;\n  }\n\n  /**\n   * Resets this Matrix to an identity (default) matrix.\n   */\n  identity(): this {\n    this.a = 1;\n    this.b = 0;\n    this.c = 0;\n    this.d = 1;\n    this.tx = 0;\n    this.ty = 0;\n\n    return this;\n  }\n\n  /**\n   * 未做旋转的矩阵\n   */\n  isSimple(): boolean {\n    return this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1;\n  }\n\n  /**\n   * Creates a new Matrix object with the same values as this one.\n   *\n   * @return A copy of this matrix.\n   */\n  clone(): Matrix {\n    const matrix = new Matrix();\n\n    matrix.a = this.a;\n    matrix.b = this.b;\n    matrix.c = this.c;\n    matrix.d = this.d;\n    matrix.tx = this.tx;\n    matrix.ty = this.ty;\n\n    return matrix;\n  }\n\n  /**\n   * Changes the values of the given matrix to be the same as the ones in this matrix\n   *\n   * @return The matrix given in parameter with its values updated.\n   */\n  copyTo(matrix: Matrix): Matrix {\n    matrix.a = this.a;\n    matrix.b = this.b;\n    matrix.c = this.c;\n    matrix.d = this.d;\n    matrix.tx = this.tx;\n    matrix.ty = this.ty;\n\n    return matrix;\n  }\n\n  /**\n   * Changes the values of the matrix to be the same as the ones in given matrix\n   */\n  copyFrom(matrix: Matrix): this {\n    this.a = matrix.a;\n    this.b = matrix.b;\n    this.c = matrix.c;\n    this.d = matrix.d;\n    this.tx = matrix.tx;\n    this.ty = matrix.ty;\n\n    return this;\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/ObservablePoint.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * @see https://github.com/pixijs/pixijs/blob/dev/packages/math/test/ObservablePoint.tests.ts\n */\nimport { vi, describe, it, expect } from 'vitest';\n\nimport { ObservablePoint } from './ObservablePoint';\n\ndescribe('ObservablePoint', () => {\n  it.skip('should create a new observable point ', () => {\n    const cb = vi.fn();\n    const pt = new ObservablePoint(cb, this);\n\n    expect(pt.x).toEqual(0);\n    expect(pt.y).toEqual(0);\n\n    pt.set(2, 5);\n    expect(pt.x).toEqual(2);\n    expect(pt.y).toEqual(5);\n\n    expect(cb).toBeCalled();\n\n    pt.set(2, 6);\n    expect(pt.x).toEqual(2);\n    expect(pt.y).toEqual(6);\n\n    pt.set(2, 0);\n    expect(pt.x).toEqual(2);\n    expect(pt.y).toEqual(0);\n\n    pt.set();\n    expect(pt.x).toEqual(0);\n    expect(pt.y).toEqual(0);\n\n    expect(cb.mock.calls).toHaveLength(4);\n  });\n\n  it('should copy a new observable point', () => {\n    function cb() {\n      // do nothing\n    }\n\n    const p1 = new ObservablePoint(cb, this, 10, 20);\n    const p2 = new ObservablePoint(cb, this, 5, 2);\n    const p3 = new ObservablePoint(cb, this, 5, 6);\n    const p4 = new ObservablePoint(cb, this, 1, 2);\n\n    p1.copyFrom(p2);\n    expect(p1.x).toEqual(p2.x);\n    expect(p1.y).toEqual(p2.y);\n\n    p1.copyFrom(p3);\n    expect(p1.y).toEqual(p3.y);\n\n    expect(p4.clone(cb, this)).toEqual(p4);\n    expect(p4.clone()).toEqual(p4);\n    expect(p4.copyTo(new ObservablePoint(cb, this))).toEqual(p4);\n  });\n\n  it('should equal to another point', () => {\n    expect(\n      new ObservablePoint(() => {}, this, 1, 2).equals(new ObservablePoint(() => {}, this, 1, 2)),\n    ).toEqual(true);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/ObservablePoint.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IPoint } from './IPoint';\n\n/**\n * The Point object represents a location in a two-dimensional coordinate system, where x represents\n * the horizontal axis and y represents the vertical axis.\n *\n * An ObservablePoint is a point that triggers a callback when the point's position is changed.\n */\nexport class ObservablePoint<T = any> implements IPoint {\n  public cb: (this: T) => any;\n\n  public scope: any;\n\n  /**\n   * @param {Function} cb - callback when changed\n   * @param {object} scope - owner of callback\n   * @param {number} [x=0] - position of the point on the x axis\n   * @param {number} [y=0] - position of the point on the y axis\n   */\n  constructor(cb: (this: T) => any, scope: T, x = 0, y = 0) {\n    this._x = x;\n    this._y = y;\n\n    this.cb = cb;\n    this.scope = scope;\n  }\n\n  _x: number;\n\n  /**\n   * The position of the displayObject on the x axis relative to the local coordinates of the parent.\n   */\n  get x(): number {\n    return this._x;\n  }\n\n  set x(value) {\n    if (this._x !== value) {\n      this._x = value;\n      this.cb.call(this.scope);\n    }\n  }\n\n  _y: number;\n\n  /**\n   * The position of the displayObject on the x axis relative to the local coordinates of the parent.\n   */\n  get y(): number {\n    return this._y;\n  }\n\n  set y(value) {\n    if (this._y !== value) {\n      this._y = value;\n      this.cb.call(this.scope);\n    }\n  }\n\n  /**\n   * Creates a clone of this point.\n   * The callback and scope params can be overidden otherwise they will default\n   * to the clone object's values.\n   *\n   * @override\n   * @param {Function} [cb=null] - callback when changed\n   * @param {object} [scope=null] - owner of callback\n   * @return {ObservablePoint} a copy of the point\n   */\n  clone(cb = this.cb, scope = this.scope): ObservablePoint {\n    return new ObservablePoint(cb, scope, this._x, this._y);\n  }\n\n  /**\n   * Sets the point to a new x and y position.\n   * If y is omitted, both x and y will be set to x.\n   *\n   * @param {number} [x=0] - position of the point on the x axis\n   * @param {number} [y=x] - position of the point on the y axis\n   * @returns {this} Returns itself.\n   */\n  set(x = 0, y = x): this {\n    if (this._x !== x || this._y !== y) {\n      this._x = x;\n      this._y = y;\n      this.cb.call(this.scope);\n    }\n\n    return this;\n  }\n\n  /**\n   * Copies x and y from the given point\n   *\n   * @param {IPoint} p - The point to copy from.\n   * @returns {this} Returns itself.\n   */\n  copyFrom(p: IPoint): this {\n    if (this._x !== p.x || this._y !== p.y) {\n      this._x = p.x;\n      this._y = p.y;\n      this.cb.call(this.scope);\n    }\n\n    return this;\n  }\n\n  /**\n   * Copies x and y into the given point\n   *\n   * @param {IPoint} p - The point to copy.\n   * @returns {IPoint} Given point with values updated\n   */\n  copyTo<T2 extends IPoint>(p: T2): T2 {\n    p.x = this._x;\n    p.y = this._y;\n\n    return p;\n  }\n\n  /**\n   * Returns true if the given point is equal to this point\n   *\n   * @param {IPoint} p - The point to check\n   * @returns {boolean} Whether the given point equal to this point\n   */\n  equals(p: IPoint): boolean {\n    return p.x === this._x && p.y === this._y;\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/Point.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Point } from './Point';\n\ndescribe('Point', () => {\n  test('Point', async () => {\n    expect(Point).not.toBeUndefined();\n    expect(new Point()).toEqual({ x: 0, y: 0 });\n    expect(new Point(1, 2)).toEqual({ x: 1, y: 2 });\n  });\n\n  test('Point/clone', async () => {\n    const p1 = new Point(1, 2);\n    const p2 = p1.clone();\n    p2.y = 3;\n    expect(p1).toEqual({ x: 1, y: 2 });\n    expect(p2).toEqual({ x: 1, y: 3 });\n  });\n\n  test('Point/copyFrom', async () => {\n    expect(new Point().copyFrom({ x: 1, y: 2 })).toEqual({ x: 1, y: 2 });\n  });\n\n  test('Point/copyTo', async () => {\n    expect(new Point(1, 2).copyTo({ x: 0, y: 0 })).toEqual({ x: 1, y: 2 });\n  });\n\n  test('Point/equals', async () => {\n    expect(new Point(1, 2).equals({ x: 1, y: 2 })).toBeTruthy();\n    expect(new Point().equals({ x: 0, y: 0 })).toBeTruthy();\n    expect(new Point(1, 2).equals({ x: 0, y: 0 })).toBeFalsy();\n  });\n\n  test('Point/set', async () => {\n    expect(new Point(1, 2).set()).toEqual({ x: 0, y: 0 });\n    expect(new Point(1, 2).set(2, 1)).toEqual({ x: 2, y: 1 });\n  });\n\n  test('getDistance', async () => {\n    expect(Point.getDistance({ x: 0, y: 0 }, { x: 3, y: 4 })).toEqual(5);\n    expect(Point.getDistance({ x: 0, y: 0 }, { x: -3, y: -4 })).toEqual(5);\n    expect(Point.getDistance({ x: 0, y: 0 }, { x: 1, y: 1 })).toEqual(Math.sqrt(2));\n  });\n\n  test('getMiddlePoint', async () => {\n    expect(Point.getMiddlePoint({ x: 0, y: 0 }, { x: 3, y: 4 })).toEqual({\n      x: 1.5,\n      y: 2,\n    });\n    expect(Point.getMiddlePoint({ x: 0, y: 0 }, { x: -3, y: -4 })).toEqual({\n      x: -1.5,\n      y: -2,\n    });\n  });\n\n  test('moveDistanceToDirection', async () => {\n    expect(Point.moveDistanceToDirection({ x: 0, y: 0 }, { x: 3, y: 4 }, 2.5)).toEqual({\n      x: 1.5,\n      y: 2,\n    });\n    expect(Point.moveDistanceToDirection({ x: 0, y: 0 }, { x: 0, y: 4 }, 2)).toEqual({\n      x: 0,\n      y: 2,\n    });\n    expect(Point.moveDistanceToDirection({ x: 0, y: 0 }, { x: 0, y: -4 }, 2)).toEqual({\n      x: 0,\n      y: -2,\n    });\n  });\n\n  test('fixZero', async () => {\n    expect(Point.fixZero({ x: -0, y: -0 })).toEqual({\n      x: 0,\n      y: 0,\n    });\n  });\n\n  test('move', async () => {\n    expect(Point.move({ x: 1, y: 2 }, { x: 1, y: 1 })).toEqual({\n      x: 2,\n      y: 3,\n    });\n    expect(Point.move({ x: 1, y: 2 }, {})).toEqual({\n      x: 1,\n      y: 2,\n    });\n    expect(Point.move({ x: 1, y: 2 }, { x: 1 })).toEqual({\n      x: 2,\n      y: 2,\n    });\n    expect(Point.move({ x: 1, y: 2 }, { y: 1 })).toEqual({\n      x: 1,\n      y: 3,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/Point.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IPoint } from './IPoint';\n\n/**\n * The Point object represents a location in a two-dimensional coordinate system, where x represents\n * the horizontal axis and y represents the vertical axis.\n *\n * @class\n * @memberof PIXI\n * @implements IPoint\n */\nexport class Point implements IPoint {\n  constructor(public x = 0, public y = 0) {}\n\n  /**\n   * Creates a clone of this point\n   *\n   * @return {Point} a copy of the point\n   */\n  clone(): Point {\n    return new Point(this.x, this.y);\n  }\n\n  /**\n   * Copies x and y from the given point\n   *\n   * @param {IPoint} p - The point to copy from\n   * @returns {this} Returns itself.\n   */\n  copyFrom(p: IPoint): this {\n    this.set(p.x, p.y);\n\n    return this;\n  }\n\n  /**\n   * Copies x and y into the given point\n   *\n   * @param {IPoint} p - The point to copy.\n   * @returns {IPoint} Given point with values updated\n   */\n  copyTo<T extends IPoint>(p: T): T {\n    p.x = this.x;\n    p.y = this.y;\n\n    return p;\n  }\n\n  /**\n   * Returns true if the given point is equal to this point\n   *\n   * @param {IPoint} p - The point to check\n   * @returns {boolean} Whether the given point equal to this point\n   */\n  equals(p: IPoint): boolean {\n    return p.x === this.x && p.y === this.y;\n  }\n\n  /**\n   * Sets the point to a new x and y position.\n   * If y is omitted, both x and y will be set to x.\n   *\n   * @param {number} [x=0] - position of the point on the x axis\n   * @param {number} [y=x] - position of the point on the y axis\n   * @returns {this} Returns itself.\n   */\n  set(x = 0, y = x): this {\n    this.x = x;\n    this.y = y;\n\n    return this;\n  }\n}\n\nexport namespace Point {\n  export const EMPTY: IPoint = { x: 0, y: 0 };\n\n  /**\n   * 获取两点间的距离\n   * @param p1\n   * @param p2\n   */\n  export function getDistance(p1: IPoint, p2: IPoint): number {\n    return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);\n  }\n\n  /**\n   * 获取两点间的中间点\n   * @param p1\n   * @param p2\n   */\n  export function getMiddlePoint(p1: IPoint, p2: IPoint): IPoint {\n    return getRatioPoint(p1, p2, 0.5);\n  }\n\n  /**\n   * 按一定比例，获取两点间的中间点\n   * @param p1\n   * @param p2\n   */\n  export function getRatioPoint(p1: IPoint, p2: IPoint, ratio: number): IPoint {\n    return {\n      x: p1.x + ratio * (p2.x - p1.x),\n      y: p1.y + ratio * (p2.y - p1.y),\n    };\n  }\n\n  export function fixZero(output: IPoint): IPoint {\n    // fix: -0\n    if (output.x === 0) output.x = 0;\n    if (output.y === 0) output.y = 0;\n    return output;\n  }\n\n  /**\n   * 往目标点移动 distance 距离\n   * @param current\n   * @param direction\n   */\n  export function move(current: IPoint, m: Partial<IPoint>): IPoint {\n    return {\n      x: current.x + (m.x || 0),\n      y: current.y + (m.y || 0),\n    };\n  }\n\n  /**\n   * 往目标点移动 distance 距离\n   * @param current\n   * @param direction\n   */\n  export function moveDistanceToDirection(\n    current: IPoint,\n    direction: IPoint,\n    distance: number,\n  ): IPoint {\n    const deltaX = direction.x - current.x;\n    const deltaY = direction.y - current.y;\n\n    const distanceX = deltaX === 0 ? 0 : Math.sqrt(distance ** 2 / (1 + deltaY ** 2 / deltaX ** 2));\n    const moveX = deltaX > 0 ? distanceX : -distanceX;\n    const distanceY = deltaX === 0 ? distance : Math.abs((distanceX * deltaY) / deltaX);\n    const moveY = deltaY > 0 ? distanceY : -distanceY;\n\n    return {\n      x: current.x + moveX,\n      y: current.y + moveY,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/Transform.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * @see https://github.com/pixijs/pixijs/blob/dev/packages/math/test/Transform.tests.ts\n */\nimport { describe, it, expect } from 'vitest';\n\nimport { Transform } from './Transform';\n\ndescribe('Transform', () => {\n  describe('setFromMatrix', () => {\n    it('should decompose negative scale into rotation', () => {\n      const eps = 1e-3;\n\n      const transform = new Transform();\n      const parent = new Transform();\n      const otherTransform = new Transform();\n\n      transform.position.set(20, 10);\n      transform.scale.set(-2, -3);\n      transform.rotation = Math.PI / 6;\n      transform.updateTransform(parent);\n\n      otherTransform.setFromMatrix(transform.worldTransform);\n\n      const { position } = otherTransform;\n      const { scale } = otherTransform;\n      const { skew } = otherTransform;\n\n      expect(position.x).toBeCloseTo(20, eps);\n      expect(position.y).toBeCloseTo(10, eps);\n      expect(scale.x).toBeCloseTo(2, eps);\n      expect(scale.y).toBeCloseTo(3, eps);\n      expect(skew.x).toEqual(0);\n      expect(skew.y).toEqual(0);\n      expect(otherTransform.rotation).toBeCloseTo((-5 * Math.PI) / 6, eps);\n    });\n\n    it('should decompose mirror into skew', () => {\n      const eps = 1e-3;\n\n      const transform = new Transform();\n      const parent = new Transform();\n      const otherTransform = new Transform();\n\n      transform.position.set(20, 10);\n      transform.scale.set(2, -3);\n      transform.rotation = Math.PI / 6;\n      transform.updateTransform(parent);\n\n      otherTransform.setFromMatrix(transform.worldTransform);\n\n      const { position } = otherTransform;\n      const { scale } = otherTransform;\n      const { skew } = otherTransform;\n\n      expect(position.x).toBeCloseTo(20, eps);\n      expect(position.y).toBeCloseTo(10, eps);\n      expect(scale.x).toBeCloseTo(2, eps);\n      expect(scale.y).toBeCloseTo(3, eps);\n      expect(skew.x).toBeCloseTo((5 * Math.PI) / 6, eps);\n      expect(skew.y).toBeCloseTo(Math.PI / 6, eps);\n      expect(otherTransform.rotation).toEqual(0);\n    });\n\n    it('should apply skew before scale, like in adobe animate and spine', () => {\n      // this example looks the same in CSS and in pixi, made with pixi-animate by @bigtimebuddy\n\n      const eps = 1e-3;\n\n      const transform = new Transform();\n      const parent = new Transform();\n      const otherTransform = new Transform();\n\n      transform.position.set(387.8, 313.95);\n      transform.scale.set(0.572, 4.101);\n      transform.skew.set(-0.873, 0.175);\n      transform.updateTransform(parent);\n\n      const mat = transform.worldTransform;\n\n      expect(mat.a).toBeCloseTo(0.563, eps);\n      expect(mat.b).toBeCloseTo(0.1, eps);\n      expect(mat.c).toBeCloseTo(-3.142, eps);\n      expect(mat.d).toBeCloseTo(2.635, eps);\n      expect(mat.tx).toBeCloseTo(387.8, eps);\n      expect(mat.ty).toBeCloseTo(313.95, eps);\n\n      otherTransform.setFromMatrix(transform.worldTransform);\n\n      const { position } = otherTransform;\n      const { scale } = otherTransform;\n      const { skew } = otherTransform;\n\n      expect(position.x).toBeCloseTo(387.8, eps);\n      expect(position.y).toBeCloseTo(313.95, eps);\n      expect(scale.x).toBeCloseTo(0.572, eps);\n      expect(scale.y).toBeCloseTo(4.101, eps);\n      expect(skew.x).toBeCloseTo(-0.873, eps);\n      expect(skew.y).toBeCloseTo(0.175, eps);\n      expect(otherTransform.rotation).toEqual(0);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/Transform.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ObservablePoint } from './ObservablePoint';\nimport { Matrix } from './Matrix';\n\n/**\n * Transform that takes care about its versions\n *\n * @class\n * @memberof PIXI\n */\nexport class Transform {\n  /**\n   * A default (identity) transform\n   *\n   * @static\n   * @constant\n   * @member {PIXI.Transform}\n   */\n  public static readonly IDENTITY = new Transform();\n\n  public worldTransform: Matrix;\n\n  public localTransform: Matrix;\n\n  public position: ObservablePoint;\n\n  public scale: ObservablePoint;\n\n  public pivot: ObservablePoint;\n\n  public skew: ObservablePoint;\n\n  public _parentID: number;\n\n  _worldID: number;\n\n  protected _rotation: number;\n\n  protected _cx: number;\n\n  protected _sx: number;\n\n  protected _cy: number;\n\n  protected _sy: number;\n\n  protected _localID: number;\n\n  protected _currentLocalID: number;\n\n  constructor() {\n    /**\n     * The world transformation matrix.\n     *\n     * @member {PIXI.Matrix}\n     */\n    this.worldTransform = new Matrix();\n\n    /**\n     * The local transformation matrix.\n     *\n     * @member {PIXI.Matrix}\n     */\n    this.localTransform = new Matrix();\n\n    /**\n     * The coordinate of the object relative to the local coordinates of the parent.\n     *\n     * @member {PIXI.ObservablePoint}\n     */\n    this.position = new ObservablePoint(this.onChange, this, 0, 0);\n\n    /**\n     * The scale factor of the object.\n     *\n     * @member {PIXI.ObservablePoint}\n     */\n    this.scale = new ObservablePoint(this.onChange, this, 1, 1);\n\n    /**\n     * The pivot point of the displayObject that it rotates around.\n     *\n     * @member {PIXI.ObservablePoint}\n     */\n    this.pivot = new ObservablePoint(this.onChange, this, 0, 0);\n\n    /**\n     * The skew amount, on the x and y axis.\n     *\n     * @member {PIXI.ObservablePoint}\n     */\n    this.skew = new ObservablePoint(this.updateSkew, this, 0, 0);\n\n    /**\n     * The rotation amount.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._rotation = 0;\n\n    /**\n     * The X-coordinate value of the normalized local X axis,\n     * the first column of the local transformation matrix without a scale.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._cx = 1;\n\n    /**\n     * The Y-coordinate value of the normalized local X axis,\n     * the first column of the local transformation matrix without a scale.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._sx = 0;\n\n    /**\n     * The X-coordinate value of the normalized local Y axis,\n     * the second column of the local transformation matrix without a scale.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._cy = 0;\n\n    /**\n     * The Y-coordinate value of the normalized local Y axis,\n     * the second column of the local transformation matrix without a scale.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._sy = 1;\n\n    /**\n     * The locally unique ID of the local transform.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._localID = 0;\n\n    /**\n     * The locally unique ID of the local transform\n     * used to calculate the current local transformation matrix.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._currentLocalID = 0;\n\n    /**\n     * The locally unique ID of the world transform.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._worldID = 0;\n\n    /**\n     * The locally unique ID of the parent's world transform\n     * used to calculate the current world transformation matrix.\n     *\n     * @protected\n     * @member {number}\n     */\n    this._parentID = 0;\n  }\n\n  /**\n   * Called when a value changes.\n   *\n   * @protected\n   */\n  protected onChange(): void {\n    this._localID++;\n  }\n\n  /**\n   * Called when the skew or the rotation changes.\n   *\n   * @protected\n   */\n  protected updateSkew(): void {\n    this._cx = Math.cos(this._rotation + this.skew.y);\n    this._sx = Math.sin(this._rotation + this.skew.y);\n    this._cy = -Math.sin(this._rotation - this.skew.x); // cos, added PI/2\n    this._sy = Math.cos(this._rotation - this.skew.x); // sin, added PI/2\n\n    this._localID++;\n  }\n\n  /**\n   * Updates the local transformation matrix.\n   */\n  updateLocalTransform(): void {\n    const lt = this.localTransform;\n\n    if (this._localID !== this._currentLocalID) {\n      // get the matrix values of the displayobject based on its transform properties..\n      lt.a = this._cx * this.scale.x;\n      lt.b = this._sx * this.scale.x;\n      lt.c = this._cy * this.scale.y;\n      lt.d = this._sy * this.scale.y;\n\n      lt.tx = this.position.x - (this.pivot.x * lt.a + this.pivot.y * lt.c);\n      lt.ty = this.position.y - (this.pivot.x * lt.b + this.pivot.y * lt.d);\n      this._currentLocalID = this._localID;\n\n      // force an update..\n      this._parentID = -1;\n    }\n  }\n\n  /**\n   * Updates the local and the world transformation matrices.\n   *\n   * @param {PIXI.Transform} parentTransform - The parent transform\n   */\n  updateTransform(parentTransform: Transform): void {\n    const lt = this.localTransform;\n\n    if (this._localID !== this._currentLocalID) {\n      // get the matrix values of the displayobject based on its transform properties..\n      lt.a = this._cx * this.scale.x;\n      lt.b = this._sx * this.scale.x;\n      lt.c = this._cy * this.scale.y;\n      lt.d = this._sy * this.scale.y;\n\n      lt.tx = this.position.x - (this.pivot.x * lt.a + this.pivot.y * lt.c);\n      lt.ty = this.position.y - (this.pivot.x * lt.b + this.pivot.y * lt.d);\n      this._currentLocalID = this._localID;\n\n      // force an update..\n      this._parentID = -1;\n    }\n\n    if (this._parentID !== parentTransform._worldID) {\n      // concat the parent matrix with the objects transform.\n      const pt = parentTransform.worldTransform;\n      const wt = this.worldTransform;\n\n      wt.a = lt.a * pt.a + lt.b * pt.c;\n      wt.b = lt.a * pt.b + lt.b * pt.d;\n      wt.c = lt.c * pt.a + lt.d * pt.c;\n      wt.d = lt.c * pt.b + lt.d * pt.d;\n      wt.tx = lt.tx * pt.a + lt.ty * pt.c + pt.tx;\n      wt.ty = lt.tx * pt.b + lt.ty * pt.d + pt.ty;\n\n      this._parentID = parentTransform._worldID;\n\n      // update the id of the transform..\n      this._worldID++;\n    }\n  }\n\n  /**\n   * Decomposes a matrix and sets the transforms properties based on it.\n   *\n   * @param {PIXI.Matrix} matrix - The matrix to decompose\n   */\n  setFromMatrix(matrix: Matrix): void {\n    matrix.decompose(this);\n    this._localID++;\n  }\n\n  /**\n   * The rotation of the object in radians.\n   *\n   * @member {number}\n   */\n  get rotation(): number {\n    return this._rotation;\n  }\n\n  set rotation(value: number) {\n    if (this._rotation !== value) {\n      this._rotation = value;\n      this.updateSkew();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/Vector2.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { Vector2 } from './Vector2';\n\ndescribe('Vector2', () => {\n  test('Vector2', async () => {\n    expect(new Vector2()).toEqual({ x: 0, y: 0 });\n    expect(new Vector2(1, 2)).toEqual({ x: 1, y: 2 });\n  });\n\n  test('Vector2/sub', async () => {\n    expect(new Vector2().sub(new Vector2(1, 2))).toEqual({ x: -1, y: -2 });\n    expect(new Vector2(1, 2).sub(new Vector2(1, 2))).toEqual({ x: 0, y: 0 });\n  });\n\n  test('Vector2/dot', async () => {\n    expect(new Vector2().dot(new Vector2(1, 2))).toEqual(0);\n    expect(new Vector2(1, 2).dot(new Vector2(1, 2))).toEqual(5);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/Vector2.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport class Vector2 {\n  constructor(public x = 0, public y = 0) {}\n\n  /**\n   * 向量减法\n   */\n  sub(v: Vector2): Vector2 {\n    return new Vector2(this.x - v.x, this.y - v.y);\n  }\n\n  /**\n   * 向量点乘\n   */\n  dot(v: Vector2): number {\n    return this.x * v.x + this.y * v.y;\n  }\n\n  /**\n   * 向量叉乘\n   */\n  // cross(v: Vector2): number {\n  // }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/angle.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { PI } from './const';\nimport { Angle } from './angle';\n\ndescribe('Angle', () => {\n  test('wrap', async () => {\n    expect(Angle.wrap(-PI * 2)).toEqual(0);\n    expect(Angle.wrap(-PI)).toEqual(-PI);\n    expect(Angle.wrap(0)).toEqual(0);\n    expect(Angle.wrap(PI / 2)).toEqual(PI / 2);\n    expect(Angle.wrap(PI)).toEqual(-PI);\n    expect(Angle.wrap(PI * 2)).toEqual(0);\n  });\n\n  test('wrapDegrees', async () => {\n    expect(Angle.wrapDegrees(-180 * 2)).toEqual(0);\n    expect(Angle.wrapDegrees(-180)).toEqual(-180);\n    expect(Angle.wrapDegrees(0)).toEqual(0);\n    expect(Angle.wrapDegrees(180 / 2)).toEqual(180 / 2);\n    expect(Angle.wrapDegrees(180)).toEqual(-180);\n    expect(Angle.wrapDegrees(180 * 2)).toEqual(0);\n  });\n\n  test('betweenPoints', async () => {\n    expect(Angle.betweenPoints({ x: 1, y: 1 }, { x: 2, y: 2 })).toEqual(0);\n    expect(Angle.betweenPoints({ x: 1, y: 0 }, { x: 0, y: 1 })).toEqual(PI / 2);\n    expect(Angle.betweenPoints({ x: 0, y: 1 }, { x: 1, y: 0 })).toEqual(-PI / 2);\n    expect(Angle.betweenPoints({ x: -1, y: 0 }, { x: 1, y: 0 })).toEqual(-PI);\n    expect(Angle.betweenPoints({ x: 1, y: 0 }, { x: -1, y: 0 })).toEqual(PI);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/angle.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { wrap as mathWrap } from './wrap';\nimport { type IPoint } from './IPoint';\n\nexport namespace Angle {\n  /**\n   * Wrap an angle.\n   *\n   * Wraps the angle to a value in the range of -PI to PI.\n   *\n   * @param angle - The angle to wrap, in radians.\n   * @return The wrapped angle, in radians.\n   */\n  export function wrap(angle: number): number {\n    return mathWrap(angle, -Math.PI, Math.PI);\n  }\n  /**\n   * Wrap an angle in degrees.\n   *\n   * Wraps the angle to a value in the range of -180 to 180.\n   *\n   * @param angle - The angle to wrap, in degrees.\n   * @return The wrapped angle, in degrees.\n   */\n  export function wrapDegrees(angle: number): number {\n    return mathWrap(angle, -180, 180);\n  }\n\n  /**\n   * 计算两个点的夹角\n   *\n   * @return The angle in radians.\n   */\n  export function betweenPoints(\n    point1: IPoint,\n    point2: IPoint,\n    originPoint: IPoint = { x: 0, y: 0 },\n  ): number {\n    const p1 = {\n      x: point1.x - originPoint.x,\n      y: point1.y - originPoint.y,\n    };\n    const p2 = {\n      x: point2.x - originPoint.x,\n      y: point2.y - originPoint.y,\n    };\n    // return Math.atan2(p2.y, p2.x) - Math.atan2(p1.y, p1.x)\n    return Math.atan2(p1.x * p2.y - p1.y * p2.x, p1.x * p2.x + p1.y * p2.y);\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/const.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { DEG_TO_RAD, PI, PI_2, RAD_TO_DEG, SHAPES } from './const';\n\ndescribe('const', () => {\n  test('PI_2', async () => {\n    expect(PI_2).toEqual(PI * 2);\n  });\n\n  test('RAD_TO_DEG', async () => {\n    expect((PI / 2) * RAD_TO_DEG).toEqual(90);\n    expect(PI * RAD_TO_DEG).toEqual(180);\n  });\n\n  test('DEG_TO_RAD', async () => {\n    expect(180 * DEG_TO_RAD).toEqual(PI);\n    expect(90 * DEG_TO_RAD).toEqual(PI / 2);\n  });\n\n  test('SHAPES', async () => {\n    expect(SHAPES.RECT).toEqual(1);\n    expect(SHAPES.RREC).toEqual(4);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/const.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const { PI } = Math;\n\n/** Two Pi. */\nexport const PI_2 = PI * 2;\n\n/** Conversion factor for converting radians to degrees. */\nexport const RAD_TO_DEG = 180 / PI;\n\n/** Conversion factor for converting degrees to radians. */\nexport const DEG_TO_RAD = PI / 180;\n\n/** Constants that identify shapes. */\nexport enum SHAPES {\n  /** Polygon */\n  POLY = 0,\n  /** Rectangle */\n  RECT = 1,\n  /** Circle */\n  CIRC = 2,\n  /** Ellipse */\n  ELIP = 3,\n  /** Rounded Rectangle */\n  RREC = 4,\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/index.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Point } from './index';\n\ndescribe('math', () => {\n  test('Point', () => {\n    expect(Point).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './shapes';\nexport * from './Matrix';\nexport * from './Point';\nexport * from './Transform';\nexport * from './angle';\nexport * from './const';\nexport * from './IPoint';\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/Circle.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Rectangle } from './Rectangle';\nimport { Circle } from './Circle';\n\ndescribe('Circle', () => {\n  test('Circle', async () => {\n    expect(new Circle()).toEqual({ radius: 0, type: 2, x: 0, y: 0 });\n  });\n\n  test('clone', async () => {\n    const c = new Circle();\n    const c1 = c.clone();\n    c.radius = 2;\n    expect(c1.radius).toEqual(0);\n  });\n\n  test('contains', async () => {\n    const r = new Circle(0, 0, 1);\n    expect(r.contains(0, 0)).toEqual(true);\n    expect(r.contains(0.5, 0.5)).toEqual(true);\n    // const d = Math.sqrt(2) / 2\n    // expect(r.contains(d, d)).toEqual(true) // why not true\n    expect(r.contains(0.707, 0.707)).toEqual(true);\n    expect(r.contains(0, 1)).toEqual(true);\n    expect(r.contains(1, 0)).toEqual(true);\n    expect(r.contains(1, 1)).toEqual(false);\n\n    expect(new Circle().contains(0, 0)).toEqual(false);\n  });\n\n  test('getBounds', async () => {\n    const r = new Circle(0, 0, 1);\n    expect(r.getBounds()).toEqual(new Rectangle(-1, -1, 2, 2));\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/Circle.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { SHAPES } from '../const';\nimport { Rectangle } from './Rectangle';\n\n/**\n * The Circle object is used to help draw graphics and can also be used to specify a hit area for displayObjects.\n */\nexport class Circle {\n  /**\n   * The type of the object, mainly used to avoid `instanceof` checks\n   */\n  public readonly type = SHAPES.CIRC;\n\n  /**\n   * @param x Circle center x\n   * @param y Circle center y\n   */\n  constructor(public x = 0, public y = 0, public radius = 0) {}\n\n  /**\n   * Creates a clone of this Circle instance\n   *\n   * @return a copy of the Circle\n   */\n  clone(): Circle {\n    return new Circle(this.x, this.y, this.radius);\n  }\n\n  /**\n   * Checks whether the x and y coordinates given are contained within this circle\n   *\n   * @return Whether the (x, y) coordinates are within this Circle\n   */\n  contains(x: number, y: number): boolean {\n    if (this.radius <= 0) {\n      return false;\n    }\n\n    const r2 = this.radius * this.radius;\n    let dx = this.x - x;\n    let dy = this.y - y;\n\n    dx *= dx;\n    dy *= dy;\n\n    return dx + dy <= r2;\n  }\n\n  /**\n   * Returns the framing rectangle of the circle as a Rectangle object\n   *\n   * @return the framing rectangle\n   */\n  getBounds(): Rectangle {\n    return new Rectangle(\n      this.x - this.radius,\n      this.y - this.radius,\n      this.radius * 2,\n      this.radius * 2,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/Rectangle.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// nolint: cyclo_complexity,method_line\nimport { describe, test, expect } from 'vitest';\n\nimport { Vector2 } from '../Vector2';\nimport { Point } from '../Point';\nimport { type IPoint } from '../IPoint';\nimport { PI } from '../const';\nimport { OBBRect, Rectangle as R, RectangleAlignType } from './Rectangle';\n\ndescribe('Rectangle', () => {\n  describe('Rectangle Class', () => {\n    test('Rectangle', async () => {\n      expect(R).not.toBeUndefined();\n\n      expect(new R()).toEqual({ x: 0, y: 0, width: 0, height: 0, type: 1 });\n      expect(new R(1)).toEqual({ x: 1, y: 0, width: 0, height: 0, type: 1 });\n      expect(new R(1, 2)).toEqual({ x: 1, y: 2, width: 0, height: 0, type: 1 });\n      expect(new R(1, 2, 3)).toEqual({\n        x: 1,\n        y: 2,\n        width: 3,\n        height: 0,\n        type: 1,\n      });\n      expect(new R(1, 2, 3, 4)).toEqual({\n        x: 1,\n        y: 2,\n        width: 3,\n        height: 4,\n        type: 1,\n      });\n    });\n\n    test('EMPTY', async () => {\n      expect(R.EMPTY).toEqual({ x: 0, y: 0, width: 0, height: 0, type: 1 });\n    });\n\n    test('left/right/top/bottom', async () => {\n      const r = new R(1, 2, 3, 4);\n      expect(r.left).toEqual(1);\n      expect(r.right).toEqual(4);\n      expect(r.top).toEqual(2);\n      expect(r.bottom).toEqual(6);\n    });\n\n    test('clone', async () => {\n      const r = new R(1, 2, 3, 4);\n      const r1 = r.clone();\n      r.y = 5;\n      expect(r).toEqual({ x: 1, y: 5, width: 3, height: 4, type: 1 });\n      expect(r1).toEqual({ x: 1, y: 2, width: 3, height: 4, type: 1 });\n    });\n\n    test('copyFrom', async () => {\n      const r = new R(1, 2, 3, 4);\n      const r1 = new R().copyFrom(r);\n      r.y = 5;\n      expect(r).toEqual({ x: 1, y: 5, width: 3, height: 4, type: 1 });\n      expect(r1).toEqual({ x: 1, y: 2, width: 3, height: 4, type: 1 });\n    });\n\n    test('copyTo', async () => {\n      const r = new R(1, 2, 3, 4);\n      const r1 = r.copyTo(new R());\n      r.y = 5;\n      expect(r).toEqual({ x: 1, y: 5, width: 3, height: 4, type: 1 });\n      expect(r1).toEqual({ x: 1, y: 2, width: 3, height: 4, type: 1 });\n    });\n\n    test('contains', async () => {\n      const r = new R(0, 0, 1, 1);\n      expect(r.contains(0, 0)).toEqual(true);\n      expect(r.contains(0.5, 0.5)).toEqual(true);\n      expect(r.contains(0, 1)).toEqual(true);\n      expect(r.contains(1, 0)).toEqual(true);\n      expect(r.contains(1, 1)).toEqual(true);\n      expect(r.contains(2, 2)).toEqual(false);\n\n      expect(R.EMPTY.contains(0, 0)).toEqual(false);\n    });\n\n    test('isEqual', async () => {\n      expect(R.EMPTY.isEqual(R.EMPTY)).toEqual(true);\n      expect(new R(1, 2, 3, 4).isEqual(new R(1, 2, 3, 4))).toEqual(true);\n      expect(new R(1, 2, 3, 4).isEqual(new R(1, 2))).toEqual(false);\n    });\n\n    test('containsRectangle', async () => {\n      expect(R.EMPTY.containsRectangle(R.EMPTY)).toEqual(true);\n      expect(new R(1, 2, 3, 4).containsRectangle(new R(1, 2, 3, 4))).toEqual(true);\n      expect(new R(0, 0, 2, 2).containsRectangle(new R(1, 1, 1, 1))).toEqual(true);\n      expect(new R(0, 0, 2, 2).containsRectangle(new R(1, 1, 2, 2))).toEqual(false);\n    });\n\n    test('pad', async () => {\n      expect(new R().pad()).toEqual(new R());\n      expect(new R().pad(1)).toEqual(new R(-1, -1, 2, 2));\n      expect(new R().pad(1, 2)).toEqual(new R(-1, -2, 2, 4));\n    });\n\n    test('fit', async () => {\n      expect(new R(0, 0, 2, 2).fit(new R(0, 0, 1, 1))).toEqual(new R(0, 0, 1, 1));\n      expect(new R(0, 0, 2, 2).fit(new R(1, 1, 2, 2))).toEqual(new R(1, 1, 1, 1));\n      expect(new R(0, 0, 2, 2).fit(new R(3, 3, 1, 1))).toEqual(new R(3, 3, 0, 0));\n    });\n\n    test('ceil', async () => {\n      expect(new R(0.1, 0.2, 2, 2).ceil()).toEqual(new R(0, 0, 3, 3));\n      expect(new R(0.5, 0.6, 2, 2).ceil()).toEqual(new R(0, 0, 3, 3));\n    });\n\n    test('enlarge', async () => {\n      expect(new R().enlarge(R.EMPTY)).toEqual(R.EMPTY);\n      expect(new R().enlarge(new R(0, 0, 1, 1))).toEqual(new R(0, 0, 1, 1));\n      expect(new R(0, 0, 1, 1).enlarge(new R(1, 1, 1, 1))).toEqual(new R(0, 0, 2, 2));\n      expect(new R(0, 0, 2, 2).enlarge(new R(1, 1, 2, 2))).toEqual(new R(0, 0, 3, 3));\n    });\n\n    test('center...crossDistance', async () => {\n      const r = new R(1, 1, 4, 4);\n      expect(r.center).toEqual({ x: 3, y: 3 });\n      expect(r.rightBottom).toEqual({ x: 5, y: 5 });\n      expect(r.leftBottom).toEqual({ x: 1, y: 5 });\n      expect(r.rightTop).toEqual({ x: 5, y: 1 });\n      expect(r.leftTop).toEqual({ x: 1, y: 1 });\n      expect(r.bottomCenter).toEqual({ x: 3, y: 5 });\n      expect(r.topCenter).toEqual({ x: 3, y: 1 });\n      expect(r.rightCenter).toEqual({ x: 5, y: 3 });\n      expect(r.leftCenter).toEqual({ x: 1, y: 3 });\n      expect(r.crossDistance).toEqual(Math.sqrt(32));\n    });\n\n    test('update', async () => {\n      expect(\n        new R(1, 1, 4, 4).update((r) => {\n          r.x = 0;\n          r.y = 0;\n          return r;\n        })\n      ).toEqual(new R(0, 0, 4, 4));\n    });\n\n    test('toStyleStr', async () => {\n      expect(new R(1, 1, 4, 4).toStyleStr()).toEqual(\n        'left: 1px; top: 1px; width: 4px; height: 4px;'\n      );\n    });\n\n    test('withPadding', async () => {\n      expect(new R(1, 1, 4, 4).withPadding({ left: 1, right: 1, top: 1, bottom: 1 })).toEqual(\n        new R(0, 0, 6, 6)\n      );\n    });\n\n    test('withoutPadding', async () => {\n      expect(\n        new R(1, 1, 4, 4).withoutPadding({\n          left: 1,\n          right: 1,\n          top: 1,\n          bottom: 1,\n        })\n      ).toEqual(new R(2, 2, 2, 2));\n    });\n\n    test('withHeight', async () => {\n      expect(new R(1, 1, 4, 4).withHeight(5)).toEqual(new R(1, 1, 4, 5));\n    });\n\n    test('clearSpace', async () => {\n      expect(new R(1, 1, 4, 4).clearSpace()).toEqual(new R(1, 1, 0, 0));\n    });\n  });\n\n  // test('Rectangle namespace', async () => {\n  //   expect(R._test).toEqual({})\n  //   R._test.a = 1\n  //   expect(R._test).toEqual({ a: 1 })\n  // })\n\n  test('align', async () => {\n    const r1 = new R(0, 0, 1, 1);\n    const r2 = new R(1, 1, 2, 2);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_BOTTOM)).toEqual([\n      new R(0, 2, 1, 1),\n      new R(1, 1, 2, 2),\n    ]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_CENTER)).toEqual([\n      new R(1, 0, 1, 1),\n      new R(0.5, 1, 2, 2),\n    ]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_LEFT)).toEqual([\n      new R(0, 0, 1, 1),\n      new R(0, 1, 2, 2),\n    ]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_MIDDLE)).toEqual([\n      new R(0, 1, 1, 1),\n      new R(1, 0.5, 2, 2),\n    ]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_RIGHT)).toEqual([\n      new R(2, 0, 1, 1),\n      new R(1, 1, 2, 2),\n    ]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.ALIGN_TOP)).toEqual([\n      new R(0, 0, 1, 1),\n      new R(1, 0, 2, 2),\n    ]);\n\n    expect(\n      R.align(\n        [new R(0, 0, 1, 1), new R(2, 0, 1, 1), new R(6, 0, 1, 1)],\n        RectangleAlignType.DISTRIBUTE_HORIZONTAL\n      )\n    ).toEqual([new R(0, 0, 1, 1), new R(3, 0, 1, 1), new R(6, 0, 1, 1)]);\n    expect(\n      R.align(\n        [new R(0, 0, 1, 1), new R(0.5, 0, 1, 1), new R(0.5, 0, 1, 1)],\n        RectangleAlignType.DISTRIBUTE_HORIZONTAL\n      )\n    ).toEqual([new R(0, 0, 1, 1), new R(0.25, 0, 1, 1), new R(0.5, 0, 1, 1)]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.DISTRIBUTE_HORIZONTAL)).toEqual([\n      r1,\n      r2,\n    ]);\n\n    expect(\n      R.align(\n        [new R(0, 0, 1, 1), new R(0, 2, 1, 1), new R(0, 6, 1, 1)],\n        RectangleAlignType.DISTRIBUTE_VERTICAL\n      )\n    ).toEqual([new R(0, 0, 1, 1), new R(0, 3, 1, 1), new R(0, 6, 1, 1)]);\n    expect(R.align([r1.clone(), r2.clone()], RectangleAlignType.DISTRIBUTE_VERTICAL)).toEqual([\n      r1,\n      r2,\n    ]);\n\n    expect(R.align([r1.clone()], RectangleAlignType.DISTRIBUTE_VERTICAL)).toEqual([r1]);\n    expect(R.align([r1.clone(), r2.clone()], '' as any)).toEqual([r1, r2]); // override default\n  });\n\n  test('enlarge', async () => {\n    expect(R.enlarge([new R(0, 0, 1, 1), new R(1, 1, 1, 1)])).toEqual(new R(0, 0, 2, 2));\n    expect(R.enlarge([new R(0, 0, 1, 1)])).toEqual(new R(0, 0, 1, 1));\n    expect(R.enlarge([])).toEqual(new R());\n  });\n\n  test('intersects', async () => {\n    expect(R.intersects(new R(0, 0, 2, 2), new R(1, 1, 2, 2))).toEqual(true);\n    expect(R.intersects(new R(0, 0, 2, 2), new R(1, 1, 2, 2), 'horizontal')).toEqual(true);\n    expect(R.intersects(new R(0, 0, 2, 2), new R(1, 1, 2, 2), 'vertical')).toEqual(true);\n\n    expect(R.intersects(new R(0, 0, 2, 2), new R(3, 3, 2, 2))).toEqual(false);\n    expect(R.intersects(new R(0, 0, 2, 2), new R(3, 3, 2, 2), 'horizontal')).toEqual(false);\n    expect(R.intersects(new R(0, 0, 2, 2), new R(3, 3, 2, 2), 'vertical')).toEqual(false);\n  });\n\n  test('OBBRect', async () => {\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(0, 1))\n    ).toBeCloseTo(0.5);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(1, 0))\n    ).toBeCloseTo(0.5);\n    expect(new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(1, 1))).toEqual(1);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, PI / 4).getProjectionRadius(new Vector2(1, 1))\n    ).toBeCloseTo(Math.sqrt(2) / 2);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, PI / 2).getProjectionRadius(new Vector2(1, 1))\n    ).toEqual(1);\n  });\n\n  test('intersectsWithRotation', async () => {\n    expect(R.intersects(new R(0, 0, 10, 10), new R(8, 8, 4, 4))).toEqual(true);\n    expect(new R(0, 0, 10, 10).fit(new R(8, 8, 4, 4))).toEqual(new R(8, 8, 2, 2));\n    expect(R.intersectsWithRotation(new R(0, 0, 10, 10), 0, new R(8, 8, 4, 4), 0)).toEqual(true);\n    expect(\n      R.intersectsWithRotation(new R(0, 0, 10, 10), PI / 4, new R(8, 8, 4, 4), PI / 4)\n    ).toEqual(false);\n    expect(\n      R.intersectsWithRotation(new R(0, 0, 10, 10), PI / 2, new R(8, 8, 4, 4), PI / 2)\n    ).toEqual(true);\n    expect(R.intersectsWithRotation(new R(0, 0, 10, 10), PI, new R(8, 8, 4, 4), PI)).toEqual(true);\n    // expect(R.intersectsWithRotation(new R(0, 0, 10, 10), PI / 4, new R(8, -12, 4, 4), PI / 4)).toEqual(false)\n    // expect(R.intersectsWithRotation(new R(0, 0, 10, 10), PI / 4, new R(-12, 8, 4, 4), PI / 4)).toEqual(false)\n  });\n\n  test('isViewportVisible', async () => {\n    expect(R.isViewportVisible(new R(0, 0, 1, 1), new R(0.5, 0.5, 1, 1))).toEqual(true);\n    expect(R.isViewportVisible(new R(0, 0, 1, 1), new R(0.5, 0.5, 1, 1), 0, true)).toEqual(false);\n    expect(R.isViewportVisible(new R(0, 0, 1, 1), new R(0.5, 0.5, 1, 1), PI / 4)).toEqual(true);\n  });\n\n  test('createRectangleWithTwoPoints', async () => {\n    expect(R.createRectangleWithTwoPoints({ x: 0, y: 0 }, { x: 1, y: 1 })).toEqual(\n      new R(0, 0, 1, 1)\n    );\n    expect(R.createRectangleWithTwoPoints({ x: 1, y: 1 }, { x: 0, y: 0 })).toEqual(\n      new R(0, 0, 1, 1)\n    );\n  });\n\n  test('OBBRect', async () => {\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(0, 1))\n    ).toBeCloseTo(0.5);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(1, 0))\n    ).toBeCloseTo(0.5);\n    expect(new OBBRect(new Point(0, 0), 1, 1, 0).getProjectionRadius(new Vector2(1, 1))).toEqual(1);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, PI / 4).getProjectionRadius(new Vector2(1, 1))\n    ).toBeCloseTo(Math.sqrt(2) / 2);\n    expect(\n      new OBBRect(new Point(0, 0), 1, 1, PI / 2).getProjectionRadius(new Vector2(1, 1))\n    ).toEqual(1);\n  });\n  test('setViewportVisible', () => {\n    const viewport = new R(0, 0, 100, 100);\n    function check(\n      rect: { x: number; y: number; width: number; height: number },\n      pos: IPoint,\n      padding = 0\n    ) {\n      const bounds = new R(rect.x, rect.y, rect.width, rect.height);\n      R.setViewportVisible(bounds, viewport, padding);\n      expect({ x: bounds.x, y: bounds.y }).toEqual(pos);\n    }\n    // no change\n    check({ x: 0, y: 0, width: 10, height: 10 }, { x: 0, y: 0 });\n    // left\n    check({ x: -10, y: 0, width: 10, height: 10 }, { x: 0, y: 0 });\n    // top\n    check({ x: 0, y: -10, width: 10, height: 10 }, { x: 0, y: 0 });\n    // right\n    check({ x: 110, y: 0, width: 10, height: 10 }, { x: 90, y: 0 });\n    // bottom\n    check({ x: 0, y: 110, width: 10, height: 10 }, { x: 0, y: 90 });\n    // 贴到边界，如果有 padding 也往下移动\n    check({ x: 0, y: 0, width: 10, height: 10 }, { x: 10, y: 10 }, 10);\n    // left with padding\n    check({ x: -10, y: 0, width: 10, height: 10 }, { x: 10, y: 10 }, 10);\n    // top with padding\n    check({ x: 0, y: -10, width: 10, height: 10 }, { x: 10, y: 10 }, 10);\n    // right with padding\n    check({ x: 110, y: 0, width: 10, height: 10 }, { x: 80, y: 10 }, 10);\n    // bottom with padding\n    check({ x: 0, y: 110, width: 10, height: 10 }, { x: 10, y: 80 }, 10);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/Rectangle.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Vector2 } from '../Vector2';\nimport { Point } from '../Point';\nimport { type IPoint } from '../IPoint';\nimport { SHAPES } from '../const';\nimport { type PaddingSchema } from '../../schema';\n\n/**\n * Size object, contains width and height\n */\nexport type ISize = { width: number; height: number };\n\n/**\n * Rectangle object is an area defined by its position, as indicated by its top-left corner\n * point (x, y) and by its width and its height.\n */\nexport class Rectangle {\n  /**\n   * The type of the object, mainly used to avoid `instanceof` checks\n   */\n  public readonly type = SHAPES.RECT;\n\n  /**\n   * @param [x] - The X coordinate of the upper-left corner of the rectangle\n   * @param [y] - The Y coordinate of the upper-left corner of the rectangle\n   * @param [width] - The overall width of this rectangle\n   * @param [height] - The overall height of this rectangle\n   */\n  constructor(public x = 0, public y = 0, public width = 0, public height = 0) {}\n\n  // static _empty: Rectangle = Object.freeze(new Rectangle(0, 0, 0, 0))\n\n  /**\n   * A constant empty rectangle. MUST NOT modify properties!\n   */\n  static get EMPTY(): Rectangle {\n    return new Rectangle(0, 0, 0, 0);\n  }\n\n  get left(): number {\n    return this.x;\n  }\n\n  get right(): number {\n    return this.x + this.width;\n  }\n\n  get top(): number {\n    return this.y;\n  }\n\n  get bottom(): number {\n    return this.y + this.height;\n  }\n\n  /**\n   * Creates a clone of this Rectangle.\n   *\n   * @return a copy of the rectangle\n   */\n  clone(): Rectangle {\n    return new Rectangle(this.x, this.y, this.width, this.height);\n  }\n\n  /**\n   * Copies another rectangle to this one.\n   *\n   * @return Returns itself.\n   */\n  copyFrom(rectangle: Rectangle): Rectangle {\n    this.x = rectangle.x;\n    this.y = rectangle.y;\n    this.width = rectangle.width;\n    this.height = rectangle.height;\n\n    return this;\n  }\n\n  /**\n   * Copies this rectangle to another one.\n   *\n   * @return Returns given rectangle.\n   */\n  copyTo(rectangle: Rectangle): Rectangle {\n    rectangle.x = this.x;\n    rectangle.y = this.y;\n    rectangle.width = this.width;\n    rectangle.height = this.height;\n\n    return rectangle;\n  }\n\n  /**\n   * Checks whether the x and y coordinates given are contained within this Rectangle\n   *\n   * @param x - The X coordinate of the point to test\n   * @param y - The Y coordinate of the point to test\n   * @return Whether the x/y coordinates are within this Rectangle\n   */\n  contains(x: number, y: number): boolean {\n    if (this.width <= 0 || this.height <= 0) {\n      return false;\n    }\n\n    if (x >= this.x && x <= this.right) {\n      if (y >= this.y && y <= this.bottom) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  isEqual(rect: Rectangle): boolean {\n    return (\n      this.x === rect.x &&\n      this.y === rect.y &&\n      this.width === rect.width &&\n      this.height === rect.height\n    );\n  }\n\n  containsRectangle(rect: Rectangle): boolean {\n    return (\n      rect.left >= this.left &&\n      rect.right <= this.right &&\n      rect.top >= this.top &&\n      rect.bottom <= this.bottom\n    );\n  }\n\n  /**\n   * Pads the rectangle making it grow in all directions.\n   * If paddingY is omitted, both paddingX and paddingY will be set to paddingX.\n   *\n   * @param [paddingX] - The horizontal padding amount.\n   * @param [paddingY] - The vertical padding amount.\n   */\n  pad(paddingX = 0, paddingY = paddingX): this {\n    this.x -= paddingX;\n    this.y -= paddingY;\n\n    this.width += paddingX * 2;\n    this.height += paddingY * 2;\n\n    return this;\n  }\n\n  /**\n   * Fits this rectangle around the passed one.\n   * Intersection 交集\n   */\n  fit(rectangle: Rectangle): this {\n    const x1 = Math.max(this.x, rectangle.x);\n    const x2 = Math.min(this.x + this.width, rectangle.x + rectangle.width);\n    const y1 = Math.max(this.y, rectangle.y);\n    const y2 = Math.min(this.y + this.height, rectangle.y + rectangle.height);\n\n    this.x = x1;\n    this.width = Math.max(x2 - x1, 0);\n    this.y = y1;\n    this.height = Math.max(y2 - y1, 0);\n\n    return this;\n  }\n\n  /**\n   * Enlarges rectangle that way its corners lie on grid\n   */\n  ceil(resolution = 1, precision = 0.001): this {\n    const x2 = Math.ceil((this.x + this.width - precision) * resolution) / resolution;\n    const y2 = Math.ceil((this.y + this.height - precision) * resolution) / resolution;\n\n    this.x = Math.floor((this.x + precision) * resolution) / resolution;\n    this.y = Math.floor((this.y + precision) * resolution) / resolution;\n\n    this.width = x2 - this.x;\n    this.height = y2 - this.y;\n\n    return this;\n  }\n\n  /**\n   * Enlarges this rectangle to include the passed rectangle.\n   */\n  enlarge(rectangle: Rectangle): this {\n    const x1 = Math.min(this.x, rectangle.x);\n    const x2 = Math.max(this.x + this.width, rectangle.x + rectangle.width);\n    const y1 = Math.min(this.y, rectangle.y);\n    const y2 = Math.max(this.y + this.height, rectangle.y + rectangle.height);\n\n    this.x = x1;\n    this.width = x2 - x1;\n    this.y = y1;\n    this.height = y2 - y1;\n\n    return this;\n  }\n\n  get center(): IPoint {\n    return {\n      x: this.x + this.width / 2,\n      y: this.y + this.height / 2,\n    };\n  }\n\n  get rightBottom(): IPoint {\n    return {\n      x: this.right,\n      y: this.bottom,\n    };\n  }\n\n  get leftBottom(): IPoint {\n    return {\n      x: this.left,\n      y: this.bottom,\n    };\n  }\n\n  get rightTop(): IPoint {\n    return {\n      x: this.right,\n      y: this.top,\n    };\n  }\n\n  get leftTop(): IPoint {\n    return {\n      x: this.left,\n      y: this.top,\n    };\n  }\n\n  get bottomCenter(): IPoint {\n    return {\n      x: this.x + this.width / 2,\n      y: this.bottom,\n    };\n  }\n\n  get topCenter(): IPoint {\n    return {\n      x: this.x + this.width / 2,\n      y: this.top,\n    };\n  }\n\n  get rightCenter(): IPoint {\n    return {\n      x: this.right,\n      y: this.y + this.height / 2,\n    };\n  }\n\n  get leftCenter(): IPoint {\n    return {\n      x: this.left,\n      y: this.y + this.height / 2,\n    };\n  }\n\n  update(fn: (rect: Rectangle) => Rectangle): Rectangle {\n    return fn(this);\n  }\n\n  get crossDistance(): number {\n    return Point.getDistance(this.leftTop, this.rightBottom);\n  }\n\n  toStyleStr(): string {\n    return `left: ${this.x}px; top: ${this.y}px; width: ${this.width}px; height: ${this.height}px;`;\n  }\n\n  withPadding(padding: PaddingSchema) {\n    this.x -= padding.left;\n    this.y -= padding.top;\n    this.width += padding.left + padding.right;\n    this.height += padding.top + padding.bottom;\n    return this;\n  }\n\n  withoutPadding(padding: PaddingSchema) {\n    this.x += padding.left;\n    this.y += padding.top;\n    this.width = this.width - padding.left - padding.right;\n    this.height = this.height - padding.top - padding.bottom;\n    return this;\n  }\n\n  withHeight(height: number) {\n    this.height = height;\n    return this;\n  }\n\n  clearSpace() {\n    this.width = 0;\n    this.height = 0;\n    return this;\n  }\n}\n\nexport enum RectangleAlignType {\n  ALIGN_LEFT = 'align-left',\n  ALIGN_CENTER = 'align-center',\n  ALIGN_RIGHT = 'align-right',\n  ALIGN_TOP = 'align-top',\n  ALIGN_MIDDLE = 'align-middle',\n  ALIGN_BOTTOM = 'align-bottom',\n  DISTRIBUTE_HORIZONTAL = 'distribute-horizontal',\n  DISTRIBUTE_VERTICAL = 'distribute-vertical',\n}\n\nexport enum RectangleAlignTitle {\n  ALIGN_LEFT = '左对齐',\n  ALIGN_CENTER = '左右居中对齐',\n  ALIGN_RIGHT = '右对齐',\n  ALIGN_TOP = '上对齐',\n  ALIGN_MIDDLE = '上下居中对齐',\n  ALIGN_BOTTOM = '下对齐',\n  DISTRIBUTE_HORIZONTAL = '水平平均分布',\n  DISTRIBUTE_VERTICAL = '垂直平均分布',\n}\n\n// `branch not covered`\n// @see https://github.com/istanbuljs/nyc/issues/1209\nexport namespace Rectangle {\n  /**\n   * 矩形对齐\n   */\n  export function align(rectangles: Rectangle[], type: RectangleAlignType): Rectangle[] {\n    if (rectangles.length <= 1) return rectangles;\n    switch (type) {\n      /**\n       * 下对齐\n       */\n      case RectangleAlignType.ALIGN_BOTTOM:\n        const maxBottom = Math.max(...rectangles.map((r) => r.bottom));\n        rectangles.forEach((rect) => {\n          rect.y = maxBottom - rect.height;\n        });\n        break;\n      /**\n       * 左右居中对齐\n       */\n      case RectangleAlignType.ALIGN_CENTER:\n        const centerX = enlarge(rectangles).center.x;\n        rectangles.forEach((rect) => {\n          rect.x = centerX - rect.width / 2;\n        });\n        break;\n      /**\n       * 左对齐\n       */\n      case RectangleAlignType.ALIGN_LEFT:\n        const minLeft = Math.min(...rectangles.map((r) => r.left));\n        rectangles.forEach((rect) => {\n          rect.x = minLeft;\n        });\n        break;\n      /**\n       * 上下居中对齐\n       */\n      case RectangleAlignType.ALIGN_MIDDLE:\n        const centerY = enlarge(rectangles).center.y;\n        rectangles.forEach((rect) => {\n          rect.y = centerY - rect.height / 2;\n        });\n        break;\n      /**\n       * 右对齐\n       */\n      case RectangleAlignType.ALIGN_RIGHT:\n        const maxRight = Math.max(...rectangles.map((r) => r.right));\n        rectangles.forEach((rect) => {\n          rect.x = maxRight - rect.width;\n        });\n        break;\n      /**\n       * 上对齐\n       */\n      case RectangleAlignType.ALIGN_TOP:\n        const minTop = Math.min(...rectangles.map((r) => r.top));\n        rectangles.forEach((rect) => {\n          rect.y = minTop;\n        });\n        break;\n      /**\n       * 水平平均分布\n       */\n      case RectangleAlignType.DISTRIBUTE_HORIZONTAL:\n        // 只支持大于三个\n        if (rectangles.length <= 2) break;\n        const sort = rectangles.slice().sort((r1, r2) => r1.left - r2.left);\n        const bounds = enlarge(rectangles);\n        const space =\n          rectangles.reduce((s, rect) => s - rect.width, bounds.width) / (rectangles.length - 1);\n        sort.reduce((left, rect) => {\n          rect.x = left;\n          return left + rect.width + space;\n        }, bounds.x);\n        break;\n      /**\n       * 垂直平均分布\n       */\n      case RectangleAlignType.DISTRIBUTE_VERTICAL:\n        if (rectangles.length <= 2) break;\n        const sort2 = rectangles.slice().sort((r1, r2) => r1.top - r2.top);\n        const bounds2 = enlarge(rectangles);\n        const space2 =\n          rectangles.reduce((s, rect) => s - rect.height, bounds2.height) / (rectangles.length - 1);\n        sort2.reduce((top, rect) => {\n          rect.y = top;\n          return top + rect.height + space2;\n        }, bounds2.y);\n        break;\n      default:\n        break;\n    }\n    return rectangles;\n  }\n\n  /**\n   * 获取所有矩形的外围最大边框\n   */\n  export function enlarge(rectangles: Rectangle[]): Rectangle {\n    const result = Rectangle.EMPTY.clone();\n    if (!rectangles.length) return result;\n    const lefts: number[] = [];\n    const tops: number[] = [];\n    const rights: number[] = [];\n    const bottoms: number[] = [];\n    rectangles.forEach((r) => {\n      lefts.push(r.left);\n      rights.push(r.right);\n      bottoms.push(r.bottom);\n      tops.push(r.top);\n    });\n    // 使用原生的 apply 减少一次复制\n    // eslint-disable-next-line prefer-spread\n    const left = Math.min.apply(Math, lefts);\n    // eslint-disable-next-line prefer-spread\n    const right = Math.max.apply(Math, rights);\n    // eslint-disable-next-line prefer-spread\n    const top = Math.min.apply(Math, tops);\n    // eslint-disable-next-line prefer-spread\n    const bottom = Math.max.apply(Math, bottoms);\n    result.x = left;\n    result.width = right - left;\n    result.y = top;\n    result.height = bottom - top;\n    return result;\n  }\n\n  /**\n   * 判断矩形相交\n   *\n   * @param [direction] 判断单一方向\n   */\n  export function intersects(\n    target1: Rectangle,\n    target2: Rectangle,\n    direction?: 'horizontal' | 'vertical'\n  ): boolean {\n    const left1 = target1.left;\n    const top1 = target1.top;\n    const right1 = target1.right;\n    const bottom1 = target1.bottom;\n    const left2 = target2.left;\n    const top2 = target2.top;\n    const right2 = target2.right;\n    const bottom2 = target2.bottom;\n\n    if (direction === 'horizontal') return right1 > left2 && left1 < right2;\n    if (direction === 'vertical') return bottom1 > top2 && top1 < bottom2;\n    if (right1 > left2 && left1 < right2) {\n      if (bottom1 > top2 && top1 < bottom2) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  /**\n   * 使用 OBB 算法判断两个旋转矩形是否相交\n   * @param rotate1 单位 radian\n   * @param rotate2 单位 radian\n   */\n  export function intersectsWithRotation(\n    rect1: Rectangle,\n    rotate1: number,\n    rect2: Rectangle,\n    rotate2: number\n  ): boolean {\n    const obb1 = new OBBRect(rect1.center, rect1.width, rect1.height, rotate1);\n    const obb2 = new OBBRect(rect2.center, rect2.width, rect2.height, rotate2);\n    const nv = obb1.centerPoint.sub(obb2.centerPoint);\n    const axisA1 = obb1.axesX;\n    if (\n      obb1.getProjectionRadius(axisA1) + obb2.getProjectionRadius(axisA1) <=\n      Math.abs(nv.dot(axisA1))\n    )\n      return false;\n    const axisA2 = obb1.axesY;\n    if (\n      obb1.getProjectionRadius(axisA2) + obb2.getProjectionRadius(axisA2) <=\n      Math.abs(nv.dot(axisA2))\n    )\n      return false;\n    const axisB1 = obb2.axesX;\n    if (\n      obb1.getProjectionRadius(axisB1) + obb2.getProjectionRadius(axisB1) <=\n      Math.abs(nv.dot(axisB1))\n    )\n      return false;\n    const axisB2 = obb2.axesY;\n    if (\n      obb1.getProjectionRadius(axisB2) + obb2.getProjectionRadius(axisB2) <=\n      Math.abs(nv.dot(axisB2))\n    )\n      return false;\n    return true;\n  }\n  /**\n   * 判断指定 rect 是否在 viewport 可见\n   *\n   * @param rotation rect 旋转，单位 radian\n   * @param isContains 整个 bounds 是否全部可见\n   */\n  export function isViewportVisible(\n    rect: Rectangle,\n    viewport: Rectangle,\n    rotation = 0,\n    isContains = false\n  ): boolean {\n    if (isContains) {\n      return viewport.containsRectangle(rect);\n    }\n    if (rotation === 0) return Rectangle.intersects(rect, viewport);\n    return Rectangle.intersectsWithRotation(rect, rotation, viewport, 0);\n  }\n\n  /**\n   * 保证bounds 永远在 viewport 里边\n   *\n   * @param bounds\n   * @param viewport\n   * @param padding 距离 viewport 的安全边界\n   */\n  export function setViewportVisible(\n    bounds: Rectangle,\n    viewport: Rectangle,\n    padding = 0\n  ): Rectangle {\n    const { left: tLeft, right: tRight, top: tTop, bottom: tBottom, width, height } = bounds;\n    const { left: vLeft, right: vRight, top: vTop, bottom: vBottom } = viewport;\n    if (tLeft <= vLeft) {\n      // 最左边\n      bounds.x = vLeft + padding;\n    } else if (tRight >= vRight) {\n      // 最右边\n      bounds.x = vRight - padding - width;\n    }\n    if (tTop <= vTop) {\n      // 最上边\n      bounds.y = vTop + padding;\n    } else if (tBottom >= vBottom) {\n      // 最下边\n      bounds.y = vBottom - padding - height;\n    }\n    return bounds;\n  }\n  /**\n   * 根据两点创建矩形\n   */\n  export function createRectangleWithTwoPoints(point1: IPoint, point2: IPoint): Rectangle {\n    const x = point1.x < point2.x ? point1.x : point2.x;\n    const y = point1.y < point2.y ? point1.y : point2.y;\n    const width = Math.abs(point1.x - point2.x);\n    const height = Math.abs(point1.y - point2.y);\n    return new Rectangle(x, y, width, height);\n  }\n}\n\n/**\n * Oriented Bounding Box (OBB)\n * @see https://en.wikipedia.org/wiki/Bounding_volume\n */\nexport class OBBRect {\n  readonly axesX: Vector2;\n\n  readonly axesY: Vector2;\n\n  readonly centerPoint: Vector2;\n\n  /**\n   * @param rotation in radian\n   */\n  constructor(\n    centerPoint: IPoint,\n    protected width: number,\n    protected height: number,\n    rotation: number\n  ) {\n    this.centerPoint = new Vector2(centerPoint.x, centerPoint.y);\n    this.axesX = new Vector2(Math.cos(rotation), Math.sin(rotation));\n    this.axesY = new Vector2(-1 * this.axesX.y, this.axesX.x);\n  }\n\n  /**\n   * 计算投影半径\n   */\n  getProjectionRadius(axis: Vector2): number {\n    return (\n      (this.width / 2) * Math.abs(axis.dot(this.axesX)) +\n      (this.height / 2) * Math.abs(axis.dot(this.axesY))\n    );\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/index.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Circle } from './index';\n\ndescribe('shapes', () => {\n  test('Circle', () => {\n    expect(Circle).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/shapes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './Circle';\nexport * from './Rectangle';\n"
  },
  {
    "path": "packages/common/utils/src/math/wrap.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { wrap } from './wrap';\n\ndescribe('wrap', () => {\n  test('wrap', async () => {\n    expect(wrap(-1, 1, 10)).toBe(8);\n    expect(wrap(0, 1, 10)).toBe(9);\n    expect(wrap(1, 1, 10)).toBe(1);\n    expect(wrap(2, 1, 10)).toBe(2);\n    expect(wrap(3, 1, 10)).toBe(3);\n    expect(wrap(4, 1, 10)).toBe(4);\n    expect(wrap(5, 1, 10)).toBe(5);\n    expect(wrap(6, 1, 10)).toBe(6);\n    expect(wrap(7, 1, 10)).toBe(7);\n    expect(wrap(8, 1, 10)).toBe(8);\n    expect(wrap(9, 1, 10)).toBe(9);\n    expect(wrap(10, 1, 10)).toBe(1);\n    expect(wrap(11, 1, 10)).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/math/wrap.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Wrap the given `value` between `min` and `max`.\n * value ∈ [min, max)\n * e.g.\n *    expect(wrap(0, 1, 10)).toBe(9)\n *    expect(wrap(1, 1, 10)).toBe(1)\n *    expect(wrap(10, 1, 10)).toBe(1)\n *\n * @return The wrapped value.\n */\nexport function wrap(value: number, min: number, max: number): number {\n  const range = max - min;\n\n  return min + ((((value - min) % range) + range) % range);\n}\n"
  },
  {
    "path": "packages/common/utils/src/objects.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport {\n  NOOP,\n  deepFreeze,\n  each,\n  filter,\n  getByKey,\n  isEmpty,\n  isPlainObject,\n  mapKeys,\n  mapValues,\n  notEmpty,\n  omit,\n  pick,\n  reduce,\n  setByKey,\n  values,\n} from './objects';\n\ndescribe('objects', () => {\n  test('deepFreeze', async () => {\n    const obj1 = { a: { b: 2 } };\n    deepFreeze(obj1);\n    expect(() => {\n      obj1.a.b = 3;\n    }).toThrow();\n\n    expect(deepFreeze(null)).toBeNull();\n    expect(deepFreeze(1)).toEqual(1);\n  });\n\n  test('notEmpty', async () => {\n    expect(notEmpty({})).toBeTruthy();\n    expect(notEmpty([])).toBeTruthy();\n    expect(notEmpty(() => {})).toBeTruthy();\n\n    expect(notEmpty(undefined)).toBeFalsy();\n    expect(notEmpty(null)).toBeFalsy();\n  });\n\n  test('isEmpty', async () => {\n    expect(isEmpty({})).toBeTruthy();\n    expect(isEmpty({ a: 1 })).toBeFalsy();\n\n    // WARNING: just for plain object\n    expect(isEmpty(() => 1)).toBeFalsy();\n  });\n\n  const obj = Object.freeze({ a: 1, b: 2, c: 3 });\n\n  test('each', async () => {\n    const ret: any[] = [];\n    each(obj, (v, k) => ret.push([k, v]));\n    expect(ret).toEqual([\n      ['a', 1],\n      ['b', 2],\n      ['c', 3],\n    ]);\n  });\n\n  test('values', async () => {\n    expect(values(obj)).toEqual([1, 2, 3]);\n\n    const _values = Object.values;\n    Object.values = null as any;\n    expect(values(obj)).toEqual([1, 2, 3]);\n    Object.values = _values;\n  });\n\n  test('filter', async () => {\n    expect(filter(obj, (v, k) => v > 1)).toEqual({ b: 2, c: 3 });\n    const dest = {};\n    expect(filter(obj, (v, k) => v > 1, dest)).toEqual({ b: 2, c: 3 });\n    expect(dest).toEqual({ b: 2, c: 3 });\n  });\n\n  test('pick', async () => {\n    expect(pick(obj, ['b', 'c'])).toEqual({ b: 2, c: 3 });\n    expect(pick(obj, ['a', 'b', 'c'])).toEqual(obj);\n    expect(pick(obj, [])).toEqual({});\n\n    const dest = {};\n    expect(pick(obj, ['b', 'c'], dest)).toEqual({ b: 2, c: 3 });\n    expect(dest).toEqual({ b: 2, c: 3 });\n  });\n\n  test('omit', async () => {\n    expect(omit(obj, ['a'])).toEqual({ b: 2, c: 3 });\n    expect(omit(obj, ['a', 'b', 'c', 'd'])).toEqual({});\n    expect(omit(obj, [])).toEqual(obj);\n\n    const dest = {};\n    expect(omit(obj, ['a'], dest)).toEqual({ b: 2, c: 3 });\n    expect(dest).toEqual({ b: 2, c: 3 });\n  });\n\n  test('reduce', async () => {\n    // sum\n    expect(reduce(obj, (res, v) => res + v, 0)).toEqual(6);\n\n    // v + 1\n    expect(\n      reduce(obj, (res, v, k) => {\n        res[k] = v + 1;\n        return res;\n      }),\n    ).toEqual({ a: 2, b: 3, c: 4 });\n\n    // entries\n    expect(\n      reduce(\n        obj,\n        (res, v, k) => {\n          res.push([k, v]);\n          return res;\n        },\n        [] as [string, number][],\n      ),\n    ).toEqual([\n      ['a', 1],\n      ['b', 2],\n      ['c', 3],\n    ]);\n  });\n\n  test('mapValues', async () => {\n    expect(mapValues(obj, v => v + 1)).toEqual({ a: 2, b: 3, c: 4 });\n    expect(mapValues(obj, (v, k) => `${k}${v}`)).toEqual({\n      a: 'a1',\n      b: 'b2',\n      c: 'c3',\n    });\n  });\n\n  test('mapKeys', async () => {\n    expect(mapKeys(obj, (v, k) => `${k}1`)).toEqual({ a1: 1, b1: 2, c1: 3 });\n    expect(mapKeys(obj, (v, k) => `${k}${v}`)).toEqual({ a1: 1, b2: 2, c3: 3 });\n  });\n\n  test('getByKey', async () => {\n    const obj1 = Object.freeze({ a: { b: { c: 1 } } });\n\n    expect(getByKey(obj1, 'a.b.c')).toEqual(1);\n    expect(getByKey(obj1, 'a.b')).toEqual({ c: 1 });\n    expect(getByKey(obj1, 'a')).toEqual({ b: { c: 1 } });\n\n    // return undefined\n    expect(getByKey(1, 'a')).toBeUndefined();\n    expect(getByKey(obj1, '')).toBeUndefined();\n    expect(getByKey(obj1, 'b')).toBeUndefined();\n    expect(getByKey(obj1, 'a.d')).toBeUndefined();\n    expect(getByKey(obj1, 'a.d.e')).toBeUndefined();\n    expect(getByKey(obj1, 'a.b.c.d')).toBeUndefined();\n  });\n\n  test('setByKey', async () => {\n    expect(setByKey({ a: { b: { c: 1 } } }, 'a.b.c', 2)).toEqual({\n      a: { b: { c: 2 } },\n    });\n    const obj1 = { a: { b: { c: 1 } } };\n    expect(setByKey(obj1, 'a.b.c', 2, true, true)).toEqual({\n      a: { b: { c: 2 } },\n    });\n    expect(obj1).toEqual({ a: { b: { c: 1 } } });\n    const arr = [1] as any;\n    arr.b = 2;\n    expect(setByKey({ a: [1] }, 'a.b', 2, true, true)).toEqual({ a: arr });\n\n    expect(setByKey(1, 'a.b.c', 2)).toEqual(1);\n    expect(setByKey({ a: { b: { c: 1 } } }, '', 2)).toEqual({\n      a: { b: { c: 1 } },\n    });\n    expect(setByKey({ a: { b: { c: 1 } } }, 'a.b.d', 2)).toEqual({\n      a: { b: { c: 1, d: 2 } },\n    });\n    expect(setByKey({ a: { b: { c: 1 } } }, 'a.b.d', 2, false)).toEqual({\n      a: { b: { c: 1, d: 2 } },\n    });\n    expect(setByKey({ a: { b: { c: 1 } } }, 'a.b.c.d', 2)).toEqual({\n      a: { b: { c: { d: 2 } } },\n    });\n    expect(setByKey({ a: { b: { c: 1 } } }, 'a.b.c.d', 2, false)).toEqual({\n      a: { b: { c: 1 } },\n    });\n  });\n\n  test('NOOP', async () => {\n    expect(NOOP()).toBeUndefined();\n  });\n\n  test('isPlainObject', async () => {\n    expect(isPlainObject({})).toBeTruthy();\n    expect(isPlainObject({ a: 1 })).toBeTruthy();\n    expect(isPlainObject({ a: { b: 1 } })).toBeTruthy();\n    // eslint-disable-next-line prefer-object-spread\n    expect(isPlainObject(Object.assign({}, { a: 1 }))).toBeTruthy();\n\n    // eslint-disable-next-line prefer-arrow-callback\n    expect(isPlainObject(function test1() {})).toBeFalsy();\n    expect(isPlainObject(() => {})).toBeFalsy();\n    expect(isPlainObject([])).toBeFalsy();\n\n    expect(isPlainObject(null)).toBeFalsy();\n    expect(isPlainObject(undefined)).toBeFalsy();\n    expect(isPlainObject('')).toBeFalsy();\n    expect(isPlainObject('1')).toBeFalsy();\n    expect(isPlainObject(0)).toBeFalsy();\n    expect(isPlainObject(1)).toBeFalsy();\n    expect(isPlainObject(BigInt(0))).toBeFalsy();\n    expect(isPlainObject(BigInt(1))).toBeFalsy();\n    expect(isPlainObject(false)).toBeFalsy();\n    expect(isPlainObject(true)).toBeFalsy();\n    expect(isPlainObject(Symbol(''))).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/objects.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { keys } = Object;\nexport function deepFreeze<T>(obj: T): T {\n  if (!obj || typeof obj !== 'object') {\n    return obj;\n  }\n  const stack: any[] = [obj];\n  while (stack.length > 0) {\n    const objectToFreeze = stack.shift();\n    Object.freeze(objectToFreeze);\n    for (const key in objectToFreeze) {\n      if (_hasOwnProperty.call(objectToFreeze, key)) {\n        const prop = objectToFreeze[key];\n        if (typeof prop === 'object' && !Object.isFrozen(prop)) {\n          stack.push(prop);\n        }\n      }\n    }\n  }\n  return obj;\n}\n\nconst _hasOwnProperty = Object.prototype.hasOwnProperty;\n\nexport function notEmpty<T>(arg: T | undefined | null): arg is T {\n  return arg !== undefined && arg !== null;\n}\n\n/**\n * filter dangerous key, prevent prototype pollution injection\n * @param key key to be filtered\n * @returns filtered key\n */\nexport const safeKey = (key: string): string => {\n  const dangerousProps = [\n    '__proto__',\n    'constructor',\n    'prototype',\n    '__defineGetter__',\n    '__defineSetter__',\n    '__lookupGetter__',\n    '__lookupSetter__',\n    'hasOwnProperty',\n    'isPrototypeOf',\n    'propertyIsEnumerable',\n    'toString',\n    'valueOf',\n    'toLocaleString',\n  ];\n\n  if (dangerousProps.includes(key.toLowerCase())) {\n    return '';\n  }\n\n  return key;\n};\n\n/**\n * `true` if the argument is an empty object. Otherwise, `false`.\n */\nexport function isEmpty(arg: Object): boolean {\n  return keys(arg).length === 0 && arg.constructor === Object;\n}\n\nexport const each = <T = any, K = string>(obj: any, fn: (value: T, key: K) => void) =>\n  keys(obj).forEach((key) => fn(obj[key], key as any));\n\nexport const values = (obj: any) =>\n  Object.values ? Object.values(obj) : keys(obj).map((k) => obj[k]);\n\nexport const filter = (obj: any, fn: (value: any, key: string) => boolean, dest?: any) =>\n  keys(obj).reduce(\n    (output, key) => (fn(obj[key], key) ? Object.assign(output, { [key]: obj[key] }) : output),\n    dest || {}\n  );\n\nexport const pick = (obj: any, fields: string[], dest?: any) =>\n  filter(obj, (n, k) => fields.indexOf(k) !== -1, dest);\n\nexport const omit = (obj: any, fields: string[], dest?: any) =>\n  filter(obj, (n, k) => fields.indexOf(k) === -1, dest);\n\nexport const reduce = <V = any, R = any>(\n  obj: any,\n  fn: (res: R, value: V, key: string) => any,\n  res: R = {} as R\n) => keys(obj).reduce((r, k) => fn(r, obj[k], k), res);\n\nexport const mapValues = <V = any>(obj: any, fn: (value: V, key: string) => any) =>\n  reduce<V>(obj, (res, value, key) => Object.assign(res, { [key]: fn(value, key) }));\n\nexport const mapKeys = <V = any>(obj: any, fn: (value: V, key: string) => any) =>\n  reduce<V>(obj, (res, value, key) => Object.assign(res, { [fn(value, key)]: value }));\n\n/**\n * @param target\n * @param key\n * @example\n *  const obj = {\n *    position: {\n *      x: 0\n *      y: 0\n *    }\n *  }\n *  getByKey(ob, 'position.x') // 0\n */\nexport function getByKey(target: any, key: string): any | undefined {\n  if (typeof target !== 'object' || !key) return undefined;\n  return key.split('.').reduce((v: any, k: string) => {\n    if (typeof v !== 'object') return undefined;\n    return v[k];\n  }, target);\n}\n\n/**\n * @param target\n * @param key\n * @param newValue\n * @param autoCreateObject\n * @example\n *  const obj = {\n *    position: {\n *      x: 0\n *      y: 0\n *    }\n *  }\n *  setByKey(ob, 'position.x', 100) // true\n *  setByKey(obj, 'size.width', 100) // false\n *  setBeyKey(obj, 'size.width', 100, true) // true\n */\nexport function setByKey(\n  target: any,\n  key: string,\n  newValue: any,\n  autoCreateObject = true,\n  clone = false\n): any {\n  if (typeof target !== 'object' || !key) return target;\n  if (clone) {\n    target = { ...target };\n  }\n  const originTarget = target;\n  const targetKeys = key.split('.');\n  while (targetKeys.length > 0) {\n    key = targetKeys.shift()!;\n    if (targetKeys.length === 0) {\n      target[safeKey(key)] = newValue;\n      return originTarget;\n    }\n    if (typeof target[key] !== 'object') {\n      if (!autoCreateObject) return originTarget;\n      target[safeKey(key)] = {};\n    }\n    if (clone) {\n      if (Array.isArray(target[key])) {\n        target[safeKey(key)] = target[key].slice();\n      } else {\n        target[safeKey(key)] = { ...target[key] };\n      }\n    }\n    target = target[key];\n  }\n  return originTarget;\n}\n\nexport const NOOP = () => {};\n\n/**\n * @param obj The object to inspect.\n * @returns True if the argument appears to be a plain object.\n */\nexport function isPlainObject(obj: any): boolean {\n  if (typeof obj !== 'object' || obj === null) return false;\n\n  let proto = obj;\n  while (Object.getPrototypeOf(proto) !== null) {\n    proto = Object.getPrototypeOf(proto);\n  }\n\n  return Object.getPrototypeOf(obj) === proto;\n}\n"
  },
  {
    "path": "packages/common/utils/src/promise-util.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { PromisePool, type PromiseTask, retry, delay } from './promise-util';\nimport { Emitter } from './event';\n\ndescribe('promise utils', () => {\n  test('timeout', async () => {\n    const cancelEmitter = new Emitter();\n    cancelEmitter.event(() => {});\n    await delay(1, {\n      isCancellationRequested: false,\n      onCancellationRequested: cancelEmitter.event,\n    });\n    cancelEmitter.fire(cancelEmitter.event);\n  });\n\n  test('retry', async () => {\n    const K = 'retry-task';\n    const task = () => {\n      throw new Error(K);\n    };\n    expect(() => retry(task, 1, 2)).rejects.toThrow(K);\n\n    const task1 = () =>\n      new Promise((resolve, reject) => {\n        reject(new Error(K));\n      });\n    expect(() => retry(task1, 1, 2)).rejects.toThrow(K);\n  });\n\n  test('PromisePool/basic', async () => {\n    const pool = new PromisePool();\n    expect(await pool.run<number>([])).toEqual([]);\n    expect(\n      await pool.run<number>([\n        () =>\n          new Promise(resolve => {\n            setTimeout(() => {\n              resolve(1);\n            }, 1);\n          }),\n      ]),\n    ).toEqual([1]);\n  });\n\n  test('PromisePool/retry', async () => {\n    let execTimes = 0;\n    const tasks: PromiseTask<number>[] = Array(10)\n      .fill(0)\n      .map(\n        (t, i) => () =>\n          new Promise(resolve => {\n            setTimeout(() => {\n              execTimes += 1;\n              resolve(i);\n            }, 10);\n          }),\n      );\n    const pool = new PromisePool({\n      intervalCount: 3,\n      intervalTime: 100,\n      retries: 3,\n    });\n    const checkIfRetry = (res: number) => {\n      if (res === 8) return true;\n      return false;\n    };\n    const result = await pool.run<number>(tasks, checkIfRetry);\n\n    expect(execTimes).toEqual(12);\n    expect(result).toEqual(\n      Array(10)\n        .fill(0)\n        .map((t, i) => i),\n    );\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/promise-util.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CancellationToken, cancelled } from './cancellation';\n\n/**\n * Simple implementation of the deferred pattern.\n * An object that exposes a promise and functions to resolve and reject it.\n */\nexport class PromiseDeferred<T> {\n  resolve: (value?: T | PromiseLike<T>) => void;\n\n  reject: (err?: any) => void;\n\n  promise = new Promise<T>((resolve, reject) => {\n    // @ts-ignore\n    this.resolve = resolve;\n    this.reject = reject;\n  });\n}\n\nexport const Deferred = PromiseDeferred;\n/**\n * @returns resolves after a specified number of milliseconds\n * @throws cancelled if a given token is cancelled before a specified number of milliseconds\n */\nexport function delay(ms: number, token = CancellationToken.None): Promise<void> {\n  const deferred = new PromiseDeferred<void>();\n  const handle = setTimeout(() => deferred.resolve(), ms);\n  token.onCancellationRequested(() => {\n    clearTimeout(handle);\n    deferred.reject(cancelled());\n  });\n  return deferred.promise;\n}\n\nexport async function retry<T>(\n  task: () => Promise<T>,\n  delayTime: number,\n  retries: number,\n  shouldRetry?: (res: T) => boolean,\n): Promise<T> {\n  let lastError: Error | undefined;\n  let result: T;\n\n  for (let i = 0; i < retries; i++) {\n    try {\n      // eslint-disable-next-line no-await-in-loop\n      result = await task();\n      if (shouldRetry && shouldRetry(result)) {\n        // eslint-disable-next-line no-await-in-loop\n        await delay(delayTime);\n        // eslint-disable-next-line no-continue\n        continue;\n      }\n      return result;\n    } catch (error: any) {\n      lastError = error;\n\n      // eslint-disable-next-line no-await-in-loop\n      await delay(delayTime);\n    }\n  }\n\n  if (lastError) {\n    throw lastError;\n  }\n  return result!;\n}\n\nexport interface PromiseTask<T> {\n  (): Promise<T>;\n}\n\nexport interface PromisePoolOpts {\n  intervalCount?: number; // 每批数目\n  intervalTime?: number; // 执行一批后的间隔时间, 默认没有间隔\n  retries?: number; // 如果某个执行失败, 尝试的次数，默认不尝试\n  retryDelay?: number;\n}\n\nconst PromisePoolOptsDefault: Required<PromisePoolOpts> = {\n  intervalCount: 10, // 每批数目\n  intervalTime: 0,\n  retries: 0,\n  retryDelay: 10,\n};\n\nexport class PromisePool {\n  protected opts: Required<PromisePoolOpts>;\n\n  constructor(opts: PromisePoolOpts = PromisePoolOptsDefault) {\n    this.opts = { ...PromisePoolOptsDefault, ...opts };\n  }\n\n  protected async tryToExec<T>(\n    task: PromiseTask<T>,\n    checkIfRetry?: (res: T) => boolean,\n  ): Promise<T> {\n    if (this.opts.retries === 0) return task();\n    return retry<T>(task, this.opts.retryDelay, this.opts.retries, checkIfRetry);\n  }\n\n  /**\n   * @param tasks 执行任务\n   * @param checkIfRetry 判断结果是否需要重试\n   */\n  async run<T>(tasks: PromiseTask<T>[], checkIfRetry?: (res: T) => boolean): Promise<T[]> {\n    if (tasks.length === 0) return [];\n    const curTasks = tasks.slice(0, this.opts.intervalCount);\n    const promises = curTasks.map(task => this.tryToExec<T>(task, checkIfRetry));\n    const result: T[] = await Promise.all(promises);\n    const nextTasks = tasks.slice(this.opts.intervalCount);\n    if (nextTasks.length === 0) return result;\n    if (this.opts.intervalTime !== 0) await delay(this.opts.intervalTime);\n    return result.concat(await this.run(nextTasks, checkIfRetry));\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/request-with-memo.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport { clearRequestCache, requestWithMemo } from './request-with-memo';\n\nfunction delay(time: number): Promise<void> {\n  return new Promise(res => {\n    setTimeout(res, time);\n  });\n}\ndescribe('request with memo', () => {\n  test('base', async () => {\n    const cb = vi.fn();\n    const requestMock = async () => cb();\n    const newRequest = requestWithMemo(requestMock);\n    await newRequest();\n    await newRequest();\n    expect(cb.mock.calls.length).toEqual(1);\n    clearRequestCache();\n    await newRequest();\n    expect(cb.mock.calls.length).toEqual(2);\n  });\n  test('timeout clear', async () => {\n    const cb = vi.fn();\n    const requestMock = async () => cb();\n    const newRequest = requestWithMemo(requestMock, 0);\n    await newRequest();\n    await delay(10);\n    await newRequest();\n    expect(cb.mock.calls.length).toEqual(2);\n  });\n  test('request with error', async () => {\n    const cb = vi.fn();\n    const requestMock = async () => {\n      cb();\n      throw new Error('requestError');\n    };\n    const newRequest = requestWithMemo(requestMock, 0);\n    let errorTimes = 0;\n    try {\n      await newRequest();\n    } catch (e) {\n      errorTimes += 1;\n    }\n    try {\n      await newRequest();\n    } catch (e) {\n      errorTimes += 1;\n    }\n    expect(errorTimes).toEqual(2);\n    // 错误发生不会被缓存\n    expect(cb.mock.calls.length).toEqual(2);\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/request-with-memo.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ntype RequestFn = (...args: any[]) => Promise<any>;\n\n/**\n * 请求缓存\n * @param req\n */\n// eslint-disable-next-line import/no-mutable-exports\nexport const RequestCache = new Map<any, Promise<any>>();\nconst CACHE_TIME = 10000; // 缓存过期时间\n\nexport function clearRequestCache(): void {\n  RequestCache.clear();\n}\n\nexport function requestWithMemo(\n  req: RequestFn,\n  cacheTime = CACHE_TIME,\n  createCacheKey?: (...args: any[]) => any,\n): RequestFn {\n  return (...args: any[]) => {\n    const cacheKey = createCacheKey ? createCacheKey(...args) : req;\n    if (RequestCache.has(cacheKey)) {\n      return Promise.resolve(RequestCache.get(cacheKey));\n    }\n    const result = req(...args);\n    const time = setTimeout(() => RequestCache.delete(cacheKey), cacheTime);\n    const withErrorResult = result.catch(e => {\n      // 请求错误情况下不缓存\n      RequestCache.delete(cacheKey);\n      clearTimeout(time);\n      throw e;\n    });\n    RequestCache.set(cacheKey, withErrorResult);\n    return withErrorResult;\n  };\n}\n"
  },
  {
    "path": "packages/common/utils/src/schema/index.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { Schema } from './index';\n\ndescribe('schema', () => {\n  test('Schema', () => {\n    expect(Schema).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './schema';\nexport * from './schema-transform';\nexport * from './schema-base';\n"
  },
  {
    "path": "packages/common/utils/src/schema/schema-base.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type PositionSchema, type SizeSchema } from './schema-transform';\nimport { type SchemaDecoration } from './schema';\n\nexport type OpacitySchema = number;\n\nexport interface FlipSchema {\n  x: boolean;\n  y: boolean;\n}\n\nexport interface ShadowSchema {\n  color: string;\n  offsetX: number;\n  offsetY: number;\n  blur: number;\n}\n\nexport interface PaddingSchema {\n  left: number;\n  right: number;\n  top: number;\n  bottom: number;\n}\n\nexport namespace PaddingSchema {\n  export const empty = () => ({ left: 0, right: 0, top: 0, bottom: 0 });\n}\n\nexport type MarginSchema = PaddingSchema;\n\nexport interface TintSchema {\n  topLeft: string;\n  topRight: string;\n  bottomLeft: string;\n  bottomRight: string;\n}\n\nexport namespace TintSchema {\n  export function isEmpty(tint: Partial<TintSchema> | undefined): boolean {\n    if (!tint) return true;\n    return (\n      tint.topLeft === undefined &&\n      tint.topRight === undefined &&\n      tint.bottomLeft === undefined &&\n      tint.bottomRight === undefined\n    );\n  }\n}\n\nexport const CropSchemaDecoration: SchemaDecoration<PositionSchema & SizeSchema> = {\n  label: '裁剪',\n  properties: {\n    width: { label: '宽', type: 'integer' },\n    height: { label: '高', type: 'integer' },\n    x: { label: 'x', type: 'integer' },\n    y: { label: 'y', type: 'integer' },\n  },\n  type: 'object',\n};\n\nexport const FlipSchemaDecoration: SchemaDecoration<FlipSchema> = {\n  label: '镜像替换',\n  properties: {\n    x: { label: '水平镜像替换', default: false, type: 'boolean' },\n    y: { label: '垂直镜像替换', default: false, type: 'boolean' },\n  },\n  type: 'object',\n};\nexport const PaddingSchemaDecoration: SchemaDecoration<PaddingSchema> = {\n  label: '留白',\n  properties: {\n    left: { label: '左', default: 0, type: 'integer' },\n    top: { label: '上', default: 0, type: 'integer' },\n    right: { label: '右', default: 0, type: 'integer' },\n    bottom: { label: '下', default: 0, type: 'integer' },\n  },\n  type: 'object',\n};\n\nexport const ShadowSchemaDecoration: SchemaDecoration<ShadowSchema> = {\n  label: '阴影',\n  properties: {\n    offsetX: { label: 'X', type: 'integer' },\n    offsetY: { label: 'Y', type: 'integer' },\n    blur: { label: '模糊', type: 'integer' },\n    color: { label: '颜色', type: 'color' },\n  },\n  type: 'object',\n};\n\nexport const TintSchemaDecoration: SchemaDecoration<TintSchema> = {\n  label: '颜色',\n  properties: {\n    topLeft: { label: '左上', type: 'color' },\n    topRight: { label: '右上', type: 'color' },\n    bottomLeft: { label: '左下', type: 'color' },\n    bottomRight: { label: '右下', type: 'color' },\n  },\n  type: 'object',\n};\n\nexport const OpacitySchemaDecoration: SchemaDecoration<OpacitySchema> = {\n  label: '透明度',\n  type: 'float',\n  min: 0,\n  max: 1,\n  default: 1,\n};\n"
  },
  {
    "path": "packages/common/utils/src/schema/schema-transform.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Schema, type SchemaDecoration } from './schema';\n\nexport interface PositionSchema {\n  x: number;\n  y: number;\n}\n\nexport type RotationSchema = number;\n\nexport interface OriginSchema {\n  x: number;\n  y: number;\n}\n\nexport interface ScaleSchema {\n  x: number;\n  y: number;\n}\n\nexport interface ScrollSchema {\n  scrollX: number;\n  scrollY: number;\n}\n\nexport interface SizeSchema {\n  width: number;\n  height: number;\n  locked?: boolean; // 是否开启等比锁\n}\n\nexport interface SkewSchema {\n  x: number;\n  y: number;\n}\n\nexport interface TransformSchema {\n  position: PositionSchema;\n  size: SizeSchema;\n  origin: OriginSchema;\n  scale: ScaleSchema;\n  skew: SkewSchema;\n  rotation: RotationSchema;\n}\n\nexport const SizeSchemaDecoration: SchemaDecoration<SizeSchema> = {\n  label: '大小',\n  properties: {\n    width: { label: '宽', default: 0, type: 'float' },\n    height: { label: '高', default: 0, type: 'float' },\n    locked: { label: '等比锁', default: false, type: 'boolean' },\n  },\n  type: 'object',\n};\n\nexport const OriginSchemaDecoration: SchemaDecoration<OriginSchema> = {\n  label: '原点',\n  description: '用于设置旋转的中心位置',\n  properties: {\n    x: { label: 'x', default: 0.5, type: 'float' },\n    y: { label: 'y', default: 0.5, type: 'float' },\n  },\n  type: 'object',\n};\n\nexport const PositionSchemaDecoration: SchemaDecoration<PositionSchema> = {\n  label: '位置',\n  properties: {\n    x: { label: 'x', default: 0, type: 'float' },\n    y: { label: 'y', default: 0, type: 'float' },\n  },\n  type: 'object',\n};\n\nexport const RotationSchemaDecoration: SchemaDecoration<RotationSchema> = {\n  label: '旋转',\n  type: 'float',\n  default: 0,\n};\n\nexport const ScaleSchemaDecoration: SchemaDecoration<ScaleSchema> = {\n  label: '缩放',\n  properties: {\n    x: { label: 'x', default: 1, type: 'float' },\n    y: { label: 'y', default: 1, type: 'float' },\n  },\n  type: 'object',\n};\nexport const SkewSchemaDecoration: SchemaDecoration<SkewSchema> = {\n  label: '倾斜',\n  properties: {\n    x: { label: 'x', default: 0, type: 'float' },\n    y: { label: 'y', default: 0, type: 'float' },\n  },\n  type: 'object',\n};\n\nexport const TransformSchemaDecoration: SchemaDecoration<TransformSchema> = {\n  properties: {\n    position: PositionSchemaDecoration,\n    size: SizeSchemaDecoration,\n    origin: OriginSchemaDecoration,\n    scale: ScaleSchemaDecoration,\n    skew: SkewSchemaDecoration,\n    rotation: RotationSchemaDecoration,\n  },\n  type: 'object',\n};\nexport namespace TransformSchema {\n  export function createDefault(): TransformSchema {\n    return Schema.createDefault<TransformSchema>(TransformSchemaDecoration);\n  }\n\n  export function toJSON(obj: TransformSchema): TransformSchema {\n    return {\n      position: { x: obj.position.x, y: obj.position.y },\n      size: {\n        width: obj.size.width,\n        height: obj.size.height,\n        locked: obj.size.locked,\n      },\n      origin: { x: obj.origin.x, y: obj.origin.y },\n      scale: { x: obj.scale.x, y: obj.scale.y },\n      skew: { x: obj.skew.x, y: obj.skew.y },\n      rotation: obj.rotation,\n    };\n  }\n  export function getDelta(\n    oldTransform: TransformSchema,\n    newTransform: TransformSchema,\n  ): TransformSchema {\n    return {\n      position: {\n        x: newTransform.position.x - oldTransform.position.x,\n        y: newTransform.position.y - oldTransform.position.y,\n      },\n      size: {\n        width: newTransform.size.width - oldTransform.size.width,\n        height: newTransform.size.height - oldTransform.size.height,\n      },\n      origin: {\n        x: newTransform.origin.x - oldTransform.origin.x,\n        y: newTransform.origin.y - oldTransform.origin.y,\n      },\n      scale: {\n        x: newTransform.scale.x - oldTransform.scale.x,\n        y: newTransform.scale.y - oldTransform.scale.y,\n      },\n      skew: {\n        x: newTransform.skew.x - oldTransform.skew.x,\n        y: newTransform.skew.y - oldTransform.skew.y,\n      },\n      rotation: newTransform.rotation - oldTransform.rotation,\n    };\n  }\n\n  export function mergeDelta(\n    oldTransform: TransformSchema,\n    newTransformDelta: TransformSchema,\n    toFixedNum?: number,\n  ): TransformSchema {\n    const toFixed =\n      toFixedNum !== undefined ? (v: number) => Math.round(v * 100) / 100 : (v: number) => v;\n    return {\n      position: {\n        x: toFixed(newTransformDelta.position.x + oldTransform.position.x),\n        y: toFixed(newTransformDelta.position.y + oldTransform.position.y),\n      },\n      size: {\n        width: toFixed(newTransformDelta.size.width + oldTransform.size.width),\n        height: toFixed(newTransformDelta.size.height + oldTransform.size.height),\n        locked: oldTransform.size.locked,\n      },\n      origin: {\n        x: toFixed(newTransformDelta.origin.x + oldTransform.origin.x),\n        y: toFixed(newTransformDelta.origin.y + oldTransform.origin.y),\n      },\n      scale: {\n        x: toFixed(newTransformDelta.scale.x + oldTransform.scale.x),\n        y: toFixed(newTransformDelta.scale.y + oldTransform.scale.y),\n      },\n      skew: {\n        x: toFixed(newTransformDelta.skew.x + oldTransform.skew.x),\n        y: toFixed(newTransformDelta.skew.y + oldTransform.skew.y),\n      },\n      rotation: newTransformDelta.rotation + oldTransform.rotation,\n    };\n  }\n\n  export function is(obj: object): obj is TransformSchema {\n    return (\n      obj &&\n      (obj as TransformSchema).position &&\n      (obj as TransformSchema).size &&\n      typeof (obj as TransformSchema).position.x === 'number' &&\n      typeof (obj as TransformSchema).size.width === 'number'\n    );\n  }\n}\n\nexport namespace SizeSchema {\n  /**\n   * 适配父节点宽高\n   *\n   * @return 返回需要缩放的比例，为 1 则不缩放\n   */\n  export function fixSize(currentSize: SizeSchema, parentSize: SizeSchema): number {\n    if (currentSize.width <= parentSize.width && currentSize.height <= parentSize.height) return 1;\n    const wScale = currentSize.width / parentSize.width;\n    const hScale = currentSize.height / parentSize.height;\n    const scale = wScale > hScale ? wScale : hScale;\n    return 1 / scale;\n  }\n\n  /**\n   * 填充父节点的宽高\n   *\n   * @return 返回放大的比例\n   */\n  export function coverSize(currentSize: SizeSchema, parentSize: SizeSchema): number {\n    const wScale = currentSize.width / parentSize.width;\n    const hScale = currentSize.height / parentSize.height;\n    const scale = wScale < hScale ? wScale : hScale;\n    return 1 / scale;\n  }\n\n  export function empty(): SizeSchema {\n    return { width: 0, height: 0 };\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/schema/schema.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// nolint: cyclo_complexity,method_line\nimport { describe, test, expect } from 'vitest';\n\nimport { deepFreeze } from '../objects';\nimport { SizeSchema, TransformSchema } from './schema-transform';\nimport { PaddingSchema, TintSchema } from './schema-base';\nimport { Schema, SchemaDecoration } from './schema';\n\ndescribe('schema', () => {\n  test('TintSchema', async () => {\n    expect(TintSchema.isEmpty(undefined)).toEqual(true);\n    expect(TintSchema.isEmpty({})).toEqual(true);\n    expect(TintSchema.isEmpty({ topLeft: '' })).toEqual(false);\n  });\n\n  test('PaddingSchema', async () => {\n    expect(PaddingSchema.empty()).toEqual({\n      left: 0,\n      right: 0,\n      top: 0,\n      bottom: 0,\n    });\n  });\n\n  test('SchemaDecoration', async () => {\n    expect(SchemaDecoration).not.toBeUndefined();\n    expect(SchemaDecoration.create({})).toEqual({\n      type: 'object',\n      properties: {},\n      mixinDefaults: {},\n    });\n    expect(\n      SchemaDecoration.create(\n        { tip: { type: 'string' } },\n        {\n          type: 'string',\n          properties: { tip: { type: 'integer' } },\n          mixinDefaults: { test: 0 },\n        },\n        { test: 1 },\n      ),\n    ).toEqual({\n      type: 'object',\n      properties: { tip: { type: 'string' } },\n      mixinDefaults: { test: 1 },\n    });\n  });\n\n  describe('Schema', () => {\n    test('Schema', async () => {\n      expect(Schema).not.toBeUndefined();\n      expect(Schema.createDefault({ type: 'object' })).toEqual(undefined);\n      expect(\n        Schema.createDefault({\n          type: 'object',\n          default: { tik: 11, tok: '22' },\n        }),\n      ).toEqual({\n        tik: 11,\n        tok: '22',\n      });\n      expect(\n        Schema.createDefault({\n          type: 'object',\n          default: () => ({ tik: 11, tok: '22' }),\n        }),\n      ).toEqual({\n        tik: 11,\n        tok: '22',\n      });\n      expect(\n        Schema.createDefault({\n          type: 'object',\n          properties: {\n            tik: { type: 'integer', default: 1 },\n            tok: { type: 'string', default: '2' },\n          },\n        }),\n      ).toEqual({\n        tik: 1,\n        tok: '2',\n      });\n      expect(\n        Schema.createDefault(\n          {\n            type: 'object',\n            properties: {\n              tik: { type: 'integer' },\n              tok: { type: 'string' },\n            },\n            mixinDefaults: { tik: 111, tok: '222' },\n          },\n          { tik: 1111, tok: '2222' },\n        ),\n      ).toEqual({\n        tik: 1111,\n        tok: '2222',\n      });\n      expect(\n        Schema.createDefault(\n          {\n            type: 'object',\n            properties: {\n              tik: { type: 'integer' },\n              tok: { type: 'string' },\n            },\n            mixinDefaults: { 'pre.tik': 111, 'pre.tok': '222' },\n          },\n          { 'pre.tik': 1111, 'pre.tok': '2222' },\n          'pre',\n        ),\n      ).toEqual({\n        tik: 1111,\n        tok: '2222',\n      });\n    });\n\n    test('isBaseType', () => {\n      expect(Schema.isBaseType({ type: 'string' })).toBeTruthy();\n      expect(Schema.isBaseType({ type: 'array' })).toBeFalsy();\n      expect(Schema.isBaseType({ type: 'object' })).toBeFalsy();\n    });\n  });\n\n  describe('TransformSchema', () => {\n    test('TransformSchema', () => {\n      expect(TransformSchema).not.toBeUndefined();\n    });\n\n    const def = deepFreeze({\n      origin: { x: 0.5, y: 0.5 },\n      position: { x: 0, y: 0 },\n      rotation: 0,\n      scale: { x: 1, y: 1 },\n      size: { width: 0, height: 0, locked: false },\n      skew: { x: 0, y: 0 },\n    });\n\n    test('createDefault', () => {\n      expect(TransformSchema.createDefault()).toEqual(def);\n    });\n\n    test('toJSON', () => {\n      const schema1 = { ...def, test: 1 };\n      expect(TransformSchema.toJSON(schema1)).toEqual(def);\n    });\n\n    test('getDelta', () => {\n      const oldTransform = def;\n      const newTransform = TransformSchema.createDefault();\n      expect(TransformSchema.getDelta(oldTransform, newTransform)).toEqual({\n        origin: { x: 0, y: 0 },\n        position: { x: 0, y: 0 },\n        rotation: 0,\n        scale: { x: 0, y: 0 },\n        size: { width: 0, height: 0 },\n        skew: { x: 0, y: 0 },\n      });\n\n      const newTransform1: TransformSchema = {\n        origin: { x: 1, y: 0.5 },\n        position: { x: 1, y: 0 },\n        rotation: 1,\n        scale: { x: 2, y: 1 },\n        size: { width: 1, height: 0, locked: false },\n        skew: { x: 1, y: 0 },\n      };\n      expect(TransformSchema.getDelta(oldTransform, newTransform1)).toEqual({\n        origin: { x: 0.5, y: 0 },\n        position: { x: 1, y: 0 },\n        rotation: 1,\n        scale: { x: 1, y: 0 },\n        size: { width: 1, height: 0 },\n        skew: { x: 1, y: 0 },\n      });\n    });\n\n    test('mergeDelta', () => {\n      const oldTransform = def;\n      const delta = {\n        origin: { x: 0.5, y: 0 },\n        position: { x: 1, y: 0 },\n        rotation: 1,\n        scale: { x: 1, y: 0 },\n        size: { width: 1, height: 0 },\n        skew: { x: 1, y: 0 },\n      };\n      expect(TransformSchema.mergeDelta(oldTransform, delta)).toEqual({\n        origin: { x: 1, y: 0.5 },\n        position: { x: 1, y: 0 },\n        rotation: 1,\n        scale: { x: 2, y: 1 },\n        size: { width: 1, height: 0, locked: false },\n        skew: { x: 1, y: 0 },\n      });\n      expect(TransformSchema.mergeDelta(oldTransform, delta, 1)).toEqual({\n        origin: { x: 1, y: 0.5 },\n        position: { x: 1, y: 0 },\n        rotation: 1,\n        scale: { x: 2, y: 1 },\n        size: { width: 1, height: 0, locked: false },\n        skew: { x: 1, y: 0 },\n      });\n    });\n\n    test('is', () => {\n      expect(TransformSchema.is(def)).toBeTruthy();\n      expect(TransformSchema.is({})).toBeFalsy();\n      // FIXME?\n      expect(\n        TransformSchema.is({\n          position: { x: 0 },\n          size: { width: 0 },\n        }),\n      ).toBeTruthy();\n    });\n  });\n\n  describe('SizeSchema', () => {\n    test('fixSize', () => {\n      expect(SizeSchema.fixSize({ width: 1, height: 1 }, { width: 1, height: 1 })).toBe(1);\n      expect(SizeSchema.fixSize({ width: 2, height: 1 }, { width: 1, height: 1 })).toBeCloseTo(0.5);\n      expect(SizeSchema.fixSize({ width: 2, height: 4 }, { width: 1, height: 1 })).toBeCloseTo(\n        0.25,\n      );\n    });\n\n    test('coverSize', () => {\n      expect(SizeSchema.coverSize({ width: 1, height: 1 }, { width: 1, height: 1 })).toBe(1);\n      expect(SizeSchema.coverSize({ width: 2, height: 1 }, { width: 1, height: 1 })).toBeCloseTo(1);\n      expect(SizeSchema.coverSize({ width: 2, height: 4 }, { width: 1, height: 1 })).toBeCloseTo(\n        0.5,\n      );\n    });\n\n    test('empty', () => {\n      expect(SizeSchema.empty()).toEqual({ width: 0, height: 0 });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/schema/schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { mapValues } from '../objects';\n\nexport type SchemaType =\n  | 'string'\n  | 'integer'\n  | 'float'\n  | 'boolean'\n  | 'enum'\n  | 'object'\n  | 'range'\n  | 'color'\n  | 'array';\n\ninterface SchemaMixinDefaults {\n  [defaultKey: string]: any;\n}\nexport interface SchemaDecoration<SCHEMA = any> {\n  type: SchemaType;\n  label?: string; // 显示的名字\n  description?: string; // 更多描述，用于 tooltip 展示\n  properties?: {\n    [K in keyof SCHEMA]: SchemaDecoration<SCHEMA[K]> & { priority?: number };\n  };\n  enumValues?: (string | number)[]; // only for enum\n  enumType?: string | number;\n  enumLabels?: string[];\n  rangeStep?: number; // range 一步大小\n  max?: number; // 最大值，只适用于数字\n  min?: number; // 最小值，只适用于数字\n  disabled?: boolean; //  是否屏蔽\n  default?: SCHEMA; // 默认值\n  mixinDefaults?: SchemaMixinDefaults;\n}\n\nexport namespace SchemaDecoration {\n  /**\n   * 扩展 SchemaDecoration\n   *\n   * @param properties - 定义新的属性\n   * @param baseDecoration - 基类\n   * @param mixinDefaults - 修改默认值\n   * @example\n   *    const MySchemaDecoration = SchemaDecoration.create({\n   *      myProp: { label: '', default: 1, type: 'number' }\n   *    },\n   *    TransformSchemaDecoration, // 继承 Transform\n   *    {\n   *      'size.width': 100, // 修改 size 的默认值\n   *      'size.height': 100,\n   *    })\n   */\n  export function create<T>(\n    properties: { [key: string]: SchemaDecoration },\n    baseDecoration?: SchemaDecoration,\n    mixinDefaults?: SchemaMixinDefaults,\n  ): SchemaDecoration<T> {\n    return {\n      type: 'object',\n      properties: {\n        ...baseDecoration?.properties,\n        ...properties,\n      },\n      mixinDefaults: {\n        ...baseDecoration?.mixinDefaults,\n        ...mixinDefaults,\n      },\n    } as SchemaDecoration;\n  }\n}\n\nexport namespace Schema {\n  export function createDefault<T>(\n    decoration: SchemaDecoration,\n    mixinDefaults?: SchemaMixinDefaults,\n    _key?: string,\n  ): T {\n    mixinDefaults = { ...decoration.mixinDefaults, ...mixinDefaults };\n    const prefixKey = _key ? `${_key}.` : '';\n    if (decoration.properties) {\n      return mapValues(decoration.properties, (v, k) => {\n        const childKey = prefixKey + k;\n        if (mixinDefaults && mixinDefaults[childKey] !== undefined) {\n          return mixinDefaults[childKey];\n        }\n        return createDefault(v, mixinDefaults, childKey);\n      }) as T;\n    }\n    return typeof decoration.default === 'function' ? decoration.default() : decoration.default;\n  }\n  /**\n   * 非 object 类\n   */\n  export function isBaseType(decoration: SchemaDecoration): boolean {\n    return (\n      decoration.type === 'string' ||\n      decoration.type === 'float' ||\n      decoration.type === 'integer' ||\n      decoration.type === 'boolean' ||\n      decoration.type === 'enum' ||\n      decoration.type === 'color' ||\n      decoration.type === 'range'\n    );\n  }\n}\n"
  },
  {
    "path": "packages/common/utils/src/types.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, test, expect } from 'vitest';\n\nimport { isNumber, isFunction, isString, getTag } from './types';\n\ndescribe('types', () => {\n  test('isNumber', () => {\n    expect(isNumber(undefined)).toBeFalsy();\n    expect(isNumber(123)).toBeTruthy();\n    expect(isNumber(Number(123))).toBeTruthy();\n    expect(isNumber('123')).toBeFalsy();\n  });\n  test('isFunction', () => {\n    expect(isFunction(undefined)).toBeFalsy();\n    expect(isFunction(() => {})).toBeTruthy();\n  });\n  test('isString', () => {\n    expect(isString(undefined)).toBeFalsy();\n    expect(isString('')).toBeTruthy();\n  });\n  test('getTag', () => {\n    expect(getTag(undefined)).toEqual('[object Undefined]');\n    expect(getTag(null)).toEqual('[object Null]');\n    expect(getTag(Number(123))).toEqual('[object Number]');\n  });\n});\n"
  },
  {
    "path": "packages/common/utils/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface AsClass<T> {\n  new (...args: any[]): T;\n}\n\ntype UnknownObject<T extends object> = Record<string | number | symbol, unknown> & {\n  [K in keyof T]: unknown;\n};\n\nexport function isObject<T extends object>(v: unknown): v is UnknownObject<T> {\n  return typeof v === 'object' && v !== null;\n}\nexport function isString(v: unknown): v is string {\n  return typeof v === 'string' || v instanceof String;\n}\nexport function isFunction<T extends (...args: unknown[]) => unknown>(v: unknown): v is T {\n  return typeof v === 'function';\n}\nconst toString = Object.prototype.toString;\n\nexport function getTag(v: unknown) {\n  if (v == null) {\n    return v === undefined ? '[object Undefined]' : '[object Null]';\n  }\n  return toString.call(v);\n}\nexport function isNumber(v: unknown): v is number {\n  return typeof v === 'number' || (isObject(v) && getTag(v) === '[object Number]');\n}\n\nexport type MaybeArray<T> = T | T[];\nexport type MaybePromise<T> = T | PromiseLike<T>;\n\nexport type RecursivePartial<T> = {\n  [P in keyof T]?: T[P] extends Array<infer I>\n    ? Array<RecursivePartial<I>>\n    : RecursivePartial<T[P]>;\n};\n\ntype Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };\n\nexport type Xor<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;\n"
  },
  {
    "path": "packages/common/utils/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"types\": [\"vitest/globals\"],\n  },\n}\n"
  },
  {
    "path": "packages/common/utils/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/coze-editor/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n  },\n});\n"
  },
  {
    "path": "packages/materials/coze-editor/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/coze-editor\",\n  \"version\": \"0.1.8\",\n  \"description\": \"This is the proxy for @coze-editor/editor, to make sure the version of coze-editor is the same as @flowgram.ai/form-materials\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/types/index.d.ts\",\n      \"require\": \"./dist/cjs/index.js\",\n      \"import\": \"./dist/esm/index.mjs\"\n    },\n    \"./react\": {\n      \"types\": \"./dist/types/react.d.ts\",\n      \"require\": \"./dist/cjs/react.js\",\n      \"import\": \"./dist/esm/react.mjs\"\n    },\n    \"./react-merge\": {\n      \"types\": \"./dist/types/react-merge.d.ts\",\n      \"require\": \"./dist/cjs/react-merge.js\",\n      \"import\": \"./dist/esm/react-merge.mjs\"\n    },\n    \"./vscode\": {\n      \"types\": \"./dist/types/vscode.d.ts\",\n      \"require\": \"./dist/cjs/vscode.js\",\n      \"import\": \"./dist/esm/vscode.mjs\"\n    },\n    \"./language-typescript\": {\n      \"types\": \"./dist/types/language-typescript.d.ts\",\n      \"require\": \"./dist/cjs/language-typescript.js\",\n      \"import\": \"./dist/esm/language-typescript.mjs\"\n    },\n    \"./language-typescript/worker\": {\n      \"types\": \"./dist/types/language-typescript/worker.d.ts\",\n      \"require\": \"./dist/cjs/language-typescript/worker.js\",\n      \"import\": \"./dist/esm/language-typescript/worker.mjs\"\n    },\n    \"./language-json\": {\n      \"types\": \"./dist/types/language-json.d.ts\",\n      \"require\": \"./dist/cjs/language-json.js\",\n      \"import\": \"./dist/esm/language-json.mjs\"\n    },\n    \"./language-shell\": {\n      \"types\": \"./dist/types/language-shell.d.ts\",\n      \"require\": \"./dist/cjs/language-shell.js\",\n      \"import\": \"./dist/esm/language-shell.mjs\"\n    },\n    \"./language-python\": {\n      \"types\": \"./dist/types/language-python.d.ts\",\n      \"require\": \"./dist/cjs/language-python.js\",\n      \"import\": \"./dist/esm/language-python.mjs\"\n    },\n    \"./language-sql\": {\n      \"types\": \"./dist/types/language-sql.d.ts\",\n      \"require\": \"./dist/cjs/language-sql.js\",\n      \"import\": \"./dist/esm/language-sql.mjs\"\n    },\n    \"./preset-universal\": {\n      \"types\": \"./dist/types/preset-universal.d.ts\",\n      \"require\": \"./dist/cjs/preset-universal.js\",\n      \"import\": \"./dist/esm/preset-universal.mjs\"\n    },\n    \"./preset-none\": {\n      \"types\": \"./dist/types/preset-none.d.ts\",\n      \"require\": \"./dist/cjs/preset-none.js\",\n      \"import\": \"./dist/esm/preset-none.mjs\"\n    },\n    \"./preset-expression\": {\n      \"types\": \"./dist/types/preset-expression.d.ts\",\n      \"require\": \"./dist/cjs/preset-expression.js\",\n      \"import\": \"./dist/esm/preset-expression.mjs\"\n    },\n    \"./preset-prompt\": {\n      \"types\": \"./dist/types/preset-prompt.d.ts\",\n      \"require\": \"./dist/cjs/preset-prompt.js\",\n      \"import\": \"./dist/esm/preset-prompt.mjs\"\n    },\n    \"./preset-variable\": {\n      \"types\": \"./dist/types/preset-variable.d.ts\",\n      \"require\": \"./dist/cjs/preset-variable.js\",\n      \"import\": \"./dist/esm/preset-variable.mjs\"\n    },\n    \"./preset-code\": {\n      \"types\": \"./dist/types/preset-code.d.ts\",\n      \"require\": \"./dist/cjs/preset-code.js\",\n      \"import\": \"./dist/esm/preset-code.mjs\"\n    }\n  },\n  \"main\": \"./dist/esm/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"cross-env NODE_ENV=production rslib build\",\n    \"build:fast\": \"cross-env NODE_ENV=development rslib build\",\n    \"build:watch\": \"npm run build:fast\",\n    \"clean\": \"rimraf dist\",\n    \"gen\": \"node scripts/gen.js\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@coze-editor/editor\": \"0.1.0-alpha.868621\",\n    \"@coze-editor/code-language-typescript\": \"0.1.0-alpha.868621\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\",\n    \"cross-env\": \"~7.0.3\",\n    \"@rslib/core\": \"~0.12.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/materials/coze-editor/rslib.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport { defineConfig } from '@rslib/core';\n\ntype RsbuildConfig = Parameters<typeof defineConfig>[0];\n\nconst commonConfig: Partial<RsbuildConfig> = {\n  source: {\n    entry: {\n      index: ['./src/**/*.{ts,tsx}'],\n    },\n    exclude: [],\n    decorators: {\n      version: 'legacy',\n    },\n  },\n  bundle: false,\n  dts: {\n    distPath: path.resolve(__dirname, './dist/types'),\n    bundle: false,\n    build: true,\n  },\n  tools: {},\n};\n\nconst formats: Partial<RsbuildConfig>[] = [\n  {\n    format: 'esm',\n    output: {\n      distPath: {\n        root: path.resolve(__dirname, './dist/esm'),\n      },\n    },\n  },\n  {\n    dts: false,\n    format: 'cjs',\n    output: {\n      distPath: {\n        root: path.resolve(__dirname, './dist/cjs'),\n      },\n    },\n  },\n].map((r) => ({ ...commonConfig, ...r }));\n\nexport default defineConfig({\n  lib: formats,\n  output: {\n    cleanDistPath: process.env.NODE_ENV === 'production',\n  },\n});\n"
  },
  {
    "path": "packages/materials/coze-editor/scripts/gen.js",
    "content": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\n\nasync function main() {\n  try {\n    console.log('🚀 Starting to generate export files...');\n\n    // 1. Get the package.json path of @coze-editor/editor package\n    const packagePath = path.resolve(__dirname, '../node_modules/@coze-editor/editor/package.json');\n\n    if (!fs.existsSync(packagePath)) {\n      console.error('❌ @coze-editor/editor package not found, please run npm install first');\n      process.exit(1);\n    }\n\n    // 2. Read package.json content\n    const packageContent = fs.readFileSync(packagePath, 'utf8');\n    const packageJson = JSON.parse(packageContent);\n\n    if (!packageJson.exports) {\n      console.error('❌ No exports field found in @coze-editor/editor package.json');\n      process.exit(1);\n    }\n\n    console.log(`📦 Found ${Object.keys(packageJson.exports).length} export items`);\n\n    // 3. Process each export item\n    const exports = packageJson.exports;\n    const srcDir = path.resolve(__dirname, '../src');\n\n    // Ensure src directory exists\n    if (!fs.existsSync(srcDir)) {\n      fs.mkdirSync(srcDir, { recursive: true });\n    }\n\n    // Read current package.json\n    const currentPackagePath = path.resolve(__dirname, '../package.json');\n    const currentPackageContent = fs.readFileSync(currentPackagePath, 'utf8');\n    const currentPackageJson = JSON.parse(currentPackageContent);\n\n    let newExportsAdded = 0;\n\n    for (const [exportPath, exportConfig] of Object.entries(exports)) {\n      // Get export name (remove leading ./, root directory uses index)\n      const exportName = exportPath === '.' ? 'index' : exportPath.replace(/^\\.\\//, '');\n\n      // 3.1 Create corresponding .ts file\n      const filePath = path.join(srcDir, `${exportName}.ts`);\n\n      const contentParts = [];\n\n      const baseImportPath =\n        exportPath === '.' ? '@coze-editor/editor' : `@coze-editor/editor/${exportName}`;\n      contentParts.push(`export * from '${baseImportPath}';`);\n\n      // if is preset, add default export\n      if (exportName.startsWith('preset')) {\n        contentParts.push(`export { default } from '@coze-editor/editor/${exportName}';`);\n      }\n\n      const fileContent = contentParts.join('\\n') + '\\n';\n\n      // Ensure directory exists (handle subdirectory case)\n      const fileDir = path.dirname(filePath);\n      if (!fs.existsSync(fileDir)) {\n        fs.mkdirSync(fileDir, { recursive: true });\n      }\n\n      // Only create file when it doesn't exist\n      if (!fs.existsSync(filePath)) {\n        fs.writeFileSync(filePath, fileContent);\n        console.log(`✅ Created file: ${path.relative(process.cwd(), filePath)}`);\n      } else {\n        console.log(`⏭️  File already exists: ${path.relative(process.cwd(), filePath)}`);\n      }\n\n      // 3.2 Update exports field in package.json\n      const exportKey = exportName === 'index' ? '.' : `./${exportName}`;\n\n      if (!currentPackageJson.exports[exportKey]) {\n        currentPackageJson.exports[exportKey] = {\n          types: `./dist/types/${exportName}.d.ts`,\n          require: `./dist/cjs/${exportName}.js`,\n          import: `./dist/esm/${exportName}.mjs`,\n        };\n        newExportsAdded++;\n        console.log(`📝 Added export configuration: ${exportKey}`);\n      } else {\n        console.log(`⏭️  Export configuration already exists: ${exportKey}`);\n      }\n    }\n\n    // Save updated package.json\n    fs.writeFileSync(currentPackagePath, JSON.stringify(currentPackageJson, null, 2) + '\\n');\n\n    console.log(`\\n🎉 Completed!`);\n    console.log(`📁 Created ${Object.keys(exports).length} export files`);\n    console.log(`📦 Added ${newExportsAdded} new export configurations`);\n    console.log(`💡 Remember to run npm run build to build new exports`);\n  } catch (error) {\n    console.error('❌ Error occurred:', error.message);\n    process.exit(1);\n  }\n}\n\n// Run this script directly\nif (require.main === module) {\n  main();\n}\n\nmodule.exports = { main };\n"
  },
  {
    "path": "packages/materials/coze-editor/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-json.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/language-json';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-python.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/language-python';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-shell.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/language-shell';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-sql.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/language-sql';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-typescript/worker.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-extraneous-dependencies */\n\n//  Copyright (c) 2025 coze-dev\n//  SPDX-License-Identifier: MIT\n\n// @ts-expect-error no members are exported from this path\nexport * from '@coze-editor/code-language-typescript/dist/esm/worker.js';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/language-typescript.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/language-typescript';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-code.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-code';\nexport { default } from '@coze-editor/editor/preset-code';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-expression';\nexport { default } from '@coze-editor/editor/preset-expression';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-none.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-none';\nexport { default } from '@coze-editor/editor/preset-none';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-prompt.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-prompt';\nexport { default } from '@coze-editor/editor/preset-prompt';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-universal.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-universal';\nexport { default } from '@coze-editor/editor/preset-universal';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/preset-variable.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/preset-variable';\nexport { default } from '@coze-editor/editor/preset-variable';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/react-merge.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/react-merge';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/react.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/react';\n"
  },
  {
    "path": "packages/materials/coze-editor/src/vscode.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from '@coze-editor/editor/vscode';\n"
  },
  {
    "path": "packages/materials/coze-editor/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist/types\",\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/materials/coze-editor/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/coze-editor/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n  },\n});\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/fixed-semi-materials\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/fixed-layout-editor\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/assets/ellipsis.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport function Ellipse() {\n  return (\n    <svg width=\"8\" height=\"8\" viewBox=\"0 0 8 8\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <circle id=\"Ellipse 465\" cx=\"4\" cy=\"4\" r=\"3\" fill=\"white\" stroke=\"#3370FF\" strokeWidth=\"2\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/assets/icons.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport function IconStyleBorder(props: any) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\" {...props}>\n      <g\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeMiterlimit=\"1.5\"\n        strokeWidth=\"1.499\"\n      >\n        <path\n          strokeDasharray=\"2 2\"\n          d=\"M16 2H8a6 6 0 0 0-6 6v8a6 6 0 0 0 6 6h8a6 6 0 0 0 6-6V8a6 6 0 0 0-6-6Z\"\n        />\n        <path d=\"M16 5H8a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3Z\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function IconParkRightBranch(props: any) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 48 48\" {...props}>\n      <g fill=\"none\">\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M22 8.01176C20.5 8.01193 16.0714 7.93811 15 13.0005C13.917 18.1177 9.85714 22.8477 8 24\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M22 40C20.5 40.0003 16.0714 40.0628 15 35.0005C13.917 29.8833 9.85714 25.1522 8 23.9999\"\n        />\n        <circle cx=\"8\" cy=\"24\" r=\"4\" fill=\"currentColor\" />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M8 24L22 24\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M30 24.001H42\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M30 8.00098H42\"\n        />\n        <path\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth=\"4\"\n          d=\"M30 40.001H42\"\n        />\n      </g>\n    </svg>\n  );\n}\n\nexport function PhCircleBold(props: any) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 256 256\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M128 236a108 108 0 1 1 108-108a108.1 108.1 0 0 1-108 108Zm0-192a84 84 0 1 0 84 84a84.1 84.1 0 0 0-84-84Z\"\n      />\n    </svg>\n  );\n}\n\nexport function BiCloud(props: any) {\n  return (\n    <svg width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773C16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593c.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318C1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z\"\n      />\n    </svg>\n  );\n}\n\nexport function BiBootstrapReboot(props: any) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      xmlnsXlink=\"http://www.w3.org/1999/xlink\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      {...props}\n    >\n      <g fill=\"currentColor\">\n        <path d=\"M1.161 8a6.84 6.84 0 1 0 6.842-6.84a.58.58 0 1 1 0-1.16a8 8 0 1 1-6.556 3.412l-.663-.577a.58.58 0 0 1 .227-.997l2.52-.69a.58.58 0 0 1 .728.633l-.332 2.592a.58.58 0 0 1-.956.364l-.643-.56A6.812 6.812 0 0 0 1.16 8z\" />\n        <path d=\"M6.641 11.671V8.843h1.57l1.498 2.828h1.314L9.377 8.665c.897-.3 1.427-1.106 1.427-2.1c0-1.37-.943-2.246-2.456-2.246H5.5v7.352h1.141zm0-3.75V5.277h1.57c.881 0 1.416.499 1.416 1.32c0 .84-.504 1.324-1.386 1.324h-1.6z\" />\n      </g>\n    </svg>\n  );\n}\n\nexport function FeAlignCenter(props: any) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      xmlnsXlink=\"http://www.w3.org/1999/xlink\"\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 24 24\"\n      {...props}\n    >\n      <path\n        fill=\"#888888\"\n        fillRule=\"evenodd\"\n        d=\"M11 13v-2H6.286C5.023 11 4 10.105 4 9s1.023-2 2.286-2H11V3a1 1 0 0 1 2 0v4h4.714C18.977 7 20 7.895 20 9s-1.023 2-2.286 2H13v2h3a2 2 0 1 1 0 4h-3v4a1 1 0 0 1-2 0v-4H8a2 2 0 1 1 0-4h3Z\"\n      />\n    </svg>\n  );\n}\n\nexport function Arrow({ color, circleColor }: { color: string; circleColor: string }) {\n  return (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <circle cx=\"8\" cy=\"8\" r=\"7\" fill={circleColor} />\n      <path\n        fill={color}\n        d=\"M10.8281 9.4715C11.0883 9.21131 11.0885 8.78909 10.8291 8.52804C10.6413 8.33892 10.4536 8.14952 10.266 7.9601C9.66706 7.35551 9.06799 6.75079 8.46068 6.15496C8.20439 5.90352 7.7947 5.90352 7.53841 6.15496C6.93103 6.75085 6.33191 7.35564 5.73291 7.96029C5.5454 8.14957 5.3579 8.33884 5.17017 8.52782C4.91075 8.78895 4.91096 9.21099 5.17124 9.47127C5.43152 9.73155 5.85355 9.73176 6.11383 9.47148L7.99955 7.58576L9.88548 9.4717C10.1457 9.73189 10.5679 9.73169 10.8281 9.4715Z\"\n      />\n      <path\n        fill={color}\n        d=\"M0.888672 7.99997C0.888672 4.07261 4.07242 0.888855 7.99978 0.888855C11.9271 0.888855 15.1109 4.07261 15.1109 7.99997C15.1109 11.9273 11.9271 15.1111 7.99978 15.1111C4.07242 15.1111 0.888672 11.9273 0.888672 7.99997ZM13.818 7.99997C13.818 4.78667 11.2131 2.18178 7.99978 2.18178C4.78649 2.18178 2.1816 4.78667 2.1816 7.99997C2.1816 11.2133 4.78649 13.8181 7.99978 13.8181C11.2131 13.8181 13.818 11.2133 13.818 7.99997Z\"\n      />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/assets/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Ellipse } from './ellipsis';\nexport {\n  Arrow,\n  IconStyleBorder,\n  IconParkRightBranch,\n  PhCircleBold,\n  BiCloud,\n  BiBootstrapReboot,\n  FeAlignCenter,\n} from './icons';\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport {\n  type FlowNodeEntity,\n  FlowNodeTransformData,\n  usePlayground,\n  useService,\n  FlowOperationService,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Popover } from '@douyinfe/semi-ui';\n\nimport Nodes from '../nodes';\nimport { AdderWrap, IconPlus } from './styles';\n\nexport default function Adder(props: {\n  from: FlowNodeEntity;\n  to?: FlowNodeEntity;\n  hoverActivated: boolean;\n}) {\n  const { from } = props;\n  const [visible, setVisible] = useState(false);\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n\n  const add = (addProps: any) => {\n    const block = flowOperationService.addFromNode(from, {\n      id: addProps.type + nanoid(5),\n      type: addProps.type,\n      blocks: addProps.blocks ? addProps.blocks() : undefined,\n    });\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.getData<FlowNodeTransformData>(FlowNodeTransformData)!.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n  };\n\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <Popover\n      visible={visible}\n      onVisibleChange={setVisible}\n      content={<Nodes onSelect={add} />}\n      placement=\"right\"\n      trigger=\"click\"\n      overlayStyle={{\n        padding: 0,\n      }}\n    >\n      <AdderWrap\n        hovered={props.hoverActivated}\n        onMouseDown={(e) => e.stopPropagation()}\n        onClick={() => {\n          setVisible(true);\n        }}\n      >\n        {props.hoverActivated ? <IconPlus /> : null}\n      </AdderWrap>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport { IconPlusCircle } from '@douyinfe/semi-icons';\n\nexport const AdderWrap = styled.div<{ hovered?: boolean }>`\n  width: ${(props) => (props.hovered ? 15 : 6)}px;\n  height: ${(props) => (props.hovered ? 15 : 6)}px;\n  background-color: rgb(143, 149, 158);\n  color: #fff;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n`;\n\nexport const IconPlus = styled(IconPlusCircle)`\n  color: #3370ff;\n  background-color: #fff;\n  border-radius: 15px;\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/branch-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { nanoid } from 'nanoid';\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  FlowOperationService,\n  usePlayground,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { Container } from './styles';\n\ninterface PropsType {\n  activated?: boolean;\n  node: FlowNodeEntity;\n}\n\nexport default function BranchAdder(props: PropsType) {\n  const { activated, node } = props;\n  const nodeData = node.firstChild?.getData<FlowNodeRenderData>(FlowNodeRenderData);\n  const playground = usePlayground();\n  const flowOperationService = useService(FlowOperationService) as FlowOperationService;\n  const { isVertical } = node;\n\n  function addBranch() {\n    const block = flowOperationService.addBlock(node, { id: nanoid(5) });\n\n    setTimeout(() => {\n      playground.scrollToView({\n        bounds: block.getData<FlowNodeTransformData>(FlowNodeTransformData)!.bounds,\n        scrollToCenter: true,\n      });\n    }, 10);\n  }\n  if (playground.config.readonlyOrDisabled) return null;\n\n  return (\n    <Container\n      isVertical={isVertical}\n      activated={activated || nodeData?.hovered}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <div\n        onClick={() => {\n          addBranch();\n        }}\n        aria-hidden=\"true\"\n        style={{ flexGrow: 1, textAlign: 'center', cursor: 'pointer' }}\n      >\n        <IconPlus />\n      </div>\n    </Container>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/branch-adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Container = styled.div<{ activated?: boolean; isVertical: boolean }>`\n  width: 28px;\n  height: 18px;\n  background: ${(props) => (props.activated ? '#82A7FC' : 'rgb(187, 191, 196)')};\n  display: flex;\n  border-radius: 9px;\n  justify-content: space-evenly;\n  align-items: center;\n  color: #fff;\n  font-size: 10px;\n  font-weight: bold;\n  transform: ${(props) => (props.isVertical ? '' : 'rotate(90deg)')};\n  div {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    svg {\n      width: 12px;\n      height: 12px;\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/collapse/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  usePlayground,\n  type CollapseProps,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport { Arrow } from '../../assets';\nimport { Container } from './styles';\n\nfunction Collapse(props: CollapseProps): JSX.Element {\n  const { collapseNode, activateNode, hoverActivated, style } = props;\n  const playground = usePlayground();\n\n  const activateData = activateNode?.getData(FlowNodeRenderData);\n  const transform = collapseNode.getData(FlowNodeTransformData)!;\n\n  if (!transform) {\n    return <></>;\n  }\n\n  const scrollToActivateNode = () => {\n    setTimeout(() => {\n      playground.config.scrollToView({\n        position: activateNode?.getData(FlowNodeTransformData)?.outputPoint,\n        scrollToCenter: true,\n      });\n    }, 100);\n  };\n\n  const collapseBlock = () => {\n    transform.collapsed = true;\n    activateData?.toggleMouseLeave();\n\n    scrollToActivateNode();\n  };\n\n  const openBlock = () => {\n    transform.collapsed = false;\n\n    scrollToActivateNode();\n  };\n\n  // expand\n  if (transform.collapsed) {\n    const childCount = collapseNode.allCollapsedChildren.filter(\n      (child) => !child.hidden && child !== activateNode\n    ).length;\n\n    return (\n      <Container\n        onClick={openBlock}\n        hoverActivated={hoverActivated}\n        aria-hidden=\"true\"\n        style={style}\n      >\n        {childCount}\n      </Container>\n    );\n  }\n\n  // dark: var(--semi-color-black)\n  // light: var(--semi-color-white)\n  const circleColor = 'var(--semi-color-white)';\n  const color = hoverActivated ? '#82A7FC' : '#BBBFC4';\n\n  // collapse\n  return (\n    <Container\n      onClick={collapseBlock}\n      hoverActivated={hoverActivated}\n      isVertical={activateNode?.isVertical}\n      isCollapse={true}\n      style={style}\n      aria-hidden=\"true\"\n    >\n      <Arrow color={color} circleColor={circleColor} />\n    </Container>\n  );\n}\n\nexport default Collapse;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/collapse/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const Container = styled.div<{\n  hoverActivated?: boolean;\n  isVertical?: boolean;\n  isCollapse?: boolean;\n}>`\n  width: 16px;\n  height: 16px;\n  font-size: 10px;\n  border-radius: 9px;\n  display: flex;\n  color: #fff;\n  cursor: pointer;\n  justify-content: center;\n  align-items: center;\n  background: ${(props) => (props.hoverActivated ? '#82A7FC' : '#BBBFC4')};\n  transform: ${(props) => (!props.isVertical && props.isCollapse ? 'rotate(-90deg)' : '')};\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/constants.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const primary = 'hsl(252 62% 54.9%)';\nexport const primaryOpacity09 = 'hsl(252deg 62% 55% / 9%)';\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/drag-highlight-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { min } from 'lodash-es';\nimport { type FlowNodeEntity, FlowNodeTransformData } from '@flowgram.ai/fixed-layout-editor';\n\nimport { Ellipse } from '../../assets';\nimport { UILineContainer, UILine } from './styles';\n\nconst getMinSize = (preWidth: number, nextWidth: number): number => {\n  if (!preWidth || preWidth < 0) {\n    return 0;\n  }\n  if (!nextWidth || nextWidth < 0) {\n    return preWidth;\n  }\n  return min([preWidth, nextWidth]) || 0;\n};\n\nexport default function DragHighlightAdder({ node }: { node: FlowNodeEntity }): JSX.Element {\n  const transformBounds = node.getData<FlowNodeTransformData>(FlowNodeTransformData)?.bounds;\n  const { isVertical } = node;\n  if (isVertical) {\n    const preWidth = (transformBounds?.width || 0) - 16;\n    const nextNodeBounds =\n      node?.next?.getData<FlowNodeTransformData>(FlowNodeTransformData)?.bounds?.width;\n    const nextWidth = (nextNodeBounds || 0) - 16;\n    const LineDom = UILine(getMinSize(preWidth, nextWidth), 2);\n    return (\n      <UILineContainer>\n        <Ellipse />\n        <LineDom />\n        <Ellipse />\n      </UILineContainer>\n    );\n  }\n  const preHeight = (transformBounds?.height || 0) - 16;\n  const nextNodeBounds =\n    node?.next?.getData<FlowNodeTransformData>(FlowNodeTransformData)?.bounds?.height;\n  const nextHeight = (nextNodeBounds || 0) - 16;\n  const LineDom = UILine(2, getMinSize(preHeight, nextHeight));\n  return (\n    <UILineContainer style={{ flexDirection: 'column' }}>\n      <Ellipse />\n      <LineDom />\n      <Ellipse />\n    </UILineContainer>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/drag-highlight-adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const UILineContainer = styled.div`\n  display: flex;\n  align-items: center;\n`;\n\nexport const UILine = (width: number, height: number) =>\n  styled.div`\n    width: ${width}px;\n    height: ${height}px;\n    background: #3370ff;\n  `;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/drag-node/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { FlowNodeEntity, FlowNodeJSON, Xor } from '@flowgram.ai/fixed-layout-editor';\n\nimport { UIDragNodeContainer, UIDragCounts } from './styles';\n\nexport type PropsType = Xor<\n  {\n    dragStart: FlowNodeEntity;\n  },\n  {\n    dragJSON: FlowNodeJSON;\n  }\n> & {\n  dragNodes: FlowNodeEntity[];\n};\n\nexport default function DragNode(props: PropsType): JSX.Element {\n  const { dragStart, dragNodes, dragJSON } = props;\n\n  const dragLength = (dragNodes || [])\n    .map((_node) =>\n      _node.allCollapsedChildren.length\n        ? _node.allCollapsedChildren.filter((_n) => !_n.hidden).length\n        : 1\n    )\n    .reduce((acm, curr) => acm + curr, 0);\n\n  return (\n    <UIDragNodeContainer>\n      {dragStart?.id || dragJSON?.id}\n      {dragLength > 1 && (\n        <>\n          <UIDragCounts>{dragLength}</UIDragCounts>\n          <UIDragNodeContainer\n            style={{\n              position: 'absolute',\n              top: 5,\n              right: -5,\n              left: 5,\n              bottom: -5,\n              opacity: 0.5,\n            }}\n          />\n        </>\n      )}\n    </UIDragNodeContainer>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/drag-node/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nimport { primary, primaryOpacity09 } from '../constants';\n\nexport const UIDragNodeContainer = styled.div`\n  position: relative;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  font-size: 19px;\n  border: 1px solid ${primary};\n  padding: 0 15px;\n  &:hover: {\n    background-color: ${primaryOpacity09};\n    color: ${primary};\n  }\n`;\n\nexport const UIDragCounts = styled.div`\n  position: absolute;\n  top: -8px;\n  right: -8px;\n  text-align: center;\n  line-height: 16px;\n  width: 16px;\n  height: 16px;\n  border-radius: 8px;\n  font-size: 12px;\n  color: #fff;\n  background-color: ${primary};\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/dragging-adder/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FlowDragLayer, usePlayground } from '@flowgram.ai/fixed-layout-editor';\n\nimport { UIDragNodeContainer } from './styles';\n\nexport default function DraggingAdder(props: any): JSX.Element {\n  const playground = usePlayground();\n  const layer = playground.getLayer(FlowDragLayer);\n  if (!layer) return <></>;\n  if (\n    layer.options.canDrop &&\n    !layer.options.canDrop({\n      dragNodes: layer.dragEntities || [],\n      dropNode: props.from,\n      isBranch: false,\n    })\n  ) {\n    return <></>;\n  }\n  return <UIDragNodeContainer />;\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/dragging-adder/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const UIDragNodeContainer = styled.div`\n  width: 16px;\n  height: 16px;\n  border-radius: 100px;\n  background-color: white;\n  border: 1px dashed #b8bcc1;\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowRendererKey } from '@flowgram.ai/fixed-layout-editor';\n\nimport { Ellipse } from '../assets';\nimport TryCatchCollapse from './try-catch-collapse';\nimport { SlotCollapse } from './slot-collapse';\nimport { SlotAdder } from './slot-adder';\nimport DraggingAdder from './dragging-adder';\nimport DragNode from './drag-node';\nimport DragHighlightAdder from './drag-highlight-adder';\nimport Collapse from './collapse';\nimport BranchAdder from './branch-adder';\nimport Adder from './adder';\n\nexport const defaultFixedSemiMaterials = {\n  [FlowRendererKey.ADDER]: Adder,\n  [FlowRendererKey.COLLAPSE]: Collapse,\n  [FlowRendererKey.TRY_CATCH_COLLAPSE]: TryCatchCollapse,\n  [FlowRendererKey.BRANCH_ADDER]: BranchAdder,\n  [FlowRendererKey.DRAG_NODE]: DragNode,\n  [FlowRendererKey.DRAGGABLE_ADDER]: DraggingAdder,\n  [FlowRendererKey.DRAG_HIGHLIGHT_ADDER]: DragHighlightAdder,\n  [FlowRendererKey.DRAG_BRANCH_HIGHLIGHT_ADDER]: Ellipse,\n  [FlowRendererKey.SLOT_COLLAPSE]: SlotCollapse,\n  [FlowRendererKey.SLOT_ADDER]: SlotAdder,\n};\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/metadata.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { nanoid } from 'nanoid';\n\nimport {\n  BiBootstrapReboot,\n  BiCloud,\n  FeAlignCenter,\n  IconStyleBorder,\n  IconParkRightBranch,\n  PhCircleBold,\n} from '../assets';\n\nconst metadata = {\n  nodes: [\n    {\n      type: 'start',\n      label: 'Start',\n      icon: <IconStyleBorder />,\n    },\n    {\n      type: 'dynamicSplit',\n      label: 'Split Branch',\n      icon: <IconParkRightBranch />,\n      blocks() {\n        return [\n          {\n            id: nanoid(5),\n          },\n          {\n            id: nanoid(5),\n          },\n        ];\n      },\n    },\n    {\n      type: 'end',\n      label: 'Branch End',\n      icon: <FeAlignCenter />,\n      branchEnd: true,\n    },\n    {\n      type: 'loop',\n      schemaType: 'loop',\n      label: 'Loop',\n      icon: <BiBootstrapReboot />,\n    },\n    {\n      type: 'tryCatch',\n      schemaType: 'tryCatch',\n      label: 'TryCatch',\n      icon: <IconParkRightBranch />,\n      blocks() {\n        return [\n          {\n            id: `try_${nanoid(5)}`, // try branch\n          },\n          {\n            id: `catch_${nanoid(5)}`, // catch branch 1\n          },\n          {\n            id: `catch_${nanoid(5)}`, // catch branch 2\n          },\n        ];\n      },\n    },\n    {\n      type: 'noop',\n      label: 'Noop Node',\n      icon: <BiCloud />,\n    },\n    {\n      type: 'end',\n      label: 'End',\n      icon: <PhCircleBold />,\n    },\n  ],\n  find: function find(type: any) {\n    return metadata.nodes.find((m) => m.type === type);\n  },\n};\n\nexport default metadata;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/nodes/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport metadata from '../metadata';\nimport { NodeWrap, NodeLabel, NodesWrap } from './styles';\n\nfunction Node(props: { label: string; icon: JSX.Element; onClick: () => void }) {\n  return (\n    <NodeWrap onClick={props.onClick}>\n      <div style={{ fontSize: 14 }}>{props.icon}</div>\n      <NodeLabel>{props.label}</NodeLabel>\n    </NodeWrap>\n  );\n}\n\nconst addings = metadata.nodes.filter((node) => node.type !== 'start');\n\nexport default function Nodes(props: { onSelect: (meta: any) => void }) {\n  return (\n    <NodesWrap style={{ width: 80 * 2 + 20 }}>\n      {addings.map((n, i) => (\n        // eslint-disable-next-line react/no-array-index-key\n        <Node key={i} icon={n.icon} label={n.label} onClick={() => props.onSelect?.(n)} />\n      ))}\n    </NodesWrap>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/nodes/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nimport { primary, primaryOpacity09 } from '../constants';\n\nexport const NodeWrap = styled.div`\n  width: 100%;\n  height: 32px;\n  border-radius: 5px;\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n  font-size: 19px;\n  padding: 0 15px;\n  &:hover: {\n    background-color: ${primaryOpacity09};\n    color: ${primary};\n  },\n`;\n\nexport const NodeLabel = styled.div`\n  font-size: 12px;\n  margin-left: 10px;\n`;\n\nexport const NodesWrap = styled.div`\n  max-height: 500px;\n  overflow: auto;\n  &::-webkit-scrollbar {\n    display: none;\n  }\n`;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/slot-adder.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { nanoid } from 'nanoid';\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowDocument,\n  useService,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\ninterface PropsType {\n  node: FlowNodeEntity;\n}\n\nexport function SlotAdder(props: PropsType) {\n  const { node } = props;\n\n  const nodeData = node.firstChild?.getData<FlowNodeRenderData>(FlowNodeRenderData);\n  const document = useService(FlowDocument) as FlowDocument;\n\n  async function addPort() {\n    document.addNode({\n      id: nanoid(5),\n      type: 'custom',\n      parent: node,\n    });\n  }\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        background: 'var(--semi-color-bg-0)',\n      }}\n      onMouseEnter={() => nodeData?.toggleMouseEnter()}\n      onMouseLeave={() => nodeData?.toggleMouseLeave()}\n    >\n      <Button\n        onClick={() => {\n          addPort();\n        }}\n        size=\"small\"\n        icon={<IconPlus />}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/slot-collapse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport {\n  type FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n} from '@flowgram.ai/fixed-layout-editor';\n\nimport Collapse from './collapse';\n\nexport function SlotCollapse({ node }: { node: FlowNodeEntity }) {\n  const [hoverActivated, setHoverActivated] = useState(false);\n\n  const icon = node.firstChild!;\n  const iconActivated = icon.getData(FlowNodeRenderData).activated;\n  const iconHeight = icon.getData(FlowNodeTransformData).size.height;\n\n  const isChildVisible = node.collapsed || hoverActivated || iconActivated;\n\n  return (\n    <div\n      style={{\n        width: 30,\n        height: iconHeight || 100,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n      }}\n      onMouseEnter={() => setHoverActivated(true)}\n      onMouseLeave={() => setHoverActivated(false)}\n    >\n      {isChildVisible && (\n        <Collapse\n          style={\n            !node.collapsed\n              ? {\n                  transform: node.isVertical ? 'rotate(-90deg)' : 'rotate(90deg)',\n                }\n              : {}\n          }\n          node={node}\n          activateNode={icon}\n          collapseNode={node}\n          hoverActivated={hoverActivated}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/tools.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';\nimport { Checkbox, IconButton, Space, Tooltip } from '@douyinfe/semi-ui';\nimport { IconUndo, IconRedo, IconShrink, IconExpand, IconGridView } from '@douyinfe/semi-icons';\n\nexport const PlaygroundTools = ({ layoutText }: { layoutText?: string }) => {\n  const tools = usePlaygroundTools();\n  const { zoom } = tools;\n\n  return (\n    <Space>\n      <Checkbox onChange={() => tools.changeLayout()} checked={!tools.isVertical}>\n        {layoutText || 'isHorizontal'}\n      </Checkbox>\n      <Tooltip content=\"fit view\">\n        <IconButton icon={<IconGridView />} onClick={() => tools.fitView()} />\n      </Tooltip>\n      <Tooltip content=\"zoom out\">\n        <IconButton icon={<IconShrink />} onClick={() => tools.zoomout()} />\n      </Tooltip>\n      <Tooltip content=\"zoom in\">\n        <IconButton icon={<IconExpand />} onClick={() => tools.zoomin()} />\n      </Tooltip>\n      <Tooltip content=\"undo\">\n        <IconButton icon={<IconUndo />} disabled={tools.canUndo} onClick={() => tools.undo()} />\n      </Tooltip>\n      <Tooltip content=\"redo\">\n        <IconButton icon={<IconRedo />} disabled={tools.canRedo} onClick={() => tools.redo()} />\n      </Tooltip>\n      <span>{Math.floor(zoom * 100)}%</span>\n    </Space>\n  );\n};\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/components/try-catch-collapse.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport {\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n  type CustomLabelProps,\n  useBaseColor,\n  FlowTextKey,\n  usePlayground,\n  FlowRendererRegistry,\n} from '@flowgram.ai/fixed-layout-editor';\nimport { IconChevronLeft } from '@douyinfe/semi-icons';\n\nfunction TryCatchCollapse(props: CustomLabelProps): JSX.Element {\n  const { node } = props;\n  const { baseColor, baseActivatedColor } = useBaseColor();\n  const playground = usePlayground();\n\n  const activateData = node.getData(FlowNodeRenderData)!;\n  const transform = node.getData(FlowNodeTransformData)!;\n\n  const [hoverActivated, setHoverActivated] = useState(false);\n\n  if (!transform || !transform.parent) {\n    return <></>;\n  }\n\n  // hotzone width & height\n  const width = transform.inputPoint.x - transform.parent.inputPoint.x;\n  const height = 40;\n\n  const scrollToActivateNode = () => {\n    setTimeout(() => {\n      playground.config.scrollToView({\n        position: node?.getData(FlowNodeTransformData)?.inputPoint,\n        scrollToCenter: true,\n      });\n    }, 100);\n  };\n\n  const collapseBlock = () => {\n    transform.collapsed = true;\n    activateData.activated = false;\n    scrollToActivateNode();\n  };\n\n  const openBlock = () => {\n    transform.collapsed = false;\n    scrollToActivateNode();\n  };\n\n  const handleMouseEnter = () => {\n    setHoverActivated(true);\n    activateData.activated = true;\n  };\n\n  const handleMouseLeave = () => {\n    setHoverActivated(false);\n    activateData.activated = false;\n  };\n\n  const renderCollapse = () => {\n    // Expand\n    if (transform.collapsed) {\n      const childCount = node.allCollapsedChildren.filter(\n        (child) => !child.hidden && child !== node\n      ).length;\n\n      return (\n        <div\n          onClick={openBlock}\n          style={{\n            width: 16,\n            height: 16,\n            fontSize: 10,\n            borderRadius: 9,\n            display: 'flex',\n            color: '#fff',\n            cursor: 'pointer',\n            justifyContent: 'center',\n            alignItems: 'center',\n            background:\n              hoverActivated || activateData.lineActivated ? baseActivatedColor : baseColor,\n          }}\n          aria-hidden=\"true\"\n        >\n          {childCount}\n        </div>\n      );\n    }\n\n    // Collapse\n    if (hoverActivated) {\n      return (\n        <div\n          onClick={collapseBlock}\n          style={{\n            width: 16,\n            height: 16,\n            fontSize: 10,\n            borderRadius: 9,\n            display: 'flex',\n            color: '#fff',\n            cursor: 'pointer',\n            justifyContent: 'center',\n            alignItems: 'center',\n            background: baseActivatedColor,\n          }}\n          aria-hidden=\"true\"\n        >\n          <IconChevronLeft />\n        </div>\n      );\n    }\n\n    return <></>;\n  };\n\n  // Collapse\n  return (\n    <div\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={{\n        width,\n        height,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        gap: 6,\n      }}\n    >\n      <div\n        data-label-id={props.labelId}\n        style={{\n          fontSize: 12,\n          color: hoverActivated || activateData.lineActivated ? baseActivatedColor : baseColor,\n          textAlign: 'center',\n          whiteSpace: 'nowrap',\n          backgroundColor: 'var(--g-editor-background)',\n          lineHeight: '20px',\n        }}\n      >\n        {node.getService(FlowRendererRegistry).getText(FlowTextKey.CATCH_TEXT)}\n      </div>\n\n      {renderCollapse()}\n    </div>\n  );\n}\n\nexport default TryCatchCollapse;\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { defaultFixedSemiMaterials } from './components';\nexport { PlaygroundTools } from './components/tools';\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/fixed-semi-materials/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-antd-materials/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/form-antd-materials\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@ant-design/icons\": \"5.x\",\n    \"@flowgram.ai/editor\": \"workspace:*\",\n    \"antd\": \"^5.25.4\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/batch-variable-selector/config.json",
    "content": "{\n  \"name\": \"batch-variable-selector\",\n  \"depMaterials\": [\"variable-selector\"],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/batch-variable-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { PrivateScopeProvider } from '@flowgram.ai/editor';\n\nimport { VariableSelector, VariableSelectorProps } from '../variable-selector';\nimport { IJsonSchema } from '../../typings';\n\nconst batchVariableSchema: IJsonSchema = {\n  type: 'array',\n  extra: { weak: true },\n};\n\nexport function BatchVariableSelector(props: VariableSelectorProps) {\n  return (\n    <PrivateScopeProvider>\n      <VariableSelector {...props} includeSchema={batchVariableSchema} />\n    </PrivateScopeProvider>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/config.json",
    "content": "{\n  \"name\": \"condition-row\",\n  \"depMaterials\": [\"variable-selector\", \"dynamic-value-input\", \"flow-value\", \"utils/json-schema\", \"typings/json-schema\"],\n  \"depPackages\": [\"styled-components\"]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IRules, Op, OpConfigs } from './types';\n\nexport const rules: IRules = {\n  string: {\n    [Op.EQ]: 'string',\n    [Op.NEQ]: 'string',\n    [Op.CONTAINS]: 'string',\n    [Op.NOT_CONTAINS]: 'string',\n    [Op.IN]: 'array',\n    [Op.NIN]: 'array',\n    [Op.IS_EMPTY]: 'string',\n    [Op.IS_NOT_EMPTY]: 'string',\n  },\n  number: {\n    [Op.EQ]: 'number',\n    [Op.NEQ]: 'number',\n    [Op.GT]: 'number',\n    [Op.GTE]: 'number',\n    [Op.LT]: 'number',\n    [Op.LTE]: 'number',\n    [Op.IN]: 'array',\n    [Op.NIN]: 'array',\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n  integer: {\n    [Op.EQ]: 'number',\n    [Op.NEQ]: 'number',\n    [Op.GT]: 'number',\n    [Op.GTE]: 'number',\n    [Op.LT]: 'number',\n    [Op.LTE]: 'number',\n    [Op.IN]: 'array',\n    [Op.NIN]: 'array',\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n  boolean: {\n    [Op.EQ]: 'boolean',\n    [Op.NEQ]: 'boolean',\n    [Op.IS_TRUE]: null,\n    [Op.IS_FALSE]: null,\n    [Op.IN]: 'array',\n    [Op.NIN]: 'array',\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n  object: {\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n  array: {\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n  map: {\n    [Op.IS_EMPTY]: null,\n    [Op.IS_NOT_EMPTY]: null,\n  },\n};\n\nexport const opConfigs: OpConfigs = {\n  [Op.EQ]: {\n    label: 'Equal',\n    abbreviation: '=',\n  },\n  [Op.NEQ]: {\n    label: 'Not Equal',\n    abbreviation: '≠',\n  },\n  [Op.GT]: {\n    label: 'Greater Than',\n    abbreviation: '>',\n  },\n  [Op.GTE]: {\n    label: 'Greater Than or Equal',\n    abbreviation: '>=',\n  },\n  [Op.LT]: {\n    label: 'Less Than',\n    abbreviation: '<',\n  },\n  [Op.LTE]: {\n    label: 'Less Than or Equal',\n    abbreviation: '<=',\n  },\n  [Op.IN]: {\n    label: 'In',\n    abbreviation: '∈',\n  },\n  [Op.NIN]: {\n    label: 'Not In',\n    abbreviation: '∉',\n  },\n  [Op.CONTAINS]: {\n    label: 'Contains',\n    abbreviation: '⊇',\n  },\n  [Op.NOT_CONTAINS]: {\n    label: 'Not Contains',\n    abbreviation: '⊉',\n  },\n  [Op.IS_EMPTY]: {\n    label: 'Is Empty',\n    abbreviation: '=',\n    rightDisplay: 'Empty',\n  },\n  [Op.IS_NOT_EMPTY]: {\n    label: 'Is Not Empty',\n    abbreviation: '≠',\n    rightDisplay: 'Empty',\n  },\n  [Op.IS_TRUE]: {\n    label: 'Is True',\n    abbreviation: '=',\n    rightDisplay: 'True',\n  },\n  [Op.IS_FALSE]: {\n    label: 'Is False',\n    abbreviation: '=',\n    rightDisplay: 'False',\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/hooks/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport type { SelectProps } from 'antd/es/select';\nimport { Select } from 'antd';\n\nexport const OpSelect: React.ComponentType<SelectProps> = styled(Select)`\n  width: 100%;\n  height: 22px;\n  width: 24px;\n\n  & .ant-select-selector {\n    padding: 0 !important;\n    text-align: center;\n  }\n\n  & .ant-select-arrow {\n    right: 6px;\n    & > .anticon {\n      pointer-events: none !important;\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/hooks/useOp.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { theme } from 'antd';\nimport { DownOutlined } from '@ant-design/icons';\n\nimport { IRule, Op } from '../types';\nimport { opConfigs } from '../constants';\nimport { OpSelect } from './styles';\n\nconst { useToken } = theme;\n\ninterface HookParams {\n  rule?: IRule;\n  op?: Op;\n  onChange: (op: Op) => void;\n  readonly?: boolean;\n}\n\nexport function useOp({ rule, op, onChange, readonly }: HookParams) {\n  const options = useMemo(\n    () =>\n      Object.keys(rule || {}).map((_op) => ({\n        ...(opConfigs[_op as Op] || {}),\n        value: _op,\n      })),\n    [rule]\n  );\n\n  const opConfig = useMemo(() => opConfigs[op as Op], [op]);\n\n  const renderOpSelect = () => {\n    const { token } = useToken();\n    return (\n      <OpSelect\n        style={{ color: token.colorPrimary }}\n        styles={{\n          popup: { root: { maxHeight: 400, minWidth: 230, overflow: 'auto' } },\n        }}\n        disabled={readonly}\n        className=\"op-select\"\n        size=\"small\"\n        value={op}\n        options={options}\n        onChange={(v) => {\n          onChange(v as Op);\n        }}\n        labelRender={({ value }) => <span>{opConfig?.abbreviation || <DownOutlined />}</span>}\n        suffixIcon={op ? null : undefined}\n      />\n    );\n  };\n\n  return { renderOpSelect, opConfig };\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/hooks/useRule.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport { useMemo } from 'react';\n\nimport { useScopeAvailable } from '@flowgram.ai/editor';\n\nimport { rules } from '../constants';\nimport { JsonSchemaUtils } from '../../../utils';\nimport { IFlowRefValue, JsonSchemaBasicType } from '../../../typings';\n\nexport function useRule(left?: IFlowRefValue) {\n  const available = useScopeAvailable();\n\n  const variable = useMemo(() => {\n    if (!left) return undefined;\n    return available.getByKeyPath(left.content);\n  }, [available, left]);\n\n  const rule = useMemo(() => {\n    if (!variable) return undefined;\n\n    const schema = JsonSchemaUtils.astToSchema(variable.type, {\n      drilldown: false,\n    });\n\n    return rules[schema?.type as JsonSchemaBasicType];\n  }, [variable?.type]);\n\n  return { rule };\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\n\nimport React, { useMemo } from 'react';\n\nimport { VariableSelector } from '../variable-selector';\nimport { DynamicValueInput } from '../dynamic-value-input';\nimport { UIInput } from '../constant-input/styles';\nimport { JsonSchemaBasicType } from '../../typings';\nimport { ConditionRowValueType, Op } from './types';\nimport { UIContainer, UILeft, UIOperator, UIRight, UIValues } from './styles';\nimport { useRule } from './hooks/useRule';\nimport { useOp } from './hooks/useOp';\n\ninterface PropTypes {\n  value?: ConditionRowValueType;\n  onChange: (value?: ConditionRowValueType) => void;\n  style?: React.CSSProperties;\n  readonly?: boolean;\n}\n\nexport function ConditionRow({ style, value, onChange, readonly }: PropTypes) {\n  const { left, operator, right } = value || {};\n  const { rule } = useRule(left);\n  const { renderOpSelect, opConfig } = useOp({\n    rule,\n    op: operator,\n    onChange: (v) => onChange({ ...value, operator: v }),\n    readonly,\n  });\n\n  const targetSchema = useMemo(() => {\n    const targetType: JsonSchemaBasicType | null = rule?.[operator as Op] || null;\n    return targetType ? { type: targetType, extra: { weak: true } } : null;\n  }, [rule, opConfig]);\n\n  return (\n    <UIContainer style={style}>\n      <UIOperator>{renderOpSelect()}</UIOperator>\n      <UIValues>\n        <UILeft>\n          <VariableSelector\n            readonly={readonly}\n            style={{ width: '100%' }}\n            value={left?.content}\n            onChange={(v) =>\n              onChange({\n                ...value,\n                left: {\n                  type: 'ref',\n                  content: v,\n                },\n              })\n            }\n            allowClear={true}\n          />\n        </UILeft>\n        <UIRight>\n          {targetSchema ? (\n            <DynamicValueInput\n              readonly={readonly || !rule}\n              value={right}\n              schema={targetSchema}\n              onChange={(v) => onChange({ ...value, right: v })}\n            />\n          ) : (\n            <UIInput\n              size=\"small\"\n              disabled\n              style={{ pointerEvents: 'none' }}\n              value={opConfig?.rightDisplay || 'Empty'}\n            />\n          )}\n        </UIRight>\n      </UIValues>\n    </UIContainer>\n  );\n}\n\nexport type { ConditionRowValueType, Op, VariableSelector };\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const UIContainer = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 4px;\n`;\n\nexport const UIOperator = styled.div``;\n\nexport const UILeft = styled.div`\n  width: 100%;\n`;\n\nexport const UIRight = styled.div`\n  width: 100%;\n`;\n\nexport const UIValues = styled.div`\n  flex-grow: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 4px;\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/condition-row/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue, IFlowRefValue, JsonSchemaBasicType } from '../../typings';\n\nexport enum Op {\n  EQ = 'eq',\n  NEQ = 'neq',\n  GT = 'gt',\n  GTE = 'gte',\n  LT = 'lt',\n  LTE = 'lte',\n  IN = 'in',\n  NIN = 'nin',\n  CONTAINS = 'contains',\n  NOT_CONTAINS = 'not_contains',\n  IS_EMPTY = 'is_empty',\n  IS_NOT_EMPTY = 'is_not_empty',\n  IS_TRUE = 'is_true',\n  IS_FALSE = 'is_false',\n}\n\nexport interface OpConfig {\n  label: string;\n  abbreviation: string;\n  // When right is not a value, display this text\n  rightDisplay?: string;\n}\n\nexport type OpConfigs = Record<Op, OpConfig>;\n\nexport type IRule = Partial<Record<Op, JsonSchemaBasicType | null>>;\n\nexport type IRules = Record<JsonSchemaBasicType, IRule>;\n\nexport interface ConditionRowValueType {\n  left?: IFlowRefValue;\n  operator?: Op;\n  right?: IFlowConstantRefValue;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/constant-input/config.json",
    "content": "\n{\n  \"name\": \"constant-input\",\n  \"depMaterials\": [\"typings/json-schema\"],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/constant-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React, { useMemo } from 'react';\n\nimport { PropsType, Strategy } from './types';\nimport { UIInput, UIInputNumber, UISelect } from './styles';\nimport { I18n } from '@flowgram.ai/editor';\n\nconst defaultStrategies: Strategy[] = [\n  {\n    hit: (schema) => schema?.type === 'string',\n    Renderer: (props) => (\n      <UIInput\n        placeholder={I18n.t('Please Input String')}\n        size=\"small\"\n        disabled={props.readonly}\n        {...props}\n      />\n    ),\n  },\n  {\n    hit: (schema) => schema?.type === 'number',\n    Renderer: (props) => (\n      <UIInputNumber\n        placeholder={I18n.t('Please Input Number')}\n        size=\"small\"\n        disabled={props.readonly}\n        {...props}\n      />\n    ),\n  },\n  {\n    hit: (schema) => schema?.type === 'integer',\n    Renderer: (props) => (\n      <UIInputNumber\n      placeholder={I18n.t('Please Input Integer')}\n        size=\"small\"\n        disabled={props.readonly}\n        precision={0}\n        {...props}\n      />\n    ),\n  },\n  {\n    hit: (schema) => schema?.type === 'boolean',\n    Renderer: (props) => {\n      const { value, onChange, ...rest } = props;\n      return (\n        <UISelect\n          placeholder=\"Please Select Boolean\"\n          size=\"small\"\n          disabled={props.readonly}\n          options={[\n            { label: 'True', value: 1 },\n            { label: 'False', value: 0 },\n          ]}\n          value={value ? 1 : 0}\n          onChange={(value) => onChange?.(!!value)}\n          {...rest}\n        />\n      );\n    },\n  },\n];\n\nexport function ConstantInput(props: PropsType) {\n  const { value, onChange, schema, strategies: extraStrategies, readonly, ...rest } = props;\n\n  const strategies = useMemo(\n    () => [...defaultStrategies, ...(extraStrategies || [])],\n    [extraStrategies]\n  );\n\n  const Renderer = useMemo(() => {\n    const strategy = strategies.find((_strategy) => _strategy.hit(schema));\n\n    return strategy?.Renderer;\n  }, [strategies, schema]);\n\n  if (!Renderer) {\n    return <UIInput size=\"small\" disabled placeholder=\"Unsupported type\" />;\n  }\n\n  return <Renderer value={value} onChange={onChange} readonly={readonly} {...rest} />;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/constant-input/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport type { SelectProps } from 'antd/es/select';\nimport type { InputNumberProps } from 'antd/es/input-number';\nimport { Input, InputNumber, Select } from 'antd';\n\nconst commonStyle = `\n  width: 100%;\n  height: 22px;\n  border-radius: 6px;\n  padding: 4px 11px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n`;\n\nexport const UIInput = styled(Input)`\n  ${commonStyle}\n`;\nexport const UIInputNumber: React.ComponentType<InputNumberProps> = styled(InputNumber)`\n  ${commonStyle}\n  padding: 4px 4px;\n`;\nexport const UISelect: React.ComponentType<SelectProps> = styled(Select)`\n  ${commonStyle}\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/constant-input/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '../../typings';\n\nexport interface Strategy<Value = any> {\n  hit: (schema: IJsonSchema) => boolean;\n  Renderer: React.FC<RendererProps<Value>>;\n}\n\nexport interface RendererProps<Value = any> {\n  value?: Value;\n  onChange?: (value: Value) => void;\n  readonly?: boolean;\n}\n\nexport interface PropsType extends RendererProps {\n  schema: IJsonSchema;\n  strategies?: Strategy[];\n  [key: string]: any;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/dynamic-value-input/config.json",
    "content": "{\n  \"name\": \"dynamic-value-input\",\n  \"depMaterials\": [\"flow-value\", \"constant-input\", \"variable-selector\"],\n  \"depPackages\": [\"styled-components\"]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/dynamic-value-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { theme } from 'antd';\nimport { SettingFilled } from '@ant-design/icons';\n\nimport { VariableSelector } from '../variable-selector';\nimport { Strategy } from '../constant-input/types';\nimport { ConstantInput } from '../constant-input';\nimport { IFlowConstantRefValue } from '../../typings/flow-value';\nimport { IJsonSchema } from '../../typings';\nimport { UIContainer, UIMain, UITrigger } from './styles';\n\nconst { useToken } = theme;\n\ninterface PropsType {\n  value?: IFlowConstantRefValue;\n  onChange: (value?: IFlowConstantRefValue) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  style?: React.CSSProperties;\n  schema?: IJsonSchema;\n  constantProps?: {\n    strategies?: Strategy[];\n    [key: string]: any;\n  };\n}\n\nexport function DynamicValueInput({\n  value,\n  onChange,\n  readonly,\n  style,\n  schema,\n  constantProps,\n}: PropsType) {\n  const { token } = useToken();\n\n  // When is number type, include integer as well\n  const includeSchema = useMemo(() => {\n    if (schema?.type === 'number') {\n      return [schema, { type: 'integer' }];\n    }\n    return schema;\n  }, [schema]);\n\n  const renderMain = () => {\n    if (value?.type === 'ref') {\n      // Display Variable Or Delete\n      return (\n        <VariableSelector\n          style={{ width: '100%' }}\n          value={value?.content}\n          onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : undefined)}\n          includeSchema={includeSchema}\n          readonly={readonly}\n        />\n      );\n    }\n\n    return (\n      <ConstantInput\n        value={value?.content}\n        onChange={(_v) => onChange({ type: 'constant', content: _v })}\n        schema={schema || { type: 'string' }}\n        readonly={readonly}\n        {...constantProps}\n      />\n    );\n  };\n\n  const renderTrigger = () => (\n    <VariableSelector\n      style={{ width: '100%' }}\n      value={value?.type === 'ref' ? value?.content : undefined}\n      onChange={(_v) => onChange({ type: 'ref', content: _v })}\n      includeSchema={includeSchema}\n      readonly={readonly}\n      triggerRender={() => <SettingFilled style={{ color: token.colorPrimary }} />}\n    />\n  );\n\n  return (\n    <UIContainer style={style}>\n      <UIMain>{renderMain()}</UIMain>\n      <UITrigger>{renderTrigger()}</UITrigger>\n    </UIContainer>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/dynamic-value-input/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const UIContainer = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 5px;\n`;\n\nexport const UIMain = styled.div`\n  flex-grow: 1;\n`;\n\nexport const UITrigger = styled.div`\n  outline: none;\n  height: 22px;\n  min-height: 22px;\n  line-height: 22px;\n\n  & .ant-select-selection-wrap {\n    display: none;\n  }\n\n  & .ant-select-arrow {\n    right: 6px;\n    & > .anticon {\n      pointer-events: none !important;\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './variable-selector';\nexport * from './type-selector';\nexport * from './json-schema-editor';\nexport * from './batch-variable-selector';\nexport * from './constant-input';\nexport * from './dynamic-value-input';\nexport * from './condition-row';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/components/blur-input.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useState } from 'react';\n\nimport { Input, InputProps } from 'antd';\n\nexport function BlurInput(props: InputProps) {\n  const [value, setValue] = useState('');\n\n  useEffect(() => {\n    setValue(props.value as string);\n  }, [props.value]);\n\n  return (\n    <Input\n      {...props}\n      value={value}\n      onChange={(value) => {\n        setValue((value as any).target?.value || '');\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/config.json",
    "content": "{\n  \"name\": \"json-schema-editor\",\n  \"depMaterials\": [\"type-selector\", \"typings/json-schema\"],\n  \"depPackages\": [\"styled-components\"]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/default-value.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useRef, useState } from 'react';\n\nimport { Button, Tooltip, theme } from 'antd';\nimport { CodeOutlined } from '@ant-design/icons';\n\nimport { ConstantInput } from '../constant-input';\nimport { IJsonSchema } from '../../typings';\nimport { getValueType } from './utils';\nimport {\n  ConstantInputWrapper,\n  JSONHeader,\n  JSONHeaderLeft,\n  JSONHeaderRight,\n  JSONViewerWrapper,\n} from './styles';\n\nconst { useToken } = theme;\n\n/**\n * 根据不同的数据类型渲染对应的默认值输入组件。\n * @param props - 组件属性，包括 value, type, placeholder, onChange。\n * @returns 返回对应类型的输入组件或 null。\n */\nexport function DefaultValue(props: {\n  value: any;\n  schema?: IJsonSchema;\n  name?: string;\n  type?: string;\n  placeholder?: string;\n  jsonFormatText?: string;\n  onChange: (value: any) => void;\n}) {\n  const { token } = useToken();\n  const { value, schema, type, onChange, placeholder, jsonFormatText } = props;\n\n  const wrapperRef = useRef<HTMLDivElement>(null);\n\n  // TODO add JsonViewer\n  // const JsonViewerRef = useRef<JsonViewer>(null);\n\n  // 为 JsonViewer 添加状态管理\n  const [internalJsonValue, setInternalJsonValue] = useState<string>(\n    getValueType(value) === 'string' ? value : ''\n  );\n\n  // 使用 useCallback 创建稳定的回调函数\n  // const handleJsonChange = useCallback((val: string) => {\n  //   // 只在值真正改变时才更新状态\n  //   if (val !== internalJsonValue) {\n  //     setInternalJsonValue(val);\n  //   }\n  // }, []);\n\n  // 处理编辑完成事件\n  const handleEditComplete = useCallback(() => {\n    // 只有当存在key，编辑完成时才触发父组件的 onChange\n    onChange(internalJsonValue);\n    // 确保在更新后移除焦点\n    requestAnimationFrame(() => {\n      // JsonViewerRef.current?.format();\n      wrapperRef.current?.blur();\n    });\n    // setJsonReadOnly(true);\n  }, [internalJsonValue, onChange]);\n\n  // const [jsonReadOnly, setJsonReadOnly] = useState<boolean>(true);\n\n  const handleFormatJson = useCallback(() => {\n    try {\n      const parsed = JSON.parse(internalJsonValue);\n      const formatted = JSON.stringify(parsed, null, 4);\n      setInternalJsonValue(formatted);\n      onChange(formatted);\n    } catch (error) {\n      console.error('Invalid JSON:', error);\n    }\n  }, [internalJsonValue, onChange]);\n\n  return type === 'object' ? (\n    <>\n      <JSONHeader>\n        <JSONHeaderLeft>json</JSONHeaderLeft>\n        <JSONHeaderRight>\n          <Tooltip title={jsonFormatText ?? 'Format'}>\n            <Button\n              icon={<CodeOutlined style={{ color: token.colorPrimary }} />}\n              size=\"small\"\n              onClick={handleFormatJson}\n            />\n          </Tooltip>\n        </JSONHeaderRight>\n      </JSONHeader>\n\n      <JSONViewerWrapper\n        ref={wrapperRef}\n        tabIndex={-1}\n        onBlur={(e) => {\n          if (wrapperRef.current && !wrapperRef.current?.contains(e.relatedTarget as Node)) {\n            handleEditComplete();\n          }\n        }}\n        onClick={(e: React.MouseEvent) => {\n          // setJsonReadOnly(false);\n        }}\n      >\n        {/* <JsonViewer\n          ref={JsonViewerRef}\n          value={getValueType(value) === 'string' ? value : ''}\n          height={120}\n          width=\"100%\"\n          showSearch={false}\n          options={{\n            readOnly: jsonReadOnly,\n            formatOptions: { tabSize: 4, insertSpaces: true, eol: '\\n' },\n          }}\n          style={{\n            padding: 0,\n          }}\n          onChange={handleJsonChange}\n        /> */}\n      </JSONViewerWrapper>\n    </>\n  ) : (\n    <ConstantInputWrapper>\n      <ConstantInput\n        value={value}\n        onChange={(_v) => onChange(_v)}\n        schema={schema || { type: 'string' }}\n        placeholder={placeholder ?? 'Default value if parameter is not provided'}\n      />\n    </ConstantInputWrapper>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/hooks.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { IJsonSchema } from '../../typings';\nimport { PropertyValueType } from './types';\n\nlet _id = 0;\nfunction genId() {\n  return _id++;\n}\n\nfunction getDrilldownSchema(\n  value?: PropertyValueType,\n  path?: (keyof PropertyValueType)[]\n): { schema?: PropertyValueType | null; path?: (keyof PropertyValueType)[] } {\n  if (!value) {\n    return {};\n  }\n\n  if (value.type === 'array' && value.items) {\n    return getDrilldownSchema(value.items, [...(path || []), 'items']);\n  }\n\n  return { schema: value, path };\n}\n\nexport function usePropertiesEdit(\n  value?: PropertyValueType,\n  onChange?: (value: PropertyValueType) => void\n) {\n  // Get drilldown (array.items.items...)\n  const drilldown = useMemo(() => getDrilldownSchema(value), [value, value?.type, value?.items]);\n\n  const isDrilldownObject = drilldown.schema?.type === 'object';\n\n  // Generate Init Property List\n  const initPropertyList = useMemo(\n    () =>\n      isDrilldownObject\n        ? Object.entries(drilldown.schema?.properties || {})\n            .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))\n            .map(\n              ([name, _value], index) =>\n                ({\n                  key: genId(),\n                  name,\n                  isPropertyRequired: drilldown.schema?.required?.includes(name) || false,\n                  ..._value,\n                  extra: {\n                    ...(_value.extra || {}),\n                    index,\n                  },\n                } as PropertyValueType)\n            )\n        : [],\n    [isDrilldownObject]\n  );\n\n  const [propertyList, setPropertyList] = useState<PropertyValueType[]>(initPropertyList);\n\n  const mountRef = useRef(false);\n\n  useEffect(() => {\n    // If initRef is true, it means the component has been mounted\n    if (mountRef.current) {\n      // If the value is changed, update the property list\n      setPropertyList((_list) => {\n        const nameMap = new Map<string, PropertyValueType>();\n\n        for (const _property of _list) {\n          if (_property.name) {\n            nameMap.set(_property.name, _property);\n          }\n        }\n        return Object.entries(drilldown.schema?.properties || {})\n          .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))\n          .map(([name, _value]) => {\n            const _property = nameMap.get(name);\n            if (_property) {\n              return {\n                key: _property.key,\n                name,\n                isPropertyRequired: drilldown.schema?.required?.includes(name) || false,\n                ..._value,\n              };\n            }\n            return {\n              key: genId(),\n              name,\n              isPropertyRequired: drilldown.schema?.required?.includes(name) || false,\n              ..._value,\n            };\n          });\n      });\n    }\n    mountRef.current = true;\n  }, [drilldown.schema]);\n\n  const updatePropertyList = (updater: (list: PropertyValueType[]) => PropertyValueType[]) => {\n    setPropertyList((_list) => {\n      const next = updater(_list);\n\n      // onChange to parent\n      const nextProperties: Record<string, IJsonSchema> = {};\n      const nextRequired: string[] = [];\n\n      for (const _property of next) {\n        if (!_property.name) {\n          continue;\n        }\n\n        nextProperties[_property.name] = _property;\n\n        if (_property.isPropertyRequired) {\n          nextRequired.push(_property.name);\n        }\n      }\n\n      let drilldownSchema = value || {};\n      if (drilldown.path) {\n        drilldownSchema = drilldown.path.reduce((acc, key) => acc[key], value || {});\n      }\n      drilldownSchema.properties = nextProperties;\n      drilldownSchema.required = nextRequired;\n\n      onChange?.(value || {});\n\n      return next;\n    });\n  };\n\n  const onAddProperty = () => {\n    updatePropertyList((_list) => [\n      ..._list,\n      {\n        key: genId(),\n        name: '',\n        type: 'string',\n        extra: { index: _list.length + 1 },\n      },\n    ]);\n  };\n\n  const onRemoveProperty = (key: number) => {\n    updatePropertyList((_list) => _list.filter((_property) => _property.key !== key));\n  };\n\n  const onEditProperty = (key: number, nextValue: PropertyValueType) => {\n    updatePropertyList((_list) =>\n      _list.map((_property) => (_property.key === key ? nextValue : _property))\n    );\n  };\n\n  useEffect(() => {\n    if (!isDrilldownObject) {\n      setPropertyList([]);\n    }\n  }, [isDrilldownObject]);\n\n  return {\n    propertyList,\n    isDrilldownObject,\n    onAddProperty,\n    onRemoveProperty,\n    onEditProperty,\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useState } from 'react';\n\nimport { Button, Checkbox } from 'antd';\nimport {\n  DownOutlined,\n  ExpandAltOutlined,\n  MinusOutlined,\n  PlusOutlined,\n  RightOutlined,\n  ShrinkOutlined,\n} from '@ant-design/icons';\n\nimport { TypeSelector } from '../type-selector';\nimport { IJsonSchema } from '../../typings';\nimport { ConfigType, PropertyValueType } from './types';\nimport {\n  DefaultValueWrapper,\n  IconAddChildren,\n  UIActions,\n  UICollapseTrigger,\n  UICollapsible,\n  UIContainer,\n  UIExpandDetail,\n  UILabel,\n  UIName,\n  UIProperties,\n  UIPropertyLeft,\n  UIPropertyMain,\n  UIPropertyRight,\n  UIRequired,\n  UIRow,\n  UIType,\n} from './styles';\nimport { usePropertiesEdit } from './hooks';\nimport { DefaultValue } from './default-value';\nimport { BlurInput } from './components/blur-input';\nimport { I18n } from '@flowgram.ai/editor';\n\nexport function JsonSchemaEditor(props: {\n  value?: IJsonSchema;\n  onChange?: (value: IJsonSchema) => void;\n  config?: ConfigType;\n}) {\n  const { value = { type: 'object' }, config = {}, onChange: onChangeProps } = props;\n  const { propertyList, onAddProperty, onRemoveProperty, onEditProperty } = usePropertiesEdit(\n    value,\n    onChangeProps\n  );\n\n  return (\n    <UIContainer>\n      <UIProperties>\n        {propertyList.map((_property, index) => (\n          <PropertyEdit\n            key={_property.key}\n            value={_property}\n            config={config}\n            $index={index}\n            onChange={(_v) => {\n              onEditProperty(_property.key!, _v);\n            }}\n            onRemove={() => {\n              onRemoveProperty(_property.key!);\n            }}\n          />\n        ))}\n      </UIProperties>\n      <Button\n        size=\"small\"\n        style={{ marginTop: 10 }}\n        icon={<PlusOutlined />}\n        onClick={onAddProperty}\n      >\n        {config?.addButtonText ?? 'Add'}\n      </Button>\n    </UIContainer>\n  );\n}\n\nfunction PropertyEdit(props: {\n  value?: PropertyValueType;\n  config?: ConfigType;\n  onChange?: (value: PropertyValueType) => void;\n  onRemove?: () => void;\n  $isLast?: boolean;\n  $index?: number;\n  $isFirst?: boolean;\n  $parentExpand?: boolean;\n  $parentType?: string;\n  $showLine?: boolean;\n  $level?: number; // 添加层级属性\n}) {\n  const {\n    value,\n    config,\n    $level = 0,\n    onChange: onChangeProps,\n    onRemove,\n    $index,\n    $isFirst,\n    $isLast,\n    $parentExpand = false,\n    $parentType = '',\n    $showLine,\n  } = props;\n\n  const [expand, setExpand] = useState(false);\n  const [collapse, setCollapse] = useState(false);\n\n  const { name, type, items, default: defaultValue, description, isPropertyRequired } = value || {};\n\n  const typeSelectorValue = useMemo(() => ({ type, items }), [type, items]);\n\n  const { propertyList, isDrilldownObject, onAddProperty, onRemoveProperty, onEditProperty } =\n    usePropertiesEdit(value, onChangeProps);\n\n  const onChange = (key: string, _value: any) => {\n    onChangeProps?.({\n      ...(value || {}),\n      [key]: _value,\n    });\n  };\n\n  const showCollapse = isDrilldownObject && propertyList.length > 0;\n\n  return (\n    <>\n      <UIPropertyLeft\n        type={type}\n        $index={$index}\n        $isFirst={$isFirst}\n        $isLast={$isLast}\n        $showLine={$showLine}\n        $isExpand={expand}\n        $parentExpand={$parentExpand}\n        $parentType={$parentType}\n      >\n        {showCollapse && (\n          <UICollapseTrigger onClick={() => setCollapse((_collapse) => !_collapse)}>\n            {collapse ? <DownOutlined /> : <RightOutlined />}\n          </UICollapseTrigger>\n        )}\n      </UIPropertyLeft>\n      <UIPropertyRight>\n        <UIPropertyMain\n          $showCollapse={showCollapse}\n          $collapse={collapse}\n          $expand={expand}\n          type={type}\n        >\n          <UIRow>\n            <UIName>\n              <BlurInput\n                placeholder={config?.placeholder ?? I18n.t('Input Variable Name')}\n                size=\"small\"\n                value={name}\n                onChange={(value) => onChange('name', value)}\n              />\n            </UIName>\n            <UIType>\n              <TypeSelector\n                value={typeSelectorValue}\n                onChange={(_value) => {\n                  onChangeProps?.({\n                    ...(value || {}),\n                    ..._value,\n                  });\n                }}\n              />\n            </UIType>\n            <UIRequired>\n              <Checkbox\n                checked={isPropertyRequired}\n                onChange={(e) => onChange('isPropertyRequired', e.target.checked)}\n              />\n            </UIRequired>\n            <UIActions>\n              <Button\n                size=\"small\"\n                // theme=\"borderless\"\n                icon={expand ? <ShrinkOutlined /> : <ExpandAltOutlined />}\n                onClick={() => {\n                  setExpand((_expand) => !_expand);\n                }}\n              />\n              {isDrilldownObject && (\n                <Button\n                  size=\"small\"\n                  icon={<IconAddChildren />}\n                  onClick={() => {\n                    onAddProperty();\n                    setCollapse(true);\n                  }}\n                />\n              )}\n              <Button size=\"small\" icon={<MinusOutlined />} onClick={onRemove} />\n            </UIActions>\n          </UIRow>\n          {expand && (\n            <UIExpandDetail>\n              <UILabel>{config?.descTitle ?? 'Description'}</UILabel>\n              <BlurInput\n                size=\"small\"\n                value={description}\n                onChange={(value) => onChange('description', value)}\n                placeholder={config?.descPlaceholder ?? 'Help LLM to understand the property'}\n              />\n              {$level === 0 && type && type !== 'array' && (\n                <>\n                  <UILabel style={{ marginTop: 10 }}>\n                    {config?.defaultValueTitle ?? 'Default Value'}\n                  </UILabel>\n                  <DefaultValueWrapper>\n                    <DefaultValue\n                      value={defaultValue}\n                      schema={value}\n                      type={type}\n                      placeholder={config?.defaultValuePlaceholder}\n                      jsonFormatText={config?.jsonFormatText}\n                      onChange={(value) => onChange('default', value)}\n                    />\n                  </DefaultValueWrapper>\n                </>\n              )}\n            </UIExpandDetail>\n          )}\n        </UIPropertyMain>\n        {showCollapse && (\n          <UICollapsible $collapse={collapse}>\n            <UIProperties $shrink={true}>\n              {propertyList.map((_property, index) => (\n                <PropertyEdit\n                  key={_property.key}\n                  value={_property}\n                  config={config}\n                  $level={$level + 1} // 传递递增的层级\n                  $parentExpand={expand}\n                  $parentType={type}\n                  onChange={(_v) => {\n                    onEditProperty(_property.key!, _v);\n                  }}\n                  onRemove={() => {\n                    onRemoveProperty(_property.key!);\n                  }}\n                  $isLast={index === propertyList.length - 1}\n                  $isFirst={index === 0}\n                  $index={index}\n                  $showLine={true}\n                />\n              ))}\n            </UIProperties>\n          </UICollapsible>\n        )}\n      </UIPropertyRight>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport styled, { css } from 'styled-components';\n\nimport { SvgIcon } from '../../utils';\n\nexport const UIContainer = styled.div``;\n\nexport const UIRow = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 6px;\n`;\n\nexport const UICollapseTrigger = styled.div`\n  cursor: pointer;\n  margin-right: 5px;\n`;\n\nexport const UIExpandDetail = styled.div`\n  display: flex;\n  flex-direction: column;\n`;\n\nexport const UILabel = styled.div`\n  font-size: 12px;\n  color: #999;\n  font-weight: 400;\n  margin-bottom: 2px;\n`;\n\nexport const UIProperties = styled.div<{ $shrink?: boolean }>`\n  display: grid;\n  grid-template-columns: auto 1fr;\n\n  ${({ $shrink }) =>\n    $shrink &&\n    css`\n      padding-left: 10px;\n      margin-top: 10px;\n    `}\n`;\n\nexport const UIPropertyLeft = styled.div<{\n  $isLast?: boolean;\n  $showLine?: boolean;\n  $isExpand?: boolean;\n  type?: string;\n  $isFirst?: boolean;\n  $index?: number;\n  $parentExpand?: boolean;\n  $parentType?: string;\n}>`\n  grid-column: 1;\n  position: relative;\n  width: 16px;\n\n  ${({ $showLine, $isLast, $parentType }) => {\n    let height = '100%';\n    if ($parentType && $isLast) {\n      height = '24px';\n    }\n\n    return (\n      $showLine &&\n      css`\n        &::before {\n          /* 竖线 */\n          content: '';\n          height: ${height};\n          position: absolute;\n          left: -22px;\n          top: -16px;\n          width: 1px;\n          background: #d9d9d9;\n          display: block;\n        }\n\n        &::after {\n          /* 横线 */\n          content: '';\n          position: absolute;\n          left: -22px; // 横线起点和竖线对齐\n          top: 8px; // 跟随你的行高调整\n          width: 18px; // 横线长度\n          height: 1px;\n          background: #d9d9d9;\n          display: block;\n        }\n      `\n    );\n  }}\n`;\n\nexport const UIPropertyRight = styled.div`\n  grid-column: 2;\n  margin-bottom: 10px;\n\n  &:last-child {\n    margin-bottom: 0px;\n  }\n`;\n\nexport const UIPropertyMain = styled.div<{\n  $expand?: boolean;\n  type?: string;\n  $collapse?: boolean;\n  $showCollapse?: boolean;\n}>`\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  position: relative;\n\n  ${({ $expand, type, $collapse, $showCollapse }) => {\n    const beforeElement = `\n      &::before {\n        /* 竖线 */\n        content: '';\n        height: 100%;\n        position: absolute;\n        left: -12px;\n        top: 18px;\n        width: 1px;\n        background: #d9d9d9;\n        display: block;\n      }`;\n\n    return (\n      $expand &&\n      css`\n        background-color: #f5f5f5;\n        padding: 10px;\n        border-radius: 4px;\n\n        ${$showCollapse &&\n        $collapse &&\n        (type === 'array' || type === 'object') &&\n        css`\n          ${beforeElement}\n        `}\n      `\n    );\n  }}\n`;\n\nexport const UICollapsible = styled.div<{ $collapse?: boolean }>`\n  display: none;\n\n  ${({ $collapse }) =>\n    $collapse &&\n    css`\n      display: block;\n    `}\n`;\n\nexport const UIName = styled.div`\n  flex-grow: 1;\n`;\n\nexport const UIType = styled.div``;\n\nexport const UIRequired = styled.div``;\n\nexport const UIActions = styled.div`\n  white-space: nowrap;\n`;\n\nconst iconAddChildrenSvg = (\n  <svg\n    className=\"icon-icon icon-icon-coz_add_node \"\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M11 6.49988C11 8.64148 9.50397 10.4337 7.49995 10.8884V15.4998C7.49995 16.0521 7.94767 16.4998 8.49995 16.4998H11.208C11.0742 16.8061 11 17.1443 11 17.4998C11 17.8554 11.0742 18.1936 11.208 18.4998H8.49995C6.8431 18.4998 5.49995 17.1567 5.49995 15.4998V10.8884C3.49599 10.4336 2 8.64145 2 6.49988C2 4.0146 4.01472 1.99988 6.5 1.99988C8.98528 1.99988 11 4.0146 11 6.49988ZM6.5 8.99988C7.88071 8.99988 9 7.88059 9 6.49988C9 5.11917 7.88071 3.99988 6.5 3.99988C5.11929 3.99988 4 5.11917 4 6.49988C4 7.88059 5.11929 8.99988 6.5 8.99988Z\"\n    ></path>\n    <path d=\"M17.5 12.4999C18.0523 12.4999 18.5 12.9476 18.5 13.4999V16.4999H21.5C22.0523 16.4999 22.5 16.9476 22.5 17.4999C22.5 18.0522 22.0523 18.4999 21.5 18.4999H18.5V21.4999C18.5 22.0522 18.0523 22.4999 17.5 22.4999C16.9477 22.4999 16.5 22.0522 16.5 21.4999V18.4999H13.5C12.9477 18.4999 12.5 18.0522 12.5 17.4999C12.5 16.9476 12.9477 16.4999 13.5 16.4999H16.5V13.4999C16.5 12.9476 16.9477 12.4999 17.5 12.4999Z\"></path>\n  </svg>\n);\n\nexport const IconAddChildren = () => <SvgIcon size=\"small\" svg={iconAddChildrenSvg} />;\n\nexport const DefaultValueWrapper = styled.div`\n  margin: 0;\n`;\n\nexport const JSONViewerWrapper = styled.div`\n  padding: 0 0 24px;\n  &:first-child {\n    margin-top: 0px;\n  }\n`;\n\nexport const JSONHeader = styled.div`\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  border-radius: 6px 6px 0 0;\n  height: 36px;\n  padding: 0 8px 0 12px;\n`;\n\nexport const JSONHeaderLeft = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 10px;\n`;\n\nexport const JSONHeaderRight = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 10px;\n`;\n\nexport const ConstantInputWrapper = styled.div`\n  flex-grow: 1;\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '../../typings';\n\nexport interface PropertyValueType extends IJsonSchema {\n  name?: string;\n  key?: number;\n  isPropertyRequired?: boolean;\n}\n\nexport type PropertiesValueType = Pick<PropertyValueType, 'properties' | 'required'>;\n\nexport type JsonSchemaProperties = IJsonSchema['properties'];\n\nexport interface ConfigType {\n  placeholder?: string;\n  descTitle?: string;\n  descPlaceholder?: string;\n  defaultValueTitle?: string;\n  defaultValuePlaceholder?: string;\n  addButtonText?: string;\n  jsonFormatText?: string;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/json-schema-editor/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Return the corresponding string description according to the type of the input value.根据输入值的类型返回对应的字符串描述。\n * @param value - 需要判断类型的值。The value whose type needs to be judged.\n * @returns 返回值的类型字符串 The type string of the return value（'string', 'integer', 'number', 'boolean', 'object', 'array', 'other'）。\n */\nexport function getValueType(value: any): string {\n  const type = typeof value;\n\n  if (type === 'string') {\n    return 'string';\n  } else if (type === 'number') {\n    return Number.isInteger(value) ? 'integer' : 'number';\n  } else if (type === 'boolean') {\n    return 'boolean';\n  } else if (type === 'object') {\n    if (value === null) {\n      return 'other';\n    }\n    return Array.isArray(value) ? 'array' : 'object';\n  } else {\n    // undefined, function, symbol, bigint etc.\n    return 'other';\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/type-selector/config.json",
    "content": "{\n  \"name\": \"type-selector\",\n  \"depMaterials\": [\"typings/json-schema\"],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/type-selector/constants.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { SvgIcon } from '../../utils';\nimport { IJsonSchema } from '../../typings';\n\ninterface CascaderData {\n  value: string | number;\n  label?: React.ReactNode;\n  disabled?: boolean;\n  children?: CascaderData[];\n  isLeaf?: boolean;\n}\n\nexport const VariableTypeIcons: { [key: string]: React.ReactNode } = {\n  custom: (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      focusable=\"false\"\n      aria-hidden=\"true\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M5.1 18L4.5032 20.1702C4.24999 21.0909 4.94281 22 5.89773 22C6.54881 22 7.11964 21.565 7.29227 20.9372L8.1 18H12.1L11.5032 20.1702C11.25 21.0909 11.9428 22 12.8977 22C13.5488 22 14.1196 21.565 14.2923 20.9372L15.1 18H19.5C20.3284 18 21 17.3284 21 16.5C21 15.6716 20.3284 15 19.5 15H15.925L17.575 9H20.5C21.3284 9 22 8.32843 22 7.5C22 6.67157 21.3284 6 20.5 6H18.4L18.9968 3.8298C19.25 2.90906 18.5572 2 17.6023 2C16.9512 2 16.3804 2.43504 16.2077 3.06281L15.4 6H11.4L11.9968 3.8298C12.25 2.90906 11.5572 2 10.6023 2C9.95119 2 9.38036 2.43504 9.20773 3.06281L8.4 6H4.5C3.67157 6 3 6.67157 3 7.5C3 8.32843 3.67157 9 4.5 9H7.575L5.925 15H3.5C2.67157 15 2 15.6716 2 16.5C2 17.3284 2.67157 18 3.5 18H5.1ZM8.925 15L10.575 9H14.575L12.925 15H8.925Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  ),\n  object: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M5.33893 1.5835C5.66613 1.5835 5.93137 1.88142 5.93137 2.20862C5.93137 2.53582 5.66613 2.76838 5.33893 2.76838H4.9099C4.34717 2.76838 4.08062 3.07557 4.08062 3.71921V6.58633C4.08062 7.30996 3.80723 7.84734 3.26798 8.19105C3.11426 8.28902 3.10884 8.55273 3.26068 8.65359C3.80476 9.01503 4.08062 9.53994 4.08062 10.2434V13.1251C4.08062 13.7395 4.34717 14.0613 4.9099 14.0613H5.33893C5.66613 14.0613 5.93137 14.3435 5.93137 14.6707C5.93137 14.9979 5.66613 15.2462 5.33893 15.2462H4.64335C3.99177 15.2462 3.48828 15.0268 3.13287 14.6172C2.80708 14.2369 2.64419 13.7103 2.64419 13.0666V10.3165C2.64419 9.8923 2.55534 9.58511 2.37764 9.39494C2.26816 9.27135 1.80618 9.17938 1.38154 9.11602C1.02726 9.06315 0.759057 8.76744 0.765747 8.4093C0.772379 8.0543 1.03439 7.7566 1.38545 7.70346C1.80778 7.63952 2.26906 7.54968 2.37764 7.43477C2.55534 7.22997 2.64419 6.92278 2.64419 6.51319V3.77772C2.64419 3.11945 2.80708 2.59284 3.13287 2.21251C3.48828 1.78829 3.99177 1.5835 4.64335 1.5835H5.33893Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.962 15.2463C10.6348 15.2463 10.3696 14.9483 10.3696 14.6211C10.3696 14.2939 10.6348 14.0614 10.962 14.0614H11.391C11.9538 14.0614 12.2203 13.7542 12.2203 13.1105V10.2434C12.2203 9.51979 12.4937 8.98241 13.033 8.6387C13.1867 8.54073 13.1921 8.27703 13.0403 8.17616C12.4962 7.81472 12.2203 7.28982 12.2203 6.58638V3.70463C12.2203 3.09024 11.9538 2.76842 11.391 2.76842L10.962 2.76842C10.6348 2.76842 10.3696 2.48627 10.3696 2.15907C10.3696 1.83188 10.6348 1.58354 10.962 1.58354L11.6576 1.58354C12.3092 1.58354 12.8127 1.80296 13.1681 2.21255C13.4939 2.59289 13.6568 3.1195 13.6568 3.76314V6.51324C13.6568 6.93745 13.7456 7.24464 13.9233 7.43481C14.03 7.5553 14.4328 7.64858 14.8186 7.71393C15.1718 7.77376 15.4401 8.06977 15.4334 8.42791C15.4268 8.78291 15.1646 9.08018 14.814 9.13633C14.4306 9.19774 14.0291 9.28303 13.9233 9.39499C13.7456 9.59978 13.6568 9.90697 13.6568 10.3166V13.052C13.6568 13.7103 13.4939 14.2369 13.1681 14.6172C12.8127 15.0415 12.3092 15.2463 11.6576 15.2463H10.962Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  boolean: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M10.668 4.66683H5.33463C3.49369 4.66683 2.0013 6.15921 2.0013 8.00016C2.0013 9.84111 3.49369 11.3335 5.33463 11.3335H10.668C12.5089 11.3335 14.0013 9.84111 14.0013 8.00016C14.0013 6.15921 12.5089 4.66683 10.668 4.66683ZM5.33463 3.3335C2.75731 3.3335 0.667969 5.42283 0.667969 8.00016C0.667969 10.5775 2.75731 12.6668 5.33463 12.6668H10.668C13.2453 12.6668 15.3346 10.5775 15.3346 8.00016C15.3346 5.42283 13.2453 3.3335 10.668 3.3335H5.33463Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M8.66797 8.00016C8.66797 6.89559 9.5634 6.00016 10.668 6.00016C11.7725 6.00016 12.668 6.89559 12.668 8.00016C12.668 9.10473 11.7725 10.0002 10.668 10.0002C9.5634 10.0002 8.66797 9.10473 8.66797 8.00016ZM10.668 7.3335C10.2998 7.3335 10.0013 7.63197 10.0013 8.00016C10.0013 8.36835 10.2998 8.66683 10.668 8.66683C11.0362 8.66683 11.3346 8.36835 11.3346 8.00016C11.3346 7.63197 11.0362 7.3335 10.668 7.3335Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  string: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M9.3342 3.33321C8.96601 3.33321 8.66753 3.63169 8.66753 3.99988C8.66753 4.36807 8.96601 4.66655 9.3342 4.66655H14.6675C15.0357 4.66655 15.3342 4.36807 15.3342 3.99988C15.3342 3.63169 15.0357 3.33321 14.6675 3.33321H9.3342Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.0009 7.99988C10.0009 7.63169 10.2993 7.33321 10.6675 7.33321H14.6675C15.0357 7.33321 15.3342 7.63169 15.3342 7.99988C15.3342 8.36807 15.0357 8.66655 14.6675 8.66655H10.6675C10.2993 8.66655 10.0009 8.36807 10.0009 7.99988Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M12.0009 11.3332C11.6327 11.3332 11.3342 11.6317 11.3342 11.9999C11.3342 12.3681 11.6327 12.6665 12.0009 12.6665H14.6675C15.0357 12.6665 15.3342 12.3681 15.3342 11.9999C15.3342 11.6317 15.0357 11.3332 14.6675 11.3332H12.0009Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M9.86659 14.1482L8.23444 10.1844H3.18136C3.13868 10.1844 3.09685 10.1808 3.05616 10.1738L1.66589 14.1129C1.53049 14.4965 1.10971 14.6978 0.726058 14.5624C0.342408 14.427 0.141166 14.0062 0.276572 13.6225L4.37566 2.00848C4.71323 1.05202 6.05321 1.01763 6.4394 1.95552L11.2289 13.5872C11.3838 13.9634 11.2044 14.394 10.8282 14.5489C10.452 14.7038 10.0215 14.5244 9.86659 14.1482ZM5.44412 3.40791L3.57241 8.71109H7.62778L5.44412 3.40791Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  integer: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M15.132 11.4601C15.644 11.0121 15.9 10.3921 15.9 9.60007C15.9 8.60807 15.5 7.93607 14.7 7.58407C15.412 7.23207 15.768 6.62407 15.768 5.76007C15.768 5.05607 15.536 4.48007 15.072 4.03207C14.608 3.59207 14.012 3.37207 13.284 3.37207C12.588 3.37207 12.008 3.58007 11.544 3.99607C11.064 4.42007 10.808 4.98807 10.776 5.70007H12C12.064 4.88407 12.492 4.47607 13.284 4.47607C14.124 4.47607 14.544 4.91607 14.544 5.79607C14.544 6.66007 14.112 7.09207 13.248 7.09207H13.044V8.16007H13.248C14.2 8.16007 14.676 8.62807 14.676 9.56407C14.676 10.5081 14.212 10.9801 13.284 10.9801C12.9 10.9801 12.584 10.8761 12.336 10.6681C12.064 10.4441 11.916 10.1161 11.892 9.68407H10.668C10.692 10.4761 10.964 11.0841 11.484 11.5081C11.948 11.8921 12.548 12.0841 13.284 12.0841C14.036 12.0841 14.652 11.8761 15.132 11.4601Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M4.46875 12.0003V10.9083L7.75675 6.91228C8.06075 6.54428 8.21275 6.16428 8.21275 5.77228C8.21275 4.90828 7.79675 4.47628 6.96475 4.47628C6.60475 4.47628 6.31275 4.57628 6.08875 4.77628C5.83275 5.00828 5.70475 5.34828 5.70475 5.79628H4.48075C4.48075 5.07628 4.71275 4.49228 5.17675 4.04428C5.64075 3.60428 6.23675 3.38428 6.96475 3.38428C7.70075 3.38428 8.29675 3.60028 8.75275 4.03228C9.20875 4.47228 9.43675 5.05628 9.43675 5.78428C9.43675 6.13628 9.36875 6.45628 9.23275 6.74428C9.12075 6.97628 8.92075 7.27228 8.63275 7.63228L5.95675 10.9083H9.43675V12.0003H4.46875Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M1.668 12.0001V4.78805L0 6.25205V4.89605L1.668 3.45605H2.892V12.0001H1.668Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  number: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M3.44151 5.3068C3.44151 3.83404 4.71542 2.64014 6.18818 2.64014C7.66094 2.64014 8.93484 3.83404 8.93484 5.3068V10.6135C8.93484 12.0862 7.66094 13.2801 6.18818 13.2801C4.71542 13.2801 3.44151 12.0862 3.44151 10.6135V5.3068ZM7.60151 5.3068C7.60151 4.57042 6.92456 3.97347 6.18818 3.97347C5.4518 3.97347 4.77484 4.57042 4.77484 5.3068V10.6135C4.77484 11.3498 5.4518 11.9468 6.18818 11.9468C6.92456 11.9468 7.60151 11.3498 7.60151 10.6135V5.3068Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M12.9882 2.64014C11.5154 2.64014 10.2415 3.83404 10.2415 5.3068V10.6135C10.2415 12.0862 11.5154 13.2801 12.9882 13.2801C14.4609 13.2801 15.7348 12.0862 15.7348 10.6135V5.3068C15.7348 3.83404 14.4609 2.64014 12.9882 2.64014ZM14.4015 10.6135C14.4015 11.3498 13.7246 11.9468 12.9882 11.9468C12.2518 11.9468 11.5748 11.3498 11.5748 10.6135V5.3068C11.5748 4.57042 12.2518 3.97347 12.9882 3.97347C13.7246 3.97347 14.4015 4.57042 14.4015 5.3068V10.6135Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M1.21484 13.2001C1.76713 13.2001 2.21484 12.7524 2.21484 12.2001C2.21484 11.6479 1.76713 11.2001 1.21484 11.2001C0.662559 11.2001 0.214844 11.6479 0.214844 12.2001C0.214844 12.7524 0.662559 13.2001 1.21484 13.2001Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  array: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M5.23759 1.00342H2.00391V14.997H5.23759V13.6251H3.35127V2.37534H5.23759V1.00342Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.7624 1.00342H13.9961V14.997H10.7624V13.6251H12.6487V2.37534H10.7624V1.00342Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n\n  stream: (\n    <svg\n      viewBox=\"0 0 1024 1024\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n    >\n      <path\n        d=\"M879.674 544.51l-158.254-0.221c-8.534 2.287-17.305-2.776-19.588-11.307l-23.862-75.877-74.742 350.891c0 0-1.523 18.507-11.518 18.507s-26.9 0.281-26.9 0.281c-8.259 2.213-16.748-2.687-18.961-10.949l-92.741-457.648-70.305 330.634c-2.261 8.291-11.94 15.206-20.385 12.986l-24.876 0.339c-8.723 2.293-17.685-2.789-20.023-11.349L270.629 544.51 143.993 544.51c-8.831 0-15.993-7.159-15.993-15.993l0-31.986c0-8.831 7.162-15.993 15.993-15.993l157.429-0.516c9.565-0.304 17.685 0.788 20.023 9.351l24.386 76.092 68.642-358.907c0 0 3.4-10.894 14.397-10.894 10.994 0 34.107-0.448 34.107-0.448 8.262-2.213 16.751 2.687 18.965 10.949l91.912 454.126 67.948-326.182c2.213-8.262 8.707-15.161 16.965-12.948l27.316-0.333c8.531-2.287 17.301 2.776 19.588 11.31l46.665 148.4 127.337 0c8.835 0 15.993 7.162 15.993 15.993l0 31.986C895.667 537.352 888.508 544.51 879.674 544.51z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  ),\n\n  map: (\n    <svg\n      viewBox=\"0 0 1024 1024\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n    >\n      <path\n        d=\"M877.860571 938.642286h-645.851428c-27.574857 0-54.052571-11.337143-73.508572-31.744a110.957714 110.957714 0 0 1-30.500571-76.8V193.828571c0-28.745143 10.971429-56.32 30.500571-76.726857a101.888 101.888 0 0 1 73.508572-31.817143h574.171428c27.501714 0 53.979429 11.337143 73.508572 31.744 19.529143 20.333714 30.500571 48.054857 30.500571 76.8v522.020572a34.157714 34.157714 0 0 1-6.948571 22.820571c-37.156571 19.382857-57.636571 39.350857-57.636572 72.630857 0 39.716571 19.894857 50.029714 57.636572 72.777143a34.816 34.816 0 0 1-8.045714 49.298286 32.256 32.256 0 0 1-17.334858 5.193143z m-32.256-254.537143V193.828571a40.228571 40.228571 0 0 0-39.497142-41.179428H232.009143a40.301714 40.301714 0 0 0-39.497143 41.252571V699.245714c17.773714-9.874286 37.449143-14.994286 57.417143-14.921143h595.675428v-0.073142z m-595.675428 187.245714h566.198857c-22.893714-11.190857-27.940571-39.497143-28.013714-59.977143 0-20.260571 3.218286-43.885714 28.013714-59.904h-566.125714c-31.670857 0-57.417143 26.843429-57.417143 59.977143 0 33.060571 25.746286 59.904 57.344 59.904z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M320 128m32.036571 0l-0.073142 0q32.036571 0 32.036571 32.036571l0 511.926858q0 32.036571-32.036571 32.036571l0.073142 0q-32.036571 0-32.036571-32.036571l0-511.926858q0-32.036571 32.036571-32.036571Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  ),\n};\n\nexport const ArrayIcons: { [key: string]: React.ReactNode } = {\n  object: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM3.82031 5.9091C3.82031 5.18535 4.40703 4.59863 5.13078 4.59863C5.85453 4.59863 6.44124 5.18535 6.44124 5.9091C6.44124 6.56485 5.9596 7.1081 5.33078 7.2044V8.70018H5.32877C5.32982 8.75093 5.33078 8.80912 5.33078 8.87034V9.72111C5.33078 10.0195 5.57268 10.2614 5.87109 10.2614H6.24124C6.55613 10.2614 6.8114 10.5167 6.8114 10.8316C6.8114 11.1465 6.55613 11.4017 6.24124 11.4017H5.87109C4.94291 11.4017 4.19047 10.6493 4.19047 9.72111V6.82186C3.96158 6.58607 3.82031 6.26397 3.82031 5.9091ZM7.33679 5.9091C7.33679 5.59421 7.59205 5.33894 7.90694 5.33894H11.6085C11.9234 5.33894 12.1786 5.59421 12.1786 5.9091C12.1786 6.22399 11.9234 6.47925 11.6085 6.47925H7.90694C7.59205 6.47925 7.33679 6.22399 7.33679 5.9091ZM7.33679 9.86846C7.33679 9.55357 7.59205 9.2983 7.90694 9.2983H11.6085C11.9234 9.2983 12.1786 9.55357 12.1786 9.86846C12.1786 10.1833 11.9234 10.4386 11.6085 10.4386H7.90694C7.59205 10.4386 7.33679 10.1833 7.33679 9.86846Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  boolean: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM2.75 7.99993C2.75 6.14518 4.25358 4.6416 6.10833 4.6416H9.775C11.6298 4.6416 13.1333 6.14518 13.1333 7.99993C13.1333 9.85469 11.6298 11.3583 9.775 11.3583H6.10833C4.25358 11.3583 2.75 9.85469 2.75 7.99993ZM6.10833 5.85827C4.92552 5.85827 3.96667 6.81713 3.96667 7.99993C3.96667 9.18274 4.92552 10.1416 6.10833 10.1416H9.775C10.9578 10.1416 11.9167 9.18274 11.9167 7.99993C11.9167 6.81713 10.9578 5.85827 9.775 5.85827H6.10833ZM8.25 7.99993C8.25 7.1577 8.93277 6.47493 9.775 6.47493C10.6172 6.47493 11.3 7.1577 11.3 7.99993C11.3 8.84217 10.6172 9.52493 9.775 9.52493C8.93277 9.52493 8.25 8.84217 8.25 7.99993ZM9.775 7.6916C9.60471 7.6916 9.46667 7.82965 9.46667 7.99993C9.46667 8.17022 9.60471 8.30827 9.775 8.30827C9.94529 8.30827 10.0833 8.17022 10.0833 7.99993C10.0833 7.82965 9.94529 7.6916 9.775 7.6916Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  string: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM5.23701 4.07158C5.50364 3.3161 6.56205 3.28894 6.86709 4.02974L10 11.6383C10.1329 11.9609 9.979 12.3302 9.65631 12.4631C9.33363 12.596 8.96434 12.4421 8.83147 12.1194L7.8021 9.61951H4.61903L3.7474 12.0891C3.63126 12.4182 3.27034 12.5908 2.94127 12.4747C2.6122 12.3585 2.43958 11.9976 2.55573 11.6685L5.23701 4.07158ZM6.08814 5.45704L5.06505 8.35579H7.28174L6.08814 5.45704ZM8.81938 6.07534C8.81938 5.75166 9.08177 5.48926 9.40545 5.48926H12.8941C13.2178 5.48926 13.4802 5.75166 13.4802 6.07534C13.4802 6.39902 13.2178 6.66142 12.8941 6.66142H9.40545C9.08177 6.66142 8.81938 6.39902 8.81938 6.07534ZM10.2668 9.69181C10.2668 9.36812 10.5292 9.10573 10.8529 9.10573H12.8941C13.2178 9.10573 13.4802 9.36812 13.4802 9.69181C13.4802 10.0155 13.2178 10.2779 12.8941 10.2779H10.8529C10.5292 10.2779 10.2668 10.0155 10.2668 9.69181Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  integer: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM10.3614 5.22374C10.7161 4.90585 11.1581 4.75 11.6762 4.75C12.2173 4.75 12.6723 4.91467 13.0281 5.25207L13.0291 5.253C13.3852 5.59688 13.561 6.03946 13.561 6.56767C13.561 6.89 13.4945 7.17448 13.3539 7.41445C13.2572 7.57972 13.1279 7.71948 12.9685 7.83428C13.1575 7.95643 13.3099 8.11182 13.4225 8.30109C13.5793 8.5644 13.6531 8.88311 13.6531 9.24936C13.6531 9.83787 13.4612 10.3151 13.0656 10.6612C12.6982 10.9795 12.2305 11.1341 11.6762 11.1341C11.1356 11.1341 10.6805 10.9925 10.324 10.6977C9.92124 10.3691 9.71723 9.90026 9.69942 9.31256L9.69473 9.15802H10.846L10.8539 9.2997C10.8689 9.5698 10.9591 9.75553 11.1096 9.87941L11.1106 9.88027C11.2519 9.99882 11.4365 10.0631 11.6762 10.0631C11.9765 10.0631 12.1743 9.98692 12.2984 9.86071C12.4229 9.73404 12.4984 9.53136 12.4984 9.22422C12.4984 8.92116 12.4215 8.72127 12.2939 8.59581C12.1658 8.46989 11.961 8.39373 11.6511 8.39373H11.3586V7.34788H11.6511C11.9297 7.34788 12.111 7.27834 12.2238 7.16555C12.3366 7.05276 12.4062 6.87138 12.4062 6.59281C12.4062 6.30696 12.3378 6.12041 12.2277 6.00501C12.1188 5.89092 11.9446 5.82098 11.6762 5.82098C11.4248 5.82098 11.2539 5.88537 11.1407 5.99325C11.0268 6.10185 10.9497 6.27522 10.9291 6.5375L10.9183 6.67577H9.76788L9.77492 6.51904C9.79886 5.98644 9.99237 5.54989 10.3614 5.22374ZM5.91032 5.26037C6.26612 4.92297 6.72112 4.7583 7.26219 4.7583C7.80751 4.7583 8.26297 4.91938 8.61401 5.25194L8.61501 5.25289C8.96719 5.59272 9.13852 6.04185 9.13852 6.58435C9.13852 6.84997 9.08709 7.09565 8.9817 7.31883L8.98114 7.31999C8.89563 7.49712 8.74775 7.71415 8.54418 7.96862L8.54322 7.96981L6.87446 10.0127H9.13852V11.0753H5.36909V10.1089L7.69946 7.27679C7.89456 7.04062 7.98374 6.80773 7.98374 6.57597C7.98374 6.29602 7.91626 6.11385 7.8078 6.00122C7.70036 5.88964 7.52811 5.8209 7.26219 5.8209C7.04017 5.8209 6.87439 5.88173 6.75075 5.99193C6.61227 6.11766 6.53226 6.30918 6.53226 6.59273V6.74273H5.37747V6.59273C5.37747 6.05443 5.55248 5.60586 5.90934 5.2613L5.91032 5.26037ZM3.50907 4.80865H4.56964V11.0754H3.41486V6.2201L2.25 7.24249V5.89561L3.50907 4.80865Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  number: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M3.6139 1.58154H0V14.4191H3.6139V13.1269H1.36702V2.87375H3.6139V1.58154ZM3.41656 13.3271V13.3269H1.17155V13.3271H3.41656ZM0.199219 14.2191H0.197344V1.78154H3.41656V1.78174H0.199219V14.2191ZM16 1.58154H12.3861V2.87375H14.633V13.1269H12.3861V14.4191H16V1.58154ZM12.5834 1.78154V2.67375H12.5853V1.78174H15.8027V1.78154H12.5834ZM12.5853 14.2191V13.3271H14.8322V2.67394H14.8303V13.3269H12.5834V14.2191H12.5853ZM6.86771 4.5C5.87019 4.5 5.00104 5.30767 5.00104 6.31667V9.63333C5.00104 10.6423 5.87019 11.45 6.86771 11.45C7.86523 11.45 8.73438 10.6423 8.73438 9.63333V6.31667C8.73438 5.30767 7.86523 4.5 6.86771 4.5ZM11.1177 4.5C10.1202 4.5 9.25104 5.30767 9.25104 6.31667V9.63333C9.25104 10.6423 10.1202 11.45 11.1177 11.45C12.1152 11.45 12.9844 10.6423 12.9844 9.63333V6.31667C12.9844 5.30767 12.1152 4.5 11.1177 4.5ZM6.13438 6.31667C6.13438 5.9503 6.47884 5.63333 6.86771 5.63333C7.25657 5.63333 7.60104 5.9503 7.60104 6.31667V9.63333C7.60104 9.9997 7.25657 10.3167 6.86771 10.3167C6.47884 10.3167 6.13438 9.9997 6.13438 9.63333V6.31667ZM10.3844 6.31667C10.3844 5.9503 10.7288 5.63333 11.1177 5.63333C11.5066 5.63333 11.851 5.9503 11.851 6.31667V9.63333C11.851 9.9997 11.5066 10.3167 11.1177 10.3167C10.7288 10.3167 10.3844 9.9997 10.3844 9.63333V6.31667ZM3.75938 9.85C3.33135 9.85 2.98438 10.197 2.98438 10.625C2.98438 11.053 3.33135 11.4 3.75938 11.4C4.1874 11.4 4.53438 11.053 4.53438 10.625C4.53438 10.197 4.1874 9.85 3.75938 9.85Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n};\n\nexport const getSchemaIcon = (value?: Partial<IJsonSchema>) => {\n  if (value?.type === 'array') {\n    return ArrayIcons[value.items?.type || 'object'];\n  }\n\n  return VariableTypeIcons[value?.type || 'object'];\n};\n\nconst labelStyle: React.CSSProperties = {\n  display: 'flex',\n  alignItems: 'center',\n  gap: 5,\n};\n\nconst firstUppercase = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);\n\nconst baseOptions: CascaderData[] = [\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'string' })} />\n        {firstUppercase('string')}\n      </div>\n    ),\n    value: 'string',\n  },\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'integer' })} />\n        {firstUppercase('integer')}\n      </div>\n    ),\n    value: 'integer',\n  },\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'number' })} />\n        {firstUppercase('number')}\n      </div>\n    ),\n    value: 'number',\n  },\n\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'boolean' })} />\n        {firstUppercase('boolean')}\n      </div>\n    ),\n    value: 'boolean',\n  },\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'object' })} />\n        {firstUppercase('object')}\n      </div>\n    ),\n    value: 'object',\n  },\n];\n\nexport const options: CascaderData[] = [\n  ...baseOptions,\n  {\n    label: (\n      <div style={labelStyle}>\n        <SvgIcon size=\"small\" svg={getSchemaIcon({ type: 'array' })} />\n        {firstUppercase('array')}\n      </div>\n    ),\n    value: 'array',\n    children: baseOptions.map((_opt) => ({\n      ..._opt,\n      value: `${_opt.value}`,\n      label: (\n        <div style={labelStyle}>\n          <SvgIcon\n            size=\"small\"\n            svg={getSchemaIcon({\n              type: 'array',\n              items: { type: _opt.value as string },\n            })}\n          />\n          {firstUppercase(_opt.value as string)}\n        </div>\n      ),\n    })),\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/type-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { Cascader } from 'antd';\n\nimport { IJsonSchema } from '../../typings';\nimport { ArrayIcons, VariableTypeIcons, getSchemaIcon } from './constants';\n\ninterface PropTypes {\n  value?: Partial<IJsonSchema>;\n  onChange: (value?: Partial<IJsonSchema>) => void;\n  disabled?: boolean;\n  style?: React.CSSProperties;\n}\n\nexport const getTypeSelectValue = (value?: Partial<IJsonSchema>): string[] | undefined => {\n  if (value?.type === 'array' && value?.items) {\n    return [value.type, ...(getTypeSelectValue(value.items) || [])];\n  }\n\n  return value?.type ? [value.type] : undefined;\n};\n\nexport const parseTypeSelectValue = (value?: string[]): Partial<IJsonSchema> | undefined => {\n  const [type, ...subTypes] = value || [];\n\n  if (type === 'array') {\n    return { type: 'array', items: parseTypeSelectValue(subTypes) };\n  }\n\n  return { type };\n};\n\nexport function TypeSelector(props: PropTypes) {\n  const { value, onChange, disabled } = props;\n\n  const selectValue = useMemo(() => getTypeSelectValue(value), [value]);\n\n  return (\n    <Cascader\n      disabled={disabled}\n      size=\"small\"\n      // TODO\n      // triggerRender={() => (\n      //   <Button size=\"small\" style={style}>\n      //     {getSchemaIcon(value)}\n      //   </Button>\n      // )}\n      // treeData={options}\n      value={selectValue}\n      // leafOnly={true}\n      onChange={(value) => {\n        onChange(parseTypeSelectValue(value as string[]));\n      }}\n    />\n  );\n}\n\nexport { ArrayIcons, VariableTypeIcons, getSchemaIcon };\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/variable-selector/config.json",
    "content": "{\n  \"name\": \"variable-selector\",\n  \"depMaterials\": [\"type-selector\", \"utils/json-schema\", \"typings/json-schema\"],\n  \"depPackages\": [\"styled-components\"]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/variable-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use client';\nimport React from 'react';\n\nimport type { TreeSelectProps, TreeNodeProps } from 'antd';\nimport { DownOutlined } from '@ant-design/icons';\n\nimport { IJsonSchema } from '../../typings/json-schema';\nimport { useVariableTree } from './use-variable-tree';\nimport { UITreeSelect } from './styles';\n\ninterface TriggerRenderProps {\n  value: string[];\n}\n\ninterface PropTypes {\n  value?: string[];\n  config?: {\n    placeholder?: string;\n    notFoundContent?: string;\n  };\n  onChange: (value?: string[]) => void;\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n  readonly?: boolean;\n  allowClear?: boolean;\n  hasError?: boolean;\n  style?: React.CSSProperties;\n  triggerRender?: (props: TriggerRenderProps) => React.ReactNode;\n}\n\nexport type VariableSelectorProps = PropTypes;\n\nexport const VariableSelector = ({\n  value,\n  config = {},\n  onChange,\n  style,\n  readonly = false,\n  allowClear = false,\n  includeSchema,\n  excludeSchema,\n  hasError,\n  triggerRender,\n}: PropTypes) => {\n  const treeData = useVariableTree({ includeSchema, excludeSchema });\n\n  const onPopupScroll: TreeSelectProps['onPopupScroll'] = (e) => {\n    console.log('onPopupScroll', e);\n  };\n\n  return (\n    <UITreeSelect\n      value={value}\n      styles={{\n        popup: { root: { maxHeight: 400, minWidth: 230, overflow: 'auto' } },\n      }}\n      style={style}\n      treeDefaultExpandAll\n      onChange={onChange}\n      treeData={treeData}\n      onPopupScroll={onPopupScroll}\n      treeIcon={true}\n      allowClear={allowClear}\n      suffixIcon={triggerRender && value ? triggerRender({ value }) : undefined}\n      switcherIcon={(props: TreeNodeProps) => (\n        <DownOutlined\n          style={{\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            height: '100%',\n          }}\n        />\n      )}\n    />\n  );\n};\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/variable-selector/styles.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\nimport type { TreeSelectProps } from 'antd/es/tree-select';\nimport { Tag, TreeSelect } from 'antd';\n\nexport const UIRootTitle = styled.span`\n  margin-right: 4px;\n`;\n\nexport const UITag = styled(Tag)`\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n`;\n\nexport const UITreeSelect: React.ComponentType<TreeSelectProps<any>> = styled(TreeSelect)`\n  height: 22px;\n  min-height: 22px;\n  line-height: 22px;\n\n  & .ant-select-clear {\n    right: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  & .ant-select-arrow {\n    right: 6px;\n    & > .anticon {\n      pointer-events: none !important;\n    }\n  }\n`;\n\nexport const ImgIconWrapper = styled.div`\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n`;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/variable-selector/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { ReactElement } from 'react';\n\nexport interface TreeNodeData<VariableMeta = any> {\n  value: string | number;\n  title: string;\n  disabled?: boolean;\n  disableCheckbox?: boolean;\n  selectable?: boolean;\n  checkable?: boolean;\n  children?: TreeNodeData[];\n  icon: ReactElement;\n  key: string;\n  keyPath: string[];\n  rootMeta: VariableMeta;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/components/variable-selector/use-variable-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { ASTMatch, BaseVariableField, useScopeAvailable } from '@flowgram.ai/editor';\n\nimport { ArrayIcons, VariableTypeIcons } from '../type-selector/constants';\nimport { JsonSchemaUtils } from '../../utils/json-schema';\nimport { SvgIcon } from '../../utils';\nimport { IJsonSchema } from '../../typings/json-schema';\nimport { TreeNodeData } from './types';\nimport { ImgIconWrapper } from './styles';\n\ntype VariableField = BaseVariableField<{\n  icon?: string;\n  title?: string;\n}>;\n\nexport function useVariableTree(params: {\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n}): TreeNodeData[] {\n  const { includeSchema, excludeSchema } = params;\n\n  const available = useScopeAvailable();\n\n  const getVariableTypeIcon = useCallback((variable: VariableField) => {\n    if (variable.meta?.icon) {\n      return (\n        <ImgIconWrapper>\n          <img style={{ marginRight: 8 }} width={12} height={12} src={variable.meta.icon} />\n        </ImgIconWrapper>\n      );\n    }\n\n    const _type = variable.type;\n\n    if (ASTMatch.isArray(_type)) {\n      return (\n        <SvgIcon svg={ArrayIcons[_type.items?.kind.toLowerCase()] || VariableTypeIcons.array} />\n      );\n    }\n\n    if (ASTMatch.isCustomType(_type)) {\n      return <SvgIcon svg={VariableTypeIcons[_type.typeName.toLowerCase()]} />;\n    }\n\n    return <SvgIcon svg={VariableTypeIcons[variable.type?.kind.toLowerCase()]} />;\n  }, []);\n\n  const renderVariable = (\n    variable: VariableField,\n    parentFields: VariableField[] = []\n  ): TreeNodeData | null => {\n    let type = variable?.type;\n\n    if (!type) {\n      return null;\n    }\n\n    let children: TreeNodeData[] | undefined;\n\n    if (ASTMatch.isObject(type)) {\n      children = (type.properties || [])\n        .map((_property) => renderVariable(_property as VariableField, [...parentFields, variable]))\n        .filter(Boolean) as TreeNodeData[];\n    }\n\n    const keyPath = [...parentFields.map((_field) => _field.key), variable.key];\n    const key = keyPath.join('.');\n\n    const isSchemaInclude = includeSchema\n      ? JsonSchemaUtils.isASTMatchSchema(type, includeSchema)\n      : true;\n    const isSchemaExclude = excludeSchema\n      ? JsonSchemaUtils.isASTMatchSchema(type, excludeSchema)\n      : false;\n    const isSchemaMatch = isSchemaInclude && !isSchemaExclude;\n\n    // If not match, and no children, return null\n    if (!isSchemaMatch && !children?.length) {\n      return null;\n    }\n\n    return {\n      key: key,\n      title: variable.meta?.title || variable.key,\n      value: key,\n      keyPath,\n      icon: getVariableTypeIcon(variable), // TODO\n      children,\n      disabled: !isSchemaMatch,\n      rootMeta: parentFields[0]?.meta,\n    };\n  };\n\n  return [...available.variables.slice(0).reverse()]\n    .map((_variable) => renderVariable(_variable as VariableField))\n    .filter(Boolean) as TreeNodeData[];\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/auto-rename-ref/config.json",
    "content": "{\n  \"name\": \"auto-rename-ref\",\n  \"depMaterials\": [\n    \"flow-value\"\n  ],\n  \"depPackages\": [\n    \"lodash-es\"\n  ]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/auto-rename-ref/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isArray, isObject } from 'lodash-es';\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  VariableFieldKeyRenameService,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '../../typings';\n\n/**\n * Auto rename ref when form item's key is renamed\n *\n * Example:\n *\n * formMeta: {\n *  effects: {\n *    \"inputsValues\": autoRenameRefEffect,\n *  }\n * }\n */\nexport const autoRenameRefEffect: EffectOptions[] = [\n  {\n    event: DataEvent.onValueInit,\n    effect: ((params) => {\n      const { context, form, name } = params;\n\n      const renameService = context.node.getService(VariableFieldKeyRenameService);\n\n      const disposable = renameService.onRename(({ before, after }) => {\n        const beforeKeyPath = [\n          ...before.parentFields.map((_field) => _field.key).reverse(),\n          before.key,\n        ];\n        const afterKeyPath = [\n          ...after.parentFields.map((_field) => _field.key).reverse(),\n          after.key,\n        ];\n\n        // traverse rename refs inside form item 'name'\n        traverseRef(name, form.getValueIn(name), (_drilldownName, _v) => {\n          if (isRefMatch(_v, beforeKeyPath)) {\n            _v.content = [...afterKeyPath, ...(_v.content || [])?.slice(beforeKeyPath.length)];\n            form.setValueIn(_drilldownName, _v);\n          }\n        });\n      });\n\n      return () => {\n        disposable.dispose();\n      };\n    }) as Effect,\n  },\n];\n\n/**\n * If ref value's keyPath is the under as targetKeyPath\n * @param value\n * @param targetKeyPath\n * @returns\n */\nfunction isRefMatch(value: IFlowRefValue, targetKeyPath: string[]) {\n  return targetKeyPath.every((_key, index) => _key === value.content?.[index]);\n}\n\n/**\n * If value is ref\n * @param value\n * @returns\n */\nfunction isRef(value: any): value is IFlowRefValue {\n  return (\n    value?.type === 'ref' && Array.isArray(value?.content) && typeof value?.content[0] === 'string'\n  );\n}\n\n/**\n * Traverse value to find ref\n * @param value\n * @param options\n * @returns\n */\nfunction traverseRef(name: string, value: any, cb: (name: string, _v: IFlowRefValue) => void) {\n  if (isObject(value)) {\n    if (isRef(value)) {\n      cb(name, value);\n      return;\n    }\n\n    Object.entries(value).forEach(([_key, _value]) => {\n      traverseRef(`${name}.${_key}`, _value, cb);\n    });\n    return;\n  }\n\n  if (isArray(value)) {\n    value.forEach((_value, idx) => {\n      traverseRef(`${name}[${idx}]`, _value, cb);\n    });\n    return;\n  }\n\n  return;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './provide-batch-input';\nexport * from './provide-batch-outputs';\nexport * from './auto-rename-ref';\nexport * from './provide-json-schema-outputs';\nexport * from './sync-variable-title';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-batch-input/config.json",
    "content": "{\n  \"name\": \"provide-batch-input\",\n  \"depMaterials\": [\"flow-value\"],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-batch-input/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  EffectOptions,\n  FlowNodeRegistry,\n  createEffectFromVariableProvider,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '../../typings';\n\nexport const provideBatchInputEffect: EffectOptions[] = createEffectFromVariableProvider({\n  private: true,\n  parse: (value: IFlowRefValue, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'item',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'index',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-batch-outputs/config.json",
    "content": "{\n  \"name\": \"provide-batch-outputs\",\n  \"depMaterials\": [\"flow-value\"],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-batch-outputs/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  EffectOptions,\n  FlowNodeRegistry,\n  createEffectFromVariableProvider,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '../../typings';\n\nexport const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: Record<string, IFlowRefValue>, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: Object.entries(value).map(([_key, value]) =>\n          ASTFactory.createProperty({\n            key: _key,\n            initializer: ASTFactory.createWrapArrayExpression({\n              wrapFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          })\n        ),\n      }),\n    }),\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-json-schema-outputs/config.json",
    "content": "{\n  \"name\": \"provide-json-schema-outputs\",\n  \"depMaterials\": [\n    \"typings/json-schema\",\n    \"utils/json-schema\"\n  ],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/provide-json-schema-outputs/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  EffectOptions,\n  FlowNodeRegistry,\n  createEffectFromVariableProvider,\n} from '@flowgram.ai/editor';\n\nimport { JsonSchemaUtils } from '../../utils';\nimport { IJsonSchema } from '../../typings';\n\nexport const provideJsonSchemaOutputs: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: IJsonSchema, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title') || ctx.node.id,\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: JsonSchemaUtils.schemaToAST(value),\n    }),\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/sync-variable-title/config.json",
    "content": "{\n  \"name\": \"sync-variable-title\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/effects/sync-variable-title/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  FlowNodeRegistry,\n  FlowNodeVariableData,\n} from '@flowgram.ai/editor';\n\nexport const syncVariableTitle: EffectOptions[] = [\n  {\n    event: DataEvent.onValueChange,\n    effect: (({ value, context }) => {\n      context.node.getData(FlowNodeVariableData).allScopes.forEach((_scope) => {\n        _scope.output.variables.forEach((_var) => {\n          _var.updateMeta({\n            title: value || context.node.id,\n            icon: context.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n          });\n        });\n      });\n    }) as Effect,\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/form-plugins/batch-outputs-plugin/config.json",
    "content": "{\n  \"name\": \"batch-outputs-plugin\",\n  \"depMaterials\": [\n    \"flow-value\"\n  ],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/form-plugins/batch-outputs-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  createEffectFromVariableProvider,\n  defineFormPluginCreator,\n  FlowNodeRegistry,\n  getNodePrivateScope,\n  getNodeScope,\n  ScopeChainTransformService,\n  type EffectOptions,\n  type FormPluginCreator,\n  FlowNodeScopeType,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '../../typings';\n\nexport const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: Record<string, IFlowRefValue>, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: Object.entries(value).map(([_key, value]) =>\n          ASTFactory.createProperty({\n            key: _key,\n            initializer: ASTFactory.createWrapArrayExpression({\n              wrapFor: ASTFactory.createKeyPathExpression({\n                keyPath: value?.content || [],\n              }),\n            }),\n          })\n        ),\n      }),\n    }),\n  ],\n});\n\n/**\n * Free Layout only right now\n */\nexport const createBatchOutputsFormPlugin: FormPluginCreator<{ outputKey: string }> =\n  defineFormPluginCreator({\n    name: 'batch-outputs-plugin',\n    onSetupFormMeta({ mergeEffect }, { outputKey }) {\n      mergeEffect({\n        [outputKey]: provideBatchOutputsEffect,\n      });\n    },\n    onInit(ctx, { outputKey }) {\n      const chainTransformService = ctx.node.getService(ScopeChainTransformService);\n\n      const batchNodeType = ctx.node.flowNodeType;\n\n      const transformerId = `${batchNodeType}-outputs`;\n\n      if (chainTransformService.hasTransformer(transformerId)) {\n        return;\n      }\n\n      chainTransformService.registerTransformer(transformerId, {\n        transformCovers: (covers, ctx) => {\n          const node = ctx.scope.meta?.node;\n\n          // Child Node's variable can cover parent\n          if (node?.parent?.flowNodeType === batchNodeType) {\n            return [...covers, getNodeScope(node.parent)];\n          }\n\n          return covers;\n        },\n        transformDeps(scopes, ctx) {\n          const scopeMeta = ctx.scope.meta;\n\n          if (scopeMeta?.type === FlowNodeScopeType.private) {\n            return scopes;\n          }\n\n          const node = scopeMeta?.node;\n\n          // Public of Loop Node depends on child Node\n          if (node?.flowNodeType === batchNodeType) {\n            // Get all child blocks\n            const childBlocks = node.blocks;\n\n            // public scope of all child blocks\n            return [\n              getNodePrivateScope(node),\n              ...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),\n            ];\n          }\n\n          return scopes;\n        },\n      });\n    },\n  });\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/form-plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createBatchOutputsFormPlugin } from './batch-outputs-plugin';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './components';\nexport * from './effects';\nexport * from './utils';\nexport * from './typings';\nexport * from './form-plugins';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/typings/flow-value/config.json",
    "content": "{\n  \"name\": \"flow-value\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/typings/flow-value/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface IFlowConstantValue {\n  type: 'constant';\n  content?: string | number | boolean;\n}\n\nexport interface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n\nexport interface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n}\n\nexport interface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n}\n\nexport type IFlowValue =\n  | IFlowConstantValue\n  | IFlowRefValue\n  | IFlowExpressionValue\n  | IFlowTemplateValue;\n\nexport type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/typings/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-value';\nexport * from './json-schema';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/typings/json-schema/config.json",
    "content": "{\n  \"name\": \"json-schema\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/typings/json-schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type JsonSchemaBasicType =\n  | 'boolean'\n  | 'string'\n  | 'integer'\n  | 'number'\n  | 'object'\n  | 'array'\n  | 'map';\n\nexport interface IJsonSchema<T = string> {\n  type?: T;\n  default?: any;\n  title?: string;\n  description?: string;\n  enum?: (string | number)[];\n  properties?: Record<string, IJsonSchema<T>>;\n  additionalProperties?: IJsonSchema<T>;\n  items?: IJsonSchema<T>;\n  required?: string[];\n  $ref?: string;\n  extra?: {\n    index?: number;\n    // Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak\n    weak?: boolean;\n    // Set the render component\n    formComponent?: string;\n    [key: string]: any;\n  };\n}\n\nexport type IBasicJsonSchema = IJsonSchema<JsonSchemaBasicType>;\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/format-legacy-refs/config.json",
    "content": "{\n  \"name\": \"format-legacy-ref\",\n  \"depMaterials\": [],\n  \"depPackages\": []\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/format-legacy-refs/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isObject } from 'lodash-es';\n\ninterface LegacyFlowRefValueSchema {\n  type: 'ref';\n  content: string;\n}\n\ninterface NewFlowRefValueSchema {\n  type: 'ref';\n  content: string[];\n}\n\n/**\n * In flowgram 0.2.0, for introducing Loop variable functionality,\n * the FlowRefValueSchema type definition is updated:\n *\n * interface LegacyFlowRefValueSchema {\n *  type: 'ref';\n *  content: string;\n * }\n *\n * interface NewFlowRefValueSchema {\n *  type: 'ref';\n *  content: string[];\n * }\n *\n *\n * For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData\n *\n * How to use:\n *\n * 1. Call formatLegacyRefOnSubmit on the formData before submitting\n * 2. Call formatLegacyRefOnInit on the formData after submitting\n *\n * Example:\n * import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n * formMeta: {\n *  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n *  formatOnInit: (data) => formatLegacyRefOnInit(data),\n * }\n */\nexport function formatLegacyRefOnSubmit(value: any): any {\n  if (isObject(value)) {\n    if (isLegacyFlowRefValueSchema(value)) {\n      return formatLegacyRefToNewRef(value);\n    }\n\n    return Object.fromEntries(\n      Object.entries(value).map(([key, value]: [string, any]) => [\n        key,\n        formatLegacyRefOnSubmit(value),\n      ])\n    );\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(formatLegacyRefOnSubmit);\n  }\n\n  return value;\n}\n\n/**\n * In flowgram 0.2.0, for introducing Loop variable functionality,\n * the FlowRefValueSchema type definition is updated:\n *\n * interface LegacyFlowRefValueSchema {\n *  type: 'ref';\n *  content: string;\n * }\n *\n * interface NewFlowRefValueSchema {\n *  type: 'ref';\n *  content: string[];\n * }\n *\n *\n * For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData\n *\n * How to use:\n *\n * 1. Call formatLegacyRefOnSubmit on the formData before submitting\n * 2. Call formatLegacyRefOnInit on the formData after submitting\n *\n * Example:\n * import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n *\n * formMeta: {\n *  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n *  formatOnInit: (data) => formatLegacyRefOnInit(data),\n * }\n */\nexport function formatLegacyRefOnInit(value: any): any {\n  if (isObject(value)) {\n    if (isNewFlowRefValueSchema(value)) {\n      return formatNewRefToLegacyRef(value);\n    }\n\n    return Object.fromEntries(\n      Object.entries(value).map(([key, value]: [string, any]) => [\n        key,\n        formatLegacyRefOnInit(value),\n      ])\n    );\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(formatLegacyRefOnInit);\n  }\n\n  return value;\n}\n\nexport function isLegacyFlowRefValueSchema(value: any): value is LegacyFlowRefValueSchema {\n  return (\n    isObject(value) &&\n    Object.keys(value).length === 2 &&\n    (value as any).type === 'ref' &&\n    typeof (value as any).content === 'string'\n  );\n}\n\nexport function isNewFlowRefValueSchema(value: any): value is NewFlowRefValueSchema {\n  return (\n    isObject(value) &&\n    Object.keys(value).length === 2 &&\n    (value as any).type === 'ref' &&\n    Array.isArray((value as any).content)\n  );\n}\n\nexport function formatLegacyRefToNewRef(value: LegacyFlowRefValueSchema) {\n  const keyPath = value.content.split('.');\n\n  if (keyPath[1] === 'outputs') {\n    return {\n      type: 'ref',\n      content: [`${keyPath[0]}.${keyPath[1]}`, ...(keyPath.length > 2 ? keyPath.slice(2) : [])],\n    };\n  }\n\n  return {\n    type: 'ref',\n    content: keyPath,\n  };\n}\n\nexport function formatNewRefToLegacyRef(value: NewFlowRefValueSchema) {\n  return {\n    type: 'ref',\n    content: value.content.join('.'),\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/format-legacy-refs/readme.md",
    "content": "# Notice\n\nIn `@flowgram.ai/form-materials@0.2.0`, for introducing loop-related materials,\n\nThe FlowRefValueSchema type definition is updated:\n\n```typescript\ninterface LegacyFlowRefValueSchema {\n  type: 'ref';\n  content: string;\n}\n\ninterface NewFlowRefValueSchema {\n  type: 'ref';\n  content: string[];\n}\n```\n\n\n\nFor making sure backend json will not be changed in your application, we provide `format-legacy-ref` utils for upgrading\n\n\nHow to use:\n\n1. Call formatLegacyRefOnSubmit on the formData before submitting\n2. Call formatLegacyRefOnInit on the formData after submitting\n\nExample:\n\n```typescript\nimport { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n\nformMeta: {\n  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n  formatOnInit: (data) => formatLegacyRefOnInit(data),\n}\n```\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './format-legacy-refs';\nexport * from './svg-icon';\nexport * from './json-schema';\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/json-schema/config.json",
    "content": "{\n  \"name\": \"json-schema\",\n  \"depMaterials\": [\n    \"typings/json-schema\"\n  ],\n  \"depPackages\": [\n    \"lodash-es\"\n  ]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/json-schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get } from 'lodash-es';\nimport { ASTFactory, ASTKind, ASTMatch, ASTNode, ASTNodeJSON, BaseType } from '@flowgram.ai/editor';\n\nimport { IJsonSchema } from '../../typings/json-schema';\n\nexport namespace JsonSchemaUtils {\n  /**\n   * Converts a JSON schema to an Abstract Syntax Tree (AST) representation.\n   * This function recursively processes the JSON schema and creates corresponding AST nodes.\n   *\n   * For more information on JSON Schema, refer to the official documentation:\n   * https://json-schema.org/\n   *\n   * @param jsonSchema - The JSON schema to convert.\n   * @returns An AST node representing the JSON schema, or undefined if the schema type is not recognized.\n   */\n  export function schemaToAST(jsonSchema: IJsonSchema): ASTNodeJSON | undefined {\n    const { type, extra } = jsonSchema || {};\n    const { weak = false } = extra || {};\n\n    if (!type) {\n      return undefined;\n    }\n\n    switch (type) {\n      case 'object':\n        if (weak) {\n          return { kind: ASTKind.Object, weak: true };\n        }\n        return ASTFactory.createObject({\n          properties: Object.entries(jsonSchema.properties || {})\n            /**\n             * Sorts the properties of a JSON schema based on the 'extra.index' field.\n             * If the 'extra.index' field is not present, the property will be treated as having an index of 0.\n             */\n            .sort((a, b) => (get(a?.[1], 'extra.index') || 0) - (get(b?.[1], 'extra.index') || 0))\n            .map(([key, _property]) => ({\n              key,\n              type: schemaToAST(_property),\n              meta: {\n                title: _property.title,\n                description: _property.description,\n              },\n            })),\n        });\n      case 'array':\n        if (weak) {\n          return { kind: ASTKind.Array, weak: true };\n        }\n        return ASTFactory.createArray({\n          items: schemaToAST(jsonSchema.items!),\n        });\n      case 'map':\n        if (weak) {\n          return { kind: ASTKind.Map, weak: true };\n        }\n        return ASTFactory.createMap({\n          valueType: schemaToAST(jsonSchema.additionalProperties!),\n        });\n      case 'string':\n        return ASTFactory.createString();\n      case 'number':\n        return ASTFactory.createNumber();\n      case 'boolean':\n        return ASTFactory.createBoolean();\n      case 'integer':\n        return ASTFactory.createInteger();\n\n      default:\n        // If the type is not recognized, return CustomType\n        return ASTFactory.createCustomType({ typeName: type });\n    }\n  }\n\n  /**\n   * Convert AST To JSON Schema\n   * @param typeAST\n   * @returns\n   */\n  export function astToSchema(\n    typeAST: ASTNode,\n    options?: { drilldown?: boolean }\n  ): IJsonSchema | undefined {\n    const { drilldown = true } = options || {};\n\n    if (ASTMatch.isString(typeAST)) {\n      return {\n        type: 'string',\n      };\n    }\n\n    if (ASTMatch.isBoolean(typeAST)) {\n      return {\n        type: 'boolean',\n      };\n    }\n\n    if (ASTMatch.isNumber(typeAST)) {\n      return {\n        type: 'number',\n      };\n    }\n\n    if (ASTMatch.isInteger(typeAST)) {\n      return {\n        type: 'integer',\n      };\n    }\n\n    if (ASTMatch.isObject(typeAST)) {\n      return {\n        type: 'object',\n        properties: drilldown\n          ? Object.fromEntries(\n              typeAST.properties.map((property) => {\n                const schema = astToSchema(property.type);\n\n                if (property.meta?.title && schema) {\n                  schema.title = property.meta.title;\n                }\n                if (property.meta?.description && schema) {\n                  schema.description = property.meta.description;\n                }\n\n                return [property.key, schema!];\n              })\n            )\n          : {},\n      };\n    }\n\n    if (ASTMatch.isArray(typeAST)) {\n      return {\n        type: 'array',\n        items: drilldown ? astToSchema(typeAST.items) : undefined,\n      };\n    }\n\n    if (ASTMatch.isMap(typeAST)) {\n      return {\n        type: 'map',\n        items: drilldown ? astToSchema(typeAST.valueType) : undefined,\n      };\n    }\n\n    if (ASTMatch.isCustomType(typeAST)) {\n      return {\n        type: typeAST.typeName,\n      };\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Check if the AST type is match the JSON Schema\n   * @param typeAST\n   * @param schema\n   * @returns\n   */\n  export function isASTMatchSchema(\n    typeAST: BaseType,\n    schema: IJsonSchema | IJsonSchema[]\n  ): boolean {\n    if (Array.isArray(schema)) {\n      return typeAST.isTypeEqual(\n        ASTFactory.createUnion({\n          types: schema.map((_schema) => schemaToAST(_schema)!).filter(Boolean),\n        })\n      );\n    }\n\n    return typeAST.isTypeEqual(schemaToAST(schema));\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/src/utils/svg-icon/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport function SvgIcon(props: {\n  size?: 'inherit' | 'extra-small' | 'small' | 'default' | 'large' | 'extra-large';\n  svg: React.ReactNode;\n}) {\n  return <span className=\"anticon\">{props.svg}</span>;\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\"\n  },\n  \"include\": [\"./src\", \"./bin/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/materials/form-antd-materials/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-antd-materials/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/materials/form-materials/bin/run.sh",
    "content": "#!/bin/sh\n#  Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n#  SPDX-License-Identifier: MIT\n\necho \"⚠️ 'npx @flowgram.ai/form-materials' is deprecated.\"\necho \"👉 Please use 'npx @flowgram.ai/cli@latest materials' to sync materials\"\nnpx @flowgram.ai/cli@latest materials \"$@\"\n"
  },
  {
    "path": "packages/materials/form-materials/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/form-materials\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/types/index.d.ts\",\n      \"import\": \"./dist/esm/index.mjs\",\n      \"require\": \"./dist/cjs/index.js\"\n    },\n    \"./components/*\": {\n      \"types\": \"./dist/types/components/*/index.d.ts\",\n      \"import\": \"./dist/esm/components/*/index.mjs\",\n      \"require\": \"./dist/cjs/components/*/index.js\"\n    },\n    \"./effects/*\": {\n      \"types\": \"./dist/types/effects/*/index.d.ts\",\n      \"import\": \"./dist/esm/effects/*/index.mjs\",\n      \"require\": \"./dist/cjs/effects/*/index.js\"\n    },\n    \"./hooks/*\": {\n      \"types\": \"./dist/types/hooks/*/index.d.ts\",\n      \"import\": \"./dist/esm/hooks/*/index.mjs\",\n      \"require\": \"./dist/cjs/hooks/*/index.js\"\n    },\n    \"./shared/*\": {\n      \"types\": \"./dist/types/shared/*/index.d.ts\",\n      \"import\": \"./dist/esm/shared/*/index.mjs\",\n      \"require\": \"./dist/cjs/shared/*/index.js\"\n    },\n    \"./form-plugins/*\": {\n      \"types\": \"./dist/types/form-plugins/*/index.d.ts\",\n      \"import\": \"./dist/esm/form-plugins/*/index.mjs\",\n      \"require\": \"./dist/cjs/form-plugins/*/index.js\"\n    },\n    \"./plugins/*\": {\n      \"types\": \"./dist/types/plugins/*/index.d.ts\",\n      \"import\": \"./dist/esm/plugins/*/index.mjs\",\n      \"require\": \"./dist/cjs/plugins/*/index.js\"\n    },\n    \"./validate/*\": {\n      \"types\": \"./dist/types/validate/*/index.d.ts\",\n      \"import\": \"./dist/esm/validate/*/index.mjs\",\n      \"require\": \"./dist/cjs/validate/*/index.js\"\n    }\n  },\n  \"main\": \"./dist/cjs/index.js\",\n  \"module\": \"./dist/esm/index.mjs\",\n  \"types\": \"./dist/types/index.d.ts\",\n  \"sideEffects\": false,\n  \"bin\": {\n    \"flowgram-form-materials\": \"./bin/run.sh\"\n  },\n  \"files\": [\n    \"dist\",\n    \"bin\",\n    \"src\"\n  ],\n  \"scripts\": {\n    \"build\": \"cross-env NODE_ENV=production rslib build\",\n    \"build:fast\": \"cross-env NODE_ENV=development rslib build\",\n    \"build:watch\": \"npm run build:fast\",\n    \"name-export\": \"node scripts/name-export.js\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\",\n    \"run-bin\": \"node bin/index.js\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/editor\": \"workspace:*\",\n    \"@flowgram.ai/json-schema\": \"workspace:*\",\n    \"@flowgram.ai/coze-editor\": \"workspace:*\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"immer\": \"~10.1.1\",\n    \"@codemirror/view\": \"~6.38.0\",\n    \"@codemirror/state\": \"~6.5.2\",\n    \"zod\": \"^3.24.4\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/inquirer\": \"^9.0.9\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\",\n    \"@rslib/core\": \"~0.12.4\",\n    \"cross-env\": \"~7.0.3\",\n    \"@rsbuild/plugin-react\": \"^1.1.1\",\n    \"date-fns\": \"~4.1.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/rslib.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport path from 'path';\n\nimport { defineConfig } from '@rslib/core';\nimport { pluginReact } from '@rsbuild/plugin-react';\n\ntype RsbuildConfig = Parameters<typeof defineConfig>[0];\n\nconst commonConfig: Partial<RsbuildConfig> = {\n  source: {\n    entry: {\n      index: ['./src/**/*.{ts,tsx,css}'],\n    },\n    exclude: [],\n    decorators: {\n      version: 'legacy',\n    },\n  },\n  bundle: false,\n  dts: {\n    distPath: path.resolve(__dirname, './dist/types'),\n    bundle: false,\n    build: true,\n  },\n  tools: {},\n};\n\nconst formats: Partial<RsbuildConfig>[] = [\n  {\n    format: 'esm',\n    output: {\n      distPath: {\n        root: path.resolve(__dirname, './dist/esm'),\n      },\n    },\n  },\n  {\n    dts: false,\n    format: 'cjs',\n    output: {\n      distPath: {\n        root: path.resolve(__dirname, './dist/cjs'),\n      },\n    },\n  },\n].map((r) => ({ ...commonConfig, ...r }));\n\nexport default defineConfig({\n  lib: formats,\n  output: {\n    target: 'web',\n    cleanDistPath: process.env.NODE_ENV === 'production',\n  },\n  plugins: [pluginReact({ swcReactOptions: {} })],\n});\n"
  },
  {
    "path": "packages/materials/form-materials/scripts/name-export.js",
    "content": "#!/usr/bin/env node\n\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\n/**\n * Convert wildcard exports to named exports in index.ts files across multiple folders\n * This script analyzes each exported file and extracts their named exports\n */\n\nconst folders = ['components', 'hooks', 'plugins', 'shared', 'validate', 'form-plugins', 'effects'];\nconst SRC_DIR = path.join(__dirname, '..', 'src');\n\n/**\n * Extract all named exports from a file, distinguishing between values and types\n * @param {string} filePath - Path of the file to analyze\n * @returns {{values: string[], types: string[]}} - Object containing value exports and type exports\n */\nfunction extractNamedExports(filePath) {\n  try {\n    const content = fs.readFileSync(filePath, 'utf-8');\n    const valueExports = [];\n    const typeExports = [];\n\n    // Collect all type definition names\n    const typeDefinitions = new Set();\n    const typePatterns = [\n      /\\b(?:type|interface)\\s+(\\w+)/g,\n      /\\bexport\\s+(?:type|interface)\\s+(\\w+)/g,\n    ];\n\n    let match;\n    for (const pattern of typePatterns) {\n      while ((match = pattern.exec(content)) !== null) {\n        typeDefinitions.add(match[1]);\n      }\n    }\n\n    // Match various export patterns\n    const exportPatterns = [\n      // export const/var/let/function/class/type/interface\n      /\\bexport\\s+(const|var|let|function|class|type|interface)\\s+(\\w+)/g,\n      // export { name1, name2 }\n      /\\bexport\\s*\\{([^}]+)\\}/g,\n      // export { name as alias }\n      /\\bexport\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+(\\w+)[^}]*\\}/g,\n      // export default function name()\n      /\\bexport\\s+default\\s+(?:function|class)\\s+(\\w+)/g,\n      // export type { Type1, Type2 }\n      /\\bexport\\s+type\\s*\\{([^}]+)\\}/g,\n      // export type { Original as Alias }\n      /\\bexport\\s+type\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+(\\w+)[^}]*\\}/g,\n    ];\n\n    // Handle first pattern: export const/var/let/function/class/type/interface\n    exportPatterns[0].lastIndex = 0;\n    while ((match = exportPatterns[0].exec(content)) !== null) {\n      const [, kind, name] = match;\n      if (kind === 'type' || kind === 'interface' || typeDefinitions.has(name)) {\n        typeExports.push(name);\n      } else {\n        valueExports.push(name);\n      }\n    }\n\n    // Handle second pattern: export { name1, name2 }\n    exportPatterns[1].lastIndex = 0;\n    while ((match = exportPatterns[1].exec(content)) !== null) {\n      const exportsList = match[1]\n        .split(',')\n        .map((item) => item.trim())\n        .filter((item) => item && !item.includes(' as '));\n\n      for (const name of exportsList) {\n        if (typeDefinitions.has(name)) {\n          typeExports.push(name);\n        } else {\n          valueExports.push(name);\n        }\n      }\n    }\n\n    // Handle third pattern: export { name as alias }\n    exportPatterns[2].lastIndex = 0;\n    while ((match = exportPatterns[2].exec(content)) !== null) {\n      const [, original, alias] = match;\n      if (typeDefinitions.has(original)) {\n        typeExports.push(alias);\n      } else {\n        valueExports.push(alias);\n      }\n    }\n\n    // Handle fourth pattern: export default function name()\n    exportPatterns[3].lastIndex = 0;\n    while ((match = exportPatterns[3].exec(content)) !== null) {\n      const name = match[1];\n      if (typeDefinitions.has(name)) {\n        typeExports.push(name);\n      } else {\n        valueExports.push(name);\n      }\n    }\n\n    // Handle fifth pattern: export type { Type1, Type2 }\n    exportPatterns[4].lastIndex = 0;\n    while ((match = exportPatterns[4].exec(content)) !== null) {\n      const exportsList = match[1]\n        .split(',')\n        .map((item) => item.trim())\n        .filter((item) => item && !item.includes(' as '));\n\n      for (const name of exportsList) {\n        typeExports.push(name);\n      }\n    }\n\n    // Handle sixth pattern: export type { Original as Alias }\n    exportPatterns[5].lastIndex = 0;\n    while ((match = exportPatterns[5].exec(content)) !== null) {\n      const [, original, alias] = match;\n      typeExports.push(alias);\n    }\n\n    // Deduplicate and sort\n    return {\n      values: [...new Set(valueExports)].sort(),\n      types: [...new Set(typeExports)].sort(),\n    };\n  } catch (error) {\n    console.error(`Failed to read file: ${filePath}`, error.message);\n    return { values: [], types: [] };\n  }\n}\n\n/**\n * Process named export conversion for a single folder\n * @param {string} folderName - Folder name\n * @param {string} baseDir - Base directory\n */\nfunction processFolder(folderName, baseDir = SRC_DIR) {\n  const folderPath = path.join(baseDir, folderName);\n  const indexFile = path.join(folderPath, 'index.ts');\n\n  console.log(`🔍 Processing folder: ${folderName}`);\n\n  try {\n    // Check if folder exists\n    if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) {\n      console.warn(`⚠️  Folder does not exist: ${folderName}`);\n      return;\n    }\n\n    // Generate new named export content\n    let newContent = '';\n\n    // Collect all subdirectory exports\n    const subDirs = fs\n      .readdirSync(folderPath, { withFileTypes: true })\n      .filter((item) => item.isDirectory() && !item.name.startsWith('.'))\n      .map((item) => item.name);\n\n    const namedExportsList = [];\n\n    // Process all subdirectories\n    for (const subDir of subDirs) {\n      const subDirPath = path.join(folderPath, subDir);\n      const subPossiblePaths = [\n        path.join(subDirPath, 'index.ts'),\n        path.join(subDirPath, 'index.tsx'),\n        path.join(subDirPath, `${subDir}.ts`),\n        path.join(subDirPath, `${subDir}.tsx`),\n      ];\n\n      const subFullPath = subPossiblePaths.find(fs.existsSync);\n      if (!subFullPath) continue;\n\n      const { values: subValues, types: subTypes } = extractNamedExports(subFullPath);\n      if (subValues.length === 0 && subTypes.length === 0) continue;\n\n      namedExportsList.push({ importPath: `./${subDir}`, values: subValues, types: subTypes });\n      console.log(\n        `✅ Found exports in ${folderName}/${subDir}:\\n (${subValues.length} values and ${subTypes.length} types)`\n      );\n    }\n\n    // Generate import statements\n    for (const { importPath, values, types } of namedExportsList) {\n      const imports = [];\n      if (values.length > 0) {\n        imports.push(...values);\n      }\n      if (types.length > 0) {\n        imports.push(...types.map((type) => `type ${type}`));\n      }\n\n      if (imports.length > 0) {\n        newContent += `export { ${imports.join(', ')} } from '${importPath}';\n`;\n      }\n    }\n\n    // Write new content\n    fs.writeFileSync(indexFile, newContent);\n    console.log(`✅ Successfully updated ${folderName}/index.ts\\n\\n`);\n  } catch (error) {\n    console.error(`❌ Failed to process ${folderName}:`, error.message);\n    console.error(error.stack);\n  }\n}\n\n/**\n * Main function: Process all configured folders\n */\nfunction convertAllFolders() {\n  console.log('🚀 Starting to process all configured folders...\\n');\n\n  for (const folder of folders) {\n    processFolder(folder);\n  }\n\n  console.log('\\n🎉 All folders processed successfully!');\n\n  processFolder('.');\n\n  console.log('\\n🎉 Index of form materials is updated!');\n}\n\n// If this script is run directly\nif (require.main === module) {\n  convertAllFolders();\n}\n\nmodule.exports = { convertAllFolders, extractNamedExports };\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/assign-row/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IconButton } from '@douyinfe/semi-ui';\nimport { IconMinus } from '@douyinfe/semi-icons';\n\nimport { IFlowConstantRefValue } from '@/shared';\nimport { InjectVariableSelector } from '@/components/variable-selector';\nimport { InjectDynamicValueInput } from '@/components/dynamic-value-input';\nimport { BlurInput } from '@/components/blur-input';\n\nimport { AssignRowProps } from './types';\n\nexport function AssignRow(props: AssignRowProps) {\n  const {\n    value = {\n      operator: 'assign',\n    },\n    onChange,\n    onDelete,\n    readonly,\n  } = props;\n\n  return (\n    <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>\n      <div style={{ width: 150, minWidth: 150, maxWidth: 150 }}>\n        {value?.operator === 'assign' ? (\n          <InjectVariableSelector\n            style={{ width: '100%', height: 26 }}\n            value={value?.left?.content}\n            config={{ placeholder: 'Select Left' }}\n            onChange={(v) =>\n              onChange?.({\n                ...value,\n                left: { type: 'ref', content: v },\n              })\n            }\n          />\n        ) : (\n          <BlurInput\n            style={{ height: 26 }}\n            size=\"small\"\n            placeholder=\"Input Name\"\n            value={value?.left}\n            onChange={(v) =>\n              onChange?.({\n                ...value,\n                left: v,\n              })\n            }\n          />\n        )}\n      </div>\n      <div style={{ flexGrow: 1 }}>\n        <InjectDynamicValueInput\n          readonly={readonly}\n          value={value?.right as IFlowConstantRefValue | undefined}\n          onChange={(v) =>\n            onChange?.({\n              ...value,\n              right: v,\n            })\n          }\n        />\n      </div>\n      {onDelete && (\n        <div>\n          <IconButton\n            size=\"small\"\n            theme=\"borderless\"\n            icon={<IconMinus />}\n            onClick={() => onDelete?.()}\n          />\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport { type AssignValueType } from './types';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/assign-row/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowRefValue, IFlowValue } from '@/shared';\n\nexport type AssignValueType =\n  | {\n      operator: 'assign';\n      left?: IFlowRefValue;\n      right?: IFlowValue;\n    }\n  | {\n      operator: 'declare';\n      left?: string;\n      right?: IFlowValue;\n    };\n\nexport interface AssignRowProps {\n  value?: AssignValueType;\n  onChange?: (value?: AssignValueType) => void;\n  onDelete?: () => void;\n  readonly?: boolean;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/assign-rows/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FieldArray } from '@flowgram.ai/editor';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { AssignRow, AssignValueType } from '@/components/assign-row';\n\ninterface AssignRowsProps {\n  name: string;\n  readonly?: boolean;\n  defaultValue?: AssignValueType[];\n}\n\nexport function AssignRows(props: AssignRowsProps) {\n  const { name, readonly, defaultValue } = props;\n\n  return (\n    <FieldArray<AssignValueType | undefined> name={name} defaultValue={defaultValue}>\n      {({ field }) => (\n        <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>\n          {field.map((childField, index) => (\n            <AssignRow\n              key={childField.key}\n              readonly={readonly}\n              value={childField.value}\n              onChange={(value) => {\n                childField.onChange(value);\n              }}\n              onDelete={() => field.remove(index)}\n            />\n          ))}\n          <div style={{ display: 'flex', gap: 5 }}>\n            <Button\n              size=\"small\"\n              theme=\"borderless\"\n              icon={<IconPlus />}\n              onClick={() => field.append({ operator: 'assign' })}\n            >\n              Assign\n            </Button>\n            <Button\n              size=\"small\"\n              theme=\"borderless\"\n              icon={<IconPlus />}\n              onClick={() => field.append({ operator: 'declare' })}\n            >\n              Declaration\n            </Button>\n          </div>\n        </div>\n      )}\n    </FieldArray>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/batch-outputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button, Input } from '@douyinfe/semi-ui';\nimport { IconDelete, IconPlus } from '@douyinfe/semi-icons';\n\nimport { useObjectList } from '@/hooks';\nimport { InjectVariableSelector } from '@/components/variable-selector';\n\nimport { PropsType } from './types';\nimport './styles.css';\n\nexport function BatchOutputs(props: PropsType) {\n  const { readonly, style } = props;\n\n  const { list, add, updateKey, updateValue, remove } = useObjectList(props);\n\n  return (\n    <div>\n      <div className=\"gedit-m-batch-outputs-rows\" style={style}>\n        {list.map((item) => (\n          <div className=\"gedit-m-batch-outputs-row\" key={item.id}>\n            <Input\n              style={{ width: 100 }}\n              disabled={readonly}\n              size=\"small\"\n              value={item.key}\n              onChange={(v) => updateKey(item.id, v)}\n            />\n            <InjectVariableSelector\n              style={{ flexGrow: 1 }}\n              readonly={readonly}\n              value={item.value?.content}\n              onChange={(v) => updateValue(item.id, { type: 'ref', content: v })}\n            />\n            <Button\n              disabled={readonly}\n              icon={<IconDelete />}\n              size=\"small\"\n              onClick={() => remove(item.id)}\n            />\n          </div>\n        ))}\n      </div>\n      <Button disabled={readonly} icon={<IconPlus />} size=\"small\" onClick={() => add()}>\n        {I18n.t('Add')}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/batch-outputs/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-batch-outputs-rows {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  margin-bottom: 10px;\n}\n\n.gedit-m-batch-outputs-row {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/batch-outputs/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowRefValue } from '@/shared';\n\nexport type ValueType = Record<string, IFlowRefValue | undefined>;\n\nexport interface OutputItem {\n  id: number;\n  key?: string;\n  value?: IFlowRefValue;\n}\n\nexport interface PropsType {\n  value?: ValueType;\n  onChange: (value?: ValueType) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  style?: React.CSSProperties;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/batch-variable-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { PrivateScopeProvider } from '@flowgram.ai/editor';\n\nimport { VariableSelector, VariableSelectorProps } from '@/components/variable-selector';\n\nconst batchVariableSchema: IJsonSchema = {\n  type: 'array',\n  extra: { weak: true },\n};\n\nexport function BatchVariableSelector(props: VariableSelectorProps) {\n  return (\n    <PrivateScopeProvider>\n      <VariableSelector {...props} includeSchema={batchVariableSchema} />\n    </PrivateScopeProvider>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/blur-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\n/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useState } from 'react';\n\nimport { Input } from '@douyinfe/semi-ui';\n\ntype InputProps = React.ComponentPropsWithRef<typeof Input>;\n\nexport function BlurInput(props: InputProps) {\n  const [value, setValue] = useState('');\n\n  useEffect(() => {\n    setValue(props.value as string);\n  }, [props.value]);\n\n  return (\n    <Input\n      ref={props.ref}\n      {...props}\n      value={value}\n      onChange={(value) => {\n        setValue(value);\n      }}\n      onBlur={(e) => {\n        props.onChange?.(value, e);\n        props.onBlur?.(e);\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-all.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CodeEditorFactory } from './factory';\nimport { loadTypescriptLanguage } from './editor-ts';\nimport { loadSqlLanguage } from './editor-sql';\nimport { loadShellLanguage } from './editor-shell';\nimport { loadPythonLanguage } from './editor-python';\nimport { loadJsonLanguage } from './editor-json';\n\nconst languageLoaders: Record<string, (languageId: string) => Promise<any>> = {\n  json: loadJsonLanguage,\n  python: loadPythonLanguage,\n  sql: loadSqlLanguage,\n  typescript: loadTypescriptLanguage,\n  shell: loadShellLanguage,\n};\n\n/**\n * @deprecated CodeEditor will bundle all languages features, use XXXCodeEditor instead for better bundle experience\n */\nexport const CodeEditor = CodeEditorFactory<false>(\n  (languageId) => languageLoaders[languageId]?.(languageId),\n  {\n    displayName: 'CodeEditor',\n    fixLanguageId: undefined,\n  }\n);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-json.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\nimport { mixLanguages } from '@flowgram.ai/coze-editor';\n\nimport { CodeEditorFactory } from './factory';\n\nexport const loadJsonLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-json').then((module) => {\n    languages.register('json', {\n      // mixLanguages is used to solve the problem that interpolation also uses parentheses, which causes incorrect highlighting\n      language: mixLanguages({\n        outerLanguage: module.json.language,\n      }),\n      languageService: module.json.languageService,\n    });\n  });\n\nexport const JsonCodeEditor = CodeEditorFactory<true>(loadJsonLanguage, {\n  displayName: 'JsonCodeEditor',\n  fixLanguageId: 'json',\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-python.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\n\nimport { CodeEditorFactory } from './factory';\n\nexport const loadPythonLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-python').then((module) =>\n    languages.register('python', module.python)\n  );\n\nexport const PythonCodeEditor = CodeEditorFactory<true>(loadPythonLanguage, {\n  displayName: 'PythonCodeEditor',\n  fixLanguageId: 'python',\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-shell.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\n\nimport { CodeEditorFactory } from './factory';\n\nexport const loadShellLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-shell').then((module) =>\n    languages.register('shell', module.shell)\n  );\n\nexport const ShellCodeEditor = CodeEditorFactory<true>(loadShellLanguage, {\n  displayName: 'ShellCodeEditor',\n  fixLanguageId: 'shell',\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-sql.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\nimport { mixLanguages } from '@flowgram.ai/coze-editor';\n\nimport { CodeEditorFactory } from './factory';\n\nexport const loadSqlLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-sql').then((module) => {\n    languages.register('sql', {\n      ...module.sql,\n      language: mixLanguages({\n        outerLanguage: module.sql.language,\n      }),\n    });\n  });\n\nexport const SQLCodeEditor = CodeEditorFactory<true>(loadSqlLanguage, {\n  displayName: 'SQLCodeEditor',\n  fixLanguageId: 'sql',\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor-ts.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\n\nimport { CodeEditorFactory } from './factory';\n\nexport const loadTypescriptLanguage = () =>\n  import('@flowgram.ai/coze-editor/language-typescript').then((module) => {\n    languages.register('typescript', module.typescript);\n\n    // Init TypeScript language service\n    const tsWorker = new Worker(\n      new URL(`@flowgram.ai/coze-editor/language-typescript/worker`, import.meta.url),\n      { type: 'module' }\n    );\n    module.typescript.languageService.initialize(tsWorker, {\n      compilerOptions: {\n        // eliminate Promise error\n        lib: ['es2015', 'dom'],\n        noImplicitAny: false,\n      },\n    });\n  });\n\nexport const TypeScriptCodeEditor = CodeEditorFactory<true>(loadTypescriptLanguage, {\n  displayName: 'TypeScriptCodeEditor',\n  fixLanguageId: 'typescript',\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useRef } from 'react';\n\nimport {\n  ActiveLinePlaceholder,\n  createRenderer,\n  EditorProvider,\n  InferValues,\n} from '@flowgram.ai/coze-editor/react';\nimport preset, { type EditorAPI } from '@flowgram.ai/coze-editor/preset-code';\nimport { EditorView } from '@codemirror/view';\n\nimport { getSuffixByLanguageId } from './utils';\n\nimport './styles.css';\n\nconst OriginCodeEditor = createRenderer(preset, [\n  EditorView.theme({\n    '&.cm-focused': {\n      outline: 'none',\n    },\n  }),\n]);\n\n// CSS styles are in styles.css\n\ntype Preset = typeof preset;\ntype Options = Partial<InferValues<Preset[number]>>;\n\nexport interface CodeEditorPropsType extends React.PropsWithChildren<{}> {\n  value?: string;\n  onChange?: (value: string) => void;\n  languageId: 'python' | 'typescript' | 'shell' | 'json' | 'sql';\n  theme?: 'dark' | 'light';\n  placeholder?: string;\n  activeLinePlaceholder?: string;\n  readonly?: boolean;\n  options?: Options;\n  mini?: boolean;\n}\n\nexport function BaseCodeEditor({\n  value,\n  onChange,\n  languageId = 'python',\n  theme = 'light',\n  children,\n  placeholder,\n  activeLinePlaceholder,\n  options,\n  readonly,\n  mini,\n}: CodeEditorPropsType) {\n  const editorRef = useRef<EditorAPI | null>(null);\n\n  const editorValue = String(value || '');\n\n  useEffect(() => {\n    // listen to value change\n    if (editorRef.current?.getValue() !== editorValue) {\n      // apply updates on readonly mode\n      const editorView = editorRef.current?.$view;\n      editorView?.dispatch({\n        changes: {\n          from: 0,\n          to: editorView?.state.doc.length,\n          insert: editorValue,\n        },\n      });\n    }\n  }, [editorValue]);\n\n  return (\n    <div className={`gedit-m-code-editor-container ${mini ? 'mini' : ''}`}>\n      <EditorProvider>\n        <OriginCodeEditor\n          defaultValue={editorValue}\n          options={{\n            uri: `file:///untitled${getSuffixByLanguageId(languageId)}`,\n            languageId,\n            theme,\n            placeholder,\n            readOnly: readonly,\n            editable: !readonly,\n            ...(mini\n              ? {\n                  lineNumbersGutter: false,\n                  foldGutter: false,\n                  minHeight: 24,\n                }\n              : {}),\n            ...(options || {}),\n          }}\n          didMount={(editor: EditorAPI) => {\n            editorRef.current = editor;\n          }}\n          onChange={(e) => onChange?.(e.value)}\n        >\n          {activeLinePlaceholder && (\n            <ActiveLinePlaceholder>{activeLinePlaceholder}</ActiveLinePlaceholder>\n          )}\n          {children}\n        </OriginCodeEditor>\n      </EditorProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/factory.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport React from 'react';\n\nimport { languages } from '@flowgram.ai/coze-editor/preset-code';\nimport { Skeleton } from '@douyinfe/semi-ui';\n\nimport { lazySuspense } from '@/shared';\n\nimport type { CodeEditorPropsType } from './editor';\n\nexport const BaseCodeEditor = lazySuspense(() =>\n  Promise.all([import('./editor'), import('./theme')]).then(([editorModule]) => ({\n    default: editorModule.BaseCodeEditor,\n  }))\n);\n\ninterface FactoryParams<FixLanguageId extends boolean> {\n  displayName: string;\n  fixLanguageId: FixLanguageId extends true ? CodeEditorPropsType['languageId'] : undefined;\n}\n\nexport const CodeEditorFactory = <FixLanguageId extends boolean>(\n  loadLanguage: (languageId: string) => Promise<any>,\n  { displayName, fixLanguageId }: FactoryParams<FixLanguageId>\n): FixLanguageId extends true\n  ? React.FC<Omit<CodeEditorPropsType, 'languageId'>>\n  : React.FC<CodeEditorPropsType> => {\n  const EditorWithLoad = (props: CodeEditorPropsType) => {\n    const { languageId = fixLanguageId } = props;\n\n    if (!languageId) {\n      throw new Error('CodeEditorFactory: languageId is required');\n    }\n\n    const [loaded, setLoaded] = useState(useMemo(() => !!languages.get(languageId), [languageId]));\n\n    useEffect(() => {\n      if (!loaded && loadLanguage) {\n        loadLanguage(languageId).then(() => {\n          setLoaded(true);\n        });\n      }\n    }, [languageId, loaded]);\n\n    if (!loaded) {\n      return <Skeleton />;\n    }\n\n    return <BaseCodeEditor {...props} languageId={fixLanguageId || languageId} />;\n  };\n  EditorWithLoad.displayName = displayName;\n\n  return EditorWithLoad as FixLanguageId extends true\n    ? React.FC<Omit<CodeEditorPropsType, 'languageId'>>\n    : React.FC<CodeEditorPropsType>;\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { CodeEditor } from './editor-all';\nexport { TypeScriptCodeEditor } from './editor-ts';\nexport { ShellCodeEditor } from './editor-shell';\nexport { JsonCodeEditor } from './editor-json';\nexport { SQLCodeEditor } from './editor-sql';\nexport { PythonCodeEditor } from './editor-python';\nexport { BaseCodeEditor, type CodeEditorPropsType } from './editor';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-code-editor-container {\n}\n\n.gedit-m-code-editor-container.mini {\n  height: 24px;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/theme/dark.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createTheme, tags as t } from '@flowgram.ai/coze-editor/preset-code';\nimport { type Extension } from '@codemirror/state';\n\nexport const colors = {\n  background: '#24292e',\n  foreground: '#d1d5da',\n  selection: '#3392FF44',\n  cursor: '#c8e1ff',\n  dropdownBackground: '#24292e',\n  dropdownBorder: '#1b1f23',\n  activeLine: '#4d566022',\n  matchingBracket: '#888892',\n  keyword: '#9197F1',\n  storage: '#f97583',\n  variable: '#ffab70',\n  variableName: '#D9DCFA',\n  parameter: '#e1e4e8',\n  function: '#FFCA66',\n  string: '#FF9878',\n  constant: '#79b8ff',\n  type: '#79b8ff',\n  class: '#b392f0',\n  number: '#2EC7D9',\n  comment: '#568B2A',\n  heading: '#79b8ff',\n  invalid: '#f97583',\n  regexp: '#9ecbff',\n  propertyName: '#9197F1',\n  separator: '#888892',\n  gutters: '#888892',\n  moduleKeyword: '#CC4FD4',\n};\n\nexport const darkTheme: Extension = createTheme({\n  variant: 'dark',\n  settings: {\n    background: colors.background,\n    foreground: colors.foreground,\n    caret: colors.cursor,\n    selection: colors.selection,\n    gutterBackground: colors.background,\n    gutterForeground: colors.foreground,\n    gutterBorderColor: 'transparent',\n    gutterBorderWidth: 0,\n    lineHighlight: 'transparent',\n    bracketColors: ['#FBBF24', '#A78BFA', '#7DD3FC'],\n    tooltip: {\n      backgroundColor: '#21262D',\n      color: '#E6EDF3',\n      border: '1px solid #30363D',\n    },\n    link: {\n      color: '#58A6FF',\n    },\n    completionItemHover: {\n      backgroundColor: '#21262D',\n    },\n    completionItemSelected: {\n      backgroundColor: colors.selection,\n      color: colors.foreground,\n    },\n    completionItemIcon: {\n      color: '#8B949E',\n    },\n    completionItemLabel: {\n      color: '#E6EDF3',\n    },\n    completionItemInfo: {\n      color: '#8B949E',\n    },\n    completionItemDetail: {\n      color: '#6E7681',\n    },\n  },\n  styles: [\n    { tag: t.keyword, color: colors.keyword },\n    { tag: t.variableName, color: colors.variableName },\n    {\n      tag: [t.name, t.deleted, t.character, t.macroName],\n      color: colors.variable,\n    },\n    { tag: [t.propertyName], color: colors.propertyName },\n    {\n      tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)],\n      color: colors.string,\n    },\n    {\n      tag: [t.function(t.variableName), t.function(t.propertyName), t.labelName],\n      color: colors.function,\n    },\n    {\n      tag: [t.moduleKeyword, t.controlKeyword],\n      color: colors.moduleKeyword,\n    },\n    {\n      tag: [t.color, t.constant(t.name), t.standard(t.name)],\n      color: colors.constant,\n    },\n    { tag: t.definition(t.name), color: colors.variable },\n    { tag: [t.className], color: colors.class },\n    {\n      tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],\n      color: colors.number,\n    },\n    { tag: [t.typeName], color: colors.type, fontStyle: colors.type },\n    { tag: [t.operatorKeyword], color: colors.keyword },\n    { tag: [t.url, t.escape, t.regexp, t.link], color: colors.regexp },\n    { tag: [t.meta, t.comment], color: colors.comment },\n    { tag: t.strong, fontWeight: 'bold' },\n    { tag: t.emphasis, fontStyle: 'italic' },\n    { tag: t.link, textDecoration: 'underline' },\n    { tag: t.heading, fontWeight: 'bold', color: colors.heading },\n    { tag: [t.atom, t.bool, t.special(t.variableName)], color: colors.variable },\n    { tag: t.invalid, color: colors.invalid },\n    { tag: t.strikethrough, textDecoration: 'line-through' },\n    { tag: t.separator, color: colors.separator },\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/theme/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { themes } from '@flowgram.ai/coze-editor/preset-code';\n\nimport { lightTheme } from './light';\nimport { darkTheme } from './dark';\n\nthemes.register('dark', darkTheme);\nthemes.register('light', lightTheme);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/theme/light.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createTheme, tags as t } from '@flowgram.ai/coze-editor/preset-code';\nimport { type Extension } from '@codemirror/state';\n\nexport const colors = {\n  background: '#f4f5f5',\n  foreground: '#444d56',\n  selection: '#0366d625',\n  cursor: '#044289',\n  dropdownBackground: '#fff',\n  dropdownBorder: '#e1e4e8',\n  activeLine: '#c6c6c622',\n  matchingBracket: '#34d05840',\n  keyword: '#d73a49',\n  storage: '#d73a49',\n  variable: '#e36209',\n  parameter: '#24292e',\n  function: '#005cc5',\n  string: '#032f62',\n  constant: '#005cc5',\n  type: '#005cc5',\n  class: '#6f42c1',\n  number: '#005cc5',\n  comment: '#6a737d',\n  heading: '#005cc5',\n  invalid: '#cb2431',\n  regexp: '#032f62',\n};\n\nexport const lightTheme: Extension = createTheme({\n  variant: 'light',\n  settings: {\n    background: colors.background,\n    foreground: colors.foreground,\n    caret: colors.cursor,\n    selection: colors.selection,\n    gutterBackground: colors.background,\n    gutterForeground: colors.foreground,\n    gutterBorderColor: 'transparent',\n    gutterBorderWidth: 0,\n    lineHighlight: 'transparent',\n    bracketColors: ['#F59E0B', '#8B5CF6', '#06B6D4'],\n    tooltip: {\n      backgroundColor: colors.dropdownBackground,\n      color: colors.foreground,\n      border: 'none',\n      boxShadow: '0 0 1px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .1)!important',\n      maxWidth: '400px',\n    },\n    link: {\n      color: '#2563EB',\n      caret: colors.cursor,\n    },\n    completionItemHover: {\n      backgroundColor: '#F3F4F6',\n    },\n    completionItemSelected: {\n      backgroundColor: colors.selection,\n      color: colors.foreground,\n    },\n    completionItemIcon: {\n      color: '#4B5563',\n    },\n    completionItemLabel: {\n      color: '#1F2937',\n    },\n    completionItemInfo: {\n      color: '#4B5563',\n    },\n    completionItemDetail: {\n      color: '#6B7280',\n    },\n  },\n  styles: [\n    { tag: t.keyword, color: colors.keyword },\n    {\n      tag: [t.name, t.deleted, t.character, t.macroName],\n      color: colors.variable,\n    },\n    { tag: [t.propertyName], color: colors.function },\n    {\n      tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)],\n      color: colors.string,\n    },\n    { tag: [t.function(t.variableName), t.labelName], color: colors.function },\n    {\n      tag: [t.color, t.constant(t.name), t.standard(t.name)],\n      color: colors.constant,\n    },\n    { tag: [t.definition(t.name), t.separator], color: colors.variable },\n    { tag: [t.className], color: colors.class },\n    {\n      tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],\n      color: colors.number,\n    },\n    { tag: [t.typeName], color: colors.type, fontStyle: colors.type },\n    { tag: [t.operator, t.operatorKeyword], color: colors.keyword },\n    { tag: [t.url, t.escape, t.regexp, t.link], color: colors.regexp },\n    { tag: [t.meta, t.comment], color: colors.comment },\n    { tag: t.strong, fontWeight: 'bold' },\n    { tag: t.emphasis, fontStyle: 'italic' },\n    { tag: t.link, textDecoration: 'underline' },\n    { tag: t.heading, fontWeight: 'bold', color: colors.heading },\n    { tag: [t.atom, t.bool, t.special(t.variableName)], color: colors.variable },\n    { tag: t.invalid, color: colors.invalid },\n    { tag: t.strikethrough, textDecoration: 'line-through' },\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function getSuffixByLanguageId(languageId: string) {\n  if (languageId === 'python') {\n    return '.py';\n  }\n  if (languageId === 'typescript') {\n    return '.ts';\n  }\n  if (languageId === 'shell') {\n    return '.sh';\n  }\n  if (languageId === 'json') {\n    return '.json';\n  }\n  if (languageId === 'sql') {\n    return '.sql';\n  }\n\n  return '';\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/code-editor-mini/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { CodeEditor, type CodeEditorPropsType } from '@/components/code-editor';\n\n/**\n * @deprecated use mini in CodeEditorPropsType instead\n */\nexport function CodeEditorMini(props: CodeEditorPropsType) {\n  return (\n    <div className=\"gedit-m-code-editor-mini\">\n      <CodeEditor\n        {...props}\n        options={{\n          lineNumbersGutter: false,\n          foldGutter: false,\n          minHeight: 24,\n          ...(props.options || {}),\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-context/context.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { createContext, useContext } from 'react';\n\nimport { IConditionRule, ConditionOpConfigs } from './types';\nimport { defaultConditionOpConfigs } from './op';\n\ninterface ContextType {\n  rules?: Record<string, IConditionRule>;\n  ops?: ConditionOpConfigs;\n}\n\nexport const ConditionContext = createContext<ContextType>({\n  rules: {},\n  ops: defaultConditionOpConfigs,\n});\n\nexport const ConditionProvider = (props: React.PropsWithChildren<ContextType>) => {\n  const { rules, ops } = props;\n  return (\n    <ConditionContext.Provider value={{ rules, ops }}>{props.children}</ConditionContext.Provider>\n  );\n};\n\nexport const useConditionContext = () => useContext(ConditionContext);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-context/hooks/use-condition.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useRef } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { useTypeManager } from '@/plugins';\n\nimport { IConditionRule, ConditionOpConfigs } from '../types';\nimport { useConditionContext } from '../context';\n\ninterface HooksParams {\n  /**\n   * Left schema of condition\n   */\n  leftSchema?: IJsonSchema;\n\n  /**\n   * Operator of condition\n   */\n  operator?: string;\n\n  /**\n   * If op is not in opOptionList, clear it\n   */\n  onClearOp?: () => void;\n\n  /**\n   * If targetSchema updated, clear it\n   */\n  onClearRight?: () => void;\n\n  /**\n   * @deprecated use ConditionProvider instead\n   * custom rule config\n   */\n  ruleConfig?: {\n    ops?: ConditionOpConfigs;\n    rules?: Record<string, IConditionRule>;\n  };\n}\n\nexport function useCondition({\n  leftSchema,\n  operator,\n  onClearOp,\n  onClearRight,\n  ruleConfig,\n}: HooksParams) {\n  const typeManager = useTypeManager();\n  const { rules: contextRules, ops: contextOps } = useConditionContext();\n\n  // Merge user rules and context rules\n  const userRules = useMemo(\n    () => ruleConfig?.rules || contextRules || {},\n    [contextRules, ruleConfig?.rules]\n  );\n\n  // Merge user operators and context operators\n  const allOps = useMemo(() => ruleConfig?.ops || contextOps || {}, [contextOps, ruleConfig?.ops]);\n\n  // Get type configuration\n  const config = useMemo(\n    () => (leftSchema ? typeManager.getTypeBySchema(leftSchema) : undefined),\n    [leftSchema, typeManager]\n  );\n\n  // Calculate rule\n  const rule = useMemo(() => {\n    if (!config) {\n      return undefined;\n    }\n    if (userRules[config.type]) {\n      return userRules[config.type];\n    }\n    if (typeof config.conditionRule === 'function') {\n      return config.conditionRule(leftSchema);\n    }\n    return config.conditionRule;\n  }, [userRules, leftSchema, config]);\n\n  // Calculate operator option list\n  const opOptionList = useMemo(\n    () =>\n      Object.keys(rule || {})\n        .filter((_op) => allOps[_op])\n        .map((_op) => ({\n          ...(allOps?.[_op] || {}),\n          value: _op,\n          label: I18n.t(allOps?.[_op]?.label || _op),\n        })),\n    [rule, allOps]\n  );\n\n  // When op not in list, clear it\n  useEffect(() => {\n    if (!operator || !rule) {\n      return;\n    }\n    if (!opOptionList.find((item) => item.value === operator)) {\n      onClearOp?.();\n    }\n  }, [operator, opOptionList, onClearOp]);\n\n  // get target schema\n  const targetSchema = useMemo(() => {\n    const targetType: string | IJsonSchema | null = rule?.[operator || ''] || null;\n\n    if (!targetType) {\n      return undefined;\n    }\n\n    if (typeof targetType === 'string') {\n      return { type: targetType, extra: { weak: true } };\n    }\n\n    return targetType;\n  }, [rule, operator]);\n\n  const prevTargetSchemaRef = useRef<IJsonSchema | undefined>(undefined);\n\n  // When type of target schema updated, clear it\n  useEffect(() => {\n    if (!prevTargetSchemaRef.current) {\n      prevTargetSchemaRef.current = targetSchema;\n      return;\n    }\n    if (prevTargetSchemaRef.current?.type !== targetSchema?.type) {\n      onClearRight?.();\n    }\n    prevTargetSchemaRef.current = targetSchema;\n  }, [targetSchema, onClearRight]);\n\n  // get current operator config\n  const opConfig = useMemo(() => allOps[operator || ''], [operator, allOps]);\n\n  return {\n    rule,\n    opConfig,\n    opOptionList,\n    targetSchema,\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-context/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  type IConditionRule,\n  type IConditionRuleFactory,\n  type ConditionOpConfigs,\n  type ConditionOpConfig,\n} from './types';\nexport { ConditionPresetOp } from './op';\nexport { ConditionProvider, useConditionContext } from './context';\nexport { useCondition } from './hooks/use-condition';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-context/op.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ConditionOpConfigs } from './types';\n\nexport enum ConditionPresetOp {\n  EQ = 'eq',\n  NEQ = 'neq',\n  GT = 'gt',\n  GTE = 'gte',\n  LT = 'lt',\n  LTE = 'lte',\n  IN = 'in',\n  NIN = 'nin',\n  CONTAINS = 'contains',\n  NOT_CONTAINS = 'not_contains',\n  IS_EMPTY = 'is_empty',\n  IS_NOT_EMPTY = 'is_not_empty',\n  IS_TRUE = 'is_true',\n  IS_FALSE = 'is_false',\n}\n\nexport const defaultConditionOpConfigs: ConditionOpConfigs = {\n  [ConditionPresetOp.EQ]: {\n    label: 'Equal',\n    abbreviation: '=',\n  },\n  [ConditionPresetOp.NEQ]: {\n    label: 'Not Equal',\n    abbreviation: '≠',\n  },\n  [ConditionPresetOp.GT]: {\n    label: 'Greater Than',\n    abbreviation: '>',\n  },\n  [ConditionPresetOp.GTE]: {\n    label: 'Greater Than or Equal',\n    abbreviation: '>=',\n  },\n  [ConditionPresetOp.LT]: {\n    label: 'Less Than',\n    abbreviation: '<',\n  },\n  [ConditionPresetOp.LTE]: {\n    label: 'Less Than or Equal',\n    abbreviation: '<=',\n  },\n  [ConditionPresetOp.IN]: {\n    label: 'In',\n    abbreviation: '∈',\n  },\n  [ConditionPresetOp.NIN]: {\n    label: 'Not In',\n    abbreviation: '∉',\n  },\n  [ConditionPresetOp.CONTAINS]: {\n    label: 'Contains',\n    abbreviation: '⊇',\n  },\n  [ConditionPresetOp.NOT_CONTAINS]: {\n    label: 'Not Contains',\n    abbreviation: '⊉',\n  },\n  [ConditionPresetOp.IS_EMPTY]: {\n    label: 'Is Empty',\n    abbreviation: '=',\n    rightDisplay: 'Empty',\n  },\n  [ConditionPresetOp.IS_NOT_EMPTY]: {\n    label: 'Is Not Empty',\n    abbreviation: '≠',\n    rightDisplay: 'Empty',\n  },\n  [ConditionPresetOp.IS_TRUE]: {\n    label: 'Is True',\n    abbreviation: '=',\n    rightDisplay: 'True',\n  },\n  [ConditionPresetOp.IS_FALSE]: {\n    label: 'Is False',\n    abbreviation: '=',\n    rightDisplay: 'False',\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-context/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport interface ConditionOpConfig {\n  label: string;\n  abbreviation: string;\n  // When right is not a value, display this text\n  rightDisplay?: string;\n}\n\nexport type OpKey = string;\n\nexport type ConditionOpConfigs = Record<OpKey, ConditionOpConfig>;\n\nexport type IConditionRule = Record<OpKey, string | IJsonSchema | null>;\nexport type IConditionRuleFactory = (\n  schema?: IJsonSchema\n) => Record<OpKey, string | IJsonSchema | null>;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-row/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport { I18n, useScopeAvailable } from '@flowgram.ai/editor';\nimport { Button, Input, Select } from '@douyinfe/semi-ui';\nimport { IconChevronDownStroked } from '@douyinfe/semi-icons';\n\nimport { InjectVariableSelector } from '@/components/variable-selector';\nimport { InjectDynamicValueInput } from '@/components/dynamic-value-input';\nimport { IConditionRule, ConditionOpConfigs, useCondition } from '@/components/condition-context';\n\nimport { ConditionRowValueType } from './types';\nimport './styles.css';\n\ninterface PropTypes {\n  value?: ConditionRowValueType;\n  onChange: (value?: ConditionRowValueType) => void;\n  style?: React.CSSProperties;\n  readonly?: boolean;\n  /**\n   * @deprecated use ConditionContext instead to pass ruleConfig to multiple\n   */\n  ruleConfig?: {\n    ops?: ConditionOpConfigs;\n    rules?: Record<string, IConditionRule>;\n  };\n}\n\nexport function ConditionRow({ style, value, onChange, readonly, ruleConfig }: PropTypes) {\n  const { left, operator, right } = value || {};\n\n  const available = useScopeAvailable();\n\n  const variable = useMemo(() => {\n    if (!left) return undefined;\n    return available.getByKeyPath(left.content);\n  }, [available, left]);\n\n  const leftSchema = useMemo(() => {\n    if (!variable) return undefined;\n    return JsonSchemaUtils.astToSchema(variable.type, { drilldown: false });\n  }, [variable?.type?.hash]);\n\n  const { rule, opConfig, opOptionList, targetSchema } = useCondition({\n    leftSchema,\n    operator,\n    ruleConfig,\n    onClearOp() {\n      onChange({\n        ...value,\n        operator: undefined,\n      });\n    },\n    onClearRight() {\n      onChange({\n        ...value,\n        right: undefined,\n      });\n    },\n  });\n\n  const renderOpSelect = () => (\n    <Select\n      style={{ height: 22 }}\n      disabled={readonly}\n      size=\"small\"\n      value={operator}\n      optionList={opOptionList}\n      onChange={(v) => {\n        onChange({\n          ...value,\n          operator: v as string,\n        });\n      }}\n      triggerRender={({ value }) => (\n        <Button size=\"small\" disabled={!rule}>\n          {opConfig?.abbreviation || <IconChevronDownStroked size=\"small\" />}\n        </Button>\n      )}\n    />\n  );\n\n  return (\n    <div className=\"gedit-m-condition-row-container\" style={style}>\n      <div className=\"gedit-m-condition-row-operator\">{renderOpSelect()}</div>\n      <div className=\"gedit-m-condition-row-values\">\n        <div className=\"gedit-m-condition-row-left\">\n          <InjectVariableSelector\n            readonly={readonly}\n            style={{ width: '100%' }}\n            value={left?.content}\n            onChange={(v) =>\n              onChange({\n                ...value,\n                left: {\n                  type: 'ref',\n                  content: v,\n                },\n              })\n            }\n          />\n        </div>\n        <div className=\"gedit-m-condition-row-right\">\n          {targetSchema ? (\n            <InjectDynamicValueInput\n              readonly={readonly || !rule}\n              value={right}\n              schema={targetSchema}\n              onChange={(v) => onChange({ ...value, right: v })}\n            />\n          ) : (\n            <Input\n              size=\"small\"\n              disabled\n              style={{ pointerEvents: 'none' }}\n              value={opConfig?.rightDisplay || I18n.t('Empty')}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport { type ConditionRowValueType };\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-row/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-condition-row-container {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.gedit-m-condition-row-operator {\n}\n\n.gedit-m-condition-row-left {\n  width: 100%;\n}\n\n.gedit-m-condition-row-right {\n  width: 100%;\n}\n\n.gedit-m-condition-row-values {\n  flex-grow: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 4px;\n  overflow: hidden;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/condition-row/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue, IFlowRefValue } from '@/shared';\n\nexport interface ConditionRowValueType {\n  left?: IFlowRefValue;\n  operator?: string;\n  right?: IFlowConstantRefValue;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/constant-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React, { useMemo } from 'react';\n\nimport { Input } from '@douyinfe/semi-ui';\n\nimport { useTypeManager } from '@/plugins';\n\nimport { PropsType, Strategy as ConstantInputStrategy } from './types';\n\nexport { type ConstantInputStrategy };\n\nexport function ConstantInput(props: PropsType) {\n  const { value, onChange, schema, strategies, fallbackRenderer, readonly, ...rest } = props;\n\n  const typeManager = useTypeManager();\n\n  const Renderer = useMemo(() => {\n    const strategy = (strategies || []).find((_strategy) => _strategy.hit(schema));\n\n    if (!strategy) {\n      return typeManager.getTypeBySchema(schema)?.ConstantRenderer;\n    }\n\n    return strategy?.Renderer;\n  }, [strategies, schema]);\n\n  if (!Renderer) {\n    if (fallbackRenderer) {\n      return React.createElement(fallbackRenderer, {\n        value,\n        onChange,\n        readonly,\n        ...rest,\n      });\n    }\n    return <Input size=\"small\" disabled placeholder=\"Unsupported type\" />;\n  }\n\n  return <Renderer value={value} onChange={onChange} readonly={readonly} {...rest} />;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/constant-input/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { ConstantRendererProps } from '@/plugins';\n\nexport interface Strategy<Value = any> {\n  hit: (schema: IJsonSchema) => boolean;\n  Renderer: React.FC<ConstantRendererProps<Value>>;\n}\n\nexport interface PropsType extends ConstantRendererProps {\n  schema: IJsonSchema;\n  strategies?: Strategy[];\n  fallbackRenderer?: React.FC<ConstantRendererProps>;\n  [key: string]: any;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/coze-editor-extensions/extensions/inputs-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useEffect, useState } from 'react';\n\nimport { isPlainObject, last } from 'lodash-es';\nimport {\n  type ArrayType,\n  ASTMatch,\n  type BaseType,\n  type BaseVariableField,\n  useCurrentScope,\n} from '@flowgram.ai/editor';\nimport {\n  Mention,\n  MentionOpenChangeEvent,\n  getCurrentMentionReplaceRange,\n  useEditor,\n  PositionMirror,\n} from '@flowgram.ai/coze-editor/react';\nimport { EditorAPI } from '@flowgram.ai/coze-editor/preset-prompt';\nimport { type TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';\nimport { Tree, Popover } from '@douyinfe/semi-ui';\n\nimport { IInputsValues } from '@/shared/flow-value/types';\nimport { FlowValueUtils } from '@/shared';\n\ntype VariableField = BaseVariableField<{ icon?: string | JSX.Element; title?: string }>;\n\nexport function InputsPicker({\n  inputsValues,\n  onSelect,\n}: {\n  inputsValues: IInputsValues;\n  onSelect: (v: string) => void;\n}) {\n  const scope = useCurrentScope();\n\n  const getArrayDrilldown = (type: ArrayType, depth = 1): { type: BaseType; depth: number } => {\n    if (ASTMatch.isArray(type.items)) {\n      return getArrayDrilldown(type.items, depth + 1);\n    }\n\n    return { type: type.items, depth: depth };\n  };\n\n  const renderVariable = (variable: VariableField, keyPath: string[]): TreeNodeData => {\n    let type = variable?.type;\n\n    let children: TreeNodeData[] | undefined;\n\n    if (ASTMatch.isObject(type)) {\n      children = (type.properties || [])\n        .map((_property) => renderVariable(_property as VariableField, [...keyPath, _property.key]))\n        .filter(Boolean) as TreeNodeData[];\n    }\n\n    if (ASTMatch.isArray(type)) {\n      const drilldown = getArrayDrilldown(type);\n\n      if (ASTMatch.isObject(drilldown.type)) {\n        children = (drilldown.type.properties || [])\n          .map((_property) =>\n            renderVariable(_property as VariableField, [\n              ...keyPath,\n              ...new Array(drilldown.depth).fill('[0]'),\n              _property.key,\n            ])\n          )\n          .filter(Boolean) as TreeNodeData[];\n      }\n    }\n\n    const key = keyPath\n      .map((_key, idx) => (_key === '[0]' || idx === 0 ? _key : `.${_key}`))\n      .join('');\n\n    return {\n      key: key,\n      label: last(keyPath),\n      value: key,\n      children,\n    };\n  };\n\n  const getTreeData = (value: any, keyPath: string[]): TreeNodeData | undefined => {\n    const currKey = keyPath.join('.');\n\n    if (FlowValueUtils.isFlowValue(value)) {\n      if (FlowValueUtils.isRef(value)) {\n        const variable = scope?.available?.getByKeyPath(value.content || []);\n        if (variable) {\n          return renderVariable(variable, keyPath);\n        }\n      }\n      return {\n        key: currKey,\n        value: currKey,\n        label: last(keyPath),\n      };\n    }\n\n    if (isPlainObject(value)) {\n      return {\n        key: currKey,\n        value: currKey,\n        label: last(keyPath),\n        children: Object.entries(value)\n          .map(([key, value]) => getTreeData(value, [...keyPath, key])!)\n          .filter(Boolean),\n      };\n    }\n  };\n\n  const treeData: TreeNodeData[] = useMemo(\n    () =>\n      Object.entries(inputsValues)\n        .map(([key, value]) => getTreeData(value, [key])!)\n        .filter(Boolean),\n    []\n  );\n\n  return <Tree treeData={treeData} onSelect={(v) => onSelect(v)} />;\n}\n\nconst DEFAULT_TRIGGER_CHARACTERS = ['{', '{}', '@'];\n\nexport function InputsTree({\n  inputsValues,\n  triggerCharacters = DEFAULT_TRIGGER_CHARACTERS,\n}: {\n  inputsValues: IInputsValues;\n  triggerCharacters?: string[];\n}) {\n  const [posKey, setPosKey] = useState('');\n  const [visible, setVisible] = useState(false);\n  const [position, setPosition] = useState(-1);\n  const editor = useEditor<EditorAPI>();\n\n  function insert(variablePath: string) {\n    const range = getCurrentMentionReplaceRange(editor.$view.state);\n\n    if (!range) {\n      return;\n    }\n\n    /**\n     * When user input {{xxxx}}, {{{xxx}}}(more brackets if possible), replace all brackets with {{xxxx}}\n     */\n    let { from, to } = range;\n    while (editor.$view.state.doc.sliceString(from - 1, from) === '{') {\n      from--;\n    }\n    while (editor.$view.state.doc.sliceString(to, to + 1) === '}') {\n      to++;\n    }\n\n    editor.replaceText({\n      ...range,\n      text: '{{' + variablePath + '}}',\n    });\n\n    setVisible(false);\n  }\n\n  function handleOpenChange(e: MentionOpenChangeEvent) {\n    setPosition(e.state.selection.main.head);\n    setVisible(e.value);\n  }\n\n  useEffect(() => {\n    if (!editor) {\n      return;\n    }\n  }, [editor, visible]);\n\n  return (\n    <>\n      <Mention triggerCharacters={triggerCharacters} onOpenChange={handleOpenChange} />\n\n      <Popover\n        visible={visible}\n        trigger=\"custom\"\n        position=\"topLeft\"\n        rePosKey={posKey}\n        content={\n          <div style={{ width: 300, maxHeight: 300, overflowY: 'auto' }}>\n            <InputsPicker\n              inputsValues={inputsValues}\n              onSelect={(v) => {\n                insert(v);\n              }}\n            />\n          </div>\n        }\n      >\n        {/* PositionMirror allows the Popover to appear at the specified cursor position */}\n        <PositionMirror\n          position={position}\n          // When Doc scroll, update position\n          onChange={() => setPosKey(String(Math.random()))}\n        />\n      </Popover>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/coze-editor-extensions/extensions/variable-tag.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useLayoutEffect } from 'react';\n\nimport { isEqual, last } from 'lodash-es';\nimport {\n  BaseVariableField,\n  Disposable,\n  DisposableCollection,\n  Scope,\n  useCurrentScope,\n} from '@flowgram.ai/editor';\nimport { useInjector } from '@flowgram.ai/coze-editor/react';\nimport { Popover, Tag } from '@douyinfe/semi-ui';\nimport { IconIssueStroked } from '@douyinfe/semi-icons';\nimport {\n  Decoration,\n  DecorationSet,\n  EditorView,\n  MatchDecorator,\n  ViewPlugin,\n  WidgetType,\n} from '@codemirror/view';\n\nimport { IPolyfillRoot, polyfillCreateRoot } from '@/shared';\n\nimport '../styles.css';\n\nclass VariableTagWidget extends WidgetType {\n  keyPath?: string[];\n\n  toDispose = new DisposableCollection();\n\n  scope: Scope;\n\n  root: IPolyfillRoot;\n\n  constructor({ keyPath, scope }: { keyPath?: string[]; scope: Scope }) {\n    super();\n\n    this.keyPath = keyPath;\n    this.scope = scope;\n  }\n\n  renderIcon = (icon: string | JSX.Element) => {\n    if (typeof icon === 'string') {\n      return <img style={{ marginRight: 8 }} width={12} height={12} src={icon} />;\n    }\n\n    return icon;\n  };\n\n  renderVariable(v?: BaseVariableField) {\n    if (!v) {\n      this.root.render(\n        <Tag className=\"gedit-m-coze-editor-tag\" color=\"amber\">\n          <IconIssueStroked style={{ marginRight: '4px' }} />\n          <span>Unknown</span>\n        </Tag>\n      );\n      return;\n    }\n\n    const rootField = last(v.parentFields) || v;\n    const isRoot = v === rootField;\n\n    const rootTitle = (\n      <span className=\"gedit-m-coze-editor-root-title\">\n        {rootField.meta?.title ? `${rootField.meta.title} ${isRoot ? '' : '-'} ` : ''}\n      </span>\n    );\n    const rootIcon = this.renderIcon(rootField?.meta.icon);\n\n    this.root.render(\n      <Popover\n        content={\n          <div className=\"gedit-m-coze-editor-popover-content\">\n            {rootIcon}\n            {rootTitle}\n            <span className=\"gedit-m-coze-editor-var-name\">{v?.keyPath.slice(1).join('.')}</span>\n          </div>\n        }\n      >\n        <Tag\n          className=\"gedit-m-coze-editor-tag\"\n          style={{ display: 'inline-flex', alignItems: 'center' }}\n        >\n          {rootIcon}\n          {rootTitle}\n          {!isRoot && <span className=\"gedit-m-coze-editor-var-name\">{v?.key}</span>}\n        </Tag>\n      </Popover>\n    );\n  }\n\n  toDOM(view: EditorView): HTMLElement {\n    const dom = document.createElement('span');\n\n    this.root = polyfillCreateRoot(dom);\n\n    this.toDispose.push(\n      Disposable.create(() => {\n        this.root.unmount();\n      })\n    );\n\n    const refresh = () => {\n      this.renderVariable(this.scope.available.getByKeyPath(this.keyPath));\n    };\n\n    this.toDispose.push(\n      this.scope.available.trackByKeyPath(this.keyPath, refresh, { triggerOnInit: false })\n    );\n\n    if (this.keyPath?.[0]) {\n      this.toDispose.push(\n        // listen to root title changed\n        this.scope.available.trackByKeyPath<{ title?: string }>([this.keyPath[0]], refresh, {\n          selector: (curr) => ({ ...curr?.meta }),\n          triggerOnInit: false,\n        })\n      );\n    }\n\n    refresh();\n\n    return dom;\n  }\n\n  eq(other: VariableTagWidget) {\n    return isEqual(this.keyPath, other.keyPath);\n  }\n\n  ignoreEvent(): boolean {\n    return false;\n  }\n\n  destroy(dom: HTMLElement): void {\n    this.toDispose.dispose();\n  }\n}\n\nexport function VariableTagInject() {\n  const injector = useInjector();\n\n  const scope = useCurrentScope({ strict: true });\n\n  // 基于 {{var}} 的正则进行匹配，匹配后进行自定义渲染\n  useLayoutEffect(() => {\n    const atMatcher = new MatchDecorator({\n      regexp: /\\{\\{([^\\}\\{]+)\\}\\}/g,\n      decoration: (match) =>\n        Decoration.replace({\n          widget: new VariableTagWidget({\n            keyPath: match[1]?.split('.') ?? [],\n            scope,\n          }),\n        }),\n    });\n\n    return injector.inject([\n      ViewPlugin.fromClass(\n        class {\n          decorations: DecorationSet;\n\n          constructor(private view: EditorView) {\n            this.decorations = atMatcher.createDeco(view);\n          }\n\n          update() {\n            this.decorations = atMatcher.createDeco(this.view);\n          }\n        },\n        {\n          decorations: (p) => p.decorations,\n          provide(p) {\n            return EditorView.atomicRanges.of(\n              (view) => view.plugin(p)?.decorations ?? Decoration.none\n            );\n          },\n        }\n      ),\n    ]);\n  }, [injector]);\n\n  return null;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/coze-editor-extensions/extensions/variable-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\n\nimport { debounce } from 'lodash-es';\nimport {\n  Mention,\n  MentionOpenChangeEvent,\n  getCurrentMentionReplaceRange,\n  useEditor,\n  PositionMirror,\n} from '@flowgram.ai/coze-editor/react';\nimport { EditorAPI } from '@flowgram.ai/coze-editor/preset-prompt';\nimport { Popover, Tree } from '@douyinfe/semi-ui';\n\nimport { useVariableTree } from '@/components/variable-selector';\n\nconst DEFAULT_TRIGGER_CHARACTER = ['{', '{}', '@'];\n\nexport function VariableTree({\n  triggerCharacters = DEFAULT_TRIGGER_CHARACTER,\n}: {\n  triggerCharacters?: string[];\n}) {\n  const [posKey, setPosKey] = useState('');\n  const [visible, setVisible] = useState(false);\n  const [position, setPosition] = useState(-1);\n  const editor = useEditor<EditorAPI>();\n\n  function insert(variablePath: string) {\n    const range = getCurrentMentionReplaceRange(editor.$view.state);\n\n    if (!range) {\n      return;\n    }\n\n    /**\n     * When user input {{xxxx}}, {{{xxx}}}(more brackets if possible), replace all brackets with {{xxxx}}\n     */\n    let { from, to } = range;\n    while (editor.$view.state.doc.sliceString(from - 1, from) === '{') {\n      from--;\n    }\n    while (editor.$view.state.doc.sliceString(to, to + 1) === '}') {\n      to++;\n    }\n\n    editor.replaceText({\n      from,\n      to,\n      text: '{{' + variablePath + '}}',\n    });\n\n    setVisible(false);\n  }\n\n  function handleOpenChange(e: MentionOpenChangeEvent) {\n    setPosition(e.state.selection.main.head);\n    setVisible(e.value);\n  }\n\n  useEffect(() => {\n    if (!editor) {\n      return;\n    }\n  }, [editor, visible]);\n\n  const treeData = useVariableTree({});\n\n  const debounceUpdatePosKey = useCallback(\n    debounce(() => setPosKey(String(Math.random())), 100),\n    []\n  );\n\n  return (\n    <>\n      <Mention triggerCharacters={triggerCharacters} onOpenChange={handleOpenChange} />\n\n      <Popover\n        visible={visible}\n        trigger=\"custom\"\n        position=\"topLeft\"\n        rePosKey={posKey}\n        content={\n          <div style={{ width: 300, maxHeight: 300, overflowY: 'auto' }}>\n            <Tree\n              treeData={treeData}\n              onExpand={() => {\n                // When Expand, an animation is triggered, so we need to update the position by debounce\n                debounceUpdatePosKey();\n              }}\n              onSelect={(v) => {\n                insert(v);\n              }}\n            />\n          </div>\n        }\n      >\n        {/* PositionMirror allows the Popover to appear at the specified cursor position */}\n        <PositionMirror\n          position={position}\n          // When Doc scroll, update position\n          onChange={() => {\n            // Update immediately to avoid the popover position lagging behind the cursor\n            setPosKey(String(Math.random()));\n          }}\n        />\n      </Popover>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/coze-editor-extensions/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { lazy } from 'react';\n\nimport { createInjectMaterial } from '@/shared';\n\nexport const EditorVariableTree = createInjectMaterial(\n  lazy(() =>\n    import('./extensions/variable-tree').then((module) => ({ default: module.VariableTree }))\n  ),\n  {\n    renderKey: 'EditorVariableTree',\n  }\n);\n\nexport const EditorVariableTagInject = createInjectMaterial(\n  lazy(() =>\n    import('./extensions/variable-tag').then((module) => ({ default: module.VariableTagInject }))\n  ),\n  {\n    renderKey: 'EditorVariableTagInject',\n  }\n);\n\nexport const EditorInputsTree = createInjectMaterial(\n  lazy(() => import('./extensions/inputs-tree').then((module) => ({ default: module.InputsTree }))),\n  {\n    renderKey: 'EditorInputsTree',\n  }\n);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/coze-editor-extensions/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-coze-editor-root-title {\n  margin-right: 4px;\n  min-width: 20px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  color: var(--semi-color-text-2);\n}\n\n.gedit-m-coze-editor-var-name {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.gedit-m-coze-editor-tag {\n  display: inline-flex;\n  align-items: center;\n  justify-content: flex-start;\n  max-width: 300px;\n\n  & .semi-tag-content-center {\n    justify-content: flex-start;\n  }\n\n  &.semi-tag {\n    margin: 0 5px;\n  }\n}\n\n.gedit-m-coze-editor-popover-content {\n  padding: 10px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/db-condition-row/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button, Icon, Input, Select } from '@douyinfe/semi-ui';\nimport { IconChevronDownStroked } from '@douyinfe/semi-icons';\n\nimport { useTypeManager } from '@/plugins';\nimport { InjectDynamicValueInput } from '@/components/dynamic-value-input';\nimport {\n  useCondition,\n  type ConditionOpConfigs,\n  type IConditionRule,\n} from '@/components/condition-context';\n\nimport { DBConditionOptionType, DBConditionRowValueType } from './types';\nimport './styles.css';\n\ninterface PropTypes {\n  value?: DBConditionRowValueType;\n  onChange: (value?: DBConditionRowValueType) => void;\n  style?: React.CSSProperties;\n  options?: DBConditionOptionType[];\n  readonly?: boolean;\n  /**\n   * @deprecated use ConditionContext instead to pass ruleConfig to multiple\n   */\n  ruleConfig?: {\n    ops?: ConditionOpConfigs;\n    rules?: Record<string, IConditionRule>;\n  };\n}\n\nexport function DBConditionRow({\n  style,\n  value,\n  onChange,\n  readonly,\n  options,\n  ruleConfig,\n}: PropTypes) {\n  const { left, operator, right } = value || {};\n\n  const typeManager = useTypeManager();\n\n  const leftSchema = useMemo(\n    () => options?.find((item) => item.value === left)?.schema,\n    [left, options]\n  );\n\n  const { opConfig, rule, opOptionList, targetSchema } = useCondition({\n    leftSchema,\n    operator,\n    ruleConfig,\n    onClearOp() {\n      onChange({\n        ...value,\n        operator: undefined,\n      });\n    },\n    onClearRight() {\n      onChange({\n        ...value,\n        right: undefined,\n      });\n    },\n  });\n\n  const renderDBOptionSelect = () => (\n    <Select\n      className=\"gedit-m-db-condition-row-select\"\n      disabled={readonly}\n      size=\"small\"\n      style={{ width: '100%' }}\n      value={left}\n      onChange={(v) =>\n        onChange({\n          ...value,\n          left: v as string,\n        })\n      }\n      optionList={\n        options?.map((item) => ({\n          label: (\n            <div className=\"gedit-m-db-condition-row-option-label\">\n              <Icon size=\"small\" svg={typeManager.getDisplayIcon(item.schema)} />\n              {item.label}\n            </div>\n          ),\n          value: item.value,\n        })) || []\n      }\n    />\n  );\n\n  const renderOpSelect = () => (\n    <Select\n      style={{ height: 22 }}\n      disabled={readonly}\n      size=\"small\"\n      value={operator}\n      optionList={opOptionList}\n      onChange={(v) => {\n        onChange({\n          ...value,\n          operator: v as string,\n        });\n      }}\n      triggerRender={({ value }) => (\n        <Button size=\"small\" disabled={!rule}>\n          {opConfig?.abbreviation || <IconChevronDownStroked size=\"small\" />}\n        </Button>\n      )}\n    />\n  );\n\n  return (\n    <div className=\"gedit-m-db-condition-row-container\" style={style}>\n      <div className=\"gedit-m-db-condition-row-operator\">{renderOpSelect()}</div>\n      <div className=\"gedit-m-db-condition-row-values\">\n        <div className=\"gedit-m-db-condition-row-left\">{renderDBOptionSelect()}</div>\n        <div className=\"gedit-m-db-condition-row-right\">\n          {targetSchema ? (\n            <InjectDynamicValueInput\n              readonly={readonly || !rule}\n              value={right}\n              schema={targetSchema}\n              onChange={(v) => onChange({ ...value, right: v })}\n            />\n          ) : (\n            <Input\n              size=\"small\"\n              disabled\n              style={{ pointerEvents: 'none' }}\n              value={opConfig?.rightDisplay || I18n.t('Empty')}\n            />\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport { type DBConditionRowValueType, type DBConditionOptionType };\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/db-condition-row/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-db-condition-row-container {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n}\n\n.gedit-m-db-condition-row-operator {\n}\n\n.gedit-m-db-condition-row-left {\n  width: 100%;\n}\n\n.gedit-m-db-condition-row-right {\n  width: 100%;\n}\n\n.gedit-m-db-condition-row-values {\n  flex-grow: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 4px;\n}\n\n.gedit-m-db-condition-row-option-label {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.gedit-m-db-condition-row-select {\n  & .semi-select-selection {\n    margin-left: 5px;\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/db-condition-row/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { IFlowConstantRefValue } from '@/shared';\n\nexport interface DBConditionRowValueType {\n  left?: string;\n  operator?: string;\n  right?: IFlowConstantRefValue;\n}\n\nexport interface DBConditionOptionType {\n  label: string | JSX.Element;\n  value: string;\n  schema: IJsonSchema;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-flow-value/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { JsonSchemaTypeManager, JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport { useScopeAvailable } from '@flowgram.ai/editor';\n\nimport { IFlowValue } from '@/shared';\nimport { FlowValueUtils } from '@/shared';\nimport { DisplaySchemaTag } from '@/components/display-schema-tag';\n\ninterface PropsType {\n  value?: IFlowValue;\n  title?: JSX.Element | string;\n  showIconInTree?: boolean;\n  typeManager?: JsonSchemaTypeManager;\n}\n\nexport function DisplayFlowValue({ value, title, showIconInTree }: PropsType) {\n  const available = useScopeAvailable();\n\n  const variable = value?.type === 'ref' ? available.getByKeyPath(value?.content) : undefined;\n\n  const schema = useMemo(() => {\n    if (value?.type === 'ref') {\n      return JsonSchemaUtils.astToSchema(variable?.type);\n    }\n    if (value?.type === 'template') {\n      return { type: 'string' };\n    }\n    if (value?.type === 'constant') {\n      return FlowValueUtils.inferConstantJsonSchema(value);\n    }\n\n    return { type: 'unknown' };\n  }, [value, variable?.hash]);\n\n  return (\n    <DisplaySchemaTag\n      title={title}\n      value={schema}\n      showIconInTree={showIconInTree}\n      warning={value?.type === 'ref' && !variable}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-inputs-values/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { isPlainObject } from 'lodash-es';\nimport { useScopeAvailable } from '@flowgram.ai/editor';\n\nimport { IInputsValues } from '@/shared/flow-value';\nimport { FlowValueUtils } from '@/shared';\nimport { DisplayFlowValue } from '@/components/display-flow-value';\n\nimport './styles.css';\nimport { DisplaySchemaTag } from '../display-schema-tag';\n\ninterface PropsType {\n  value?: IInputsValues;\n  showIconInTree?: boolean;\n}\n\nexport function DisplayInputsValues({ value, showIconInTree }: PropsType) {\n  const childEntries = Object.entries(value || {});\n\n  return (\n    <div className=\"gedit-m-display-inputs-wrapper\">\n      {childEntries.map(([key, value]) => {\n        if (FlowValueUtils.isFlowValue(value)) {\n          return (\n            <DisplayFlowValue key={key} title={key} value={value} showIconInTree={showIconInTree} />\n          );\n        }\n\n        if (isPlainObject(value)) {\n          return (\n            <DisplayInputsValueAllInTag\n              key={key}\n              title={key}\n              value={value}\n              showIconInTree={showIconInTree}\n            />\n          );\n        }\n\n        return null;\n      })}\n    </div>\n  );\n}\n\nexport function DisplayInputsValueAllInTag({\n  value,\n  title,\n  showIconInTree,\n}: PropsType & {\n  title: string;\n}) {\n  const available = useScopeAvailable();\n\n  const schema = useMemo(\n    () => FlowValueUtils.inferJsonSchema(value, available.scope),\n    [available.version, value]\n  );\n\n  return <DisplaySchemaTag title={title} value={schema} showIconInTree={showIconInTree} />;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-inputs-values/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-display-inputs-wrapper {\n  display: flex;\n  gap: 5px;\n  flex-wrap: wrap;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-outputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useLayoutEffect } from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeManager, JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport { useCurrentScope, useRefresh } from '@flowgram.ai/editor';\n\nimport { DisplaySchemaTag } from '@/components/display-schema-tag';\n\nimport './styles.css';\n\ninterface PropsType {\n  value?: IJsonSchema;\n  showIconInTree?: boolean;\n  displayFromScope?: boolean;\n  typeManager?: JsonSchemaTypeManager;\n  style?: React.CSSProperties;\n}\n\nexport function DisplayOutputs({ value, showIconInTree, displayFromScope, style }: PropsType) {\n  const scope = useCurrentScope();\n  const refresh = useRefresh();\n\n  useLayoutEffect(() => {\n    if (!displayFromScope || !scope) {\n      return () => null;\n    }\n\n    const disposable = scope.output.onListOrAnyVarChange(() => {\n      refresh();\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, [displayFromScope]);\n\n  const properties: IJsonSchema['properties'] = displayFromScope\n    ? (scope?.output.variables || []).reduce((acm, curr) => {\n        acm = {\n          ...acm,\n          ...(JsonSchemaUtils.astToSchema(curr.type)?.properties || {}),\n        };\n        return acm;\n      }, {})\n    : value?.properties || {};\n\n  const childEntries = Object.entries(properties || {});\n\n  return (\n    <div className=\"gedit-m-display-outputs-wrapper\" style={style}>\n      {childEntries.map(([key, schema]) => (\n        <DisplaySchemaTag\n          key={key}\n          title={key}\n          value={schema}\n          showIconInTree={showIconInTree}\n          warning={!schema}\n        />\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-outputs/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-display-outputs-wrapper {\n  display: flex;\n  gap: 5px;\n  flex-wrap: wrap;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-schema-tag/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Popover, Tag } from '@douyinfe/semi-ui';\n\nimport { useTypeManager } from '@/plugins';\nimport { DisplaySchemaTree } from '@/components/display-schema-tree';\n\nimport './styles.css';\n\ninterface PropsType {\n  title?: JSX.Element | string;\n  value?: IJsonSchema;\n  showIconInTree?: boolean;\n  warning?: boolean;\n}\n\nexport function DisplaySchemaTag({ value = {}, showIconInTree, title, warning }: PropsType) {\n  const typeManager = useTypeManager();\n  const icon =\n    typeManager?.getDisplayIcon(value) || typeManager.getDisplayIcon({ type: 'unknown' });\n\n  return (\n    <Popover\n      content={\n        <div className=\"gedit-m-display-schema-tag-popover-content\">\n          <DisplaySchemaTree value={value} typeManager={typeManager} showIcon={showIconInTree} />\n        </div>\n      }\n    >\n      <Tag color={warning ? 'amber' : 'white'} className=\"gedit-m-display-schema-tag-tag\">\n        {icon &&\n          React.cloneElement(icon, {\n            className: 'tag-icon',\n          })}\n        {title && <span className=\"gedit-m-display-schema-tag-title\">{title}</span>}\n      </Tag>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-schema-tag/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-display-schema-tag-popover-content {\n  padding: 10px;\n}\n\n.gedit-m-display-schema-tag-tag {\n  padding: 4px;\n\n  & .tag-icon {\n    width: 12px;\n    height: 12px;\n  }\n}\n\n.gedit-m-display-schema-tag-title {\n  display: inline-block;\n  margin-left: 4px;\n  margin-top: -1px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-schema-tree/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  type IJsonSchema,\n  type JsonSchemaTypeManager,\n  useTypeManager,\n} from '@flowgram.ai/json-schema';\n\nimport './styles.css';\n\ninterface PropsType {\n  value?: IJsonSchema;\n  parentKey?: string;\n  depth?: number;\n  drilldown?: boolean;\n  showIcon?: boolean;\n  typeManager?: JsonSchemaTypeManager;\n}\n\nexport function DisplaySchemaTree(props: Omit<PropsType, 'parentKey' | 'depth'>) {\n  return <SchemaTree {...props} />;\n}\n\nfunction SchemaTree(props: PropsType) {\n  const {\n    value: schema = {},\n    drilldown = true,\n    depth = 0,\n    showIcon = true,\n    parentKey = '',\n  } = props || {};\n\n  const typeManager = useTypeManager() as JsonSchemaTypeManager;\n\n  const config = typeManager.getTypeBySchema(schema);\n  const title = typeManager.getComplexText(schema);\n  const icon = typeManager?.getDisplayIcon(schema);\n  let properties: IJsonSchema['properties'] =\n    drilldown && config ? config.getTypeSchemaProperties(schema) : {};\n  const childEntries = Object.entries(properties || {});\n\n  return (\n    <div className={`gedit-m-display-schema-tree-item depth-${depth}`} key={parentKey || 'root'}>\n      <div className=\"gedit-m-display-schema-tree-row\">\n        {depth !== 0 && <div className=\"gedit-m-display-schema-tree-horizontal-line\" />}\n        {showIcon &&\n          icon &&\n          React.cloneElement(icon, {\n            className: 'tree-icon',\n          })}\n        <div className=\"gedit-m-display-schema-tree-title\">\n          {parentKey ? (\n            <>\n              {`${parentKey} (`}\n              {title}\n              {')'}\n            </>\n          ) : (\n            title\n          )}\n        </div>\n      </div>\n      {childEntries?.length ? (\n        <div className=\"gedit-m-display-schema-tree-level\">\n          {childEntries.map(([key, value]) => (\n            <SchemaTree key={key} {...props} parentKey={key} value={value} depth={depth + 1} />\n          ))}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/display-schema-tree/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-display-schema-tree-row {\n  display: flex;\n  align-items: center;\n\n  & .tree-icon {\n    margin-right: 8px;\n    width: 14px;\n    height: 14px;\n  }\n\n  height: 27px;\n  white-space: nowrap;\n}\n\n.gedit-m-display-schema-tree-horizontal-line {\n  position: relative;\n\n  &::before,\n  &::after {\n    content: \"\";\n    position: absolute;\n    background-color: var(--semi-color-text-3);\n  }\n\n  &::after {\n    top: 0px;\n    right: 6px;\n    width: 15px;\n    height: 1px;\n  }\n}\n\n.gedit-m-display-schema-tree-title {\n  /* overflow: hidden;\n  text-overflow: ellipsis; */\n}\n\n.gedit-m-display-schema-tree-level {\n  padding-left: 30px;\n  position: relative;\n\n  /* &::before {\n    content: '';\n    position: absolute;\n    background-color: var(--semi-color-text-3);\n    top: 0px;\n    bottom: 0px;\n    left: -22px;\n    width: 1px;\n  } */\n}\n\n.gedit-m-display-schema-tree-item {\n  position: relative;\n\n  &::before {\n    content: \"\";\n    position: absolute;\n    background-color: var(--semi-color-text-3);\n  }\n\n  &:not(:last-child)::before {\n    width: 1px;\n    top: 0;\n    bottom: 0;\n    left: -22px;\n  }\n\n  &:last-child::before {\n    width: 1px;\n    top: 0;\n    height: 14px;\n    left: -22px;\n  }\n\n  &.depth-0::before {\n    width: 0px !important;\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/dynamic-value-input/hooks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useRef, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { useScopeAvailable } from '@flowgram.ai/editor';\n\nimport { IFlowConstantRefValue } from '@/shared';\n\nexport function useRefVariable(value?: IFlowConstantRefValue) {\n  const available = useScopeAvailable();\n  const refVariable = useMemo(() => {\n    if (value?.type === 'ref') {\n      return available.getByKeyPath(value.content);\n    }\n  }, [value, available]);\n\n  return refVariable;\n}\n\nexport function useSelectSchema(\n  schemaFromProps?: IJsonSchema,\n  constantProps?: {\n    schema?: IJsonSchema;\n  },\n  value?: IFlowConstantRefValue\n) {\n  let defaultSelectSchema = schemaFromProps || constantProps?.schema || { type: 'string' };\n  if (value?.type === 'constant') {\n    defaultSelectSchema = value?.schema || defaultSelectSchema;\n  }\n\n  const changeVersion = useRef(0);\n  const effectVersion = useRef(0);\n\n  const [selectSchema, setSelectSchema] = useState(defaultSelectSchema);\n\n  useEffect(() => {\n    effectVersion.current += 1;\n    if (changeVersion.current === effectVersion.current) {\n      return;\n    }\n    effectVersion.current = changeVersion.current;\n\n    if (value?.type === 'constant' && value?.schema) {\n      setSelectSchema(value?.schema);\n      return;\n    }\n  }, [value]);\n\n  const setSelectSchemaWithVersionUpdate = (schema: IJsonSchema) => {\n    setSelectSchema(schema);\n    changeVersion.current += 1;\n  };\n\n  return [selectSchema, setSelectSchemaWithVersionUpdate] as const;\n}\n\nexport function useIncludeSchema(schemaFromProps?: IJsonSchema) {\n  const includeSchema = useMemo(() => {\n    if (!schemaFromProps) {\n      return;\n    }\n    if (schemaFromProps?.type === 'number') {\n      return [schemaFromProps, { type: 'integer' }];\n    }\n    return { ...schemaFromProps, extra: { weak: true, ...schemaFromProps?.extra } };\n  }, [schemaFromProps]);\n\n  return includeSchema;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/dynamic-value-input/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  JsonSchemaUtils,\n  IJsonSchema,\n  useTypeManager,\n  type JsonSchemaTypeManager,\n} from '@flowgram.ai/json-schema';\nimport { IconButton } from '@douyinfe/semi-ui';\nimport { IconSetting } from '@douyinfe/semi-icons';\n\nimport { IFlowConstantRefValue, IFlowConstantValue } from '@/shared';\nimport { createInjectMaterial } from '@/shared';\nimport { InjectVariableSelector } from '@/components/variable-selector';\nimport { TypeSelector } from '@/components/type-selector';\nimport { ConstantInput, ConstantInputStrategy } from '@/components/constant-input';\n\nimport './styles.css';\nimport { useIncludeSchema, useRefVariable, useSelectSchema } from './hooks';\n\ninterface PropsType {\n  value?: IFlowConstantRefValue;\n  onChange: (value?: IFlowConstantRefValue) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  style?: React.CSSProperties;\n  schema?: IJsonSchema;\n  constantProps?: {\n    strategies?: ConstantInputStrategy[];\n    schema?: IJsonSchema; // set schema of constant input only\n    [key: string]: any;\n  };\n}\n\nconst DEFAULT_VALUE: IFlowConstantValue = {\n  type: 'constant',\n  content: '',\n  schema: { type: 'string' },\n};\n\nexport function DynamicValueInput({\n  value,\n  onChange,\n  readonly,\n  style,\n  schema: schemaFromProps,\n  constantProps,\n}: PropsType) {\n  const refVariable = useRefVariable(value);\n  const [selectSchema, setSelectSchema] = useSelectSchema(schemaFromProps, constantProps, value);\n  const includeSchema = useIncludeSchema(schemaFromProps);\n\n  const typeManager = useTypeManager() as JsonSchemaTypeManager;\n\n  const renderTypeSelector = () => {\n    if (schemaFromProps) {\n      return <TypeSelector value={schemaFromProps} readonly={true} />;\n    }\n\n    if (value?.type === 'ref') {\n      const schema = refVariable?.type ? JsonSchemaUtils.astToSchema(refVariable?.type) : undefined;\n\n      return <TypeSelector value={schema} readonly={true} />;\n    }\n\n    return (\n      <TypeSelector\n        value={selectSchema}\n        onChange={(_v) => {\n          setSelectSchema(_v || { type: 'string' });\n\n          const schema = _v || { type: 'string' };\n          let content = typeManager.getDefaultValue(schema);\n          if (_v?.type === 'object') {\n            content = '{}';\n          }\n          if (_v?.type === 'array') {\n            content = '[]';\n          }\n\n          onChange({\n            type: 'constant',\n            content,\n            schema,\n          });\n        }}\n        readonly={readonly}\n      />\n    );\n  };\n\n  const renderMain = () => {\n    if (value?.type === 'ref') {\n      // Display Variable Or Delete\n      return (\n        <InjectVariableSelector\n          style={{ width: '100%' }}\n          value={value?.content}\n          onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : DEFAULT_VALUE)}\n          includeSchema={includeSchema}\n          readonly={readonly}\n        />\n      );\n    }\n\n    const constantSchema = schemaFromProps || selectSchema || { type: 'string' };\n\n    return (\n      <ConstantInput\n        value={value?.content}\n        onChange={(_v) => onChange({ type: 'constant', content: _v, schema: constantSchema })}\n        schema={constantSchema || { type: 'string' }}\n        readonly={readonly}\n        fallbackRenderer={() => (\n          <InjectVariableSelector\n            style={{ width: '100%' }}\n            onChange={(_v) => onChange(_v ? { type: 'ref', content: _v } : DEFAULT_VALUE)}\n            includeSchema={includeSchema}\n            readonly={readonly}\n          />\n        )}\n        {...constantProps}\n        strategies={[...(constantProps?.strategies || [])]}\n      />\n    );\n  };\n\n  const renderTrigger = () => (\n    <InjectVariableSelector\n      style={{ width: '100%' }}\n      value={value?.type === 'ref' ? value?.content : undefined}\n      onChange={(_v) => onChange({ type: 'ref', content: _v })}\n      includeSchema={includeSchema}\n      readonly={readonly}\n      triggerRender={() => (\n        <IconButton disabled={readonly} size=\"small\" icon={<IconSetting size=\"small\" />} />\n      )}\n    />\n  );\n\n  return (\n    <div className=\"gedit-m-dynamic-value-input-container\" style={style}>\n      <div className=\"gedit-m-dynamic-value-input-type\">{renderTypeSelector()}</div>\n      <div className=\"gedit-m-dynamic-value-input-main\">{renderMain()}</div>\n      <div className=\"gedit-m-dynamic-value-input-trigger\">{renderTrigger()}</div>\n    </div>\n  );\n}\n\nDynamicValueInput.renderKey = 'dynamic-value-input-render-key';\nexport const InjectDynamicValueInput = createInjectMaterial(DynamicValueInput);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/dynamic-value-input/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-dynamic-value-input-container {\n  display: flex;\n  align-items: center;\n  border-radius: 4px;\n  border: 1px solid var(--semi-color-border);\n  line-height: normal;\n  overflow: hidden;\n  background-color: var(--semi-color-fill-0);\n}\n\n.gedit-m-dynamic-value-input-main {\n  flex-grow: 1;\n  overflow: hidden;\n  min-width: 0;\n  border-left: 1px solid var(--semi-color-border);\n  border-right: 1px solid var(--semi-color-border);\n\n  & .semi-tree-select,\n  & .semi-input-number,\n  & .semi-select {\n    width: 100%;\n    border: none;\n    border-radius: 0;\n  }\n\n  & .semi-input-wrapper {\n    border: none;\n    border-radius: 0;\n  }\n\n  & .semi-input-textarea-wrapper {\n    border: none;\n    border-radius: 0;\n  }\n\n  & .semi-input-textarea {\n    padding: 2px 6px;\n    border: none;\n    border-radius: 0;\n    word-break: break-all;\n  }\n}\n\n.gedit-m-dynamic-value-input-type {\n  & .semi-button {\n    border-radius: 0;\n  }\n}\n\n.gedit-m-dynamic-value-input-trigger {\n  & .semi-button {\n    border-radius: 0;\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { AssignRow, type AssignValueType } from './assign-row';\nexport { AssignRows } from './assign-rows';\nexport { BatchOutputs } from './batch-outputs';\nexport { BatchVariableSelector } from './batch-variable-selector';\nexport { BlurInput } from './blur-input';\nexport {\n  BaseCodeEditor,\n  CodeEditor,\n  JsonCodeEditor,\n  PythonCodeEditor,\n  SQLCodeEditor,\n  ShellCodeEditor,\n  TypeScriptCodeEditor,\n  type CodeEditorPropsType,\n} from './code-editor';\nexport { CodeEditorMini } from './code-editor-mini';\nexport {\n  ConditionPresetOp,\n  ConditionProvider,\n  type ConditionOpConfig,\n  type ConditionOpConfigs,\n  type IConditionRule,\n  type IConditionRuleFactory,\n  useCondition,\n  useConditionContext,\n} from './condition-context';\nexport { ConditionRow, type ConditionRowValueType } from './condition-row';\nexport { ConstantInput, type ConstantInputStrategy } from './constant-input';\nexport {\n  EditorInputsTree,\n  EditorVariableTagInject,\n  EditorVariableTree,\n} from './coze-editor-extensions';\nexport {\n  DBConditionRow,\n  type DBConditionOptionType,\n  type DBConditionRowValueType,\n} from './db-condition-row';\nexport { DisplayFlowValue } from './display-flow-value';\nexport { DisplayInputsValueAllInTag, DisplayInputsValues } from './display-inputs-values';\nexport { DisplayOutputs } from './display-outputs';\nexport { DisplaySchemaTag } from './display-schema-tag';\nexport { DisplaySchemaTree } from './display-schema-tree';\nexport { DynamicValueInput, InjectDynamicValueInput } from './dynamic-value-input';\nexport { InputsValues } from './inputs-values';\nexport { InputsValuesTree } from './inputs-values-tree';\nexport {\n  JsonEditorWithVariables,\n  type JsonEditorWithVariablesProps,\n} from './json-editor-with-variables';\nexport { JsonSchemaCreator, type JsonSchemaCreatorProps } from './json-schema-creator';\nexport { JsonSchemaEditor } from './json-schema-editor';\nexport { PromptEditor, type PromptEditorPropsType } from './prompt-editor';\nexport {\n  PromptEditorWithInputs,\n  type PromptEditorWithInputsProps,\n} from './prompt-editor-with-inputs';\nexport {\n  PromptEditorWithVariables,\n  type PromptEditorWithVariablesProps,\n} from './prompt-editor-with-variables';\nexport {\n  SQLEditorWithVariables,\n  type SQLEditorWithVariablesProps,\n} from './sql-editor-with-variables';\nexport {\n  InjectTypeSelector,\n  TypeSelector,\n  getTypeSelectValue,\n  parseTypeSelectValue,\n  type TypeSelectorProps,\n} from './type-selector';\nexport {\n  InjectVariableSelector,\n  VariableSelector,\n  VariableSelectorProvider,\n  useVariableTree,\n  type VariableSelectorProps,\n} from './variable-selector';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button, IconButton } from '@douyinfe/semi-ui';\nimport { IconDelete, IconPlus } from '@douyinfe/semi-icons';\n\nimport { IFlowConstantRefValue, IFlowValue } from '@/shared';\nimport { useObjectList } from '@/hooks';\nimport { InjectDynamicValueInput } from '@/components/dynamic-value-input';\nimport { BlurInput } from '@/components/blur-input';\n\nimport { PropsType } from './types';\nimport './styles.css';\n\nexport function InputsValues({\n  value,\n  onChange,\n  style,\n  readonly,\n  constantProps,\n  schema,\n  hasError,\n}: PropsType) {\n  const { list, updateKey, updateValue, remove, add } = useObjectList<IFlowValue | undefined>({\n    value,\n    onChange,\n    sortIndexKey: 'extra.index',\n  });\n\n  return (\n    <div>\n      <div className=\"gedit-m-inputs-values-rows\" style={style}>\n        {list.map((item) => (\n          <div className=\"gedit-m-inputs-values-row\" key={item.id}>\n            <BlurInput\n              style={{ width: 100, minWidth: 100, maxWidth: 100 }}\n              disabled={readonly}\n              size=\"small\"\n              value={item.key}\n              onChange={(v) => updateKey(item.id, v)}\n              placeholder={I18n.t('Input Key')}\n            />\n            <InjectDynamicValueInput\n              style={{ flexGrow: 1 }}\n              readonly={readonly}\n              value={item.value as IFlowConstantRefValue}\n              onChange={(v) => updateValue(item.id, v)}\n              schema={schema}\n              hasError={hasError}\n              constantProps={{\n                ...constantProps,\n                strategies: [...(constantProps?.strategies || [])],\n              }}\n            />\n            <IconButton\n              disabled={readonly}\n              theme=\"borderless\"\n              icon={<IconDelete size=\"small\" />}\n              size=\"small\"\n              onClick={() => remove(item.id)}\n            />\n          </div>\n        ))}\n      </div>\n      <Button\n        disabled={readonly}\n        icon={<IconPlus />}\n        size=\"small\"\n        onClick={() =>\n          add({\n            type: 'constant',\n            content: '',\n            schema: { type: 'string' },\n          })\n        }\n      >\n        {I18n.t('Add')}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-inputs-values-rows {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  margin-bottom: 10px;\n}\n\n.gedit-m-inputs-values-row {\n  display: flex;\n  align-items: flex-start;\n  gap: 5px;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { IFlowValue } from '@/shared';\nimport { ConstantInputStrategy } from '@/components/constant-input';\n\nexport interface PropsType {\n  value?: Record<string, IFlowValue | undefined>;\n  onChange: (value?: Record<string, IFlowValue | undefined>) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  schema?: IJsonSchema;\n  style?: React.CSSProperties;\n  constantProps?: {\n    strategies?: ConstantInputStrategy[];\n    [key: string]: any;\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/hooks/use-child-list.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { isPlainObject } from 'lodash-es';\n\nimport { FlowValueUtils } from '@/shared';\nimport { useObjectList } from '@/hooks';\n\ninterface ListItem {\n  id: string;\n  key?: string;\n  value?: any;\n}\n\nexport function useChildList(\n  value?: any,\n  onChange?: (value: any) => void\n): {\n  canAddField: boolean;\n  hasChildren: boolean;\n  list: ListItem[];\n  add: (defaultValue?: any) => void;\n  updateKey: (id: string, key: string) => void;\n  updateValue: (id: string, value: any) => void;\n  remove: (id: string) => void;\n} {\n  const canAddField = useMemo(() => {\n    if (!isPlainObject(value)) {\n      return false;\n    }\n\n    if (FlowValueUtils.isFlowValue(value)) {\n      // Constant Object Value Can Add child fields\n      return FlowValueUtils.isConstant(value) && value?.schema?.type === 'object';\n    }\n\n    return true;\n  }, [value]);\n\n  const objectListValue = useMemo(() => {\n    if (isPlainObject(value)) {\n      if (FlowValueUtils.isFlowValue(value)) {\n        return undefined;\n      }\n      return value;\n    }\n    return undefined;\n  }, [value]);\n\n  const { list, add, updateKey, updateValue, remove } = useObjectList<any>({\n    value: objectListValue,\n    onChange: (value) => {\n      onChange?.(value);\n    },\n    sortIndexKey: (value) => (FlowValueUtils.isFlowValue(value) ? 'extra.index' : ''),\n  });\n\n  const hasChildren = useMemo(\n    () => canAddField && (list.length > 0 || Object.keys(objectListValue || {}).length > 0),\n    [canAddField, list.length, Object.keys(objectListValue || {}).length]\n  );\n\n  return {\n    canAddField,\n    hasChildren,\n    list,\n    add,\n    updateKey,\n    updateValue,\n    remove,\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/icon.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport Icon from '@douyinfe/semi-icons';\n\nconst iconAddChildrenSvg = (\n  <svg\n    className=\"icon-icon icon-icon-coz_add_node \"\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M11 6.49988C11 8.64148 9.50397 10.4337 7.49995 10.8884V15.4998C7.49995 16.0521 7.94767 16.4998 8.49995 16.4998H11.208C11.0742 16.8061 11 17.1443 11 17.4998C11 17.8554 11.0742 18.1936 11.208 18.4998H8.49995C6.8431 18.4998 5.49995 17.1567 5.49995 15.4998V10.8884C3.49599 10.4336 2 8.64145 2 6.49988C2 4.0146 4.01472 1.99988 6.5 1.99988C8.98528 1.99988 11 4.0146 11 6.49988ZM6.5 8.99988C7.88071 8.99988 9 7.88059 9 6.49988C9 5.11917 7.88071 3.99988 6.5 3.99988C5.11929 3.99988 4 5.11917 4 6.49988C4 7.88059 5.11929 8.99988 6.5 8.99988Z\"\n    ></path>\n    <path d=\"M17.5 12.4999C18.0523 12.4999 18.5 12.9476 18.5 13.4999V16.4999H21.5C22.0523 16.4999 22.5 16.9476 22.5 17.4999C22.5 18.0522 22.0523 18.4999 21.5 18.4999H18.5V21.4999C18.5 22.0522 18.0523 22.4999 17.5 22.4999C16.9477 22.4999 16.5 22.0522 16.5 21.4999V18.4999H13.5C12.9477 18.4999 12.5 18.0522 12.5 17.4999C12.5 16.9476 12.9477 16.4999 13.5 16.4999H16.5V13.4999C16.5 12.9476 16.9477 12.4999 17.5 12.4999Z\"></path>\n  </svg>\n);\n\nexport const IconAddChildren = () => <Icon size=\"small\" svg={iconAddChildrenSvg} />;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button } from '@douyinfe/semi-ui';\nimport { IconPlus } from '@douyinfe/semi-icons';\n\nimport { FlowValueUtils, IFlowValue, IInputsValues } from '@/shared';\nimport { useObjectList } from '@/hooks';\n\nimport { PropsType } from './types';\nimport './styles.css';\nimport { InputValueRow } from './row';\n\nexport function InputsValuesTree(props: PropsType) {\n  const { value, onChange, readonly, hasError, constantProps } = props;\n\n  const { list, updateKey, updateValue, remove, add } = useObjectList<\n    IInputsValues | IFlowValue | undefined\n  >({\n    value,\n    onChange: (v) => onChange?.(v as IInputsValues),\n    sortIndexKey: (value) => (FlowValueUtils.isFlowValue(value) ? 'extra.index' : ''),\n  });\n\n  return (\n    <div>\n      <div className=\"gedit-m-inputs-values-tree-tree-items\">\n        {list.map((item) => (\n          <InputValueRow\n            key={item.id}\n            keyName={item.key}\n            value={item.value}\n            onUpdateKey={(key) => updateKey(item.id, key)}\n            onUpdateValue={(value) => updateValue(item.id, value)}\n            onRemove={() => remove(item.id)}\n            readonly={readonly}\n            hasError={hasError}\n            constantProps={constantProps}\n          />\n        ))}\n      </div>\n      <Button\n        style={{ marginTop: 10, marginLeft: 16 }}\n        disabled={readonly}\n        icon={<IconPlus />}\n        size=\"small\"\n        onClick={() => {\n          add({\n            type: 'constant',\n            content: '',\n            schema: { type: 'string' },\n          });\n        }}\n      >\n        {I18n.t('Add')}\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/row.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useState } from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { IconButton, Input } from '@douyinfe/semi-ui';\nimport { IconChevronDown, IconChevronRight, IconDelete } from '@douyinfe/semi-icons';\n\nimport { IFlowConstantValue } from '@/shared';\nimport { ConstantInputStrategy } from '@/components/constant-input';\n\nimport { PropsType } from './types';\nimport './styles.css';\nimport { useChildList } from './hooks/use-child-list';\nimport { InjectDynamicValueInput } from '../dynamic-value-input';\nimport { BlurInput } from '../blur-input';\nimport { IconAddChildren } from './icon';\n\nconst AddObjectChildStrategy: ConstantInputStrategy = {\n  hit: (schema) => schema.type === 'object',\n  Renderer: () => (\n    <Input\n      size=\"small\"\n      disabled\n      style={{ pointerEvents: 'none' }}\n      value={I18n.t('Configure via child fields')}\n    />\n  ),\n};\n\nexport function InputValueRow(\n  props: {\n    keyName?: string;\n    value?: any;\n    onUpdateKey: (key: string) => void;\n    onUpdateValue: (value: any) => void;\n    onRemove?: () => void;\n    $isLast?: boolean;\n    $level?: number;\n  } & Pick<PropsType, 'constantProps' | 'hasError' | 'readonly'>\n) {\n  const {\n    keyName,\n    value,\n    $level = 0,\n    onUpdateKey,\n    onUpdateValue,\n    $isLast,\n    onRemove,\n    constantProps,\n    hasError,\n    readonly,\n  } = props;\n  const [collapse, setCollapse] = useState(false);\n\n  const { canAddField, hasChildren, list, add, updateKey, updateValue, remove } = useChildList(\n    value,\n    onUpdateValue\n  );\n\n  const strategies = useMemo(\n    () => [...(hasChildren ? [AddObjectChildStrategy] : []), ...(constantProps?.strategies || [])],\n    [hasChildren, constantProps?.strategies]\n  );\n\n  const flowDisplayValue = useMemo(\n    () =>\n      hasChildren\n        ? ({\n            type: 'constant',\n            schema: { type: 'object' },\n          } as IFlowConstantValue)\n        : value,\n    [hasChildren, value]\n  );\n\n  return (\n    <>\n      <div\n        className={`gedit-m-inputs-values-tree-tree-item-left ${$level > 0 ? 'show-line' : ''} ${\n          $isLast ? 'is-last' : ''\n        } ${hasChildren ? 'show-collapse' : ''}`}\n      >\n        {hasChildren && (\n          <div\n            className=\"gedit-m-inputs-values-tree-collapse-trigger\"\n            onClick={() => setCollapse((_collapse) => !_collapse)}\n          >\n            {collapse ? <IconChevronDown size=\"small\" /> : <IconChevronRight size=\"small\" />}\n          </div>\n        )}\n      </div>\n      <div className=\"gedit-m-inputs-values-tree-tree-item-right\">\n        <div className=\"gedit-m-inputs-values-tree-tree-item-main\">\n          <div className=\"gedit-m-inputs-values-tree-row\">\n            <BlurInput\n              style={{ width: 100, minWidth: 100, maxWidth: 100 }}\n              disabled={readonly}\n              size=\"small\"\n              value={keyName}\n              onChange={(v) => onUpdateKey?.(v)}\n              placeholder={I18n.t('Input Key')}\n            />\n            <InjectDynamicValueInput\n              style={{ flexGrow: 1 }}\n              readonly={readonly}\n              value={flowDisplayValue}\n              onChange={(v) => onUpdateValue(v)}\n              hasError={hasError}\n              constantProps={{\n                ...constantProps,\n                strategies,\n              }}\n            />\n            <div className=\"gedit-m-inputs-values-tree-actions\">\n              {canAddField && (\n                <IconButton\n                  disabled={readonly}\n                  size=\"small\"\n                  theme=\"borderless\"\n                  icon={<IconAddChildren />}\n                  onClick={() => {\n                    add({\n                      type: 'constant',\n                      content: '',\n                      schema: { type: 'string' },\n                    });\n                    setCollapse(true);\n                  }}\n                />\n              )}\n              <IconButton\n                disabled={readonly}\n                theme=\"borderless\"\n                icon={<IconDelete size=\"small\" />}\n                size=\"small\"\n                onClick={() => onRemove?.()}\n              />\n            </div>\n          </div>\n        </div>\n        {hasChildren && (\n          <div className={`gedit-m-inputs-values-tree-collapsible ${collapse ? 'collapse' : ''}`}>\n            <div className=\"gedit-m-inputs-values-tree-tree-items shrink\">\n              {list.map((_item, index) => (\n                <InputValueRow\n                  readonly={readonly}\n                  hasError={hasError}\n                  constantProps={constantProps}\n                  key={_item.id}\n                  keyName={_item.key}\n                  value={_item.value}\n                  $level={$level + 1} // 传递递增的层级\n                  onUpdateValue={(_v) => {\n                    updateValue(_item.id, _v);\n                  }}\n                  onUpdateKey={(k) => {\n                    updateKey(_item.id, k);\n                  }}\n                  onRemove={() => {\n                    remove(_item.id);\n                  }}\n                  $isLast={index === list.length - 1}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-inputs-values-tree-container {\n}\n\n.gedit-m-inputs-values-tree-row {\n  display: flex;\n  align-items: flex-start;\n  gap: 5px;\n}\n\n.gedit-m-inputs-values-tree-collapse-trigger {\n  cursor: pointer;\n  margin-right: 5px;\n}\n\n.gedit-m-inputs-values-tree-tree-items {\n  display: grid;\n  grid-template-columns: auto 1fr;\n}\n\n.gedit-m-inputs-values-tree-tree-items.shrink {\n  padding-left: 3px;\n  margin-top: 10px;\n}\n\n.gedit-m-inputs-values-tree-tree-item-left {\n  grid-column: 1;\n  position: relative;\n  width: 16px;\n}\n\n.gedit-m-inputs-values-tree-tree-item-left.show-line::before {\n  /* 竖线 */\n  content: \"\";\n  height: var(--line-height, 100%);\n  position: absolute;\n  left: -14px;\n  top: -16px;\n  width: 1px;\n  background: #d9d9d9;\n  display: block;\n}\n\n.gedit-m-inputs-values-tree-tree-item-left.show-line::after {\n  /* 横线 */\n  content: \"\";\n  position: absolute;\n  left: -14px; /* 横线起点和竖线对齐 */\n  top: 8px; /* 跟随你的行高调整 */\n  width: var(--line-width, 30px); /* 横线长度 */\n  height: 1px;\n  background: #d9d9d9;\n  display: block;\n}\n\n.gedit-m-inputs-values-tree-tree-item-left.show-line.is-last::before {\n  height: 24px;\n}\n\n.gedit-m-inputs-values-tree-tree-item-left.show-line.show-collapse::after {\n  width: 12px;\n}\n\n.gedit-m-inputs-values-tree-tree-item-right {\n  grid-column: 2;\n  margin-bottom: 10px;\n\n  &:last-child {\n    margin-bottom: 0px;\n  }\n}\n\n.gedit-m-inputs-values-tree-tree-item-main {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  position: relative;\n}\n\n.gedit-m-inputs-values-tree-collapsible {\n  display: none;\n}\n\n.gedit-m-inputs-values-tree-collapsible.collapse {\n  display: block;\n}\n\n.gedit-m-inputs-values-tree-actions {\n  white-space: nowrap;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/inputs-values-tree/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { IInputsValues } from '@/shared';\nimport { ConstantInputStrategy } from '@/components/constant-input';\n\nexport interface PropsType {\n  value?: IInputsValues;\n  onChange: (value?: IInputsValues) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  schema?: IJsonSchema;\n  style?: React.CSSProperties;\n  constantProps?: {\n    strategies?: ConstantInputStrategy[];\n    [key: string]: any;\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-editor-with-variables/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { transformerCreator } from '@flowgram.ai/coze-editor/preset-code';\nimport { Text } from '@flowgram.ai/coze-editor/language-json';\n\nimport { EditorVariableTree, EditorVariableTagInject } from '@/components/coze-editor-extensions';\nimport { JsonCodeEditor, type CodeEditorPropsType } from '@/components/code-editor';\n\nconst TRIGGER_CHARACTERS = ['@'];\n\ntype Match = { match: string; range: [number, number] };\nfunction findAllMatches(inputString: string, regex: RegExp): Match[] {\n  const globalRegex = new RegExp(\n    regex,\n    regex.flags.includes('g') ? regex.flags : regex.flags + 'g'\n  );\n  let match;\n  const matches: Match[] = [];\n\n  while ((match = globalRegex.exec(inputString)) !== null) {\n    if (match.index === globalRegex.lastIndex) {\n      globalRegex.lastIndex++;\n    }\n    matches.push({\n      match: match[0],\n      range: [match.index, match.index + match[0].length],\n    });\n  }\n\n  return matches;\n}\n\nconst transformer = transformerCreator((text: Text) => {\n  const originalSource = text.toString();\n  const matches = findAllMatches(originalSource, /\\{\\{([^\\}]*)\\}\\}/g);\n\n  if (matches.length > 0) {\n    matches.forEach(({ range }) => {\n      text.replaceRange(range[0], range[1], 'null');\n    });\n  }\n\n  return text;\n});\n\nexport interface JsonEditorWithVariablesProps extends Omit<CodeEditorPropsType, 'languageId'> {}\n\nexport function JsonEditorWithVariables(props: JsonEditorWithVariablesProps) {\n  return (\n    <JsonCodeEditor\n      activeLinePlaceholder={I18n.t(\"Press '@' to Select variable\")}\n      {...props}\n      options={{\n        transformer,\n        ...(props.options || {}),\n      }}\n    >\n      <EditorVariableTree triggerCharacters={TRIGGER_CHARACTERS} />\n      <EditorVariableTagInject />\n    </JsonCodeEditor>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-editor-with-variables/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { lazySuspense } from '@/shared';\n\nexport const JsonEditorWithVariables = lazySuspense(() =>\n  import('./editor').then((module) => ({ default: module.JsonEditorWithVariables }))\n);\n\nexport type { JsonEditorWithVariablesProps } from './editor';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-creator/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { JsonSchemaCreator } from './json-schema-creator';\nexport type { JsonSchemaCreatorProps } from './json-schema-creator';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-creator/json-input-modal.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport type { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\nimport { Modal, Typography } from '@douyinfe/semi-ui';\n\nimport { jsonToSchema } from './utils/json-to-schema';\nimport { JsonCodeEditor } from '../code-editor';\n\nconst { Text } = Typography;\n\ninterface JsonInputModalProps {\n  visible: boolean;\n  onClose: () => void;\n  onConfirm: (schema: IJsonSchema) => void;\n}\n\nexport function JsonInputModal({ visible, onClose, onConfirm }: JsonInputModalProps) {\n  const [jsonInput, setJsonInput] = useState('');\n  const [error, setError] = useState('');\n\n  const handleConfirm = () => {\n    try {\n      const schema = jsonToSchema(jsonInput);\n      onConfirm(schema);\n      setJsonInput('');\n      setError('');\n    } catch (err) {\n      setError((err as Error).message);\n    }\n  };\n\n  return (\n    <Modal\n      visible={visible}\n      onCancel={onClose}\n      onOk={handleConfirm}\n      title={I18n.t('JSON to JSONSchema')}\n      okText={I18n.t('Generate')}\n      cancelText={I18n.t('Cancel')}\n      width={600}\n    >\n      <div style={{ marginBottom: 8 }}>\n        <Text>{I18n.t('Paste JSON data')}：</Text>\n      </div>\n      <div style={{ minHeight: 300 }}>\n        <JsonCodeEditor value={jsonInput} onChange={(value) => setJsonInput(value || '')} />\n      </div>\n      {error && (\n        <div style={{ marginTop: 8 }}>\n          <Text type=\"danger\">{error}</Text>\n        </div>\n      )}\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-creator/json-schema-creator.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport type { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button } from '@douyinfe/semi-ui';\n\nimport { JsonInputModal } from './json-input-modal';\n\nexport interface JsonSchemaCreatorProps {\n  /** 生成 schema 后的回调 */\n  onSchemaCreate?: (schema: IJsonSchema) => void;\n}\n\nexport function JsonSchemaCreator({ onSchemaCreate }: JsonSchemaCreatorProps) {\n  const [visible, setVisible] = useState(false);\n\n  const handleCreate = (schema: IJsonSchema) => {\n    onSchemaCreate?.(schema);\n    setVisible(false);\n  };\n\n  return (\n    <>\n      <Button onClick={() => setVisible(true)}>{I18n.t('JSON to JSONSchema')}</Button>\n      <JsonInputModal\n        visible={visible}\n        onClose={() => setVisible(false)}\n        onConfirm={handleCreate}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-creator/utils/json-to-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport function jsonToSchema(jsonString: string): IJsonSchema {\n  // 1. 解析 JSON\n  const data = JSON.parse(jsonString); // 会自动抛出语法错误\n\n  // 2. 生成 schema\n  return generateSchema(data);\n}\n\nfunction generateSchema(value: any): IJsonSchema {\n  // null\n  if (value === null) {\n    return { type: 'string' };\n  }\n\n  // array\n  if (Array.isArray(value)) {\n    const schema: IJsonSchema = { type: 'array' };\n    if (value.length > 0) {\n      schema.items = generateSchema(value[0]);\n    }\n    return schema;\n  }\n\n  // object\n  if (typeof value === 'object') {\n    const schema: IJsonSchema = {\n      type: 'object',\n      properties: {},\n      required: [],\n    };\n\n    for (const [key, val] of Object.entries(value)) {\n      schema.properties![key] = generateSchema(val);\n      schema.required!.push(key);\n    }\n\n    return schema;\n  }\n\n  // primitive types\n  const type = typeof value;\n  return { type: type as any };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/default-value.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { ConstantInput } from '@/components/constant-input';\n\n/**\n * Renders the corresponding default value input component based on different data types.\n * @param props - Component properties, including value, type, placeholder, onChange.\n * @returns Returns the input component of the corresponding type or null.\n */\nexport function DefaultValue(props: {\n  value: any;\n  schema?: IJsonSchema;\n  placeholder?: string;\n  onChange: (value: any) => void;\n}) {\n  const { value, schema, onChange, placeholder } = props;\n\n  return (\n    <div className=\"gedit-m-json-schema-editor-constant-input-wrapper\">\n      <ConstantInput\n        value={value}\n        onChange={(_v) => onChange(_v)}\n        schema={schema || { type: 'string' }}\n        placeholder={placeholder ?? I18n.t('Default value if parameter is not provided')}\n        enableMultiLineStr\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/hooks.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { difference, omit } from 'lodash-es';\nimport { produce } from 'immer';\nimport { IJsonSchema, type JsonSchemaTypeManager, useTypeManager } from '@flowgram.ai/json-schema';\n\nimport { PropertyValueType } from './types';\n\nlet _id = 0;\nfunction genId() {\n  return _id++;\n}\n\nexport function usePropertiesEdit(\n  value?: PropertyValueType,\n  onChange?: (value: PropertyValueType) => void\n) {\n  const typeManager = useTypeManager() as JsonSchemaTypeManager;\n\n  // Get drilldown properties (array.items.items.properties...)\n  const drilldownSchema = typeManager.getPropertiesParent(value || {});\n  const canAddField = typeManager.canAddField(value || {});\n\n  const [propertyList, setPropertyList] = useState<PropertyValueType[]>([]);\n  const latestPropertyListRef = useRef(propertyList);\n\n  const effectVersion = useRef(0);\n  const changeVersion = useRef(0);\n\n  useEffect(() => {\n    effectVersion.current = effectVersion.current + 1;\n    if (effectVersion.current === changeVersion.current) {\n      return;\n    }\n    effectVersion.current = changeVersion.current;\n\n    // If the value is changed, update the property list\n    const _list = latestPropertyListRef.current;\n\n    const newNames = Object.entries(drilldownSchema?.properties || {})\n      .sort(([, a], [, b]) => (a.extra?.index ?? 0) - (b.extra?.index ?? 0))\n      .map(([key]) => key);\n\n    const oldNames = _list.map((item) => item.name).filter(Boolean) as string[];\n    const addNames = difference(newNames, oldNames);\n\n    const next = _list\n      .filter((item) => !item.name || newNames.includes(item.name))\n      .map((item) => ({\n        key: item.key,\n        name: item.name,\n        isPropertyRequired: drilldownSchema?.required?.includes(item.name || '') || false,\n        ...(drilldownSchema?.properties?.[item.name || ''] || item || {}),\n      }))\n      .concat(\n        addNames.map((_name) => ({\n          key: genId(),\n          name: _name,\n          isPropertyRequired: drilldownSchema?.required?.includes(_name) || false,\n          ...(drilldownSchema?.properties?.[_name] || {}),\n        }))\n      );\n\n    latestPropertyListRef.current = next;\n\n    setPropertyList(next);\n  }, [drilldownSchema]);\n\n  const updatePropertyList = (updater: (list: PropertyValueType[]) => PropertyValueType[]) => {\n    changeVersion.current = changeVersion.current + 1;\n\n    const next = updater(latestPropertyListRef.current);\n    latestPropertyListRef.current = next;\n    setPropertyList(next);\n\n    // onChange to parent\n    const nextProperties: Record<string, IJsonSchema> = {};\n    const nextRequired: string[] = [];\n\n    for (const _property of next) {\n      if (!_property.name) {\n        continue;\n      }\n\n      nextProperties[_property.name] = omit(_property, ['key', 'name', 'isPropertyRequired']);\n\n      if (_property.isPropertyRequired) {\n        nextRequired.push(_property.name);\n      }\n    }\n\n    onChange?.(\n      produce(value || {}, (draft) => {\n        const propertiesParent = typeManager.getPropertiesParent(draft);\n\n        if (propertiesParent) {\n          propertiesParent.properties = nextProperties;\n          propertiesParent.required = nextRequired;\n          return;\n        }\n      })\n    );\n  };\n\n  const onAddProperty = () => {\n    const _list = latestPropertyListRef.current;\n    const next = [\n      ..._list,\n      { key: genId(), name: '', type: 'string', extra: { index: _list.length + 1 } },\n    ];\n\n    latestPropertyListRef.current = next;\n    setPropertyList(next);\n  };\n\n  const onRemoveProperty = (key: number) => {\n    updatePropertyList((_list) => _list.filter((_property) => _property.key !== key));\n  };\n\n  const onEditProperty = (key: number, nextValue: PropertyValueType) => {\n    updatePropertyList((_list) =>\n      _list.map((_property) => (_property.key === key ? nextValue : _property))\n    );\n  };\n\n  useEffect(() => {\n    if (!canAddField) {\n      latestPropertyListRef.current = [];\n      setPropertyList([]);\n    }\n  }, [canAddField]);\n\n  return {\n    propertyList,\n    canAddField,\n    onAddProperty,\n    onRemoveProperty,\n    onEditProperty,\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/icon.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport Icon from '@douyinfe/semi-icons';\n\nconst iconAddChildrenSvg = (\n  <svg\n    className=\"icon-icon icon-icon-coz_add_node \"\n    width=\"1em\"\n    height=\"1em\"\n    viewBox=\"0 0 24 24\"\n    fill=\"currentColor\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n  >\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M11 6.49988C11 8.64148 9.50397 10.4337 7.49995 10.8884V15.4998C7.49995 16.0521 7.94767 16.4998 8.49995 16.4998H11.208C11.0742 16.8061 11 17.1443 11 17.4998C11 17.8554 11.0742 18.1936 11.208 18.4998H8.49995C6.8431 18.4998 5.49995 17.1567 5.49995 15.4998V10.8884C3.49599 10.4336 2 8.64145 2 6.49988C2 4.0146 4.01472 1.99988 6.5 1.99988C8.98528 1.99988 11 4.0146 11 6.49988ZM6.5 8.99988C7.88071 8.99988 9 7.88059 9 6.49988C9 5.11917 7.88071 3.99988 6.5 3.99988C5.11929 3.99988 4 5.11917 4 6.49988C4 7.88059 5.11929 8.99988 6.5 8.99988Z\"\n    ></path>\n    <path d=\"M17.5 12.4999C18.0523 12.4999 18.5 12.9476 18.5 13.4999V16.4999H21.5C22.0523 16.4999 22.5 16.9476 22.5 17.4999C22.5 18.0522 22.0523 18.4999 21.5 18.4999H18.5V21.4999C18.5 22.0522 18.0523 22.4999 17.5 22.4999C16.9477 22.4999 16.5 22.0522 16.5 21.4999V18.4999H13.5C12.9477 18.4999 12.5 18.0522 12.5 17.4999C12.5 16.9476 12.9477 16.4999 13.5 16.4999H16.5V13.4999C16.5 12.9476 16.9477 12.4999 17.5 12.4999Z\"></path>\n  </svg>\n);\n\nexport const IconAddChildren = () => <Icon size=\"small\" svg={iconAddChildrenSvg} />;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\nimport { Button, Checkbox, IconButton } from '@douyinfe/semi-ui';\nimport {\n  IconExpand,\n  IconShrink,\n  IconPlus,\n  IconChevronDown,\n  IconChevronRight,\n  IconMinus,\n} from '@douyinfe/semi-icons';\n\nimport { InjectTypeSelector } from '@/components/type-selector';\nimport { BlurInput } from '@/components/blur-input';\n\nimport { ConfigType, PropertyValueType } from './types';\nimport { IconAddChildren } from './icon';\nimport { usePropertiesEdit } from './hooks';\nimport { DefaultValue } from './default-value';\n\nimport './styles.css';\n\nconst DEFAULT = { type: 'object' };\n\nexport function JsonSchemaEditor(props: {\n  value?: IJsonSchema;\n  onChange?: (value: IJsonSchema) => void;\n  config?: ConfigType;\n  className?: string;\n  readonly?: boolean;\n}) {\n  const { value = DEFAULT, config = {}, onChange: onChangeProps, readonly } = props;\n  const { propertyList, onAddProperty, onRemoveProperty, onEditProperty } = usePropertiesEdit(\n    value,\n    onChangeProps\n  );\n\n  return (\n    <div className=\"gedit-m-json-schema-editor-container\">\n      <div className=\"gedit-m-json-schema-editor-tree-items\">\n        {propertyList.map((_property) => (\n          <PropertyEdit\n            readonly={readonly}\n            key={_property.key}\n            value={_property}\n            config={config}\n            onChange={(_v) => {\n              onEditProperty(_property.key!, _v);\n            }}\n            onRemove={() => {\n              onRemoveProperty(_property.key!);\n            }}\n          />\n        ))}\n      </div>\n      <Button\n        disabled={readonly}\n        size=\"small\"\n        style={{ marginTop: 10, marginLeft: 16 }}\n        icon={<IconPlus />}\n        onClick={onAddProperty}\n      >\n        {config?.addButtonText ?? I18n.t('Add')}\n      </Button>\n    </div>\n  );\n}\n\nfunction PropertyEdit(props: {\n  value?: PropertyValueType;\n  config?: ConfigType;\n  onChange?: (value: PropertyValueType) => void;\n  onRemove?: () => void;\n  readonly?: boolean;\n  $isLast?: boolean;\n  $level?: number; // 添加层级属性\n}) {\n  const { value, config, readonly, $level = 0, onChange: onChangeProps, onRemove, $isLast } = props;\n\n  const [expand, setExpand] = useState(false);\n  const [collapse, setCollapse] = useState(false);\n\n  const { name, type, items, default: defaultValue, description, isPropertyRequired } = value || {};\n\n  const typeSelectorValue = useMemo(() => ({ type, items }), [type, items]);\n\n  const { propertyList, canAddField, onAddProperty, onRemoveProperty, onEditProperty } =\n    usePropertiesEdit(value, onChangeProps);\n\n  const onChange = (key: string, _value: any) => {\n    onChangeProps?.({\n      ...(value || {}),\n      [key]: _value,\n    });\n  };\n\n  const showCollapse = canAddField && propertyList.length > 0;\n\n  return (\n    <>\n      <div\n        className={`gedit-m-json-schema-editor-tree-item-left ${$level > 0 ? 'show-line' : ''} ${\n          $isLast ? 'is-last' : ''\n        } ${showCollapse ? 'show-collapse' : ''}`}\n      >\n        {showCollapse && (\n          <div\n            className=\"gedit-m-json-schema-editor-collapse-trigger\"\n            onClick={() => setCollapse((_collapse) => !_collapse)}\n          >\n            {collapse ? <IconChevronDown size=\"small\" /> : <IconChevronRight size=\"small\" />}\n          </div>\n        )}\n      </div>\n      <div className=\"gedit-m-json-schema-editor-tree-item-right\">\n        <div className=\"gedit-m-json-schema-editor-tree-item-main\">\n          <div className=\"gedit-m-json-schema-editor-row\">\n            <div className=\"gedit-m-json-schema-editor-name\">\n              <BlurInput\n                disabled={readonly}\n                placeholder={config?.placeholder ?? I18n.t('Input Variable Name')}\n                size=\"small\"\n                value={name}\n                onChange={(value) => onChange('name', value)}\n              />\n            </div>\n            <div className=\"gedit-m-json-schema-editor-type\">\n              <InjectTypeSelector\n                value={typeSelectorValue}\n                readonly={readonly}\n                onChange={(_value) => {\n                  onChangeProps?.({\n                    ...(value || {}),\n                    ..._value,\n                  });\n                }}\n              />\n            </div>\n            <div className=\"gedit-m-json-schema-editor-required\">\n              <Checkbox\n                disabled={readonly}\n                checked={isPropertyRequired}\n                onChange={(e) => onChange('isPropertyRequired', e.target.checked)}\n              />\n            </div>\n            <div className=\"gedit-m-json-schema-editor-actions\">\n              <IconButton\n                disabled={readonly}\n                size=\"small\"\n                theme=\"borderless\"\n                icon={expand ? <IconShrink size=\"small\" /> : <IconExpand size=\"small\" />}\n                onClick={() => {\n                  setExpand((_expand) => !_expand);\n                }}\n              />\n              {canAddField && (\n                <IconButton\n                  disabled={readonly}\n                  size=\"small\"\n                  theme=\"borderless\"\n                  icon={<IconAddChildren />}\n                  onClick={() => {\n                    onAddProperty();\n                    setCollapse(true);\n                  }}\n                />\n              )}\n              <IconButton\n                disabled={readonly}\n                size=\"small\"\n                theme=\"borderless\"\n                icon={<IconMinus size=\"small\" />}\n                onClick={onRemove}\n              />\n            </div>\n          </div>\n          {expand && (\n            <div className=\"gedit-m-json-schema-editor-expand-detail\">\n              <div className=\"gedit-m-json-schema-editor-label\">\n                {config?.descTitle ?? I18n.t('Description')}\n              </div>\n              <BlurInput\n                disabled={readonly}\n                size=\"small\"\n                value={description}\n                onChange={(value) => onChange('description', value)}\n                placeholder={\n                  config?.descPlaceholder ?? I18n.t('Help LLM to understand the property')\n                }\n              />\n              {$level === 0 && (\n                <>\n                  <div className=\"gedit-m-json-schema-editor-label\" style={{ marginTop: 10 }}>\n                    {config?.defaultValueTitle ?? I18n.t('Default Value')}\n                  </div>\n                  <div className=\"gedit-m-json-schema-editor-default-value-wrapper\">\n                    <DefaultValue\n                      value={defaultValue}\n                      schema={value}\n                      placeholder={config?.defaultValuePlaceholder ?? I18n.t('Default Value')}\n                      onChange={(value) => onChange('default', value)}\n                    />\n                  </div>\n                </>\n              )}\n            </div>\n          )}\n        </div>\n        {showCollapse && (\n          <div className={`gedit-m-json-schema-editor-collapsible ${collapse ? 'collapse' : ''}`}>\n            <div className=\"gedit-m-json-schema-editor-tree-items shrink\">\n              {propertyList.map((_property, index) => (\n                <PropertyEdit\n                  readonly={readonly}\n                  key={_property.key}\n                  value={_property}\n                  config={config}\n                  $level={$level + 1} // 传递递增的层级\n                  onChange={(_v) => {\n                    onEditProperty(_property.key!, _v);\n                  }}\n                  onRemove={() => {\n                    onRemoveProperty(_property.key!);\n                  }}\n                  $isLast={index === propertyList.length - 1}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-json-schema-editor-container {\n  /* & .semi-input {\n    background-color: #fff;\n    border-radius: 6px;\n    height: 24px;\n  } */\n}\n\n.gedit-m-json-schema-editor-row {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n}\n\n.gedit-m-json-schema-editor-collapse-trigger {\n  cursor: pointer;\n  margin-right: 5px;\n}\n\n.gedit-m-json-schema-editor-expand-detail {\n  display: flex;\n  flex-direction: column;\n}\n\n.gedit-m-json-schema-editor-label {\n  font-size: 12px;\n  color: #999;\n  font-weight: 400;\n  margin-bottom: 2px;\n}\n\n.gedit-m-json-schema-editor-tree-items {\n  display: grid;\n  grid-template-columns: auto 1fr;\n}\n\n.gedit-m-json-schema-editor-tree-items.shrink {\n  padding-left: 3px;\n  margin-top: 10px;\n}\n\n.gedit-m-json-schema-editor-tree-item-left {\n  grid-column: 1;\n  position: relative;\n  width: 16px;\n}\n\n.gedit-m-json-schema-editor-tree-item-left.show-line::before {\n  /* 竖线 */\n  content: \"\";\n  height: var(--line-height, 100%);\n  position: absolute;\n  left: -14px;\n  top: -16px;\n  width: 1px;\n  background: #d9d9d9;\n  display: block;\n}\n\n.gedit-m-json-schema-editor-tree-item-left.show-line::after {\n  /* 横线 */\n  content: \"\";\n  position: absolute;\n  left: -14px; /* 横线起点和竖线对齐 */\n  top: 8px; /* 跟随你的行高调整 */\n  width: var(--line-width, 30px); /* 横线长度 */\n  height: 1px;\n  background: #d9d9d9;\n  display: block;\n}\n\n.gedit-m-json-schema-editor-tree-item-left.show-line.is-last::before {\n  height: 24px;\n}\n\n.gedit-m-json-schema-editor-tree-item-left.show-line.show-collapse::after {\n  width: 12px;\n}\n\n.gedit-m-json-schema-editor-tree-item-right {\n  grid-column: 2;\n  margin-bottom: 10px;\n\n  &:last-child {\n    margin-bottom: 0px;\n  }\n}\n\n.gedit-m-json-schema-editor-tree-item-main {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  position: relative;\n}\n\n.gedit-m-json-schema-editor-collapsible {\n  display: none;\n}\n\n.gedit-m-json-schema-editor-collapsible.collapse {\n  display: block;\n}\n\n.gedit-m-json-schema-editor-name {\n  flex-grow: 1;\n}\n\n.gedit-m-json-schema-editor-type {\n}\n\n.gedit-m-json-schema-editor-required {\n}\n\n.gedit-m-json-schema-editor-actions {\n  white-space: nowrap;\n}\n\n.gedit-m-json-schema-editor-default-value-wrapper {\n  margin: 0;\n}\n\n.gedit-m-json-schema-editor-constant-input-wrapper {\n  flex-grow: 1;\n\n  & .semi-tree-select,\n  & .semi-input-number,\n  & .semi-select {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/json-schema-editor/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport interface PropertyValueType extends IJsonSchema {\n  name?: string;\n  key?: number;\n  isPropertyRequired?: boolean;\n}\n\nexport type PropertiesValueType = Pick<PropertyValueType, 'properties' | 'required'>;\n\nexport type JsonSchemaProperties = IJsonSchema['properties'];\n\nexport interface ConfigType {\n  placeholder?: string;\n  descTitle?: string;\n  descPlaceholder?: string;\n  defaultValueTitle?: string;\n  defaultValuePlaceholder?: string;\n  addButtonText?: string;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useRef } from 'react';\n\nimport {\n  Renderer,\n  EditorProvider,\n  ActiveLinePlaceholder,\n  InferValues,\n} from '@flowgram.ai/coze-editor/react';\nimport preset, { EditorAPI } from '@flowgram.ai/coze-editor/preset-prompt';\n\nimport { PropsType } from './types';\nimport MarkdownHighlight from './extensions/markdown';\nimport LanguageSupport from './extensions/language-support';\nimport JinjaHighlight from './extensions/jinja';\n\nimport './styles.css';\n\ntype Preset = typeof preset;\ntype Options = Partial<InferValues<Preset[number]>>;\n\nexport interface PromptEditorPropsType extends PropsType {\n  options?: Options;\n}\n\nexport function PromptEditor(props: PromptEditorPropsType) {\n  const {\n    value,\n    onChange,\n    readonly,\n    placeholder,\n    activeLinePlaceholder,\n    style,\n    hasError,\n    children,\n    disableMarkdownHighlight,\n    options,\n  } = props || {};\n\n  const editorRef = useRef<EditorAPI | null>(null);\n\n  const editorValue = String(value?.content || '');\n\n  useEffect(() => {\n    // listen to value change\n    if (editorRef.current?.getValue() !== editorValue) {\n      // apply updates on readonly mode\n      const editorView = editorRef.current?.$view;\n      editorView?.dispatch({\n        changes: {\n          from: 0,\n          to: editorView?.state.doc.length,\n          insert: editorValue,\n        },\n      });\n    }\n  }, [editorValue]);\n\n  return (\n    <div className={`gedit-m-prompt-editor-container ${hasError ? 'has-error' : ''}`} style={style}>\n      <EditorProvider>\n        <Renderer\n          didMount={(editor: EditorAPI) => {\n            editorRef.current = editor;\n          }}\n          plugins={preset}\n          defaultValue={editorValue}\n          options={{\n            readOnly: readonly,\n            editable: !readonly,\n            placeholder,\n            ...options,\n          }}\n          onChange={(e) => {\n            onChange({ type: 'template', content: e.value });\n          }}\n        />\n        {activeLinePlaceholder && (\n          <ActiveLinePlaceholder>{activeLinePlaceholder}</ActiveLinePlaceholder>\n        )}\n        {!disableMarkdownHighlight && <MarkdownHighlight />}\n        <LanguageSupport />\n        <JinjaHighlight />\n        {children}\n      </EditorProvider>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/extensions/jinja.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { useInjector } from '@flowgram.ai/coze-editor/react';\nimport { astDecorator } from '@flowgram.ai/coze-editor';\nimport { EditorView } from '@codemirror/view';\n\nfunction JinjaHighlight() {\n  const injector = useInjector();\n\n  useLayoutEffect(\n    () =>\n      injector.inject([\n        astDecorator.whole.of((cursor) => {\n          if (cursor.name === 'JinjaStatementStart' || cursor.name === 'JinjaStatementEnd') {\n            return {\n              type: 'className',\n              className: 'jinja-statement-bracket',\n            };\n          }\n\n          if (cursor.name === 'JinjaComment') {\n            return {\n              type: 'className',\n              className: 'jinja-comment',\n            };\n          }\n\n          if (cursor.name === 'JinjaExpression') {\n            return {\n              type: 'className',\n              className: 'jinja-expression',\n            };\n          }\n        }),\n        EditorView.theme({\n          '.jinja-statement-bracket': {\n            color: '#D1009D',\n          },\n          '.jinja-comment': {\n            color: '#0607094D',\n          },\n          '.jinja-expression': {\n            color: '#4E40E5',\n          },\n        }),\n      ]),\n    [injector]\n  );\n\n  return null;\n}\n\nexport default JinjaHighlight;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/extensions/language-support.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { useInjector } from '@flowgram.ai/coze-editor/react';\nimport { languageSupport } from '@flowgram.ai/coze-editor/preset-prompt';\n\nfunction LanguageSupport() {\n  const injector = useInjector();\n\n  useLayoutEffect(() => injector.inject([languageSupport]), [injector]);\n\n  return null;\n}\n\nexport default LanguageSupport;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/extensions/markdown.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { useInjector } from '@flowgram.ai/coze-editor/react';\nimport { astDecorator } from '@flowgram.ai/coze-editor';\nimport { EditorView } from '@codemirror/view';\n\nfunction MarkdownHighlight() {\n  const injector = useInjector();\n\n  useLayoutEffect(\n    () =>\n      injector.inject([\n        astDecorator.whole.of((cursor) => {\n          // # heading\n          if (cursor.name.startsWith('ATXHeading')) {\n            return {\n              type: 'className',\n              className: 'heading',\n            };\n          }\n\n          // *italic*\n          if (cursor.name === 'Emphasis') {\n            return {\n              type: 'className',\n              className: 'emphasis',\n            };\n          }\n\n          // **bold**\n          if (cursor.name === 'StrongEmphasis') {\n            return {\n              type: 'className',\n              className: 'strong-emphasis',\n            };\n          }\n\n          // -\n          // 1.\n          // >\n          if (cursor.name === 'ListMark' || cursor.name === 'QuoteMark') {\n            return {\n              type: 'className',\n              className: 'mark',\n            };\n          }\n        }),\n        EditorView.theme({\n          '.heading': {\n            color: '#00818C',\n            fontWeight: 'bold',\n          },\n          '.emphasis': {\n            fontStyle: 'italic',\n          },\n          '.strong-emphasis': {\n            fontWeight: 'bold',\n          },\n          '.mark': {\n            color: '#4E40E5',\n          },\n        }),\n      ]),\n    [injector]\n  );\n\n  return null;\n}\n\nexport default MarkdownHighlight;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { lazySuspense } from '@/shared';\n\nexport const PromptEditor = lazySuspense(() =>\n  import('./editor').then((module) => ({ default: module.PromptEditor }))\n);\n\nexport type { PromptEditorPropsType } from './editor';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-prompt-editor-container {\n  background-color: var(--semi-color-fill-0);\n  padding-left: 10px;\n  padding-right: 6px;\n}\n\n.gedit-m-prompt-editor-container.has-error {\n  border: 1px solid var(--semi-color-danger-6);\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor/types.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IFlowTemplateValue } from '@/shared';\n\nexport type PropsType = React.PropsWithChildren<{\n  value?: IFlowTemplateValue;\n  onChange: (value?: IFlowTemplateValue) => void;\n  readonly?: boolean;\n  hasError?: boolean;\n  placeholder?: string;\n  activeLinePlaceholder?: string;\n  disableMarkdownHighlight?: boolean;\n  style?: React.CSSProperties;\n}>;\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor-with-inputs/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { IInputsValues } from '@/shared/flow-value';\nimport { PromptEditor, PromptEditorPropsType } from '@/components/prompt-editor';\nimport { EditorInputsTree } from '@/components/coze-editor-extensions';\n\nexport interface PromptEditorWithInputsProps extends PromptEditorPropsType {\n  inputsValues: IInputsValues;\n}\n\nexport function PromptEditorWithInputs({\n  inputsValues,\n  ...restProps\n}: PromptEditorWithInputsProps) {\n  return (\n    <PromptEditor {...restProps}>\n      <EditorInputsTree inputsValues={inputsValues} />\n    </PromptEditor>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/prompt-editor-with-variables/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { PromptEditor, PromptEditorPropsType } from '@/components/prompt-editor';\nimport { EditorVariableTree, EditorVariableTagInject } from '@/components/coze-editor-extensions';\n\nexport interface PromptEditorWithVariablesProps extends PromptEditorPropsType {}\n\nexport function PromptEditorWithVariables(props: PromptEditorWithVariablesProps) {\n  return (\n    <PromptEditor {...props}>\n      <EditorVariableTree />\n      <EditorVariableTagInject />\n    </PromptEditor>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/sql-editor-with-variables/editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { EditorVariableTree, EditorVariableTagInject } from '@/components/coze-editor-extensions';\nimport { SQLCodeEditor, type CodeEditorPropsType } from '@/components/code-editor';\n\nexport interface SQLEditorWithVariablesProps extends Omit<CodeEditorPropsType, 'languageId'> {}\n\nexport function SQLEditorWithVariables(props: SQLEditorWithVariablesProps) {\n  return (\n    <SQLCodeEditor\n      activeLinePlaceholder={I18n.t(\"Press '@' to Select variable\")}\n      {...props}\n      options={{\n        ...(props.options || {}),\n      }}\n    >\n      <EditorVariableTree />\n      <EditorVariableTagInject />\n    </SQLCodeEditor>\n  );\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/sql-editor-with-variables/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { lazySuspense } from '@/shared';\n\nexport const SQLEditorWithVariables = lazySuspense(() =>\n  import('./editor').then((module) => ({ default: module.SQLEditorWithVariables }))\n);\n\nexport type { SQLEditorWithVariablesProps } from './editor';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/type-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { IJsonSchema, useTypeManager, JsonSchemaTypeManager } from '@flowgram.ai/json-schema';\nimport { Cascader, Icon, IconButton } from '@douyinfe/semi-ui';\n\nimport { createInjectMaterial } from '@/shared/inject-material';\n\nexport interface TypeSelectorProps {\n  value?: Partial<IJsonSchema>;\n  onChange?: (value?: Partial<IJsonSchema>) => void;\n  readonly?: boolean;\n  /**\n   * @deprecated use readonly instead\n   */\n  disabled?: boolean;\n  style?: React.CSSProperties;\n}\n\nconst labelStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: 5 };\n\nexport const getTypeSelectValue = (value?: Partial<IJsonSchema>): string[] | undefined => {\n  if (value?.type === 'array' && value?.items) {\n    return [value.type, ...(getTypeSelectValue(value.items) || [])];\n  }\n\n  return value?.type ? [value.type] : undefined;\n};\n\nexport const parseTypeSelectValue = (value?: string[]): Partial<IJsonSchema> | undefined => {\n  const [type, ...subTypes] = value || [];\n\n  if (type === 'array') {\n    return { type: 'array', items: parseTypeSelectValue(subTypes) };\n  }\n\n  return { type };\n};\n\nexport function TypeSelector(props: TypeSelectorProps) {\n  const { value, onChange, readonly, disabled, style } = props;\n\n  const selectValue = useMemo(() => getTypeSelectValue(value), [value]);\n\n  const typeManager = useTypeManager() as JsonSchemaTypeManager;\n\n  const icon = typeManager.getDisplayIcon(value || {});\n\n  const options = useMemo(\n    () =>\n      typeManager.getTypeRegistriesWithParentType().map((_type) => {\n        const isArray = _type.type === 'array';\n\n        return {\n          label: (\n            <div style={labelStyle}>\n              <Icon size=\"small\" svg={_type.icon} />\n              {typeManager.getTypeBySchema(_type)?.label || _type.type}\n            </div>\n          ),\n          value: _type.type,\n          children: isArray\n            ? typeManager.getTypeRegistriesWithParentType('array').map((_type) => ({\n                label: (\n                  <div style={labelStyle}>\n                    <Icon\n                      size=\"small\"\n                      svg={typeManager.getDisplayIcon({\n                        type: 'array',\n                        items: { type: _type.type },\n                      })}\n                    />\n                    {typeManager.getTypeBySchema(_type)?.label || _type.type}\n                  </div>\n                ),\n                value: _type.type,\n              }))\n            : [],\n        };\n      }),\n    []\n  );\n\n  const isDisabled = readonly || disabled;\n\n  return (\n    <Cascader\n      disabled={isDisabled}\n      size=\"small\"\n      triggerRender={() => (\n        <IconButton\n          size=\"small\"\n          style={{\n            ...(isDisabled ? { pointerEvents: 'none' } : {}),\n            ...(style || {}),\n          }}\n          disabled={isDisabled}\n          icon={icon}\n        />\n      )}\n      treeData={options}\n      value={selectValue}\n      leafOnly={true}\n      onChange={(value) => {\n        onChange?.(parseTypeSelectValue(value as string[]));\n      }}\n    />\n  );\n}\n\nTypeSelector.renderKey = 'type-selector-render-key';\nexport const InjectTypeSelector = createInjectMaterial(TypeSelector);\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/variable-selector/context.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { createContext, useContext, useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { BaseVariableField } from '@flowgram.ai/editor';\n\ntype VariableField = BaseVariableField<{\n  icon?: string | JSX.Element;\n  title?: string;\n  disabled?: boolean;\n}>;\n\nexport const VariableSelectorContext = createContext<{\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n  skipVariable?: (variable: VariableField) => boolean;\n}>({});\n\nexport const useVariableSelectorContext = () => useContext(VariableSelectorContext);\n\nexport const VariableSelectorProvider = ({\n  children,\n  skipVariable,\n  includeSchema,\n  excludeSchema,\n}: {\n  skipVariable?: (variable?: BaseVariableField) => boolean;\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n  children: React.ReactNode;\n}) => {\n  const context = useMemo(\n    () => ({\n      skipVariable,\n      includeSchema,\n      excludeSchema,\n    }),\n    [skipVariable, includeSchema, excludeSchema]\n  );\n\n  return (\n    <VariableSelectorContext.Provider value={context}>{children}</VariableSelectorContext.Provider>\n  );\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/variable-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { I18n } from '@flowgram.ai/editor';\nimport { type TriggerRenderProps } from '@douyinfe/semi-ui/lib/es/treeSelect';\nimport { type TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';\nimport { Popover, Tag, TreeSelect } from '@douyinfe/semi-ui';\nimport { IconChevronDownStroked, IconIssueStroked } from '@douyinfe/semi-icons';\n\nimport { createInjectMaterial } from '@/shared';\n\nimport { useVariableTree } from './use-variable-tree';\nimport { useVariableSelectorContext } from './context';\n\nimport './styles.css';\n\nexport interface VariableSelectorProps {\n  value?: string[];\n  config?: {\n    placeholder?: string;\n    notFoundContent?: string;\n  };\n  onChange: (value?: string[]) => void;\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n  readonly?: boolean;\n  hasError?: boolean;\n  style?: React.CSSProperties;\n  triggerRender?: (props: TriggerRenderProps) => React.ReactNode;\n}\n\nexport { useVariableTree };\n\nexport const VariableSelector = ({\n  value,\n  config = {},\n  onChange,\n  style,\n  readonly = false,\n  includeSchema,\n  excludeSchema,\n  hasError,\n  triggerRender,\n}: VariableSelectorProps) => {\n  const { skipVariable } = useVariableSelectorContext();\n\n  const treeData = useVariableTree({\n    includeSchema,\n    excludeSchema,\n    skipVariable,\n  });\n\n  const treeValue = useMemo(() => {\n    if (typeof value === 'string') {\n      console.warn(\n        'The Value of VariableSelector is a string, it should be an ARRAY. \\n',\n        'Please check the value of VariableSelector \\n'\n      );\n      return value;\n    }\n    return value?.join('.');\n  }, [value]);\n\n  const renderIcon = (icon: string | JSX.Element) => {\n    if (typeof icon === 'string') {\n      return <img style={{ marginRight: 8 }} width={12} height={12} src={icon} />;\n    }\n\n    return icon;\n  };\n\n  return (\n    <>\n      <TreeSelect\n        className={`gedit-m-variable-selector-tree-select ${hasError ? 'error' : ''}`}\n        dropdownMatchSelectWidth={false}\n        disabled={readonly}\n        treeData={treeData}\n        size=\"small\"\n        value={treeValue}\n        clearIcon={null}\n        style={style}\n        validateStatus={hasError ? 'error' : undefined}\n        dropdownClassName=\"gedit-m-variable-selector-dropdown\"\n        onChange={(_, _config) => {\n          onChange((_config as TreeNodeData).keyPath as string[]);\n        }}\n        renderSelectedItem={(_option: TreeNodeData) => {\n          if (!_option?.keyPath) {\n            return (\n              <Tag\n                className=\"gedit-m-variable-selector-tag\"\n                prefixIcon={<IconIssueStroked />}\n                color=\"amber\"\n                closable={!readonly}\n                onClose={() => onChange(undefined)}\n              >\n                {config?.notFoundContent ?? 'Undefined'}\n              </Tag>\n            );\n          }\n\n          const rootIcon = renderIcon(_option.rootMeta?.icon || _option?.icon);\n\n          const rootTitle = (\n            <div className=\"gedit-m-variable-selector-root-title\">\n              {_option.rootMeta?.title\n                ? `${_option.rootMeta?.title} ${_option.isRoot ? '' : '-'} `\n                : null}\n            </div>\n          );\n\n          return (\n            <div>\n              <Popover\n                content={\n                  <div className=\"gedit-m-variable-selector-tag-pop\">\n                    {rootIcon}\n                    {rootTitle}\n                    <div className=\"gedit-m-variable-selector-var-name\">\n                      {_option.keyPath.slice(1).join('.')}\n                    </div>\n                  </div>\n                }\n              >\n                <Tag\n                  className=\"gedit-m-variable-selector-tag\"\n                  prefixIcon={rootIcon}\n                  closable={!readonly}\n                  onClose={() => onChange(undefined)}\n                >\n                  {rootTitle}\n                  {!_option.isRoot && (\n                    <div className=\"gedit-m-variable-selector-var-name in-selector\">\n                      {_option.label}\n                    </div>\n                  )}\n                </Tag>\n              </Popover>\n            </div>\n          );\n        }}\n        showClear={false}\n        arrowIcon={<IconChevronDownStroked size=\"small\" />}\n        triggerRender={triggerRender}\n        placeholder={config?.placeholder ?? I18n.t('Select Variable')}\n      />\n    </>\n  );\n};\n\nVariableSelector.renderKey = 'variable-selector-render-key';\nexport const InjectVariableSelector = createInjectMaterial(VariableSelector);\n\nexport { VariableSelectorProvider } from './context';\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/variable-selector/styles.css",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.gedit-m-variable-selector-root-title {\n  margin-right: 4px;\n  min-width: 20px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  color: var(--semi-color-text-2);\n}\n\n.gedit-m-variable-selector-var-name {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  &.in-selector {\n    min-width: 50%;\n  }\n}\n\n.gedit-m-variable-selector-tag {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  margin: 0;\n  height: 22px;\n\n  .semi-tag-content-center {\n    justify-content: flex-start;\n  }\n}\n\n.gedit-m-variable-selector-tree-select {\n  outline: none;\n\n  &.error {\n    outline: 1px solid red;\n  }\n\n  .semi-tree-select-selection {\n    padding: 0px;\n    height: 22px;\n  }\n\n  .semi-tree-select-selection-content {\n    width: 100%;\n  }\n\n  .semi-tree-select-selection-placeholder {\n    padding-left: 10px;\n  }\n}\n\n.gedit-m-variable-selector-tag-pop {\n  padding: 10px;\n  display: inline-flex;\n  align-items: center;\n  justify-content: flex-start;\n  white-space: nowrap;\n}\n\n.gedit-m-variable-selector-dropdown {\n  max-height: 300px;\n  overflow: auto;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/components/variable-selector/use-variable-tree.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport {\n  IJsonSchema,\n  JsonSchemaTypeManager,\n  JsonSchemaUtils,\n  useTypeManager,\n} from '@flowgram.ai/json-schema';\nimport { ASTMatch, BaseVariableField, useAvailableVariables } from '@flowgram.ai/editor';\nimport { TreeNodeData } from '@douyinfe/semi-ui/lib/es/tree';\nimport { Icon } from '@douyinfe/semi-ui';\n\nimport { useVariableSelectorContext } from './context';\n\ntype VariableField = BaseVariableField<{\n  icon?: string | JSX.Element;\n  title?: string;\n  disabled?: boolean;\n}>;\n\nexport function useVariableTree(params: {\n  includeSchema?: IJsonSchema | IJsonSchema[];\n  excludeSchema?: IJsonSchema | IJsonSchema[];\n  skipVariable?: (variable: VariableField) => boolean;\n}): TreeNodeData[] {\n  const context = useVariableSelectorContext();\n\n  const {\n    includeSchema = context.includeSchema,\n    excludeSchema = context.excludeSchema,\n    skipVariable = context.skipVariable,\n  } = params;\n\n  const typeManager = useTypeManager() as JsonSchemaTypeManager;\n  const variables = useAvailableVariables();\n\n  const getVariableTypeIcon = useCallback((variable: VariableField) => {\n    if (variable.meta?.icon) {\n      if (typeof variable.meta.icon === 'string') {\n        return <img style={{ marginRight: 8 }} width={12} height={12} src={variable.meta.icon} />;\n      }\n\n      return variable.meta.icon;\n    }\n\n    const schema = JsonSchemaUtils.astToSchema(variable.type, { drilldownObject: false });\n\n    return <Icon size=\"small\" svg={typeManager.getDisplayIcon(schema || {})} />;\n  }, []);\n\n  const renderVariable = (\n    variable: VariableField,\n    parentFields: VariableField[] = []\n  ): TreeNodeData | null => {\n    let type = variable?.type;\n\n    if (!type) {\n      return null;\n    }\n\n    let children: TreeNodeData[] | undefined;\n\n    if (ASTMatch.isObject(type)) {\n      children = (type.properties || [])\n        .map((_property) => renderVariable(_property as VariableField, [...parentFields, variable]))\n        .filter(Boolean) as TreeNodeData[];\n    }\n\n    const keyPath = [...parentFields.map((_field) => _field.key), variable.key];\n    const key = keyPath.join('.');\n\n    const isSchemaInclude = includeSchema\n      ? JsonSchemaUtils.isASTMatchSchema(type, includeSchema)\n      : true;\n    const isSchemaExclude = excludeSchema\n      ? JsonSchemaUtils.isASTMatchSchema(type, excludeSchema)\n      : false;\n    const isCustomSkip = skipVariable ? skipVariable(variable) : false;\n\n    // disabled in meta when created\n    const isMetaDisabled = variable.meta?.disabled;\n\n    const isSchemaMatch = isSchemaInclude && !isSchemaExclude && !isCustomSkip && !isMetaDisabled;\n\n    // If not match, and no children, return null\n    if (!isSchemaMatch && !children?.length) {\n      return null;\n    }\n\n    return {\n      key: key,\n      label: variable.meta?.title || variable.key,\n      value: key,\n      keyPath,\n      icon: getVariableTypeIcon(variable),\n      children,\n      disabled: !isSchemaMatch,\n      rootMeta: parentFields[0]?.meta || variable.meta,\n      isRoot: !parentFields?.length,\n    };\n  };\n\n  return [...variables.slice(0).reverse()]\n    .map((_variable) => renderVariable(_variable as VariableField))\n    .filter(Boolean) as TreeNodeData[];\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/auto-rename-ref/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  VariableFieldKeyRenameService,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue, IFlowTemplateValue } from '@/shared';\nimport { FlowValueUtils } from '@/shared';\n\n/**\n * Auto rename ref when form item's key is renamed\n *\n * Example:\n *\n * formMeta: {\n *  effects: {\n *    \"inputsValues\": autoRenameRefEffect,\n *  }\n * }\n */\nexport const autoRenameRefEffect: EffectOptions[] = [\n  {\n    event: DataEvent.onValueInit,\n    effect: ((params) => {\n      const { context, form, name } = params;\n\n      const renameService = context.node.getService(VariableFieldKeyRenameService);\n\n      const disposable = renameService.onRename(({ before, after }) => {\n        const beforeKeyPath = [\n          ...before.parentFields.map((_field) => _field.key).reverse(),\n          before.key,\n        ];\n        const afterKeyPath = [\n          ...after.parentFields.map((_field) => _field.key).reverse(),\n          after.key,\n        ];\n\n        // traverse rename refs inside form item 'name'\n        traverseRef(name, form.getValueIn(name), (_drilldownName, _v) => {\n          if (_v.type === 'ref') {\n            // ref auto rename\n            if (isKeyPathMatch(_v.content, beforeKeyPath)) {\n              _v.content = [...afterKeyPath, ...(_v.content || [])?.slice(beforeKeyPath.length)];\n              form.setValueIn(_drilldownName, _v);\n            }\n          } else if (_v.type === 'template') {\n            // template auto rename\n            const templateKeyPaths = FlowValueUtils.getTemplateKeyPaths(_v);\n            let hasMatch = false;\n\n            templateKeyPaths.forEach((_keyPath) => {\n              if (isKeyPathMatch(_keyPath, beforeKeyPath)) {\n                hasMatch = true;\n                const nextKeyPath = [\n                  ...afterKeyPath,\n                  ...(_keyPath || [])?.slice(beforeKeyPath.length),\n                ];\n                _v.content = _v.content?.replace(\n                  `{{${_keyPath.join('.')}}`,\n                  `{{${nextKeyPath.join('.')}}`\n                );\n              }\n            });\n\n            if (hasMatch) {\n              form.setValueIn(_drilldownName, { ..._v });\n            }\n          }\n        });\n      });\n\n      return () => {\n        disposable.dispose();\n      };\n    }) as Effect,\n  },\n];\n\n/**\n * If ref value's keyPath is the under as targetKeyPath\n * @param value\n * @param targetKeyPath\n * @returns\n */\nfunction isKeyPathMatch(keyPath: string[] = [], targetKeyPath: string[]) {\n  return targetKeyPath.every((_key, index) => _key === keyPath[index]);\n}\n\n/**\n * Traverse value to find ref\n * @param value\n * @param options\n * @returns\n */\nfunction traverseRef(\n  name: string,\n  value: any,\n  cb: (name: string, _v: IFlowRefValue | IFlowTemplateValue) => void\n) {\n  for (const { value: _v, path } of FlowValueUtils.traverse(value, {\n    includeTypes: ['ref', 'template'],\n    path: name,\n  })) {\n    cb(path, _v as IFlowRefValue | IFlowTemplateValue);\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { autoRenameRefEffect } from './auto-rename-ref';\nexport { listenRefSchemaChange } from './listen-ref-schema-change';\nexport { listenRefValueChange } from './listen-ref-value-change';\nexport { provideBatchInputEffect } from './provide-batch-input';\nexport { provideJsonSchemaOutputs } from './provide-json-schema-outputs';\nexport { syncVariableTitle } from './sync-variable-title';\nexport { validateWhenVariableSync } from './validate-when-variable-sync';\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/listen-ref-schema-change/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema, JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport {\n  BaseType,\n  DataEvent,\n  Effect,\n  EffectFuncProps,\n  EffectOptions,\n  getNodeScope,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '@/shared';\n\n/**\n * Example:\n * const formMeta = {\n *   effect: {\n *     'inputsValues.*': listenRefSchemaChange(({ name, schema, form }) => {\n *       form.setValueIn(`${name}.schema`, schema);\n *     })\n *   }\n * }\n * @param cb\n * @returns\n */\nexport const listenRefSchemaChange = (\n  cb: (props: EffectFuncProps<IFlowRefValue> & { schema?: IJsonSchema }) => void\n): EffectOptions[] => [\n  {\n    event: DataEvent.onValueInitOrChange,\n    effect: ((params) => {\n      const { context, value } = params;\n\n      if (value?.type !== 'ref') {\n        return () => null;\n      }\n\n      const disposable = getNodeScope(context.node).available.trackByKeyPath<BaseType | undefined>(\n        value?.content || [],\n        (_type) => {\n          cb({ ...params, schema: JsonSchemaUtils.astToSchema(_type) });\n        },\n        {\n          selector: (_v) => _v?.type,\n        }\n      );\n      return () => {\n        disposable.dispose();\n      };\n    }) as Effect,\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/listen-ref-value-change/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  BaseVariableField,\n  DataEvent,\n  Effect,\n  EffectFuncProps,\n  EffectOptions,\n  getNodeScope,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '@/shared';\n\n/**\n * Example:\n * const formMeta = {\n *   effect: {\n *     'inputsValues.*': listenRefValueChange(({ name, variable, form }) => {\n *       const schema = JsonSchemaUtils.astToSchema(variable?.type);\n *       form.setValueIn(`${name}.schema`, schema);\n *     })\n *   }\n * }\n * @param cb\n * @returns\n */\nexport const listenRefValueChange = (\n  cb: (props: EffectFuncProps<IFlowRefValue> & { variable?: BaseVariableField }) => void\n): EffectOptions[] => [\n  {\n    event: DataEvent.onValueInitOrChange,\n    effect: ((params) => {\n      const { context, value } = params;\n\n      if (value?.type !== 'ref') {\n        return () => null;\n      }\n\n      const disposable = getNodeScope(context.node).available.trackByKeyPath(\n        value?.content || [],\n        (v) => {\n          cb({ ...params, variable: v });\n        }\n      );\n      return () => {\n        disposable.dispose();\n      };\n    }) as Effect,\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/provide-batch-input/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTFactory,\n  EffectOptions,\n  FlowNodeRegistry,\n  createEffectFromVariableProvider,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '@/shared';\n\nexport const provideBatchInputEffect: EffectOptions[] = createEffectFromVariableProvider({\n  private: true,\n  parse: (value: IFlowRefValue, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}_locals`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: [\n          ASTFactory.createProperty({\n            key: 'item',\n            initializer: ASTFactory.createEnumerateExpression({\n              enumerateFor: ASTFactory.createKeyPathExpression({\n                keyPath: value.content || [],\n              }),\n            }),\n          }),\n          ASTFactory.createProperty({\n            key: 'index',\n            type: ASTFactory.createNumber(),\n          }),\n        ],\n      }),\n    }),\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/provide-json-schema-outputs/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { JsonSchemaUtils, IJsonSchema } from '@flowgram.ai/json-schema';\nimport {\n  ASTFactory,\n  EffectOptions,\n  FlowNodeRegistry,\n  createEffectFromVariableProvider,\n} from '@flowgram.ai/editor';\n\nexport const provideJsonSchemaOutputs: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: IJsonSchema, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title') || ctx.node.id,\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: JsonSchemaUtils.schemaToAST(value),\n    }),\n  ],\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/sync-variable-title/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  FlowNodeRegistry,\n  FlowNodeVariableData,\n} from '@flowgram.ai/editor';\n\nexport const syncVariableTitle: EffectOptions[] = [\n  {\n    event: DataEvent.onValueChange,\n    effect: (({ value, context }) => {\n      context.node.getData(FlowNodeVariableData).allScopes.forEach((_scope) => {\n        _scope.output.variables.forEach((_var) => {\n          _var.updateMeta({\n            ...(_var.meta || {}),\n            title: value || context.node.id,\n            icon: context.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n          });\n        });\n      });\n    }) as Effect,\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-materials/src/effects/validate-when-variable-sync/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  getNodeScope,\n  getNodePrivateScope,\n} from '@flowgram.ai/editor';\n\nexport const validateWhenVariableSync = ({\n  scope,\n}: {\n  scope?: 'private' | 'public';\n} = {}): EffectOptions[] => [\n  {\n    event: DataEvent.onValueInit,\n    effect: (({ context, form, name }) => {\n      const nodeScope =\n        scope === 'private' ? getNodePrivateScope(context.node) : getNodeScope(context.node);\n\n      const disposable = nodeScope.available.onListOrAnyVarChange(() => {\n        const errorKeys = Object.entries(form.state.errors || {})\n          .filter(([_, errors]) => errors?.length > 0)\n          .filter(([key]) => key.startsWith(name) || name.startsWith(key))\n          .map(([key]) => key);\n\n        if (errorKeys.length > 0) {\n          form.validate();\n        }\n      });\n\n      return () => disposable.dispose();\n    }) as Effect,\n  },\n];\n"
  },
  {
    "path": "packages/materials/form-materials/src/form-plugins/batch-outputs-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { set } from 'lodash-es';\nimport { JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport {\n  ASTFactory,\n  createEffectFromVariableProvider,\n  defineFormPluginCreator,\n  FlowNodeRegistry,\n  getNodePrivateScope,\n  getNodeScope,\n  ScopeChainTransformService,\n  type EffectOptions,\n  type FormPluginCreator,\n  FlowNodeScopeType,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue } from '@/shared';\n\nexport const provideBatchOutputsEffect: EffectOptions[] = createEffectFromVariableProvider({\n  parse: (value: Record<string, IFlowRefValue>, ctx) => [\n    ASTFactory.createVariableDeclaration({\n      key: `${ctx.node.id}`,\n      meta: {\n        title: ctx.node.form?.getValueIn('title'),\n        icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n      },\n      type: ASTFactory.createObject({\n        properties: Object.entries(value).map(([_key, value]) =>\n          ASTFactory.createProperty({\n            key: _key,\n            initializer: ASTFactory.createWrapArrayExpression({\n              wrapFor: ASTFactory.createKeyPathExpression({\n                keyPath: value?.content || [],\n              }),\n            }),\n          })\n        ),\n      }),\n    }),\n  ],\n});\n\n/**\n * Free Layout only right now\n */\nexport const createBatchOutputsFormPlugin: FormPluginCreator<{\n  outputKey: string;\n  /**\n   * if set, infer json schema to inferTargetKey when submit\n   */\n  inferTargetKey?: string;\n}> = defineFormPluginCreator({\n  name: 'batch-outputs-plugin',\n  onSetupFormMeta({ mergeEffect, addFormatOnSubmit }, { outputKey, inferTargetKey }) {\n    mergeEffect({\n      [outputKey]: provideBatchOutputsEffect,\n    });\n\n    if (inferTargetKey) {\n      addFormatOnSubmit((formData, ctx) => {\n        const outputVariable = getNodeScope(ctx.node).output.variables?.[0];\n\n        if (outputVariable?.type) {\n          set(formData, inferTargetKey, JsonSchemaUtils.astToSchema(outputVariable?.type));\n        }\n\n        return formData;\n      });\n    }\n  },\n  onInit(ctx, { outputKey }) {\n    const chainTransformService = ctx.node.getService(ScopeChainTransformService);\n\n    const batchNodeType = ctx.node.flowNodeType;\n\n    const transformerId = `${batchNodeType}-outputs`;\n\n    if (chainTransformService.hasTransformer(transformerId)) {\n      return;\n    }\n\n    chainTransformService.registerTransformer(transformerId, {\n      transformCovers: (covers, ctx) => {\n        const node = ctx.scope.meta?.node;\n\n        // Child Node's variable can cover parent\n        if (node?.parent?.flowNodeType === batchNodeType) {\n          return [...covers, getNodeScope(node.parent)];\n        }\n\n        return covers;\n      },\n      transformDeps(scopes, ctx) {\n        const scopeMeta = ctx.scope.meta;\n\n        if (scopeMeta?.type === FlowNodeScopeType.private) {\n          return scopes;\n        }\n\n        const node = scopeMeta?.node;\n\n        // Public of Loop Node depends on child Node\n        if (node?.flowNodeType === batchNodeType) {\n          // Get all child blocks\n          const childBlocks = node.blocks;\n\n          // public scope of all child blocks\n          return [\n            getNodePrivateScope(node),\n            ...childBlocks.map((_childBlock) => getNodeScope(_childBlock)),\n          ];\n        }\n\n        return scopes;\n      },\n    });\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/form-plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createBatchOutputsFormPlugin, provideBatchOutputsEffect } from './batch-outputs-plugin';\nexport { createInferAssignPlugin } from './infer-assign-plugin';\nexport { createInferInputsPlugin } from './infer-inputs-plugin';\n"
  },
  {
    "path": "packages/materials/form-materials/src/form-plugins/infer-assign-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { set, uniqBy } from 'lodash-es';\nimport { JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport {\n  ASTFactory,\n  createEffectFromVariableProvider,\n  defineFormPluginCreator,\n  FlowNodeRegistry,\n  getNodeScope,\n} from '@flowgram.ai/editor';\n\nimport { IFlowRefValue, IFlowValue } from '@/shared';\n\ntype AssignValueType =\n  | {\n      operator: 'assign';\n      left?: IFlowRefValue;\n      right?: IFlowValue;\n    }\n  | {\n      operator: 'declare';\n      left?: string;\n      right?: IFlowValue;\n    };\n\ninterface InputConfig {\n  assignKey: string;\n  outputKey: string;\n}\n\nexport const createInferAssignPlugin = defineFormPluginCreator<InputConfig>({\n  onSetupFormMeta({ addFormatOnSubmit, mergeEffect }, { assignKey, outputKey }) {\n    if (!assignKey || !outputKey) {\n      return;\n    }\n\n    mergeEffect({\n      [assignKey]: createEffectFromVariableProvider({\n        parse: (value: AssignValueType[], ctx) => {\n          const declareRows = uniqBy(\n            value.filter((_v) => _v.operator === 'declare' && _v.left && _v.right),\n            'left'\n          );\n\n          return [\n            ASTFactory.createVariableDeclaration({\n              key: `${ctx.node.id}`,\n              meta: {\n                title: ctx.node.form?.getValueIn('title'),\n                icon: ctx.node.getNodeRegistry<FlowNodeRegistry>().info?.icon,\n              },\n              type: ASTFactory.createObject({\n                properties: declareRows.map((_v) =>\n                  ASTFactory.createProperty({\n                    key: _v.left as string,\n                    type:\n                      _v.right?.type === 'constant'\n                        ? JsonSchemaUtils.schemaToAST(_v.right?.schema || {})\n                        : undefined,\n                    initializer:\n                      _v.right?.type === 'ref'\n                        ? ASTFactory.createKeyPathExpression({\n                            keyPath: _v.right?.content || [],\n                          })\n                        : {},\n                  })\n                ),\n              }),\n            }),\n          ];\n        },\n      }),\n    });\n\n    addFormatOnSubmit((formData, ctx) => {\n      set(\n        formData,\n        outputKey,\n        JsonSchemaUtils.astToSchema(getNodeScope(ctx.node).output.variables?.[0]?.type)\n      );\n\n      return formData;\n    });\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/form-plugins/infer-inputs-plugin/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get, omit, set } from 'lodash-es';\nimport { Immer } from 'immer';\nimport { defineFormPluginCreator, getNodePrivateScope, getNodeScope } from '@flowgram.ai/editor';\n\nimport { FlowValueUtils } from '@/shared';\n\nconst { produce } = new Immer({ autoFreeze: false });\n\ninterface InputConfig {\n  sourceKey: string;\n  targetKey: string;\n  scope?: 'private' | 'public';\n  /**\n   * For backend runtime, constant schema is redundant, so we can choose to ignore it\n   */\n  ignoreConstantSchema?: boolean;\n}\n\nexport const createInferInputsPlugin = defineFormPluginCreator<InputConfig>({\n  onSetupFormMeta(\n    { addFormatOnSubmit, addFormatOnInit },\n    { sourceKey, targetKey, scope, ignoreConstantSchema }\n  ) {\n    if (!sourceKey || !targetKey) {\n      return;\n    }\n\n    addFormatOnSubmit((formData, ctx) =>\n      produce(formData, (draft: any) => {\n        const sourceData = get(formData, sourceKey);\n\n        set(\n          draft,\n          targetKey,\n          FlowValueUtils.inferJsonSchema(\n            sourceData,\n            scope === 'private' ? getNodePrivateScope(ctx.node) : getNodeScope(ctx.node)\n          )\n        );\n\n        if (ignoreConstantSchema) {\n          for (const { value, path } of FlowValueUtils.traverse(sourceData, {\n            includeTypes: ['constant'],\n          })) {\n            if (FlowValueUtils.isConstant(value) && value?.schema) {\n              set(formData, `${sourceKey}.${path}`, omit(value, ['schema']));\n            }\n          }\n        }\n      })\n    );\n\n    if (ignoreConstantSchema) {\n      // Revert Schema in frontend\n      addFormatOnInit((formData, ctx) => {\n        const targetSchema = get(formData, targetKey);\n\n        if (!targetSchema) {\n          return formData;\n        }\n\n        // For backend data, it's not necessary to use immer\n        for (const { value, pathArr } of FlowValueUtils.traverse(get(formData, sourceKey), {\n          includeTypes: ['constant'],\n        })) {\n          if (FlowValueUtils.isConstant(value) && !value?.schema) {\n            const schemaPath = pathArr.map((_item) => `properties.${_item}`).join('.');\n            const schema = get(targetSchema, schemaPath);\n            if (schema) {\n              set(value, 'schema', schema);\n            }\n          }\n        }\n\n        return formData;\n      });\n    }\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useObjectList } from './use-object-list';\n"
  },
  {
    "path": "packages/materials/form-materials/src/hooks/use-object-list/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { difference, get, isObject, set } from 'lodash-es';\n\nfunction genId() {\n  return nanoid();\n}\n\ninterface ListItem<ValueType> {\n  id: string;\n  key?: string;\n  value?: ValueType;\n}\n\ntype ObjectType<ValueType> = Record<string, ValueType | undefined>;\n\nexport function useObjectList<ValueType>({\n  value,\n  onChange,\n  sortIndexKey,\n}: {\n  value?: ObjectType<ValueType>;\n  onChange: (value?: ObjectType<ValueType>) => void;\n  sortIndexKey?: string | ((item: ValueType | undefined) => string);\n}) {\n  const [list, setList] = useState<ListItem<ValueType>[]>([]);\n\n  const effectVersion = useRef(0);\n  const changeVersion = useRef(0);\n\n  const getSortIndex = (value?: ValueType) => {\n    if (typeof sortIndexKey === 'function') {\n      return get(value, sortIndexKey(value)) || 0;\n    }\n    return get(value, sortIndexKey || '') || 0;\n  };\n\n  useEffect(() => {\n    effectVersion.current = effectVersion.current + 1;\n    if (effectVersion.current === changeVersion.current) {\n      return;\n    }\n    effectVersion.current = changeVersion.current;\n\n    setList((_prevList) => {\n      const newKeys = Object.entries(value || {})\n        .sort((a, b) => getSortIndex(a[1]) - getSortIndex(b[1]))\n        .map(([key]) => key);\n\n      const oldKeys = _prevList.map((item) => item.key).filter(Boolean) as string[];\n      const addKeys = difference(newKeys, oldKeys);\n\n      return _prevList\n        .filter((item) => !item.key || newKeys.includes(item.key))\n        .map((item) => ({\n          id: item.id,\n          key: item.key,\n          value: item.key ? value?.[item.key!] : item.value,\n        }))\n        .concat(\n          addKeys.map((_key) => ({\n            id: genId(),\n            key: _key,\n            value: value?.[_key],\n          }))\n        );\n    });\n  }, [value]);\n\n  const add = (defaultValue?: ValueType) => {\n    setList((prevList) => [\n      ...prevList,\n      {\n        id: genId(),\n        value: defaultValue,\n      },\n    ]);\n  };\n\n  const updateValue = (itemId: string, value: ValueType) => {\n    changeVersion.current = changeVersion.current + 1;\n\n    setList((prevList) => {\n      const nextList = prevList.map((_item) => {\n        if (_item.id === itemId) {\n          return {\n            ..._item,\n            value,\n          };\n        }\n        return _item;\n      });\n\n      onChange(\n        Object.fromEntries(\n          nextList\n            .filter((item) => item.key)\n            .map((item) => [item.key!, item.value])\n            .map((_res, idx) => {\n              const indexKey =\n                typeof sortIndexKey === 'function'\n                  ? sortIndexKey(_res[1] as ValueType | undefined)\n                  : sortIndexKey;\n\n              if (isObject(_res[1]) && indexKey) {\n                set(_res[1], indexKey, idx);\n              }\n              return _res;\n            })\n        )\n      );\n\n      return nextList;\n    });\n  };\n\n  const updateKey = (itemId: string, key: string) => {\n    changeVersion.current = changeVersion.current + 1;\n\n    setList((prevList) => {\n      const nextList = prevList.map((_item) => {\n        if (_item.id === itemId) {\n          return {\n            ..._item,\n            key,\n          };\n        }\n        return _item;\n      });\n\n      onChange(\n        Object.fromEntries(\n          nextList.filter((item) => item.key).map((item) => [item.key!, item.value])\n        )\n      );\n\n      return nextList;\n    });\n  };\n\n  const remove = (itemId: string) => {\n    changeVersion.current = changeVersion.current + 1;\n\n    setList((prevList) => {\n      const nextList = prevList.filter((_item) => _item.id !== itemId);\n\n      onChange(\n        Object.fromEntries(\n          nextList.filter((item) => item.key).map((item) => [item.key!, item.value])\n        )\n      );\n\n      return nextList;\n    });\n  };\n\n  return { list, add, updateKey, updateValue, remove };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  AssignRow,\n  AssignRows,\n  BaseCodeEditor,\n  BatchOutputs,\n  BatchVariableSelector,\n  BlurInput,\n  CodeEditor,\n  CodeEditorMini,\n  ConditionPresetOp,\n  ConditionProvider,\n  ConditionRow,\n  ConstantInput,\n  DBConditionRow,\n  DisplayFlowValue,\n  DisplayInputsValueAllInTag,\n  DisplayInputsValues,\n  DisplayOutputs,\n  DisplaySchemaTag,\n  DisplaySchemaTree,\n  DynamicValueInput,\n  EditorInputsTree,\n  EditorVariableTagInject,\n  EditorVariableTree,\n  InjectDynamicValueInput,\n  InjectTypeSelector,\n  InjectVariableSelector,\n  InputsValues,\n  InputsValuesTree,\n  JsonCodeEditor,\n  JsonEditorWithVariables,\n  JsonSchemaCreator,\n  JsonSchemaEditor,\n  PromptEditor,\n  PromptEditorWithInputs,\n  PromptEditorWithVariables,\n  PythonCodeEditor,\n  SQLCodeEditor,\n  SQLEditorWithVariables,\n  ShellCodeEditor,\n  TypeScriptCodeEditor,\n  TypeSelector,\n  VariableSelector,\n  VariableSelectorProvider,\n  getTypeSelectValue,\n  parseTypeSelectValue,\n  type AssignValueType,\n  type CodeEditorPropsType,\n  type ConditionOpConfig,\n  type ConditionOpConfigs,\n  type ConditionRowValueType,\n  type ConstantInputStrategy,\n  type DBConditionOptionType,\n  type DBConditionRowValueType,\n  type IConditionRule,\n  type IConditionRuleFactory,\n  type JsonEditorWithVariablesProps,\n  type JsonSchemaCreatorProps,\n  type PromptEditorPropsType,\n  type PromptEditorWithInputsProps,\n  type PromptEditorWithVariablesProps,\n  type SQLEditorWithVariablesProps,\n  type TypeSelectorProps,\n  type VariableSelectorProps,\n  useCondition,\n  useConditionContext,\n  useVariableTree,\n} from './components';\nexport {\n  autoRenameRefEffect,\n  listenRefSchemaChange,\n  listenRefValueChange,\n  provideBatchInputEffect,\n  provideJsonSchemaOutputs,\n  syncVariableTitle,\n  validateWhenVariableSync,\n} from './effects';\nexport {\n  createBatchOutputsFormPlugin,\n  createInferAssignPlugin,\n  createInferInputsPlugin,\n  provideBatchOutputsEffect,\n} from './form-plugins';\nexport { useObjectList } from './hooks';\nexport {\n  JsonSchemaTypePresetProvider,\n  JsonSchemaUtils,\n  createDisableDeclarationPlugin,\n  createTypePresetPlugin,\n  type ConstantRendererProps,\n  type IJsonSchema,\n  type JsonSchemaBasicType,\n  type JsonSchemaTypeRegistry,\n  useTypeManager,\n} from './plugins';\nexport {\n  FlowValueUtils,\n  createInjectMaterial,\n  formatLegacyRefOnInit,\n  formatLegacyRefOnSubmit,\n  formatLegacyRefToNewRef,\n  formatNewRefToLegacyRef,\n  isLegacyFlowRefValueSchema,\n  isNewFlowRefValueSchema,\n  lazySuspense,\n  polyfillCreateRoot,\n  type FlowValueType,\n  type IFlowConstantRefValue,\n  type IFlowConstantValue,\n  type IFlowExpressionValue,\n  type IFlowRefValue,\n  type IFlowTemplateValue,\n  type IFlowValue,\n  type IFlowValueExtra,\n  type IInputsValues,\n  type IPolyfillRoot,\n  unstableSetCreateRoot,\n  withSuspense,\n} from './shared';\nexport { validateFlowValue } from './validate';\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/disable-declaration-plugin/create-disable-declaration-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ASTMatch,\n  definePluginCreator,\n  type GlobalEventActionType,\n  VariableEngine,\n} from '@flowgram.ai/editor';\n\nexport const createDisableDeclarationPlugin = definePluginCreator<void>({\n  onInit(ctx) {\n    const variableEngine = ctx.get(VariableEngine);\n\n    const handleEvent = (action: GlobalEventActionType) => {\n      if (ASTMatch.isVariableDeclaration(action.ast)) {\n        if (!action.ast.meta?.disabled) {\n          action.ast.updateMeta({\n            ...(action.ast.meta || {}),\n            disabled: true,\n          });\n        }\n      }\n    };\n\n    variableEngine.onGlobalEvent('NewAST', handleEvent);\n    variableEngine.onGlobalEvent('UpdateAST', handleEvent);\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/disable-declaration-plugin/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createDisableDeclarationPlugin } from './create-disable-declaration-plugin';\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createDisableDeclarationPlugin } from './disable-declaration-plugin';\nexport {\n  JsonSchemaTypePresetProvider,\n  JsonSchemaUtils,\n  createTypePresetPlugin,\n  type ConstantRendererProps,\n  type IJsonSchema,\n  type JsonSchemaBasicType,\n  type JsonSchemaTypeRegistry,\n  useTypeManager,\n} from './json-schema-preset';\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/create-type-preset-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  BaseTypeManager,\n  jsonSchemaContainerModule,\n  JsonSchemaTypeManager,\n} from '@flowgram.ai/json-schema';\nimport { definePluginCreator, type PluginCreator } from '@flowgram.ai/editor';\n\nimport { JsonSchemaTypeRegistry } from './types';\nimport { initRegistries, jsonSchemaTypePreset } from './type-definition';\n\ninitRegistries();\n\ntype TypePresetRegistry = Partial<JsonSchemaTypeRegistry> & Pick<JsonSchemaTypeRegistry, 'type'>;\n\ninterface TypePresetPluginOptions {\n  types?: TypePresetRegistry[];\n  unregisterTypes?: string[];\n}\n\nexport const createTypePresetPlugin: PluginCreator<TypePresetPluginOptions> =\n  definePluginCreator<TypePresetPluginOptions>({\n    onInit(ctx, opts) {\n      const typeManager = ctx.get(BaseTypeManager) as JsonSchemaTypeManager;\n      jsonSchemaTypePreset.forEach((_type) => typeManager.register(_type));\n\n      opts.types?.forEach((_type) => typeManager.register(_type));\n      opts.unregisterTypes?.forEach((_type) => typeManager.unregister(_type));\n    },\n    containerModules: [jsonSchemaContainerModule],\n  });\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type JsonSchemaBasicType,\n  JsonSchemaUtils,\n  type IJsonSchema,\n} from '@flowgram.ai/json-schema';\n\nimport { type ConstantRendererProps, type JsonSchemaTypeRegistry } from './types';\nimport { useTypeManager, JsonSchemaTypePresetProvider } from './react';\nimport { createTypePresetPlugin } from './create-type-preset-plugin';\n\nexport {\n  createTypePresetPlugin,\n  useTypeManager,\n  JsonSchemaTypePresetProvider,\n  JsonSchemaUtils,\n  type IJsonSchema,\n  type JsonSchemaTypeRegistry,\n  type ConstantRendererProps,\n  type JsonSchemaBasicType,\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/react.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  type IJsonSchema,\n  useTypeManager as useOriginTypeManager,\n  TypePresetProvider as OriginTypePresetProvider,\n  JsonSchemaTypeManager,\n} from '@flowgram.ai/json-schema';\n\nimport { type JsonSchemaTypeRegistry } from './types';\nimport { initRegistries, jsonSchemaTypePreset } from './type-definition';\n\n// If you want to use new type Manager, init registries\ninitRegistries();\n\nexport const useTypeManager = () =>\n  useOriginTypeManager() as JsonSchemaTypeManager<IJsonSchema, JsonSchemaTypeRegistry>;\n\nexport const JsonSchemaTypePresetProvider = ({\n  types = [],\n  children,\n}: React.PropsWithChildren<{ types: JsonSchemaTypeRegistry[] }>) => (\n  <OriginTypePresetProvider types={[...jsonSchemaTypePreset, ...types]}>\n    {children}\n  </OriginTypePresetProvider>\n);\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/array.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\nimport { JsonCodeEditor } from '@/components/code-editor';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const arrayRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'array',\n  ConstantRenderer: (props) => (\n    <JsonCodeEditor\n      mini\n      value={props.value}\n      onChange={(v) => props.onChange?.(v)}\n      placeholder={I18n.t('Please Input Array')}\n      readonly={props.readonly}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.IS_EMPTY]: null,\n    [ConditionPresetOp.IS_NOT_EMPTY]: null,\n    [ConditionPresetOp.CONTAINS]: { type: 'array', extra: { weak: true } },\n    [ConditionPresetOp.NOT_CONTAINS]: { type: 'array', extra: { weak: true } },\n    [ConditionPresetOp.EQ]: { type: 'array', extra: { weak: true } },\n    [ConditionPresetOp.NEQ]: { type: 'array', extra: { weak: true } },\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/boolean.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Select } from '@douyinfe/semi-ui';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const booleanRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'boolean',\n  ConstantRenderer: (props) => {\n    const { value, onChange, ...rest } = props;\n    return (\n      <Select\n        placeholder={I18n.t('Please Select Boolean')}\n        size=\"small\"\n        disabled={props.readonly}\n        optionList={[\n          { label: I18n.t('True'), value: 1 },\n          { label: I18n.t('False'), value: 0 },\n        ]}\n        value={value ? 1 : 0}\n        onChange={(value) => onChange?.(!!value)}\n        {...rest}\n      />\n    );\n  },\n  conditionRule: {\n    [ConditionPresetOp.EQ]: { type: 'boolean' },\n    [ConditionPresetOp.NEQ]: { type: 'boolean' },\n    [ConditionPresetOp.IS_TRUE]: null,\n    [ConditionPresetOp.IS_FALSE]: null,\n    [ConditionPresetOp.IN]: {\n      type: 'array',\n      items: { type: 'boolean' },\n    },\n    [ConditionPresetOp.NIN]: {\n      type: 'array',\n      items: { type: 'boolean' },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/date-time.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { format } from 'date-fns';\nimport { type DatePickerProps } from '@douyinfe/semi-ui/lib/es/datePicker';\nimport { DatePicker } from '@douyinfe/semi-ui';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const dateTimeRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'date-time',\n  ConstantRenderer: (props: DatePickerProps & { readonly?: boolean }) => (\n    <DatePicker\n      size=\"small\"\n      type=\"dateTime\"\n      density=\"compact\"\n      defaultValue={Date.now()}\n      style={{ width: '100%', ...(props.style || {}) }}\n      disabled={props.readonly}\n      {...props}\n      onChange={(date) => {\n        props.onChange?.(format(date as Date, \"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'\"));\n      }}\n      value={props.value}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.EQ]: { type: 'date-time' },\n    [ConditionPresetOp.NEQ]: { type: 'date-time' },\n    [ConditionPresetOp.GT]: { type: 'date-time' },\n    [ConditionPresetOp.GTE]: { type: 'date-time' },\n    [ConditionPresetOp.LT]: { type: 'date-time' },\n    [ConditionPresetOp.LTE]: { type: 'date-time' },\n    [ConditionPresetOp.IS_EMPTY]: null,\n    [ConditionPresetOp.IS_NOT_EMPTY]: null,\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { jsonSchemaTypeManager } from '@flowgram.ai/json-schema';\n\nimport { stringRegistry } from './string';\nimport { objectRegistry } from './object';\nimport { numberRegistry } from './number';\nimport { mapRegistry } from './map';\nimport { integerRegistry } from './integer';\nimport { dateTimeRegistry } from './date-time';\nimport { booleanRegistry } from './boolean';\nimport { arrayRegistry } from './array';\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const jsonSchemaTypePreset = [\n  stringRegistry,\n  objectRegistry,\n  numberRegistry,\n  integerRegistry,\n  booleanRegistry,\n  arrayRegistry,\n  mapRegistry,\n  dateTimeRegistry,\n];\n\nexport const initRegistries = () => {\n  if ((jsonSchemaTypeManager.getTypeByName('string') as JsonSchemaTypeRegistry)?.ConstantRenderer) {\n    return;\n  }\n\n  jsonSchemaTypePreset.forEach((_type) => jsonSchemaTypeManager.register(_type));\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/integer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { InputNumber } from '@douyinfe/semi-ui';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const integerRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'integer',\n  ConstantRenderer: (props) => (\n    <InputNumber\n      placeholder={I18n.t('Please Input Integer')}\n      size=\"small\"\n      disabled={props.readonly}\n      precision={0}\n      {...props}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.EQ]: { type: 'number' },\n    [ConditionPresetOp.NEQ]: { type: 'number' },\n    [ConditionPresetOp.GT]: { type: 'number' },\n    [ConditionPresetOp.GTE]: { type: 'number' },\n    [ConditionPresetOp.LT]: { type: 'number' },\n    [ConditionPresetOp.LTE]: { type: 'number' },\n    [ConditionPresetOp.IN]: {\n      type: 'array',\n      extra: { weak: true },\n    },\n    [ConditionPresetOp.NIN]: {\n      type: 'array',\n      extra: { weak: true },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/map.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\nimport { JsonCodeEditor } from '@/components/code-editor';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const mapRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'map',\n  ConstantRenderer: (props) => (\n    <JsonCodeEditor\n      mini\n      value={props.value}\n      onChange={(v) => props.onChange?.(v)}\n      placeholder={I18n.t('Please Input Map')}\n      readonly={props.readonly}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.IS_EMPTY]: null,\n    [ConditionPresetOp.IS_NOT_EMPTY]: null,\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/number.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { InputNumber } from '@douyinfe/semi-ui';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const numberRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'number',\n  ConstantRenderer: (props) => (\n    <InputNumber\n      placeholder={I18n.t('Please Input Number')}\n      size=\"small\"\n      disabled={props.readonly}\n      hideButtons\n      {...props}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.EQ]: { type: 'number' },\n    [ConditionPresetOp.NEQ]: { type: 'number' },\n    [ConditionPresetOp.GT]: { type: 'number' },\n    [ConditionPresetOp.GTE]: { type: 'number' },\n    [ConditionPresetOp.LT]: { type: 'number' },\n    [ConditionPresetOp.LTE]: { type: 'number' },\n    [ConditionPresetOp.IN]: {\n      type: 'array',\n      extra: { weak: true },\n    },\n    [ConditionPresetOp.NIN]: {\n      type: 'array',\n      extra: { weak: true },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/object.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\nimport { JsonCodeEditor } from '@/components/code-editor';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const objectRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'object',\n  ConstantRenderer: (props) => (\n    <JsonCodeEditor\n      mini\n      value={props.value}\n      onChange={(v) => props.onChange?.(v)}\n      placeholder={I18n.t('Please Input Object')}\n      readonly={props.readonly}\n    />\n  ),\n  conditionRule: {\n    [ConditionPresetOp.IS_EMPTY]: null,\n    [ConditionPresetOp.IS_NOT_EMPTY]: null,\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/type-definition/string.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/editor';\nimport { Input, TextArea } from '@douyinfe/semi-ui';\n\nimport { ConditionPresetOp } from '@/components/condition-context/op';\n\nimport { type JsonSchemaTypeRegistry } from '../types';\n\nexport const stringRegistry: Partial<JsonSchemaTypeRegistry> = {\n  type: 'string',\n  ConstantRenderer: (props) =>\n    props?.enableMultiLineStr ? (\n      <TextArea\n        autosize\n        rows={1}\n        placeholder={I18n.t('Please Input String')}\n        disabled={props.readonly}\n        {...props}\n      />\n    ) : (\n      <Input\n        size=\"small\"\n        placeholder={I18n.t('Please Input String')}\n        disabled={props.readonly}\n        {...props}\n      />\n    ),\n  conditionRule: {\n    [ConditionPresetOp.EQ]: { type: 'string' },\n    [ConditionPresetOp.NEQ]: { type: 'string' },\n    [ConditionPresetOp.CONTAINS]: { type: 'string' },\n    [ConditionPresetOp.NOT_CONTAINS]: { type: 'string' },\n    [ConditionPresetOp.IN]: {\n      type: 'array',\n      items: { type: 'string' },\n    },\n    [ConditionPresetOp.NIN]: {\n      type: 'array',\n      items: { type: 'string' },\n    },\n    [ConditionPresetOp.IS_EMPTY]: null,\n    [ConditionPresetOp.IS_NOT_EMPTY]: null,\n  },\n};\n"
  },
  {
    "path": "packages/materials/form-materials/src/plugins/json-schema-preset/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { JsonSchemaTypeRegistry as OriginJsonSchemaTypeRegistry } from '@flowgram.ai/json-schema';\n\nimport { IConditionRule, IConditionRuleFactory } from '@/components/condition-context/types';\n\nexport interface ConstantRendererProps<Value = any> {\n  value?: Value;\n  onChange?: (value: Value) => void;\n  readonly?: boolean;\n  [key: string]: any;\n}\nexport interface JsonSchemaTypeRegistry<Value = any> extends OriginJsonSchemaTypeRegistry {\n  /**\n   * Render Constant Input\n   */\n  ConstantRenderer: React.FC<ConstantRendererProps<Value>>;\n\n  /**\n   * Condition Rules\n   */\n  conditionRule?: IConditionRule | IConditionRuleFactory;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/flow-value/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowValueUtils } from './utils';\nexport {\n  type IFlowValueExtra,\n  type FlowValueType,\n  type IFlowValue,\n  type IFlowConstantValue,\n  type IFlowRefValue,\n  type IFlowExpressionValue,\n  type IFlowTemplateValue,\n  type IFlowConstantRefValue,\n  type IInputsValues,\n} from './types';\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/flow-value/schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\n// Shared extra schema for flow value types\nexport const extraSchema = z\n  .object({\n    index: z.number().optional(),\n  })\n  .optional();\n\nexport const constantSchema = z.object({\n  type: z.literal('constant'),\n  content: z.any().optional(),\n  schema: z.any().optional(),\n  extra: extraSchema,\n});\n\nexport const refSchema = z.object({\n  type: z.literal('ref'),\n  content: z.array(z.string()).optional(),\n  extra: extraSchema,\n});\n\nexport const expressionSchema = z.object({\n  type: z.literal('expression'),\n  content: z.string().optional(),\n  extra: extraSchema,\n});\n\nexport const templateSchema = z.object({\n  type: z.literal('template'),\n  content: z.string().optional(),\n  extra: extraSchema,\n});\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/flow-value/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport interface IFlowValueExtra {\n  index?: number;\n}\n\nexport type FlowValueType = 'constant' | 'ref' | 'expression' | 'template';\n\nexport interface IFlowConstantValue {\n  type: 'constant';\n  content?: any;\n  schema?: IJsonSchema;\n  extra?: IFlowValueExtra;\n}\n\nexport interface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n  extra?: IFlowValueExtra;\n}\n\nexport interface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n  extra?: IFlowValueExtra;\n}\n\nexport interface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n  extra?: IFlowValueExtra;\n}\n\nexport type IFlowValue =\n  | IFlowConstantValue\n  | IFlowRefValue\n  | IFlowExpressionValue\n  | IFlowTemplateValue;\n\nexport type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue;\n\nexport interface IInputsValues {\n  [key: string]: IInputsValues | IFlowValue | undefined;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/flow-value/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isArray, isObject, isPlainObject, uniq } from 'lodash-es';\nimport { IJsonSchema, JsonSchemaUtils } from '@flowgram.ai/json-schema';\nimport { Scope } from '@flowgram.ai/editor';\n\nimport {\n  IFlowConstantValue,\n  IFlowRefValue,\n  IFlowExpressionValue,\n  IFlowTemplateValue,\n  IFlowValue,\n  IFlowConstantRefValue,\n  FlowValueType,\n} from './types';\nimport { constantSchema, refSchema, expressionSchema, templateSchema } from './schema';\n\nexport namespace FlowValueUtils {\n  /**\n   * Check if the value is a constant type\n   */\n  export function isConstant(value: any): value is IFlowConstantValue {\n    return constantSchema.safeParse(value).success;\n  }\n\n  /**\n   * Check if the value is a reference type\n   */\n  export function isRef(value: any): value is IFlowRefValue {\n    return refSchema.safeParse(value).success;\n  }\n\n  /**\n   * Check if the value is an expression type\n   */\n  export function isExpression(value: any): value is IFlowExpressionValue {\n    return expressionSchema.safeParse(value).success;\n  }\n\n  /**\n   * Check if the value is a template type\n   */\n  export function isTemplate(value: any): value is IFlowTemplateValue {\n    return templateSchema.safeParse(value).success;\n  }\n\n  /**\n   * Check if the value is either a constant or reference type\n   */\n  export function isConstantOrRef(value: any): value is IFlowConstantRefValue {\n    return isConstant(value) || isRef(value);\n  }\n\n  /**\n   * Check if the value is a valid flow value type\n   */\n  export function isFlowValue(value: any): value is IFlowValue {\n    return isConstant(value) || isRef(value) || isExpression(value) || isTemplate(value);\n  }\n\n  /**\n   * Traverse all flow values in the given value\n   * @param value The value to traverse\n   * @param options The options to traverse\n   * @returns A generator of flow values\n   */\n  export function* traverse(\n    value: any,\n    options: {\n      includeTypes: FlowValueType[];\n      path?: string;\n      pathArr?: string[];\n    }\n  ): Generator<{ value: IFlowValue; path: string; pathArr: string[] }> {\n    const {\n      includeTypes = ['ref', 'template', 'expression', 'constant'],\n      path = '',\n      pathArr = [],\n    } = options || {};\n\n    if (isPlainObject(value)) {\n      if (isRef(value) && includeTypes.includes('ref')) {\n        yield { value, path, pathArr };\n        return;\n      }\n\n      if (isTemplate(value) && includeTypes.includes('template')) {\n        yield { value, path, pathArr };\n        return;\n      }\n\n      if (isExpression(value) && includeTypes.includes('expression')) {\n        yield { value, path, pathArr };\n        return;\n      }\n\n      if (isConstant(value) && includeTypes.includes('constant')) {\n        yield { value, path, pathArr };\n        return;\n      }\n\n      for (const [_key, _value] of Object.entries(value)) {\n        yield* traverse(_value, {\n          ...options,\n          path: path ? `${path}.${_key}` : _key,\n          pathArr: [...pathArr, _key],\n        });\n      }\n      return;\n    }\n\n    if (isArray(value)) {\n      for (const [_idx, _value] of value.entries()) {\n        yield* traverse(_value, {\n          ...options,\n          path: path ? `${path}[${_idx}]` : `[${_idx}]`,\n          pathArr: [...pathArr, `[${_idx}]`],\n        });\n      }\n      return;\n    }\n\n    return;\n  }\n\n  /**\n   * Get all key paths in the template value\n   * @param value The template value\n   * @returns A list of key paths\n   */\n  export function getTemplateKeyPaths(value: IFlowTemplateValue) {\n    // find all keyPath wrapped in {{}}\n    const keyPathReg = /\\{\\{([^\\}\\{]+)\\}\\}/g;\n    return uniq(value.content?.match(keyPathReg) || []).map((_keyPath) =>\n      _keyPath.slice(2, -2).split('.')\n    );\n  }\n\n  /**\n   * Infer the schema of the constant value\n   * @param value\n   * @returns\n   */\n  export function inferConstantJsonSchema(value: IFlowConstantValue): IJsonSchema | undefined {\n    if (value?.schema) {\n      return value.schema;\n    }\n\n    if (typeof value.content === 'string') {\n      return {\n        type: 'string',\n      };\n    }\n\n    if (typeof value.content === 'number') {\n      return {\n        type: 'number',\n      };\n    }\n\n    if (typeof value.content === 'boolean') {\n      return {\n        type: 'boolean',\n      };\n    }\n\n    if (isObject(value.content)) {\n      return {\n        type: 'object',\n      };\n    }\n    return undefined;\n  }\n\n  /**\n   * Infer the schema of the flow value\n   * @param values The flow value or object contains flow value\n   * @param scope\n   * @returns\n   */\n  export function inferJsonSchema(values: any, scope: Scope): IJsonSchema | undefined {\n    if (isPlainObject(values)) {\n      if (isConstant(values)) {\n        return inferConstantJsonSchema(values);\n      }\n\n      if (isRef(values)) {\n        const variable = scope.available.getByKeyPath(values?.content);\n        const schema = variable?.type ? JsonSchemaUtils.astToSchema(variable?.type) : undefined;\n\n        return schema;\n      }\n\n      if (isTemplate(values)) {\n        return { type: 'string' };\n      }\n\n      return {\n        type: 'object',\n        properties: Object.keys(values).reduce((acc, key) => {\n          const schema = inferJsonSchema((values as any)[key], scope);\n          if (schema) {\n            acc[key] = schema;\n          }\n          return acc;\n        }, {} as Record<string, IJsonSchema>),\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/format-legacy-refs/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isObject } from 'lodash-es';\n\ninterface LegacyFlowRefValueSchema {\n  type: 'ref';\n  content: string;\n}\n\ninterface NewFlowRefValueSchema {\n  type: 'ref';\n  content: string[];\n}\n\n/**\n * In flowgram 0.2.0, for introducing Loop variable functionality,\n * the FlowRefValueSchema type definition is updated:\n *\n * interface LegacyFlowRefValueSchema {\n *  type: 'ref';\n *  content: string;\n * }\n *\n * interface NewFlowRefValueSchema {\n *  type: 'ref';\n *  content: string[];\n * }\n *\n *\n * For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData\n *\n * How to use:\n *\n * 1. Call formatLegacyRefOnSubmit on the formData before submitting\n * 2. Call formatLegacyRefOnInit on the formData after submitting\n *\n * Example:\n * import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n * formMeta: {\n *  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n *  formatOnInit: (data) => formatLegacyRefOnInit(data),\n * }\n */\nexport function formatLegacyRefOnSubmit(value: any): any {\n  if (isObject(value)) {\n    if (isLegacyFlowRefValueSchema(value)) {\n      return formatLegacyRefToNewRef(value);\n    }\n\n    return Object.fromEntries(\n      Object.entries(value).map(([key, value]: [string, any]) => [\n        key,\n        formatLegacyRefOnSubmit(value),\n      ])\n    );\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(formatLegacyRefOnSubmit);\n  }\n\n  return value;\n}\n\n/**\n * In flowgram 0.2.0, for introducing Loop variable functionality,\n * the FlowRefValueSchema type definition is updated:\n *\n * interface LegacyFlowRefValueSchema {\n *  type: 'ref';\n *  content: string;\n * }\n *\n * interface NewFlowRefValueSchema {\n *  type: 'ref';\n *  content: string[];\n * }\n *\n *\n * For making sure backend json will not be changed, we provide format legacy ref utils for updating the formData\n *\n * How to use:\n *\n * 1. Call formatLegacyRefOnSubmit on the formData before submitting\n * 2. Call formatLegacyRefOnInit on the formData after submitting\n *\n * Example:\n * import { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n *\n * formMeta: {\n *  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n *  formatOnInit: (data) => formatLegacyRefOnInit(data),\n * }\n */\nexport function formatLegacyRefOnInit(value: any): any {\n  if (isObject(value)) {\n    if (isNewFlowRefValueSchema(value)) {\n      return formatNewRefToLegacyRef(value);\n    }\n\n    return Object.fromEntries(\n      Object.entries(value).map(([key, value]: [string, any]) => [\n        key,\n        formatLegacyRefOnInit(value),\n      ])\n    );\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(formatLegacyRefOnInit);\n  }\n\n  return value;\n}\n\nexport function isLegacyFlowRefValueSchema(value: any): value is LegacyFlowRefValueSchema {\n  return (\n    isObject(value) &&\n    Object.keys(value).length === 2 &&\n    (value as any).type === 'ref' &&\n    typeof (value as any).content === 'string'\n  );\n}\n\nexport function isNewFlowRefValueSchema(value: any): value is NewFlowRefValueSchema {\n  return (\n    isObject(value) &&\n    Object.keys(value).length === 2 &&\n    (value as any).type === 'ref' &&\n    Array.isArray((value as any).content)\n  );\n}\n\nexport function formatLegacyRefToNewRef(value: LegacyFlowRefValueSchema) {\n  const keyPath = value.content.split('.');\n\n  if (keyPath[1] === 'outputs') {\n    return {\n      type: 'ref',\n      content: [`${keyPath[0]}.${keyPath[1]}`, ...(keyPath.length > 2 ? keyPath.slice(2) : [])],\n    };\n  }\n\n  return {\n    type: 'ref',\n    content: keyPath,\n  };\n}\n\nexport function formatNewRefToLegacyRef(value: NewFlowRefValueSchema) {\n  return {\n    type: 'ref',\n    content: value.content.join('.'),\n  };\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/format-legacy-refs/readme.md",
    "content": "# Notice\n\nIn `@flowgram.ai/form-materials@0.2.0`, for introducing loop-related materials,\n\nThe FlowRefValueSchema type definition is updated:\n\n```typescript\ninterface LegacyFlowRefValueSchema {\n  type: 'ref';\n  content: string;\n}\n\ninterface NewFlowRefValueSchema {\n  type: 'ref';\n  content: string[];\n}\n```\n\n\n\nFor making sure backend json will not be changed in your application, we provide `format-legacy-ref` utils for upgrading\n\n\nHow to use:\n\n1. Call formatLegacyRefOnSubmit on the formData before submitting\n2. Call formatLegacyRefOnInit on the formData after submitting\n\nExample:\n\n```typescript\nimport { formatLegacyRefOnSubmit, formatLegacyRefOnInit } from '@flowgram.ai/form-materials';\n\nformMeta: {\n  formatOnSubmit: (data) => formatLegacyRefOnSubmit(data),\n  formatOnInit: (data) => formatLegacyRefOnInit(data),\n}\n```\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  FlowValueUtils,\n  type FlowValueType,\n  type IFlowConstantRefValue,\n  type IFlowConstantValue,\n  type IFlowExpressionValue,\n  type IFlowRefValue,\n  type IFlowTemplateValue,\n  type IFlowValue,\n  type IFlowValueExtra,\n  type IInputsValues,\n} from './flow-value';\nexport {\n  formatLegacyRefOnInit,\n  formatLegacyRefOnSubmit,\n  formatLegacyRefToNewRef,\n  formatNewRefToLegacyRef,\n  isLegacyFlowRefValueSchema,\n  isNewFlowRefValueSchema,\n} from './format-legacy-refs';\nexport { createInjectMaterial } from './inject-material';\nexport { lazySuspense, withSuspense } from './lazy-suspense';\nexport {\n  polyfillCreateRoot,\n  unstableSetCreateRoot,\n  type IPolyfillRoot,\n} from './polyfill-create-root';\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/inject-material/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport {\n  FlowRendererComponentType,\n  FlowRendererRegistry,\n  usePlaygroundContainer,\n} from '@flowgram.ai/editor';\n\ntype WithRenderKey<T> = T & { renderKey?: string };\n\n/**\n * Creates a material component wrapper with dependency injection support\n *\n * This Higher-Order Component (HOC) implements a dynamic component replacement mechanism\n * for material components. It automatically checks if a custom renderer is registered\n * in the editor context, using the injected component if available, otherwise\n * falling back to the default component.\n *\n * @example\n * ```tsx\n * // 1.Create an injectable material component\n * const InjectVariableSelector = createInjectMaterial(VariableSelector)\n *\n * // 2. Register custom components in editor\n * // Configure in use-editor-props.tsx:\n * const editorProps = {\n *   materials: {\n *     components: {\n *       [VariableSelector.renderKey]: YourCustomVariableSelector\n *     }\n *   }\n * }\n * ```\n *\n * @description\n * Data flow explanation:\n * - Register components to FlowRendererRegistry in use-editor-props\n * - InjectMaterial reads renderers from FlowRendererRegistry\n * - If registered renderer exists and type is REACT, use injected component\n * - If not exists or type mismatch, fallback to default component\n *\n * @param Component - Default React component\n * @param params - Optional parameters\n * @param params.renderKey - Custom render key name, highest priority\n * @returns Wrapper component with dependency injection support\n */\nexport function createInjectMaterial<Props>(\n  Component: WithRenderKey<React.FC<Props> | React.ExoticComponent<Props>>,\n  params?: {\n    renderKey?: string;\n  }\n): WithRenderKey<React.FC<Props>> {\n  // Determine render key: prioritize param specified, then component renderKey, finally component name\n  const renderKey = params?.renderKey || Component.renderKey || Component.name || '';\n\n  const InjectComponent: WithRenderKey<React.FC<Props>> = (props) => {\n    const container = usePlaygroundContainer();\n\n    // Check if renderer registry is bound in container\n    if (!container?.isBound?.(FlowRendererRegistry)) {\n      // If no registry, use default component directly\n      return React.createElement(Component as (props?: any) => any, { ...props });\n    }\n\n    // Get renderer registry instance\n    const rendererRegistry = container.get(FlowRendererRegistry);\n\n    // Get corresponding renderer from registry\n    const renderer = rendererRegistry.tryToGetRendererComponent(renderKey);\n\n    // Check if renderer exists and type is React component\n    if (renderer?.type !== FlowRendererComponentType.REACT) {\n      // If no suitable renderer found, fallback to default component\n      return React.createElement(Component as (props?: any) => any, { ...props });\n    }\n\n    // Render using injected React component\n    return React.createElement(renderer.renderer, {\n      ...props,\n    });\n  };\n\n  InjectComponent.renderKey = renderKey;\n\n  return InjectComponent;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/lazy-suspense/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { ComponentType, lazy, Suspense } from 'react';\n\nimport { Skeleton } from '@douyinfe/semi-ui';\n\nexport function withSuspense<T extends ComponentType<any>>(\n  Component: T,\n  fallback?: React.ReactNode\n): T {\n  const WithSuspenseComponent: T = ((props: any) => (\n    <Suspense fallback={fallback || <Skeleton.Paragraph style={{ width: '100%' }} rows={1} />}>\n      <Component {...props} />\n    </Suspense>\n  )) as any;\n\n  return WithSuspenseComponent;\n}\n\nexport function lazySuspense<T extends ComponentType<any>>(\n  params: Parameters<typeof lazy<T>>[0],\n  fallback?: React.ReactNode\n) {\n  return withSuspense(lazy(params), fallback);\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/shared/polyfill-create-root/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as ReactDOM from 'react-dom';\n\nexport interface IPolyfillRoot {\n  render(children: React.ReactNode): void;\n  unmount(): void;\n}\n\n/**\n * React 18 polyfill\n * @param dom\n * @returns\n */\nlet unstableCreateRoot = (dom: HTMLElement): IPolyfillRoot => ({\n  render(children: JSX.Element) {\n    ReactDOM.render(children, dom);\n  },\n  unmount() {\n    ReactDOM.unmountComponentAtNode(dom);\n  },\n});\n\nexport function polyfillCreateRoot(dom: HTMLElement): IPolyfillRoot {\n  return unstableCreateRoot(dom);\n}\n\nexport function unstableSetCreateRoot(createRoot: (dom: HTMLElement) => IPolyfillRoot) {\n  unstableCreateRoot = createRoot;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/src/validate/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { validateFlowValue } from './validate-flow-value';\n"
  },
  {
    "path": "packages/materials/form-materials/src/validate/validate-flow-value/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { FeedbackLevel, FlowNodeEntity, getNodeScope } from '@flowgram.ai/editor';\n\nimport { type IFlowValue, FlowValueUtils } from '@/shared';\n\ninterface Context {\n  node: FlowNodeEntity;\n  required?: boolean;\n  errorMessages?: {\n    required?: string;\n    unknownVariable?: string;\n  };\n}\n\nexport function validateFlowValue(value: IFlowValue | undefined, ctx: Context) {\n  const { node, required, errorMessages } = ctx;\n\n  const {\n    required: requiredMessage = 'Field is required',\n    unknownVariable: unknownVariableMessage = 'Unknown Variable',\n  } = errorMessages || {};\n\n  if (required && (isNil(value) || isNil(value?.content) || value?.content === '')) {\n    return {\n      level: FeedbackLevel.Error,\n      message: requiredMessage,\n    };\n  }\n\n  if (value?.type === 'ref') {\n    const variable = getNodeScope(node).available.getByKeyPath(value?.content || []);\n    if (!variable) {\n      return {\n        level: FeedbackLevel.Error,\n        message: unknownVariableMessage,\n      };\n    }\n  }\n\n  if (value?.type === 'template') {\n    const allRefs = FlowValueUtils.getTemplateKeyPaths(value);\n\n    for (const ref of allRefs) {\n      const variable = getNodeScope(node).available.getByKeyPath(ref);\n      if (!variable) {\n        return {\n          level: FeedbackLevel.Error,\n          message: unknownVariableMessage,\n        };\n      }\n    }\n  }\n\n  return undefined;\n}\n"
  },
  {
    "path": "packages/materials/form-materials/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"isolatedModules\": true,\n    \"baseUrl\": \".\",\n    \"types\": [],\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    },\n    \"rootDir\": \"./src\",\n    \"outDir\": \"./dist/types\",\n  },\n  \"include\": [\n    \"src\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n  ]\n}\n"
  },
  {
    "path": "packages/materials/form-materials/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/form-materials/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/materials/type-editor/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n    'react/prop-types': 'off',\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/type-editor\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@douyinfe/semi-icons\": \"^2.80.0\",\n    \"@douyinfe/semi-illustrations\": \"^2.80.0\",\n    \"@douyinfe/semi-ui\": \"^2.80.0\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@flowgram.ai/json-schema\": \"workspace:*\",\n    \"classnames\": \"^2.5.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"inversify\": \"^6.0.1\",\n    \"react-dnd\": \"16.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"react-dnd-html5-backend\": \"16.0.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/feedback/feedback.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { type Level } from './types';\nimport { FeedbackStyle, RelativeWrapper } from './style';\n\ninterface FeedbackProps {\n  // message 内容\n  message: string;\n  // 不hover状态下是否占用文档流\n  layout: 'relative' | 'absolute';\n  // 错误等级： error | warning\n  level?: Level;\n  // 是否展开：该状态仅适配layout 为relative 的状态\n  expanded?: boolean;\n}\n\nexport const Feedback = ({ message, layout, level = 'error', expanded = false }: FeedbackProps) => {\n  const feedbackContent = useMemo(\n    () => (\n      <FeedbackStyle\n        expanded={expanded}\n        error={level === 'error'}\n        warning={level === 'warning'}\n        expandable={!expanded}\n      >\n        {message}\n      </FeedbackStyle>\n    ),\n    [level, message, expanded]\n  );\n\n  return layout === 'relative' ? (\n    <RelativeWrapper expanded={expanded}>{feedbackContent}</RelativeWrapper>\n  ) : (\n    feedbackContent\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/feedback/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Feedback } from './feedback';\nexport * from './types';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/feedback/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const FeedbackStyle = styled.div<{\n  // 是否展开\n  expanded?: boolean;\n  // 是否错误\n  error?: boolean;\n  // 是否警告\n  warning?: boolean;\n  // 是否可展开\n  expandable?: boolean;\n}>`\n  position: absolute;\n\n  ${(props) =>\n    props.expanded\n      ? `\n  position: relative;\n  `\n      : ''}\n\n  color: #fff;\n  max-width: 100%;\n  width: fit-content;\n  padding: 0 4px;\n  font-size: 10px;\n  line-height: 18px;\n  word-break: break-all;\n\n  &.expandable {\n  }\n\n  ${(props) =>\n    props.expandable\n      ? `\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  &:hover {\n    white-space: normal;\n    overflow: visible;\n  }\n  `\n      : ''}\n\n  ${(props) =>\n    props.error\n      ? `\n    background-color: var(--semi-color-danger);\n    `\n      : ''}\n\n\n  ${(props) =>\n    props.warning\n      ? `\n    background-color:var(--semi-color-warning);\n    `\n      : ''}\n\n\n  z-index: 1;\n`;\n\nexport const RelativeWrapper = styled.div<{\n  // 是否展开\n  expanded?: boolean;\n}>`\n  position: relative;\n  height: 18px;\n\n  ${(props) =>\n    props.expanded\n      ? `\n      height: fit-content;\n    `\n      : ''}\n`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/feedback/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type Level = 'error' | 'warning';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './type-editor';\nexport * from './type-selector';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/body.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport classNames from 'classnames';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Empty } from '@douyinfe/semi-ui';\nimport { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';\n\nimport {\n  TypeEditorColumnType,\n  TypeEditorRowData,\n  TypeEditorColumnViewConfig,\n  TypeChangeContext,\n} from '../../types';\nimport { DragRowContainer } from './style';\nimport { useRowDrag } from './hooks/drag-drop';\nimport { ViewCell } from './cell';\n\ninterface Props<TypeSchema extends Partial<IJsonSchema>> {\n  displayColumn: TypeEditorColumnType[];\n  dataSourceMap: Record<string, TypeEditorRowData<TypeSchema>>;\n  /**\n   * 自定义空状态\n   */\n  customEmptyNode?: React.ReactElement;\n  /**\n   * 每个列的配置\n   */\n  viewConfigs: TypeEditorColumnViewConfig[];\n  /**\n   * 只读态\n   */\n  readonly?: boolean;\n  onFieldChange?: (ctx: TypeChangeContext) => void;\n  onChange: () => void;\n  onPaste?: (typeSchema?: TypeSchema) => TypeSchema | undefined;\n  onError?: (msg?: string[]) => void;\n  onChildrenVisibleChange: (rowDataId: string, newVal: boolean) => void;\n  unOpenKeys: Record<string, boolean>;\n}\n\nconst Row = <TypeSchema extends Partial<IJsonSchema>>({\n  displayColumn,\n  data,\n  ...rest\n}: Props<TypeSchema> & {\n  data: TypeEditorRowData<TypeSchema>;\n  rowIndex: number;\n}) => {\n  const { preview, drag, isDragging } = useRowDrag(data.id, rest.onChildrenVisibleChange);\n\n  return (\n    <DragRowContainer ref={preview} dragging={isDragging} className={classNames('semi-table-row')}>\n      {displayColumn.map((column, columnIndex) => (\n        <ViewCell\n          dragSource={data.cannotDrag ? undefined : drag}\n          key={data.id + column}\n          columnType={column}\n          columnIndex={columnIndex}\n          rowData={data}\n          {...rest}\n        />\n      ))}\n    </DragRowContainer>\n  );\n};\n\nexport const Body = <TypeSchema extends Partial<IJsonSchema>>({\n  dataSource,\n  customEmptyNode,\n  ...rest\n}: Props<TypeSchema> & {\n  dataSource: TypeEditorRowData<TypeSchema>[];\n}) => {\n  if (dataSource.length === 0) {\n    const rows = rest.displayColumn.length;\n    return (\n      <tbody>\n        <tr>\n          <td colSpan={rows}>\n            {customEmptyNode ? (\n              customEmptyNode\n            ) : (\n              <Empty\n                style={{ marginTop: 40 }}\n                image={<IllustrationNoContent style={{ width: 100, height: 100 }} />}\n                darkModeImage={<IllustrationNoContentDark style={{ width: 100, height: 100 }} />}\n                description=\"No content. Please add.\"\n              />\n            )}\n          </td>\n        </tr>\n      </tbody>\n    );\n  }\n\n  return (\n    <tbody className=\"semi-table-tbody\">\n      <>\n        {dataSource.map((data, rowIndex) => (\n          <Row key={data.id} {...rest} data={data} rowIndex={rowIndex} />\n        ))}\n      </>\n    </tbody>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/cell.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type ConnectDragSource } from 'react-dnd';\nimport React, { useCallback, useMemo } from 'react';\n\nimport classNames from 'classnames';\nimport { NOOP } from '@flowgram.ai/utils';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { Feedback } from '../feedback';\nimport {\n  TypeEditorRowData,\n  TypeChangeContext,\n  TypeEditorColumnViewConfig,\n  TypeEditorColumnType,\n} from '../../types';\nimport { TypeEditorService } from '../../services';\nimport { useService } from '../../contexts';\nimport { EditCellContainer, EditorTableCell, ErrorMsgContainer } from './style';\nimport { useIsBlink } from './hooks/blink';\nimport { useActivePos, useCellDrop, useViewCellErrorMsg, useEditCellErrorMsg } from './hooks';\nimport { ErrorCellBorder } from './error';\nimport { CELL_HIGHT, HEADER_HIGHT, TAB_BRA_HIGHT } from './common';\n\nexport const ViewCell = <TypeSchema extends Partial<IJsonSchema>>({\n  rowData,\n  rowIndex,\n  columnIndex,\n  onChange,\n  dataSourceMap,\n  onError,\n  onFieldChange,\n  onPaste,\n  unOpenKeys,\n  viewConfigs,\n  readonly,\n  onChildrenVisibleChange,\n  dragSource,\n  columnType,\n}: {\n  rowData: TypeEditorRowData<TypeSchema>;\n  rowIndex: number;\n  onError?: (msg: string[]) => void;\n  onPaste?: (typeSchema?: TypeSchema) => TypeSchema | undefined;\n  onChange: () => void;\n  columnIndex: number;\n  onFieldChange?: (ctx: TypeChangeContext) => void;\n  dragSource?: ConnectDragSource;\n  dataSourceMap: Record<string, TypeEditorRowData<TypeSchema>>;\n  onChildrenVisibleChange: (rowDataId: string, val: boolean) => void;\n  /**\n   * 只读态\n   */\n  readonly?: boolean;\n  /**\n   * 每个列的配置\n   */\n  viewConfigs: TypeEditorColumnViewConfig[];\n  unOpenKeys: Record<string, boolean>;\n  columnType: TypeEditorColumnType;\n}) => {\n  const typeEditorService = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const config = typeEditorService.getConfigByType(columnType)!;\n\n  const { drop } = useCellDrop(rowData, rowIndex, dataSourceMap, onChange);\n\n  const handleEditMode = useCallback(() => {\n    if (!readonly) {\n      typeEditorService.setActivePos({ x: columnIndex, y: rowIndex });\n    }\n  }, [columnIndex, readonly, rowIndex]);\n\n  const handleViewMode = useCallback(() => {\n    typeEditorService.clearActivePos();\n  }, []);\n\n  const Render = config.viewRender;\n\n  const feedbackInfo = useViewCellErrorMsg(rowData, config, {\n    x: columnIndex,\n    y: rowIndex,\n  });\n\n  return (\n    <EditorTableCell\n      ref={config.customDrop ? undefined : drop}\n      className={classNames('semi-table-row-cell', 'cell-container')}\n    >\n      {feedbackInfo && (\n        <>\n          <ErrorCellBorder level={feedbackInfo.level} />\n          <ErrorMsgContainer>\n            <Feedback message={feedbackInfo.msg!} level={feedbackInfo.level} layout=\"absolute\" />\n          </ErrorMsgContainer>\n        </>\n      )}\n\n      {Render ? (\n        <Render\n          readonly={readonly}\n          config={viewConfigs.find((v) => v.type === columnType) || {}}\n          dragSource={dragSource}\n          unOpenKeys={unOpenKeys}\n          onFieldChange={onFieldChange}\n          typeEditor={typeEditorService}\n          onPaste={onPaste}\n          error={!!feedbackInfo}\n          onError={onError}\n          onChange={onChange}\n          onChildrenVisibleChange={onChildrenVisibleChange}\n          onViewMode={handleViewMode}\n          onEditMode={handleEditMode}\n          rowData={rowData}\n        />\n      ) : (\n        `${(rowData as unknown as Record<string, unknown>)[columnType]}`\n      )}\n    </EditorTableCell>\n  );\n};\n\nexport const EditCell = <TypeSchema extends Partial<IJsonSchema>>({\n  displayColumn,\n  onFieldChange,\n  dataSource,\n  onPaste,\n  unOpenKeys,\n  onError,\n  viewConfigs,\n  onChildrenVisibleChange,\n  tableDom,\n  onChange,\n}: {\n  displayColumn: TypeEditorColumnType[];\n  tableDom: HTMLTableElement;\n  onError?: (msg: string[]) => void;\n  onPaste?: (typeSchema?: TypeSchema) => TypeSchema | undefined;\n\n  /**\n   * 每个列的配置\n   */\n  viewConfigs: TypeEditorColumnViewConfig[];\n  unOpenKeys: Record<string, boolean>;\n  onChildrenVisibleChange: (rowDataId: string, newVal: boolean) => void;\n  onChange: () => void;\n  onFieldChange?: (ctx: TypeChangeContext) => void;\n  dataSource: TypeEditorRowData<TypeSchema>[];\n}) => {\n  const typeEditorService = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const activePos = useActivePos();\n\n  const handleViewMode = useCallback(() => {\n    typeEditorService.clearActivePos();\n  }, []);\n\n  const columnType = useMemo(() => displayColumn[activePos.x], [displayColumn, activePos.x]);\n\n  /**\n   * 获取每一列的宽度\n   */\n  const columnEachWidth = useMemo(() => {\n    const headerNodes = tableDom.childNodes?.[0]?.childNodes?.[0];\n    const width: number[] = [];\n    for (const item of headerNodes.childNodes.values()) {\n      // 1px border\n      width.push((item as HTMLElement).clientWidth + 1);\n    }\n\n    return width;\n  }, [displayColumn, tableDom, tableDom.clientWidth]);\n\n  /**\n   * 获取每一列距离 left: 0 的宽度\n   */\n  const columnAccWidth = useMemo(() => {\n    let acc = 0;\n    return columnEachWidth.map((width) => {\n      acc += width;\n      return acc - width;\n    });\n  }, [columnEachWidth]);\n\n  const rowData = useMemo(() => dataSource[activePos.y], [dataSource, activePos.y]);\n\n  const config = typeEditorService.getConfigByType(columnType);\n\n  const Render = config && (config.editRender || config.viewRender);\n\n  const errorMsg = useEditCellErrorMsg(activePos);\n\n  const blink = useIsBlink();\n\n  if (!config) {\n    return <></>;\n  }\n\n  return (\n    <>\n      {rowData && (\n        <EditCellContainer\n          error={!!errorMsg}\n          blink={blink}\n          key={rowData.key + columnType}\n          style={{\n            width: columnEachWidth[activePos.x] + 1,\n            top: HEADER_HIGHT + TAB_BRA_HIGHT + activePos.y * CELL_HIGHT + 1,\n            left: columnAccWidth[activePos.x],\n          }}\n        >\n          {Render ? (\n            <Render\n              error={!!errorMsg}\n              onError={onError}\n              config={viewConfigs.find((v) => v.type === columnType) || {}}\n              key={rowData.id}\n              unOpenKeys={unOpenKeys}\n              onChange={onChange}\n              onPaste={onPaste}\n              typeEditor={typeEditorService}\n              onFieldChange={onFieldChange}\n              onChildrenVisibleChange={onChildrenVisibleChange}\n              onEditMode={NOOP}\n              onViewMode={handleViewMode}\n              rowData={rowData}\n            />\n          ) : (\n            `${(rowData as unknown as Record<string, unknown>)[columnType]}`\n          )}\n          {errorMsg && (\n            <ErrorMsgContainer style={{ left: -1 }}>\n              <Feedback message={errorMsg} level=\"error\" layout=\"absolute\" />\n            </ErrorMsgContainer>\n          )}\n        </EditCellContainer>\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/default.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip, Typography } from '@douyinfe/semi-ui';\n\nimport { useDisabled } from '../hooks/disabled';\nimport { TypeEditorColumnConfig, TypeEditorColumnType, TypeEditorRowData } from '../../../types';\nimport { TypeEditorService } from '../../../services';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport {\n  GlobalSelectStyle,\n  KeyEditorContainer,\n  KeyViewContainer,\n  TypeTextContainer,\n} from './style';\n\nconst useFormatValue = ({\n  rowData,\n  onChange,\n  typeEditor,\n}: {\n  rowData: TypeEditorRowData<IJsonSchema>;\n  typeEditor: TypeEditorService<IJsonSchema>;\n  onChange: () => void;\n}) => {\n  const valueRef = useRef(rowData.self.default);\n  const [value, setValue] = useState(rowData.self.default);\n\n  const typeDefinition = useTypeDefinitionManager();\n\n  useEffect(() => {\n    typeEditor.editValue = value;\n  }, [typeEditor, value]);\n\n  useEffect(() => {\n    setValue(rowData.self.default);\n    valueRef.current = rowData.self.default;\n  }, [rowData.self.default]);\n\n  const handleSubmit = useCallback(() => {\n    rowData.self.default = valueRef.current;\n\n    const config = typeDefinition.getTypeBySchema(rowData.self);\n\n    if (config?.formatDefault) {\n      config.formatDefault(valueRef.current, rowData.self);\n    }\n\n    const parenConfig = rowData.parent && typeDefinition.getTypeBySchema(rowData.parent);\n\n    if (parenConfig?.formatDefault) {\n      parenConfig.formatDefault(valueRef.current, rowData.parent!);\n    }\n\n    onChange();\n\n    // onViewMode();\n  }, [onChange, rowData, typeDefinition]);\n\n  const deFormatValue = useMemo(() => {\n    const config = typeDefinition.getTypeBySchema(rowData.self);\n    return config?.deFormatDefault ? config.deFormatDefault(value) : value;\n  }, [value, rowData]);\n\n  const handleChange = useCallback((v: unknown) => {\n    valueRef.current = v;\n    setValue(v);\n  }, []);\n\n  return {\n    handleChange,\n    handleSubmit,\n    deFormatValue,\n  };\n};\n\nexport const TypeText = <TypeSchema extends Partial<IJsonSchema>>({\n  value,\n  type,\n}: {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value?: any;\n  type: TypeSchema;\n}) => {\n  const typeDefinition = useTypeDefinitionManager();\n\n  const content = useMemo(() => {\n    const config = typeDefinition.getTypeBySchema(type);\n\n    return config?.getValueText ? config.getValueText(value) : value;\n  }, [value, type]);\n\n  return (\n    <TypeTextContainer>\n      <Typography.Text ellipsis={{ showTooltip: true }}>{content}</Typography.Text>\n    </TypeTextContainer>\n  );\n};\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({\n  rowData,\n  typeEditor,\n  onEditMode,\n  onChange,\n}) => {\n  const disabled = useDisabled(TypeEditorColumnType.Default, rowData);\n\n  const typeDefinition = useTypeDefinitionManager();\n  const { defaultMode = 'default' } = rowData.extraConfig;\n\n  const { customDefaultView } = rowData.extraConfig;\n\n  const { deFormatValue, handleChange, handleSubmit } = useFormatValue({\n    rowData,\n    onChange,\n    typeEditor,\n  });\n\n  const customNode =\n    customDefaultView &&\n    customDefaultView({\n      rowData,\n      disabled,\n      value: deFormatValue,\n      onChange: handleChange,\n      onSubmit: (v) => {\n        handleChange(v);\n        handleSubmit();\n      },\n    });\n\n  if (customNode) {\n    return <> {customNode}</>;\n  }\n\n  if (disabled) {\n    return (\n      <KeyViewContainer disabled>\n        <Tooltip content={disabled}>\n          <div style={{ width: '100%', height: '100%' }}>\n            <TypeText value={deFormatValue} type={rowData.self} />\n          </div>\n        </Tooltip>\n      </KeyViewContainer>\n    );\n  }\n\n  if (defaultMode === 'server') {\n    const config = typeDefinition.getTypeBySchema(rowData.self);\n\n    return (\n      <Tooltip content=\"The default value is not allowed to be modified.\">\n        <KeyViewContainer disabled>\n          <Typography.Text>{JSON.stringify(config?.getDefaultValue?.())}</Typography.Text>\n        </KeyViewContainer>\n      </Tooltip>\n    );\n  }\n\n  return (\n    <KeyViewContainer onClick={() => onEditMode()}>\n      <TypeText value={deFormatValue} type={rowData.self} />\n    </KeyViewContainer>\n  );\n};\n\nconst EditRender: TypeEditorColumnConfig<IJsonSchema>['editRender'] = ({\n  rowData,\n  onChange,\n  typeEditor,\n  onViewMode,\n}) => {\n  const { deFormatValue, handleChange, handleSubmit } = useFormatValue({\n    rowData,\n    onChange,\n    typeEditor,\n  });\n\n  const typeDefinition = useTypeDefinitionManager();\n  const config = useMemo(() => typeDefinition.getTypeBySchema(rowData.self), [rowData.self]);\n  return (\n    <KeyEditorContainer>\n      <GlobalSelectStyle />\n      {config &&\n        config?.getInputNode?.({\n          value: deFormatValue,\n          onChange: handleChange,\n          type: rowData.self,\n          onSubmit: () => {\n            handleSubmit();\n            onViewMode();\n          },\n        })}\n    </KeyEditorContainer>\n  );\n};\n\nexport const defaultColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Default,\n  width: 15,\n  label: 'Default',\n  viewRender: ViewRender,\n  shortcuts: {\n    onEnter: ({ rowData, onChange, typeEditor, value, typeDefinitionService }) => {\n      const config = typeDefinitionService.getTypeBySchema(rowData.self);\n      if (config?.typeInputConfig?.canEnter) {\n        rowData.self.default = value;\n\n        onChange();\n\n        typeEditor.moveActivePosToNextLineWithAddLine(rowData);\n      }\n    },\n    onTab: ({ rowData, value, onChange, typeEditor }) => {\n      rowData.self.default = value;\n      onChange();\n      typeEditor.moveActivePosToNextItem();\n    },\n  },\n  editRender: EditRender,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/description.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip } from '@douyinfe/semi-ui';\n\nimport { useDisabled } from '../hooks';\nimport { TypeEditorColumnConfig, TypeEditorColumnType } from '../../../types';\nimport { KeyEditorContainer, KeyEditorInput, KeyViewContainer, KeyViewText } from './style';\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({ rowData, onEditMode }) => {\n  const disabled = useDisabled(TypeEditorColumnType.Description, rowData);\n\n  if (disabled) {\n    return (\n      <KeyViewContainer disabled>\n        <Tooltip content={disabled}>\n          <KeyViewText>{rowData.description}</KeyViewText>\n        </Tooltip>\n      </KeyViewContainer>\n    );\n  }\n\n  return (\n    <KeyViewContainer onClick={() => onEditMode()}>\n      <KeyViewText>{rowData.description}</KeyViewText>\n    </KeyViewContainer>\n  );\n};\n\nconst EditRender: TypeEditorColumnConfig<IJsonSchema>['editRender'] = ({\n  rowData,\n  onChange,\n  typeEditor,\n  onViewMode,\n}) => {\n  const [value, setValue] = useState(rowData.description);\n\n  useEffect(() => {\n    typeEditor.editValue = value;\n  }, [typeEditor, value]);\n\n  useEffect(() => {\n    setValue(rowData.description);\n  }, [rowData.description]);\n\n  return (\n    <KeyEditorContainer>\n      <KeyEditorInput\n        onChange={setValue}\n        autoFocus\n        onBlur={() => {\n          const { self } = rowData;\n          self.description = value;\n          onChange();\n\n          onViewMode();\n        }}\n        value={value}\n      />\n    </KeyEditorContainer>\n  );\n};\n\nexport const descriptionColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Description,\n  width: 15,\n  label: 'Description',\n  viewRender: ViewRender,\n  shortcuts: {\n    onEnter: ({ rowData, value, onChange, typeEditor }) => {\n      rowData.self.description = value;\n      onChange();\n      typeEditor.moveActivePosToNextLineWithAddLine(rowData);\n    },\n    onTab: ({ rowData, value, onChange, typeEditor }) => {\n      rowData.self.description = value;\n      onChange();\n      typeEditor.moveActivePosToNextItem();\n    },\n  },\n  editRender: EditRender,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { valueColumnConfig } from './value';\nimport { typeColumnConfig } from './type';\nimport { requiredColumnConfig } from './required';\nimport { privateColumnConfig } from './private';\nimport { operateColumnConfig } from './operate';\nimport { keyColumnConfig } from './key';\nimport { descriptionColumnConfig } from './description';\nimport { defaultColumnConfig } from './default';\n\nexport const columnConfigs = [\n  keyColumnConfig,\n  typeColumnConfig,\n  requiredColumnConfig,\n  descriptionColumnConfig,\n  privateColumnConfig,\n  valueColumnConfig,\n  defaultColumnConfig,\n  operateColumnConfig,\n];\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/key.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable complexity */\n\nimport React, { useEffect, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip } from '@douyinfe/semi-ui';\nimport { IconCopyAdd, IconHandle, IconPlus, IconTreeTriangleDown } from '@douyinfe/semi-icons';\n\nimport { getComponentId, typeEditorUtils } from '../utils';\nimport { WidthIndent } from '../indent';\nimport { useAddType, usePasteAddType } from '../hooks/type-edit';\nimport { usePasteData } from '../hooks/paste-data';\nimport { useKeyVisible } from '../hooks/key-visible';\nimport { useDisabled } from '../hooks/disabled';\nimport {\n  ShortcutContext,\n  TypeEditorColumnConfig,\n  TypeEditorColumnType,\n  TypeEditorRowData,\n} from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport {\n  BaseIcon,\n  KeyEditorContainer,\n  KeyEditorInput,\n  KeyViewContainer,\n  KeyViewContent,\n  KeyViewText,\n} from './style';\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({\n  rowData,\n  onEditMode,\n  readonly,\n  onChange,\n  onPaste,\n  unOpenKeys,\n  onChildrenVisibleChange,\n  dragSource,\n}) => {\n  const { extraConfig } = rowData;\n\n  const typeService = useTypeDefinitionManager();\n\n  const config = typeService.getTypeBySchema(rowData.self);\n\n  const disabled = useDisabled(TypeEditorColumnType.Key, rowData);\n\n  const { hiddenDrag: originHiddenDrag } = extraConfig;\n\n  const hiddenDrag = originHiddenDrag || readonly;\n\n  const text = (\n    <KeyViewText disabled={!!disabled} ellipsis={{ showTooltip: true }}>\n      {typeEditorUtils.formateKey(rowData.key)}\n    </KeyViewText>\n  );\n\n  const visibleNode = useKeyVisible(rowData, onChange, extraConfig);\n\n  const handleAddType = useAddType(rowData, onChange);\n\n  const draggable = !(rowData.cannotDrag || disabled || hiddenDrag);\n\n  const { pasteData } = usePasteData();\n  const handlePasteAddType = usePasteAddType(rowData, onChange, onPaste);\n\n  if (config && config.canAddField?.(rowData)) {\n    const disabledAdd = extraConfig.disabledAdd ? extraConfig.disabledAdd(rowData) : undefined;\n\n    return (\n      <KeyViewContainer\n        id={getComponentId('key-text')}\n        onClick={\n          disabled\n            ? undefined\n            : () => {\n                onEditMode();\n              }\n        }\n      >\n        {!hiddenDrag && (\n          <BaseIcon draggable disabled={!draggable}>\n            <IconHandle ref={draggable ? dragSource : undefined} />\n          </BaseIcon>\n        )}\n\n        <WidthIndent width={rowData.level * 16} />\n        <KeyViewContent>\n          {rowData.childrenCount ? (\n            <BaseIcon triangle isRotate={!unOpenKeys[rowData.id]}>\n              <IconTreeTriangleDown\n                size=\"small\"\n                onClick={(e) => {\n                  onChildrenVisibleChange(rowData.id, !unOpenKeys[rowData.id]);\n                  e.stopPropagation();\n                }}\n              />\n            </BaseIcon>\n          ) : (\n            <WidthIndent width={12} />\n          )}\n\n          {disabled ? (\n            <div style={{ flex: 1 }}>\n              <Tooltip content={disabled}>{text}</Tooltip>\n            </div>\n          ) : (\n            text\n          )}\n\n          {!readonly && (\n            <>\n              {disabledAdd ? (\n                <Tooltip content={disabledAdd}>\n                  <BaseIcon disabled>\n                    <IconPlus id={getComponentId('add-field')} size=\"small\" />\n                  </BaseIcon>\n                </Tooltip>\n              ) : (\n                <Tooltip content=\"add child field\">\n                  <BaseIcon>\n                    <IconPlus\n                      size=\"small\"\n                      onClick={handleAddType}\n                      id={getComponentId('add-field')}\n                    />\n                  </BaseIcon>\n                </Tooltip>\n              )}\n              {disabledAdd || pasteData.type === 'invalid' ? (\n                <Tooltip\n                  content={disabledAdd || 'Please confirm whether the clipboard value is correct'}\n                >\n                  <BaseIcon disabled>\n                    <IconCopyAdd size=\"small\" />\n                  </BaseIcon>\n                </Tooltip>\n              ) : (\n                <Tooltip content=\"paste as new child fields\">\n                  <BaseIcon>\n                    <IconCopyAdd size=\"small\" onClick={handlePasteAddType} />\n                  </BaseIcon>\n                </Tooltip>\n              )}\n            </>\n          )}\n\n          {extraConfig.editorVisible && visibleNode}\n        </KeyViewContent>\n      </KeyViewContainer>\n    );\n  }\n\n  return (\n    <KeyViewContainer\n      id={getComponentId('key-text')}\n      onClick={\n        disabled\n          ? undefined\n          : () => {\n              onEditMode();\n            }\n      }\n    >\n      {!hiddenDrag && (\n        <BaseIcon draggable disabled={!draggable}>\n          <IconHandle ref={draggable ? dragSource : undefined} />\n        </BaseIcon>\n      )}\n\n      <WidthIndent width={(rowData.level + 1) * 16} />\n      <KeyViewContent>\n        {disabled ? <Tooltip content={disabled}>{text}</Tooltip> : text}\n        {extraConfig.editorVisible && visibleNode}\n      </KeyViewContent>\n    </KeyViewContainer>\n  );\n};\n\nconst validate = (value: string, rowData: TypeEditorRowData<IJsonSchema>): string | undefined => {\n  const res = rowData.extraConfig?.customValidateName?.(value);\n\n  const lastValue = rowData.key;\n\n  const parentTypeSchema = rowData.parent;\n\n  if (value !== lastValue && parentTypeSchema?.properties?.[value]) {\n    return 'The same key exists at the current level.';\n  }\n  if (res) {\n    return res;\n  }\n};\n\nconst changeValue = ({\n  rowData,\n  value,\n}: {\n  rowData: TypeEditorRowData<IJsonSchema>;\n  value: string;\n}) => {\n  const lastValue = rowData.key;\n\n  const parentTypeSchema = rowData.parent;\n\n  if (value !== lastValue) {\n    if (parentTypeSchema && parentTypeSchema.properties) {\n      parentTypeSchema.properties[value] = parentTypeSchema.properties[lastValue];\n    }\n\n    if (parentTypeSchema?.properties) {\n      delete parentTypeSchema.properties[lastValue];\n    }\n  }\n};\n\nconst EditRender: TypeEditorColumnConfig<IJsonSchema>['editRender'] = ({\n  rowData,\n  onChange,\n  typeEditor,\n  onError,\n  onFieldChange,\n  onViewMode,\n}) => {\n  const [value, setValue] = useState(typeEditorUtils.formateKey(rowData.key));\n\n  useEffect(() => {\n    typeEditor.editValue = value;\n  }, [typeEditor, value]);\n\n  return (\n    <KeyEditorContainer>\n      <WidthIndent\n        style={{ background: 'var(--semi-color-fill-0)' }}\n        width={\n          (rowData.level + 2) * 14 +\n          (2 * rowData.level - 1) +\n          (rowData.extraConfig.hiddenDrag ? -16 : 0)\n        }\n      />\n      <KeyEditorInput\n        id={getComponentId('key-edit')}\n        onChange={(v) => {\n          setValue(v);\n          typeEditor.dataSourceTouchedMap[rowData.id] = true;\n          const res = validate(v, rowData);\n          typeEditor.setErrorMsg(typeEditor.activePos, res);\n          if (onError) {\n            onError(res ? [res] : []);\n          }\n        }}\n        autoFocus\n        onBlur={(e) => {\n          const oldValue = rowData.key;\n\n          if (value && validate(value, rowData)) {\n            typeEditor.blink.update(true);\n            e.stopPropagation();\n            e.preventDefault();\n            return;\n          }\n\n          const newVal = typeEditorUtils.deFormateKey(value, oldValue);\n\n          changeValue({ rowData, value: newVal });\n\n          onFieldChange &&\n            onFieldChange({\n              type: TypeEditorColumnType.Key,\n              oldValue,\n              newValue: newVal,\n            });\n\n          if (onError) {\n            onError([]);\n          }\n          onChange();\n          onViewMode();\n        }}\n        value={value}\n      />\n    </KeyEditorContainer>\n  );\n};\n\nconst dealMove = (\n  { rowData, value, onChange, typeEditor }: ShortcutContext<IJsonSchema>,\n  cb: () => void\n) => {\n  const validateRes = validate(value, rowData);\n  if (value && validateRes) {\n    typeEditor.blink.update(true);\n  } else {\n    changeValue({\n      value: typeEditorUtils.deFormateKey(value),\n      rowData,\n    });\n    typeEditor.setErrorMsg(typeEditor.activePos);\n    onChange();\n    cb();\n  }\n};\n\nexport const keyColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Key,\n  label: 'Key',\n  viewRender: ViewRender,\n  width: 43,\n  validateCell: (rowData, ctx) => {\n    const key = typeEditorUtils.formateKey(rowData.key);\n    if (!key) {\n      return {\n        level: 'warning',\n        msg: 'Empty lines will not be saved.',\n      };\n    }\n\n    if (ctx.customValidateName) {\n      const customValidateRes = ctx.customValidateName(key);\n      if (customValidateRes) {\n        return {\n          level: 'error',\n          msg: customValidateRes,\n        };\n      }\n      return;\n    }\n\n    if (validate(key, rowData)) {\n      return {\n        level: 'error',\n        msg: validate(key, rowData),\n      };\n    }\n  },\n  shortcuts: {\n    onEnter: (ctx) => {\n      dealMove(ctx, () => {\n        ctx.typeEditor.moveActivePosToNextLineWithAddLine(ctx.rowData);\n      });\n    },\n    onTab: (ctx) => {\n      dealMove(ctx, () => {\n        ctx.typeEditor.moveActivePosToNextItem();\n      });\n    },\n  },\n  editRender: EditRender,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/operate.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { pick } from '@flowgram.ai/utils';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport toast from '@douyinfe/semi-ui/lib/es/toast';\nimport { Toast, Tooltip } from '@douyinfe/semi-ui';\nimport { IconCopy, IconCopyAdd, IconDelete } from '@douyinfe/semi-icons';\n\nimport { getComponentId, typeEditorUtils } from '../utils';\nimport { useRemoveType } from '../hooks/type-edit';\nimport { type PasteDataType, usePasteData } from '../hooks/paste-data';\nimport { useHasErrorCell } from '../hooks/error-cell';\nimport { useDisabled } from '../hooks/disabled';\n\n// import s from './index.module.less';\nimport { TypeEditorRowData, TypeEditorColumnConfig, TypeEditorColumnType } from '../../../types';\nimport { ClipboardService } from '../../../services';\nimport { useService, useTypeDefinitionManager } from '../../../contexts';\nimport { BaseIcon, CenterContainer } from './style';\n\nconst pasteTooltipsMap: Record<PasteDataType<IJsonSchema>['type'], string | undefined> = {\n  invalid: 'Please confirm whether the clipboard value is correct',\n  multiple: 'Multiple fields are not supported to be pasted into this field.',\n  single: undefined,\n};\n\nconst setCellValue = (\n  rowData: TypeEditorRowData<IJsonSchema>,\n  newData: IJsonSchema & {\n    extra?: {\n      key: string;\n      required: boolean;\n    };\n  },\n  onPaste?: (typeSchema?: IJsonSchema) => IJsonSchema | undefined\n): void => {\n  const othersProps = pick(rowData.self, ['required', 'description', 'flow', 'extra']);\n\n  if (rowData.parent?.properties) {\n    const { parent } = rowData;\n    // const key = newDat\n    if (newData.extra) {\n      let { key } = newData.extra;\n      const { required } = newData.extra;\n\n      // key !== rowData.key 处理重复对某个字段进行 paste\n      while (parent.properties![key] && key !== rowData.key) {\n        key = `${key}__copy`;\n      }\n\n      delete parent.properties![rowData.key];\n\n      delete newData.extra;\n      let newPasteData = {\n        ...newData,\n        ...othersProps,\n      };\n      if (onPaste) {\n        newPasteData = onPaste(newPasteData) || newPasteData;\n      }\n\n      parent.properties![key] = newPasteData;\n\n      if (!parent.required) {\n        parent.required = [];\n      }\n\n      const idx = parent.required.findIndex((v) => v === rowData.key);\n\n      if (idx !== -1) {\n        parent.required.splice(idx, 1);\n      }\n      if (required) {\n        parent.required.push(key);\n      }\n    } else {\n      let newPasteData = {\n        ...newData,\n        ...othersProps,\n      };\n      if (onPaste) {\n        newPasteData = onPaste(newPasteData) || newPasteData;\n      }\n      parent.properties![rowData.key] = newPasteData;\n    }\n  }\n};\n\nexport const operateColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Operate,\n  label: 'Operate',\n  width: 11,\n  focusable: false,\n  viewRender: ({ rowData, onChange, typeEditor, onPaste }) => {\n    const disabled = useDisabled(TypeEditorColumnType.Operate, rowData);\n\n    const clipboard = useService<ClipboardService>(ClipboardService);\n\n    const { pasteData } = usePasteData();\n\n    const typeDefinition = useTypeDefinitionManager();\n\n    const hasError = useHasErrorCell(rowData, typeEditor);\n\n    const handleRemove = useRemoveType(rowData, onChange);\n\n    const pasteErrorTips = pasteTooltipsMap[pasteData.type];\n\n    return (\n      <CenterContainer>\n        <Tooltip content={disabled || 'Remove'}>\n          <BaseIcon primary disabled={!!disabled}>\n            <IconDelete\n              id={getComponentId('remove-field')}\n              onClick={disabled ? undefined : handleRemove}\n              size=\"small\"\n            />\n          </BaseIcon>\n        </Tooltip>\n        <Tooltip\n          content={\n            hasError\n              ? 'The current field is incorrect and does not support copying.'\n              : 'Copy this field'\n          }\n        >\n          <BaseIcon primary disabled={!!hasError}>\n            <IconCopy\n              onClick={() => {\n                if (hasError) {\n                  return;\n                }\n                const copyData = {\n                  ...rowData.self,\n                  extra: {\n                    key: rowData.key,\n                    required: rowData.isRequired,\n                    ...(rowData.self.extra || {}),\n                  },\n                };\n\n                clipboard.writeData(JSON.stringify(copyData));\n                toast.success('copy this field success!');\n              }}\n              size=\"small\"\n            />\n          </BaseIcon>\n        </Tooltip>\n        <Tooltip\n          content={disabled ? disabled : !pasteErrorTips ? 'Paste field here' : pasteErrorTips}\n        >\n          <BaseIcon primary disabled={!!(pasteErrorTips || disabled)}>\n            <IconCopyAdd\n              size=\"small\"\n              onClick={() => {\n                if (pasteErrorTips || disabled) {\n                  return;\n                }\n                clipboard.readData().then((data) => {\n                  const parseData = typeEditorUtils.jsonParse(data) as IJsonSchema;\n\n                  const originIndex = rowData.extra?.index || 0;\n\n                  const config = parseData && typeDefinition.getTypeBySchema(parseData);\n                  if (config && typeof parseData === 'object') {\n                    typeEditorUtils.fixFlowIndex(parseData);\n                    parseData.extra!.index = originIndex;\n                    setCellValue(rowData, parseData as any, onPaste);\n\n                    onChange();\n                  } else {\n                    Toast.warning('Please paste the correct type schema!');\n                  }\n                });\n              }}\n            />\n          </BaseIcon>\n        </Tooltip>\n      </CenterContainer>\n    );\n  },\n\n  shortcuts: {\n    onEnter: ({ typeEditor }) => {\n      typeEditor.moveActivePosToNextLine();\n    },\n    onTab: ({ typeEditor }) => {\n      typeEditor.moveActivePosToNextItem();\n    },\n\n    onDown: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextLine();\n    },\n    onLeft: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastColumn();\n    },\n    onUp: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastLine();\n    },\n    onRight: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextColumn();\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/private.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Checkbox, Toast, Tooltip } from '@douyinfe/semi-ui';\n\nimport { typeEditorUtils } from '../utils';\nimport { useDisabled } from '../hooks/disabled';\n\n// import s from './index.module.less';\nimport { TypeEditorColumnConfig, TypeEditorColumnType, TypeEditorRowData } from '../../../types';\nimport { CenterContainer } from './style';\n\nconst setCellValue = (rowData: TypeEditorRowData<IJsonSchema>) => {\n  if (!rowData.extra) {\n    rowData.extra = {};\n  }\n  rowData.extra.private = !rowData.extra.private;\n};\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({\n  rowData,\n  onChange,\n  onEditMode,\n}) => {\n  const disabled = useDisabled(TypeEditorColumnType.Required, rowData);\n\n  const child = (\n    <Checkbox\n      disabled={!!disabled}\n      checked={!!rowData.extra?.private}\n      onChange={() => {\n        setCellValue(rowData);\n        onChange();\n        onEditMode();\n      }}\n    />\n  );\n\n  return (\n    <CenterContainer onClick={!disabled ? () => onEditMode() : undefined}>\n      {disabled ? <Tooltip content={disabled}>{child}</Tooltip> : child}\n    </CenterContainer>\n  );\n};\n\nexport const privateColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Private,\n  label: 'Private',\n  width: 11,\n  viewRender: ViewRender,\n  info: () => 'Private under the current project.',\n  shortcuts: {\n    onEnter: ({ onChange, rowData, typeEditor }) => {\n      setCellValue(rowData);\n      onChange();\n      typeEditor.moveActivePosToNextLine();\n    },\n    onTab: ({ typeEditor }) => {\n      typeEditor.moveActivePosToNextItem();\n    },\n\n    onCopy: (ctx) => {\n      const {\n        rowData,\n        typeEditor: { clipboard },\n      } = ctx;\n\n      clipboard.writeData(\n        JSON.stringify({\n          private: !!rowData.extra?.private,\n        })\n      );\n\n      Toast.success('Copy required success!');\n    },\n\n    onPaste: (ctx) => {\n      const {\n        typeEditor: { clipboard, onChange },\n        rowData,\n      } = ctx;\n      clipboard.readData().then((data) => {\n        const parseData = typeEditorUtils.jsonParse(data);\n        const disabled = (rowData.disableEditColumn || []).find(\n          (r) => r.column === TypeEditorColumnType.Private\n        );\n        if (disabled) {\n          Toast.warning('The current cell does not support editing!');\n          return;\n        }\n        if (parseData && typeof parseData === 'object' && parseData.private !== undefined) {\n          if (!rowData.extra) {\n            rowData.extra = {};\n          }\n          rowData.extra.private = parseData.private;\n          onChange();\n        } else {\n          Toast.warning('Please paste the correct info!');\n        }\n      });\n    },\n\n    onDown: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextLine();\n    },\n    onLeft: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastColumn();\n    },\n    onUp: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastLine();\n    },\n    onRight: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextColumn();\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/required.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Checkbox, Toast, Tooltip } from '@douyinfe/semi-ui';\n\nimport { typeEditorUtils } from '../utils';\nimport { useDisabled } from '../hooks/disabled';\nimport { TypeEditorRowData, TypeEditorColumnConfig, TypeEditorColumnType } from '../../../types';\nimport { CenterContainer } from './style';\n\nconst setCellValue = (rowData: TypeEditorRowData<IJsonSchema>) => {\n  const { parent } = rowData;\n\n  if (!parent) {\n    return;\n  }\n\n  if (!parent.required) {\n    parent.required = [];\n  }\n\n  const idx = parent.required.findIndex((key) => key === rowData.key);\n  if (idx !== -1) {\n    parent.required.splice(idx, 1);\n  } else {\n    parent.required.push(rowData.key);\n  }\n};\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({\n  rowData,\n  onChange,\n  onEditMode,\n}) => {\n  const disabled = useDisabled(TypeEditorColumnType.Required, rowData);\n\n  const child = (\n    <Checkbox\n      disabled={!!disabled}\n      checked={!!rowData.isRequired}\n      onChange={() => {\n        setCellValue(rowData);\n        onChange();\n\n        onEditMode();\n      }}\n    />\n  );\n\n  return (\n    <CenterContainer onClick={!disabled ? () => onEditMode() : undefined}>\n      {disabled ? <Tooltip content={disabled}>{child}</Tooltip> : child}\n    </CenterContainer>\n  );\n};\n\nexport const requiredColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Required,\n  label: 'Required',\n  width: 11,\n  viewRender: ViewRender,\n  shortcuts: {\n    onEnter: ({ onChange, rowData, typeEditor }) => {\n      setCellValue(rowData);\n      onChange();\n      typeEditor.moveActivePosToNextLine();\n    },\n    onTab: ({ typeEditor }) => {\n      typeEditor.moveActivePosToNextItem();\n    },\n\n    onCopy: (ctx) => {\n      const {\n        rowData,\n        typeEditor: { clipboard },\n      } = ctx;\n\n      clipboard.writeData(\n        JSON.stringify({\n          required: rowData.isRequired,\n        })\n      );\n\n      Toast.success('Copy required success!');\n    },\n\n    onPaste: (ctx) => {\n      const {\n        typeEditor: { clipboard, onChange },\n        rowData,\n      } = ctx;\n      clipboard.readData().then((data) => {\n        const parseData = typeEditorUtils.jsonParse(data);\n        const disabled = (rowData.disableEditColumn || []).find(\n          (r) => r.column === TypeEditorColumnType.Required\n        );\n        if (disabled) {\n          Toast.warning('The current cell does not support editing!');\n          return;\n        }\n        if (parseData && typeof parseData === 'object' && parseData.required !== undefined) {\n          const { parent } = rowData;\n          if (!parent) {\n            return;\n          }\n          if (!parent.required) {\n            parent.required = [];\n          }\n          const idx = parent.required.findIndex((key) => key === rowData.key);\n          const originRequired = idx !== -1;\n          // 值不一致才更新\n          if (originRequired !== parseData.required) {\n            if (idx !== -1) {\n              parent.required.splice(idx, 1);\n            } else {\n              parent.required.push(rowData.key);\n            }\n            onChange();\n          }\n        } else {\n          Toast.warning('Please paste the correct info!');\n        }\n      });\n    },\n\n    onDown: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextLine();\n    },\n    onLeft: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastColumn();\n    },\n    onUp: (ctx) => {\n      ctx.typeEditor.moveActivePosToLastLine();\n    },\n    onRight: (ctx) => {\n      ctx.typeEditor.moveActivePosToNextColumn();\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled, { createGlobalStyle } from 'styled-components';\nimport { Input, Typography } from '@douyinfe/semi-ui';\n\nexport const KeyViewContainer = styled.div<{\n  disabled?: boolean;\n}>`\n  width: 100%;\n  display: flex;\n  align-items: center;\n  height: 20px;\n  ${(props) => (props.disabled ? 'cursor: not-allowed !important;' : '')}\n  svg {\n    width: 12px;\n    height: 12px;\n    flex-shrink: 0;\n  }\n`;\n\nexport const KeyViewContent = styled.div`\n  font-size: 12px;\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  width: 0;\n  flex: 1;\n`;\n\nexport const KeyEditorContainer = styled.div`\n  height: 100%;\n  width: 100%;\n  display: flex;\n\n  svg {\n    flex-shrink: 0;\n  }\n\n  .semi-input-wrapper-focus {\n    border-color: transparent !important;\n  }\n\n  .semi-input-wrapper:hover {\n    background-color: var(--semi-color-fill-0);\n  }\n\n  .semi-cascader:hover {\n    background-color: var(--semi-color-fill-0);\n  }\n\n  .semi-cascader:focus {\n    border-color: transparent !important;\n  }\n\n  .semi-cascader-focus {\n    border-color: transparent !important;\n  }\n`;\n\nexport const KeyEditorInput = styled(Input)`\n  flex: 1;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  input {\n    font-size: 12px !important;\n  }\n`;\n\nexport const KeyViewText = styled(Typography.Text)<{\n  disabled?: boolean;\n}>`\n  flex: 1;\n  cursor: text;\n  font-size: 12px;\n  height: 100%;\n  ${(props) =>\n    props.disabled\n      ? `\n      cursor: not-allowed !important;\n    `\n      : ''}\n`;\n\nexport const BaseIcon = styled.div<{\n  draggable?: boolean;\n  triangle?: boolean;\n  disabled?: boolean;\n  primary?: boolean;\n  isRotate?: boolean;\n}>`\n  color: var(--semi-color-text-2);\n  cursor: pointer;\n\n  ${(props) =>\n    props.draggable\n      ? `\n    cursor: grab;\n    margin-right: 4px;\n  `\n      : ''}\n\n  ${(props) =>\n    props.triangle\n      ? `\n    transform: rotate(270deg);\n\n  `\n      : ''}\n\n  ${(props) =>\n    props.isRotate\n      ? `\n    transform: rotate(360deg);\n\n  `\n      : ''}\n\n  ${(props) =>\n    props.disabled\n      ? `\n      opacity: 0.5;\n      cursor: not-allowed;\n    `\n      : ''}\n\n  ${(props) =>\n    props.primary\n      ? `\n        color: var(--semi-color-primary) !important;\n      `\n      : ''}\n\n  &:hover {\n    color: var(--semi-color-primary);\n  }\n`;\n\nexport const CenterContainer = styled.div`\n  width: 100%;\n  height: 20px;\n  display: flex;\n  align-items: center;\n  gap: 12px;\n  justify-content: center;\n`;\n\nexport const TypeDisableViewContainer = styled(KeyViewContainer)`\n  color: var(--semi-color-disabled-text) !important;\n  cursor: not-allowed;\n  font-size: 12px;\n\n  :global {\n    .semi-typography {\n      color: var(--semi-color-disabled-text) !important;\n    }\n  }\n`;\n\nexport const TypeTextContainer = styled.div`\n  width: 100%;\n  height: 100%;\n  display: flex;\n  box-sizing: border-box;\n  align-items: center;\n  padding: 1px 5px;\n`;\n\nexport const GlobalSelectStyle = createGlobalStyle`\n  .semi-select {\n      border: none !important\n    }\n\n`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/type.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useMemo, useState } from 'react';\n\nimport { pick } from '@flowgram.ai/utils';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Toast, Tooltip } from '@douyinfe/semi-ui';\nimport { IconHelpCircle } from '@douyinfe/semi-icons';\n\nimport { typeEditorUtils } from '../utils';\nimport { useDisabled } from '../hooks/disabled';\nimport { TypeSelector } from '../../type-selector';\nimport { TypeEditorColumnConfig, TypeEditorColumnType, TypeEditorRowData } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport { BaseIcon, KeyEditorContainer, KeyViewContainer, TypeDisableViewContainer } from './style';\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({ rowData, onEditMode }) => {\n  const typeService = useTypeDefinitionManager();\n\n  const disabled = useDisabled(TypeEditorColumnType.Type, rowData);\n  const [refresh, setRefresh] = useState(0);\n\n  useEffect(() => {\n    typeService.onTypeRegistryChange(() => {\n      setRefresh((v) => v + 1);\n    });\n  }, [typeService]);\n\n  const typeConfig = useMemo(() => typeService.getTypeBySchema(rowData.self), [refresh, rowData]);\n\n  const unknownNode = (\n    <BaseIcon>\n      <IconHelpCircle style={{ marginRight: 4 }} />\n      Unknown\n    </BaseIcon>\n  );\n\n  return disabled ? (\n    <>\n      <TypeDisableViewContainer>\n        <Tooltip content={disabled}>\n          {typeConfig?.getDisplayLabel?.(rowData) || unknownNode}\n        </Tooltip>\n      </TypeDisableViewContainer>\n    </>\n  ) : (\n    <KeyViewContainer\n      onClick={() => {\n        onEditMode();\n      }}\n    >\n      {typeConfig?.getDisplayLabel?.(rowData) || unknownNode}\n    </KeyViewContainer>\n  );\n};\n\nconst setCellValue = (rowData: TypeEditorRowData<IJsonSchema>, newData: IJsonSchema): void => {\n  const othersProps = pick(rowData.self, ['required', 'description', 'flow']);\n\n  if (rowData.parent?.properties) {\n    rowData.parent.properties[rowData.key] = {\n      ...newData,\n      ...othersProps,\n    };\n  }\n};\n\nconst EditRender: TypeEditorColumnConfig<IJsonSchema>['editRender'] = ({\n  rowData,\n  onChange,\n  typeEditor,\n  onViewMode,\n}) => {\n  const [value, setValue] = useState(rowData.self);\n  const typeService = useTypeDefinitionManager();\n\n  return (\n    <KeyEditorContainer>\n      <TypeSelector\n        value={value}\n        typeRegistryCreators={typeEditor.typeRegistryCreators}\n        disableTypes={rowData.extraConfig?.customDisabledTypes}\n        onDropdownVisibleChange={(vis) => {\n          if (!vis) {\n            onViewMode();\n          }\n        }}\n        onChange={(newData, ctx) => {\n          if (!newData) {\n            return;\n          }\n\n          setCellValue(rowData, newData);\n\n          setValue(newData);\n          onChange();\n\n          let unClose = false;\n\n          typeEditorUtils.traverseIJsonSchema(newData, (type) => {\n            const def = typeService.getTypeBySchema(type);\n            if (def?.typeCascaderConfig?.unClosePanelAfterSelect) {\n              unClose = true;\n            }\n          });\n\n          if (ctx.source === 'type-selector' && !unClose) {\n            onViewMode();\n          }\n        }}\n        defaultOpen\n        onBlur={() => {\n          onViewMode();\n        }}\n      />\n    </KeyEditorContainer>\n  );\n};\nexport const typeColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Type,\n  label: 'Type',\n  width: 20,\n  viewRender: ViewRender,\n  shortcuts: {\n    onTab: ({ typeEditor }) => {\n      typeEditor.moveActivePosToNextItem();\n    },\n    onCopy: (ctx) => {\n      const {\n        typeEditor: { typeDefinition, clipboard },\n        rowData,\n      } = ctx;\n      const config = typeDefinition.getTypeBySchema(rowData.self);\n      if (config) {\n        const optionValue = config.getStringValueByTypeSchema?.(rowData.self);\n        const type =\n          optionValue && config.getTypeSchemaByStringValue\n            ? config.getTypeSchemaByStringValue(optionValue)\n            : config.getDefaultSchema();\n        type.properties = rowData.properties;\n        type.required = rowData.required;\n        clipboard.writeData(JSON.stringify(type));\n        Toast.success('Copy type success!');\n      } else {\n        Toast.error('Copy type failed: this type undefined');\n      }\n    },\n\n    onPaste: (ctx) => {\n      const {\n        typeEditor: { typeDefinition, clipboard, onChange },\n        rowData,\n      } = ctx;\n      clipboard.readData().then((data) => {\n        const parseData = typeEditorUtils.jsonParse(data) as IJsonSchema;\n        const originIndex = rowData.extra?.index || 0;\n        const config = parseData && typeDefinition.getTypeBySchema(parseData);\n        if (config && typeof parseData === 'object') {\n          typeEditorUtils.fixFlowIndex(parseData);\n          if (parseData.extra) {\n            parseData.extra.index = originIndex;\n          }\n          setCellValue(rowData, parseData);\n          onChange();\n        } else {\n          Toast.warning('Please paste the correct type schema!');\n        }\n      });\n    },\n  },\n  editRender: EditRender,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/columns/value.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip, Typography } from '@douyinfe/semi-ui';\n// import { TypeInput, TypeText } from '@api-builder/base-type-definition';\n\nimport { useDisabled } from '../hooks/disabled';\nimport { TypeEditorColumnConfig, TypeEditorColumnType } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport {\n  GlobalSelectStyle,\n  KeyEditorContainer,\n  KeyViewContainer,\n  TypeTextContainer,\n} from './style';\n\nexport const TypeText: FC<{\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value?: any;\n  type: IJsonSchema;\n}> = ({ value, type }) => {\n  const typeDefinition = useTypeDefinitionManager();\n\n  const content = useMemo(() => {\n    const config = typeDefinition.getTypeBySchema(type);\n\n    return config?.getValueText ? config.getValueText(value) : value || '';\n  }, [value, type]);\n\n  return (\n    <TypeTextContainer>\n      <Typography.Text ellipsis={{ showTooltip: true }}>{content}</Typography.Text>\n    </TypeTextContainer>\n  );\n};\n\nconst ViewRender: TypeEditorColumnConfig<IJsonSchema>['viewRender'] = ({ rowData, onEditMode }) => {\n  const disabled = useDisabled(TypeEditorColumnType.Value, rowData);\n\n  if (disabled) {\n    return (\n      <KeyViewContainer disabled>\n        <Tooltip content={disabled}>\n          <div style={{ width: '100%', height: '100%' }}>\n            <TypeText value={rowData.self?.extra?.value} type={rowData.self} />\n          </div>\n        </Tooltip>\n      </KeyViewContainer>\n    );\n  }\n\n  return (\n    <KeyViewContainer onClick={() => onEditMode()}>\n      <TypeText value={rowData.self?.extra?.value} type={rowData.self} />\n    </KeyViewContainer>\n  );\n};\n\nconst EditRender: TypeEditorColumnConfig<IJsonSchema>['editRender'] = ({\n  rowData,\n  onChange,\n  typeEditor,\n  onViewMode,\n}) => {\n  const valueRef = useRef(rowData.self.extra?.value);\n  const [value, setValue] = useState(rowData.self.extra?.value);\n  const typeDefinition = useTypeDefinitionManager();\n  const config = useMemo(() => typeDefinition.getTypeBySchema(rowData.self), [rowData.self]);\n  useEffect(() => {\n    typeEditor.editValue = value;\n  }, [typeEditor, value]);\n\n  useEffect(() => {\n    setValue(rowData.self.extra?.value);\n    valueRef.current = rowData.self.extra?.value;\n  }, [rowData.self.extra?.value]);\n\n  const handleSubmit = useCallback(() => {\n    if (!rowData.self.extra) {\n      rowData.self.extra = {};\n    }\n\n    rowData.self.extra.value = valueRef.current;\n    onChange();\n\n    onViewMode();\n  }, [onChange, rowData]);\n\n  return (\n    <KeyEditorContainer>\n      <GlobalSelectStyle />\n      {config &&\n        config?.getInputNode?.({\n          value: value,\n          onChange: (v: unknown) => {\n            valueRef.current = v;\n            setValue(v);\n          },\n          type: rowData.self,\n          onSubmit: () => {\n            handleSubmit();\n            onViewMode();\n          },\n        })}\n    </KeyEditorContainer>\n  );\n};\n\nexport const valueColumnConfig: TypeEditorColumnConfig<IJsonSchema> = {\n  type: TypeEditorColumnType.Value,\n  width: 15,\n  label: 'Value',\n  viewRender: ViewRender,\n  shortcuts: {\n    onEnter: ({ rowData, onChange, typeEditor, value, typeDefinitionService }) => {\n      const config = typeDefinitionService.getTypeBySchema(rowData.self);\n      if (config?.typeInputConfig?.canEnter) {\n        rowData.self.description = value;\n\n        onChange();\n        typeEditor.moveActivePosToNextLine();\n      }\n    },\n    onTab: ({ rowData, value, onChange, typeEditor }) => {\n      rowData.self.description = value;\n      onChange();\n      typeEditor.moveActivePosToNextItem();\n    },\n  },\n  editRender: EditRender,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/common.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * empty key 前缀\n */\nexport const SUFFIX = '___empty___';\n\n/**\n * 表格 header 高度\n */\nexport const HEADER_HIGHT = 36;\n/**\n * 表格 cell 高度\n * 36 高度 + 1px border\n */\nexport const CELL_HIGHT = 36 + 1;\n\n/**\n * cell padding 宽度\n */\nexport const CELL_PADDING = 8;\n\n/**\n * 单个缩进的宽度\n */\nexport const INDENT_WIDTH = 16;\n\n/**\n * 操作栏高度\n */\nexport const TAB_BRA_HIGHT = 0;\n\n/**\n * test id 前缀\n */\nexport const COMPONENT_ID_PREFIX = 'type-editor-component-id';\n\n/**\n * root 字段 id\n */\nexport const ROOT_FIELD_ID = 'root';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/drop-tip.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useState } from 'react';\nimport React from 'react';\n\nimport styled from 'styled-components';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorRowData } from '../../types';\nimport { TypeEditorService } from '../../services';\nimport { useService, useTypeDefinitionManager } from '../../contexts';\nimport { CELL_HIGHT, CELL_PADDING, HEADER_HIGHT, INDENT_WIDTH, TAB_BRA_HIGHT } from './common';\n\nconst StyledDragTip = styled.div`\n  position: absolute;\n  height: 1px;\n  background-color: var(--semi-color-primary);\n`;\n\nexport const DropTip = <TypeSchema extends Partial<IJsonSchema>>({\n  dataSource,\n}: {\n  dataSource: Record<string, TypeEditorRowData<TypeSchema>>;\n}) => {\n  const typeEditor = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const [dropInfo, setDropInfo] = useState(typeEditor.dropInfo);\n\n  const typeService = useTypeDefinitionManager();\n\n  useEffect(() => {\n    const dispose = typeEditor.onDropInfoChange.event(setDropInfo);\n    return () => {\n      dispose.dispose();\n    };\n  }, []);\n\n  const rowData = useMemo(() => dataSource[dropInfo.rowDataId], [dropInfo.rowDataId]);\n\n  const rowDataCanHasChildren = useMemo(\n    () =>\n      rowData && rowData.type && typeService.getTypeByName(rowData.type)?.canAddField?.(rowData),\n    [rowData]\n  );\n\n  return (\n    <>\n      {(rowData || dropInfo.rowDataId === 'header') && (\n        <StyledDragTip\n          style={{\n            top: HEADER_HIGHT + TAB_BRA_HIGHT + (dropInfo.index + 1) * CELL_HIGHT - 1,\n            right: 0,\n            width: `calc(100% - ${\n              /**\n               * 表格 padding\n               * 初始 drag icon 占一个缩进\n               * rowData 本身 level 个缩进，如果是含有子节点，因为默认添加到子元素内，还需要额外加一个缩进\n               */\n              CELL_PADDING +\n              INDENT_WIDTH +\n              (dropInfo.indent + (rowDataCanHasChildren ? 1 : 0) + 1) * INDENT_WIDTH\n            }px)`,\n          }}\n        />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/error.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FC } from 'react';\nimport React from 'react';\n\nimport styled from 'styled-components';\n\nconst TopLine = styled.div<{\n  level?: 'error' | 'warning';\n}>`\n  position: absolute;\n  background-color: ${(props) =>\n    props.level === 'warning' ? 'var(--semi-color-warning)' : 'var(--semi-color-danger)'};\n  width: 100%;\n  height: 1px;\n  top: 0;\n  left: 0;\n`;\n\nconst BottomLine = styled.div<{\n  level?: 'error' | 'warning';\n}>`\n  position: absolute;\n  background-color: ${(props) =>\n    props.level === 'warning' ? 'var(--semi-color-warning)' : 'var(--semi-color-danger)'};\n  width: 100%;\n  height: 1px;\n  bottom: -1px;\n  left: 0;\n`;\n\nconst RightLine = styled.div<{\n  level?: 'error' | 'warning';\n}>`\n  position: absolute;\n  background-color: ${(props) =>\n    props.level === 'warning' ? 'var(--semi-color-warning)' : 'var(--semi-color-danger)'};\n  height: calc(100% + 1px);\n  width: 1px;\n  bottom: -1px;\n  right: 0px;\n`;\n\nconst LeftLine = styled.div<{\n  level?: 'error' | 'warning';\n}>`\n  position: absolute;\n  background-color: ${(props) =>\n    props.level === 'warning' ? 'var(--semi-color-warning)' : 'var(--semi-color-danger)'};\n  height: calc(100% + 1px);\n  width: 1px;\n  bottom: -1px;\n  left: 0;\n`;\n\nexport const ErrorCellBorder: FC<{\n  level?: 'error' | 'warning';\n}> = ({ level }) => (\n  <>\n    <TopLine level={level} />\n    <BottomLine level={level} />\n    <LeftLine level={level} />\n    <RightLine level={level} />\n  </>\n);\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/formatter/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { SUFFIX } from '../common';\nexport type Formatter = (typeSchema: Partial<IJsonSchema>) => void;\n\nexport const extraFormatter: Formatter = (type) => {\n  if (type.extra !== undefined) {\n    type.extra = type.extra;\n\n    delete type.extra;\n  }\n};\n\nexport const extraDeFormatter: Formatter = (type) => {\n  if (type.extra !== undefined) {\n    type.extra = type.extra;\n    delete type.extra;\n  }\n};\n\nexport const emptyKeyFormatter: Formatter = (type) => {\n  if (type.properties) {\n    Object.keys(type.properties).forEach((k) => {\n      if (k.startsWith(SUFFIX)) {\n        delete type.properties![k];\n      }\n    });\n  }\n};\n\nexport const disableFixIndexFormatter: Formatter = (type) => {\n  if (type.extra) {\n    delete type.extra.index;\n\n    if (Object.keys(type.extra).length === 0) {\n      delete type.extra;\n    }\n  }\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/header.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip } from '@douyinfe/semi-ui';\nimport { IconInfoCircle } from '@douyinfe/semi-icons';\n\nimport { TypeEditorColumnType, TypeEditorRowData } from '../../types/type-editor';\nimport { TypeEditorService } from '../../services';\nimport { useService } from '../../contexts';\nimport { EditorTableTitle, EditorTableHeader, BaseIcon } from './style';\nimport { useHeaderDrop } from './hooks';\n\nexport const Header = <TypeSchema extends Partial<IJsonSchema>>({\n  displayColumn,\n  value,\n  readonly,\n  dataSourceMap,\n  onChange,\n}: {\n  displayColumn: TypeEditorColumnType[];\n  dataSourceMap: Record<string, TypeEditorRowData<TypeSchema>>;\n  onChange: () => void;\n\n  readonly?: boolean;\n\n  value: TypeSchema;\n}) => {\n  const typeEditorService = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const { drop } = useHeaderDrop(value, dataSourceMap, onChange);\n\n  return (\n    <thead className=\"semi-table-thead\">\n      <tr ref={drop} className=\"semi-table-row\">\n        {displayColumn.map((v) => {\n          const config = typeEditorService.getConfigByType(v);\n\n          return (\n            <EditorTableHeader\n              key={v}\n              style={{\n                width: config?.width ? `${config.width}%` : undefined,\n                paddingLeft: readonly ? '24px !important' : undefined,\n              }}\n              className=\"semi-table-row-head\"\n              scope=\"col\"\n            >\n              <EditorTableTitle>\n                {config?.label}\n                {config?.info && (\n                  <Tooltip content={config.info()}>\n                    <BaseIcon>\n                      <IconInfoCircle style={{ marginLeft: 4 }} size=\"small\" />\n                    </BaseIcon>\n                  </Tooltip>\n                )}\n              </EditorTableTitle>\n            </EditorTableHeader>\n          );\n        })}\n      </tr>\n    </thead>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/active-pos.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorService } from '../../../services';\nimport { useService } from '../../../contexts';\n\nexport const useActivePos = (): TypeEditorService<IJsonSchema>['activePos'] => {\n  const typeEditorService = useService<TypeEditorService<IJsonSchema>>(TypeEditorService);\n\n  const [activePos, setActivePos] = useState<{ x: number; y: number }>(typeEditorService.activePos);\n\n  useEffect(() => {\n    const dispose = typeEditorService.onActivePosChange.event((v) => {\n      setActivePos(v);\n    });\n    return () => {\n      dispose.dispose();\n    };\n  }, []);\n\n  return activePos;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/blink.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorService } from '../../../services';\nimport { useService } from '../../../contexts';\n\nexport const useIsBlink = () => {\n  const typeEditor = useService<TypeEditorService<IJsonSchema>>(TypeEditorService);\n\n  const blinkData = useMemo(() => typeEditor.blink, [typeEditor.blink]);\n  const [blink, setBlink] = useState(blinkData.data);\n\n  useEffect(() => {\n    const dispose = blinkData.onDataChange(({ next }) => {\n      setBlink(next);\n\n      if (next) {\n        setTimeout(() => {\n          blinkData.update(false);\n        }, 200);\n      }\n    });\n\n    return () => {\n      dispose.dispose();\n    };\n  }, [blinkData]);\n\n  return blink;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/disabled.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorColumnType, TypeEditorRowData } from '../../../types';\n\nexport const useDisabled = <TypeSchema extends Partial<IJsonSchema>>(\n  type: TypeEditorColumnType,\n  rowData: TypeEditorRowData<TypeSchema>\n): string | undefined => {\n  const disabled = (rowData.disableEditColumn || []).find((r) => r.column === type);\n\n  if (disabled?.reason) {\n    return disabled?.reason;\n  }\n\n  if (typeof rowData.extra?.editable === 'string') {\n    return rowData.extra?.editable;\n  }\n\n  return rowData.extra?.editable === false ? `${type}  is not editable.` : undefined;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/drag-drop.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable max-params */\n\nimport { useDrag, useDrop } from 'react-dnd';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport { typeEditorUtils } from '../utils';\nimport { TypeEditorRowData } from '../../../types';\nimport { TypeEditorService } from '../../../services';\nimport { useService, useTypeDefinitionManager } from '../../../contexts';\n\nexport const useRowDrag = (\n  id: string,\n  onChildrenVisibleChange: (rowDataId: string, newVal: boolean) => void\n) => {\n  const [{ isDragging }, drag, preview] = useDrag(\n    () => ({\n      type: 'node-editor-dnd',\n      item: () => {\n        onChildrenVisibleChange(id, true);\n        return {\n          id,\n        };\n      },\n      collect: (monitor) => ({\n        isDragging: monitor.isDragging(),\n      }),\n    }),\n    [id]\n  );\n\n  return {\n    drag,\n    isDragging,\n    preview,\n  };\n};\n\ninterface DragItem {\n  id: string;\n}\n\nexport const useCellDrop = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  index: number,\n  dataSource: Record<string, TypeEditorRowData<TypeSchema>>,\n  onChange: () => void\n) => {\n  const typeEditor = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const typeService = useTypeDefinitionManager();\n  const [{ isOver }, drop] = useDrop(\n    () => ({\n      accept: 'node-editor-dnd',\n      drop: (item: DragItem) => {\n        if (item.id === rowData.id) {\n          typeEditor.clearDropInfo();\n          return;\n        }\n\n        const dragData = dataSource[item.id];\n\n        const definition = typeService.getTypeBySchema(rowData.self);\n\n        if (definition) {\n          const canHasChild = definition.canAddField?.(rowData.self);\n\n          let dropParent: IJsonSchema;\n          let dropIndex: number;\n          if (canHasChild) {\n            dropParent = definition.getPropertiesParent?.(rowData.self)! as IJsonSchema;\n            dropIndex = -1;\n          } else {\n            dropParent = rowData.parent! as IJsonSchema;\n            dropIndex = (rowData.extra?.index || 0) + 0.1;\n          }\n\n          if (dropParent?.properties?.[dragData.key] && dropParent !== dragData.parent) {\n            Toast.error('drop error: there is a duplicate key in the current object');\n            typeEditor.clearDropInfo();\n\n            return;\n          }\n\n          // 删除原来的引用\n          if (dragData.parent?.properties) {\n            delete dragData.parent.properties[dragData.key];\n          }\n\n          // 添加到新的节点\n          if (!dropParent.properties) {\n            dropParent.properties = {};\n          }\n          dropParent.properties[dragData.key] = dragData.self as IJsonSchema;\n          typeEditorUtils.fixFlowIndex(dragData);\n          dragData.extra!.index = dropIndex;\n\n          // 分别重新 sort dropParent 和 dragParent\n          typeEditorUtils.sortProperties(dropParent);\n          typeEditorUtils.sortProperties(dragData.parent!);\n          onChange();\n        }\n\n        typeEditor.clearDropInfo();\n      },\n      hover() {\n        typeEditor.setDropInfo({\n          indent: rowData.level,\n          index,\n          rowDataId: rowData.id,\n        });\n      },\n      collect: (monitor) => ({\n        isOver: monitor.isOver(),\n      }),\n    }),\n    [rowData]\n  );\n\n  return {\n    drop,\n    isOver,\n  };\n};\n\nexport const useHeaderDrop = <TypeSchema extends Partial<IJsonSchema>>(\n  root: TypeSchema,\n  dataSource: Record<string, TypeEditorRowData<TypeSchema>>,\n  onChange: () => void\n) => {\n  const typeEditor = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const typeService = useTypeDefinitionManager();\n\n  const [{ isOver }, drop] = useDrop(\n    () => ({\n      accept: 'node-editor-dnd',\n      drop: (item: DragItem) => {\n        const dragData = dataSource[item.id];\n\n        const definition = typeService.getTypeBySchema(root);\n\n        if (definition) {\n          const canHasChild = definition.canAddField(root);\n\n          let dropParent: IJsonSchema;\n          let dropIndex: number;\n          if (canHasChild) {\n            dropParent = definition.getPropertiesParent(root)! as IJsonSchema;\n            dropIndex = -1;\n\n            if (dropParent?.properties?.[dragData.key] && dropParent !== dragData.parent) {\n              Toast.error('drop error: there is a duplicate key in the current object');\n              typeEditor.clearDropInfo();\n\n              return;\n            }\n\n            // 删除原来的引用\n            if (dragData.parent?.properties) {\n              delete dragData.parent.properties[dragData.key];\n            }\n\n            // 添加到新的节点\n            if (!dropParent.properties) {\n              dropParent.properties = {};\n            }\n            dropParent.properties[dragData.key] = dragData.self as IJsonSchema;\n            typeEditorUtils.fixFlowIndex(dragData);\n            dragData.extra!.index = dropIndex;\n\n            // 分别重新 sort dropParent 和 dragParent\n            typeEditorUtils.sortProperties(dropParent);\n            typeEditorUtils.sortProperties(dragData.parent!);\n            onChange();\n          }\n\n          typeEditor.clearDropInfo();\n        }\n      },\n      hover() {\n        typeEditor.setDropInfo({\n          indent: 0,\n          index: -1,\n          rowDataId: 'header',\n        });\n      },\n      collect: (monitor) => ({\n        isOver: monitor.isOver(),\n      }),\n    }),\n    [root]\n  );\n\n  return {\n    drop,\n    isOver,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/editor-listener.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorProp } from '../type';\nimport { columnConfigs } from '../columns';\nimport { TypeEditorColumnConfig } from '../../../types';\nimport { TypeEditorService } from '../../../services';\nimport { useService } from '../../../contexts';\nimport { useTypeEditorHotKey } from './hot-key';\n\nexport const TypeEditorListener = <TypeSchema extends Partial<IJsonSchema>>({\n  configs = [],\n  children,\n}: React.PropsWithChildren & {\n  configs: TypeEditorProp<'type-definition', TypeSchema>['viewConfigs'];\n}) => {\n  const dom = useRef();\n\n  const lastWidth = useRef(0);\n\n  const typeEditorService = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const [init, setInit] = useState(false);\n\n  useEffect(() => {\n    if (dom.current) {\n      const el = dom.current;\n\n      const resize = new ResizeObserver((entries) => {\n        if (lastWidth.current === 0) {\n          lastWidth.current = entries[0].contentRect.width;\n        }\n        if (entries[0].contentRect.width !== lastWidth.current) {\n          typeEditorService.clearActivePos();\n        }\n      });\n\n      resize.observe(el);\n      return () => {\n        resize.unobserve(el);\n      };\n    }\n  }, [dom.current, typeEditorService]);\n\n  useEffect(() => {\n    typeEditorService.registerConfigs(\n      columnConfigs as unknown as TypeEditorColumnConfig<TypeSchema>[]\n    );\n\n    configs.forEach(\n      (config) => config.config && typeEditorService.addConfigProps(config.type, config.config)\n    );\n\n    setInit(true);\n  }, [typeEditorService, configs]);\n\n  const composition = useRef(false);\n\n  const hotkeys = useTypeEditorHotKey();\n\n  return (\n    <div\n      style={{ width: '100%' }}\n      onCompositionStart={() => (composition.current = true)}\n      onCompositionEnd={() => (composition.current = false)}\n      onKeyDown={(e) => {\n        if (composition.current) return;\n        const hotKey = hotkeys.find((item) => item.matcher(e));\n        hotKey?.callback();\n        if (hotKey?.preventDefault) {\n          e.preventDefault();\n          e.stopPropagation();\n        }\n      }}\n      ref={dom as unknown as React.LegacyRef<HTMLDivElement>}\n    >\n      {init && children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/error-cell.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable max-params */\nimport { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { useMonitorData } from '../../../utils';\nimport { TypeEditorPos, TypeEditorRowData, TypeEditorColumnConfig } from '../../../types';\nimport { TypeEditorService } from '../../../services';\nimport { useService } from '../../../contexts';\nimport { useActivePos } from './active-pos';\n\nexport const useEditCellErrorMsg = (pos: TypeEditorPos): string | undefined => {\n  const typeEditor = useService<TypeEditorService<IJsonSchema>>(TypeEditorService);\n\n  const { data: errorMsgs } = useMonitorData(typeEditor.errorMsgs);\n\n  const msg = useMemo(\n    () => errorMsgs?.find((v) => v.pos.x === pos.x && v.pos.y === pos.y)?.msg,\n    [pos, errorMsgs]\n  );\n\n  return msg;\n};\n\nexport const useViewCellErrorMsg = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  config: TypeEditorColumnConfig<TypeSchema>,\n  pos: TypeEditorPos\n) => {\n  const activePos = useActivePos();\n\n  const res = useMemo(\n    () =>\n      (activePos.x !== pos.x || activePos.y !== pos.y) && config.validateCell\n        ? config.validateCell(rowData, rowData.extraConfig)\n        : undefined,\n    [rowData, config, activePos, pos]\n  );\n\n  return res;\n};\n\nexport const useHasErrorCell = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n\n  typeEditor: TypeEditorService<TypeSchema>\n): boolean => {\n  const res = useMemo(() => {\n    const configs = typeEditor.columnViewConfig.map((v) => typeEditor.getConfigByType(v.type));\n\n    return (\n      configs\n        .map((config) =>\n          config?.validateCell ? config.validateCell(rowData, rowData.extraConfig) : undefined\n        )\n        .filter(Boolean).length !== 0\n    );\n  }, [rowData, typeEditor]);\n\n  return res;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/formatter-value.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { ModeValueConfig, TypeEditorMode, TypeEditorProp, TypeEditorValue } from '../type';\nimport { modeValueConfig } from '../mode';\nimport { extraDeFormatter, extraFormatter, Formatter } from '../formatter';\nimport { traverseIJsonSchema } from '../../../services/utils';\n\nexport const useFormatter = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>({\n  extraConfig = {},\n  mode,\n}: Pick<TypeEditorProp<Mode, TypeSchema>, 'mode' | 'extraConfig'>) => {\n  const { useExtra } = extraConfig;\n  const modeConfig = useMemo(\n    () =>\n      modeValueConfig.find((v) => v.mode === mode)! as unknown as ModeValueConfig<Mode, TypeSchema>,\n    [mode]\n  );\n\n  const formatter = useMemo(\n    () => (value: TypeEditorValue<Mode, TypeSchema> | undefined) => {\n      const originSchema = value && modeConfig.convertValueToSchema(value as any);\n      let res = originSchema ? JSON.parse(JSON.stringify(originSchema)) : originSchema;\n\n      const formatters: Formatter[] = [];\n      if (useExtra) {\n        formatters.push(extraFormatter);\n      }\n\n      if (formatters.length !== 0 && res) {\n        traverseIJsonSchema(res, (type) => {\n          formatters.forEach((f) => f(type));\n        });\n      }\n      return res;\n    },\n    [modeConfig, useExtra]\n  );\n\n  const deFormatter = useMemo(\n    () => (originSchema: TypeSchema | undefined) => {\n      let schema = originSchema ? JSON.parse(JSON.stringify(originSchema)) : originSchema;\n\n      const formatters: Formatter[] = [];\n      if (useExtra) {\n        formatters.push(extraDeFormatter);\n      }\n\n      if (formatters.length !== 0 && schema) {\n        traverseIJsonSchema(schema, (type) => {\n          formatters.forEach((f) => f(type));\n        });\n      }\n\n      return schema && modeConfig.convertSchemaToValue(schema as TypeSchema);\n    },\n    [modeConfig, useExtra]\n  );\n\n  return {\n    formatter,\n    deFormatter,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/hot-key.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorOperationService, TypeEditorService } from '../../../services';\nimport { useService } from '../../../contexts';\n\ninterface HotKeyConfig {\n  matcher: (event: React.KeyboardEvent) => boolean;\n  callback: () => void;\n  preventDefault?: boolean;\n}\n\nexport const useTypeEditorHotKey = () => {\n  const typeEditor = useService<TypeEditorService<IJsonSchema>>(TypeEditorService);\n  const operator = useService<TypeEditorOperationService<IJsonSchema>>(TypeEditorOperationService);\n\n  const hotKeyConfig: HotKeyConfig[] = useMemo(() => {\n    const res: HotKeyConfig[] = [\n      {\n        matcher: (e) => e.key === 'Enter',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('enter');\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => e.key === 'ArrowUp',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('up');\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => e.key === 'Tab',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('tab');\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => e.key === 'ArrowLeft',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('left');\n        },\n      },\n      {\n        matcher: (e) => e.key === 'ArrowRight',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('right');\n        },\n      },\n      {\n        matcher: (e) => e.key === 'ArrowDown',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('down');\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => (e.metaKey || e.ctrlKey) && e.key === 'c',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('copy');\n        },\n      },\n      {\n        matcher: (e) => (e.metaKey || e.ctrlKey) && e.key === 'v',\n        callback: () => {\n          typeEditor.triggerShortcutEvent('paste');\n        },\n      },\n      {\n        matcher: (e) => (e.metaKey || e.ctrlKey) && e.key === 'z',\n        callback: () => {\n          operator.undo();\n        },\n      },\n      {\n        matcher: (e) => (e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'z',\n        callback: () => {\n          operator.redo();\n        },\n      },\n    ];\n\n    return res;\n  }, [typeEditor]);\n\n  return hotKeyConfig;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useActivePos } from './active-pos';\nexport { TypeEditorListener } from './editor-listener';\nexport { useRowDrag, useCellDrop, useHeaderDrop } from './drag-drop';\nexport { useKeyVisible } from './key-visible';\nexport { useAddType, useRemoveType } from './type-edit';\nexport { useEditCellErrorMsg, useViewCellErrorMsg } from './error-cell';\nexport { useDisabled } from './disabled';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/key-visible.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip } from '@douyinfe/semi-ui';\nimport { IconEyeClosed, IconEyeOpened } from '@douyinfe/semi-icons';\n\nimport { BaseIcon } from '../columns/style';\nimport { TypeEditorRowData, TypeEditorSpecialConfig, TypeEditorColumnType } from '../../../types';\nimport { useDisabled } from './disabled';\n\nexport const useKeyVisible = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  onChange: () => void,\n  extraConfig: TypeEditorSpecialConfig<TypeSchema>\n): React.JSX.Element => {\n  const disabled = useDisabled(TypeEditorColumnType.Key, rowData);\n\n  const handleVisibleChange = useCallback(\n    (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {\n      const currentSchema = rowData.self;\n\n      if (!currentSchema.extra) {\n        currentSchema.extra = {};\n      }\n\n      currentSchema.extra.hidden = !currentSchema.extra.hidden;\n      onChange();\n\n      e.stopPropagation();\n      e.preventDefault();\n    },\n    [rowData.self, onChange]\n  );\n\n  const disableContent =\n    disabled || typeof extraConfig.editorVisible === 'string'\n      ? disabled || extraConfig.editorVisible\n      : undefined;\n\n  const visibleNode = (\n    <>\n      {disableContent ? (\n        <Tooltip content={disableContent}>\n          <BaseIcon disabled>\n            <IconEyeOpened size=\"small\" />\n          </BaseIcon>\n        </Tooltip>\n      ) : rowData.extra?.hidden ? (\n        <BaseIcon>\n          <IconEyeClosed onClick={handleVisibleChange} size=\"small\" />\n        </BaseIcon>\n      ) : (\n        <BaseIcon>\n          <IconEyeOpened onClick={handleVisibleChange} size=\"small\" />\n        </BaseIcon>\n      )}\n    </>\n  );\n\n  return extraConfig.editorVisible ? visibleNode : <></>;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/paste-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { typeEditorUtils } from '../utils';\nimport { ClipboardService } from '../../../services';\nimport { useService, useTypeDefinitionManager } from '../../../contexts';\n\nexport type PasteDataType<TypeSchema> =\n  | {\n      type: 'single';\n      value: TypeSchema;\n    }\n  | {\n      type: 'multiple';\n      value: TypeSchema[];\n    }\n  | {\n      type: 'invalid';\n    };\n\nexport const usePasteData = <TypeSchema extends Partial<IJsonSchema>>() => {\n  const clipboard = useService<ClipboardService>(ClipboardService);\n\n  const [pasteData, setPasteData] = useState<PasteDataType<TypeSchema>>({\n    type: 'invalid',\n  });\n\n  const typeDefinition = useTypeDefinitionManager();\n\n  const pasteDataFormate = useMemo(\n    () =>\n      (data: string): PasteDataType<TypeSchema> => {\n        const parseData = typeEditorUtils.jsonParse(data);\n\n        if (!parseData || typeof parseData !== 'object') {\n          return {\n            type: 'invalid',\n          };\n        }\n\n        if (Array.isArray(parseData)) {\n          const isValid = parseData.every((item) => {\n            const config = typeDefinition.getTypeBySchema(item);\n            return !!config;\n          });\n\n          if (isValid) {\n            return {\n              type: 'multiple',\n              value: parseData,\n            };\n          } else {\n            return {\n              type: 'invalid',\n            };\n          }\n        } else {\n          const config = typeDefinition.getTypeBySchema(parseData);\n\n          if (config) {\n            return {\n              type: 'single',\n              value: parseData,\n            };\n          } else {\n            return {\n              type: 'invalid',\n            };\n          }\n        }\n      },\n    [typeDefinition]\n  );\n\n  useEffect(() => {\n    clipboard.readData().then(\n      (v) => {\n        if (v !== undefined) {\n          setPasteData(pasteDataFormate(v));\n        }\n      },\n      (error) => {\n        // console.log(error);\n      }\n    );\n\n    const dispose = clipboard.onClipboardChanged((newData) => {\n      setPasteData(pasteDataFormate(newData));\n    });\n\n    return () => {\n      dispose.dispose();\n    };\n  }, [pasteDataFormate]);\n\n  return {\n    pasteDataFormate,\n    pasteData,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/hooks/type-edit.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Toast } from '@douyinfe/semi-ui';\n\nimport { typeEditorUtils } from '../utils';\nimport { TypeEditorRowData } from '../../../types';\nimport { TypeEditorService, ClipboardService } from '../../../services';\nimport { useService, useTypeDefinitionManager } from '../../../contexts';\nimport { usePasteData } from './paste-data';\n\nexport const useAddType = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  onChange: () => void\n) => {\n  const typeEditor = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  const typeService = useTypeDefinitionManager();\n\n  const config = typeService.getTypeBySchema(rowData.self);\n\n  const handleAddType = useCallback(\n    (e: React.MouseEvent) => {\n      if (!config) {\n        return;\n      }\n      const currentSchema = rowData.self;\n      if (currentSchema) {\n        let index = -1;\n\n        const parent = config.getPropertiesParent?.(currentSchema);\n\n        if (!parent) {\n          return false;\n        }\n\n        if (!parent.properties) {\n          parent.properties = {};\n        }\n\n        Object.values(parent.properties).forEach((val) => {\n          if (!val.extra) {\n            val.extra = {\n              index: 0,\n            };\n          }\n\n          index = Math.max(index, val.extra?.index || 0);\n        });\n\n        const [key, schema] = typeEditorUtils.genNewTypeSchema(index + 1);\n\n        parent.properties[key] = schema as IJsonSchema;\n\n        const dataSource = typeEditor.getDataSource();\n\n        let addIndex = rowData.index + 1;\n        for (let i = rowData.index + 1, len = dataSource.length; i < len; i++) {\n          if (dataSource[i].level > rowData.level) {\n            addIndex++;\n          } else {\n            break;\n          }\n        }\n\n        const newPos = {\n          x: 0,\n          y: addIndex,\n        };\n        onChange();\n        typeEditor.setActivePos(newPos);\n      }\n\n      e.stopPropagation();\n      e.preventDefault();\n    },\n    [rowData, config, onChange]\n  );\n\n  return handleAddType;\n};\n\nexport const usePasteAddType = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  onChange: () => void,\n  onPaste?: (type?: TypeSchema) => TypeSchema | undefined\n) => {\n  const typeService = useTypeDefinitionManager();\n\n  const clipboard = useService<ClipboardService>(ClipboardService);\n\n  const { pasteDataFormate } = usePasteData<TypeSchema>();\n\n  const handlePasteAddType = useCallback(\n    (e: React.MouseEvent) => {\n      clipboard.readData().then((data) => {\n        const parseData = pasteDataFormate(data);\n        if (parseData.type === 'invalid') {\n          Toast.warning('Please paste the correct type schema!');\n          e.stopPropagation();\n          e.preventDefault();\n          return;\n        }\n        const currentSchema = rowData.self;\n        let index = -1;\n        const pasteDataItems =\n          parseData.type === 'single' ? [parseData.value] : [...parseData.value];\n        const config = typeService.getTypeBySchema(currentSchema);\n        const parent = config?.getPropertiesParent?.(currentSchema);\n        if (!parent) {\n          return;\n        }\n        if (!parent.properties) {\n          parent.properties = {};\n        }\n        Object.values(parent.properties).forEach((val) => {\n          if (!val.extra) {\n            val.extra = {\n              index: 0,\n            };\n          }\n          index = Math.max(index, val.extra.index || 0);\n        });\n        pasteDataItems.forEach((item) => {\n          let itemKey =\n            (item as { extra?: { key?: string } })?.extra?.key || typeEditorUtils.genEmptyKey();\n          // key !== rowData.key 处理重复对某个字段进行 paste\n          while (parent.properties![itemKey]) {\n            itemKey = `${itemKey}__copy`;\n          }\n          if (!(item as { extra?: { value?: unknown } })?.extra?.value) {\n            delete (item as { extra?: { value?: string } }).extra;\n          }\n          item.extra = { index: ++index };\n          if (onPaste) {\n            item = onPaste(item) || item;\n          }\n          parent.properties![itemKey] = item as IJsonSchema;\n        });\n        onChange();\n      });\n      e.stopPropagation();\n      e.preventDefault();\n    },\n\n    [rowData, onChange]\n  );\n\n  return handlePasteAddType;\n};\n\nexport const useRemoveType = <TypeSchema extends Partial<IJsonSchema>>(\n  rowData: TypeEditorRowData<TypeSchema>,\n  onChange: () => void\n) => {\n  const typeEditorService = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n\n  return useCallback(() => {\n    const { parent } = rowData;\n    if (!parent) {\n      return;\n    }\n    const { properties = {} } = parent;\n\n    delete properties[rowData.key];\n    typeEditorUtils.sortProperties(rowData.parent!);\n\n    if (typeEditorService.activePos.y === rowData.index) {\n      typeEditorService.clearActivePos();\n    }\n    typeEditorService.refreshErrorMsgAfterRemove(rowData.index);\n\n    onChange();\n  }, [onChange, rowData]);\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/indent.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\nimport { type CSSProperties, type FC } from 'react';\n\nexport const Indent: FC<{\n  count: number;\n  style?: CSSProperties;\n}> = ({ count, style = {} }) => <div style={{ height: '100%', width: count * 12, ...style }} />;\n\nexport const WidthIndent: FC<{\n  width: number;\n  style?: CSSProperties;\n}> = ({ width, style = {} }) => <div style={{ height: '100%', flexShrink: 0, width, ...style }} />;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useImperativeHandle, useState } from 'react';\n\nimport { noop } from 'lodash-es';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Space } from '@douyinfe/semi-ui';\n\nimport { fixedTSForwardRef } from './utils';\nimport { TypeEditorTable } from './type-editor';\nimport { TypeEditorMode, TypeEditorProp, TypeEditorRef } from './type';\nimport { ToolBar } from './tool-bar';\nexport * from './type';\nexport { columnConfigs as TypeEditorColumnConfigs } from './columns';\n\nconst TypeEditorContainer = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>(\n  props: TypeEditorProp<Mode, TypeSchema>,\n  ref: React.Ref<TypeEditorRef<Mode, TypeSchema>>\n) => {\n  const [instance, setInstance] = useState<TypeEditorRef<Mode, TypeSchema>>();\n\n  useImperativeHandle(ref, () => ({\n    getContainer: () => instance?.getContainer(),\n    setValue: instance?.setValue || noop,\n    getService: instance?.getService || (() => undefined),\n    undo: instance?.undo || noop,\n    redo: instance?.redo || noop,\n    getValue() {\n      return instance?.getValue();\n    },\n    getOperator() {\n      return instance?.getOperator();\n    },\n  }));\n\n  return (\n    <Space spacing={4} vertical>\n      {instance && <ToolBar {...props} editor={instance} />}\n      <TypeEditorTable\n        onInit={(editor) => {\n          setInstance(editor.current);\n        }}\n        {...props}\n      />\n    </Space>\n  );\n};\n\nexport const TypeEditor = fixedTSForwardRef(TypeEditorContainer);\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/mode/declare-assign.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { set, get } from 'lodash-es';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { typeEditorUtils } from '../utils';\nimport { type ModeValueConfig } from '../type';\nimport { TypeEditorColumnType, TypeEditorSchema } from '../../../types';\n\nconst traverseIJsonSchema = (\n  root: TypeEditorSchema<IJsonSchema> | undefined,\n  path: string[],\n  cb: (type: TypeEditorSchema<IJsonSchema>, path: string[]) => void\n): void => {\n  if (root) {\n    cb(root, path);\n\n    if (root.properties) {\n      Object.keys(root.properties).forEach((k) => {\n        traverseIJsonSchema(root.properties![k], [...path, k], cb);\n      });\n    }\n  }\n};\n\nexport const declareAssignConfig: ModeValueConfig<'declare-assign', IJsonSchema> = {\n  mode: 'declare-assign',\n  convertSchemaToValue: (val) => {\n    const data = {};\n    const newSchema = JSON.parse(JSON.stringify(val));\n\n    traverseIJsonSchema(newSchema, [], (type, path) => {\n      if (type.extra?.value !== undefined) {\n        set(data, path, type.extra?.value);\n      }\n      if (type.extra) {\n        delete type.extra;\n      }\n    });\n\n    return {\n      data,\n      definition: { schema: newSchema },\n    };\n  },\n  convertValueToSchema: (schema) => {\n    const { data } = schema;\n    const newSchema = JSON.parse(JSON.stringify(schema.definition.schema));\n    traverseIJsonSchema(newSchema, [], (type, path) => {\n      const value = get(data, path);\n      if (value !== undefined && type.type !== 'object') {\n        type.extra = { value };\n      }\n    });\n    return newSchema;\n  },\n  commonValueToSubmitValue: (val) => {\n    const type: IJsonSchema = val\n      ? typeEditorUtils.valueToTypeSchema(val)\n      : {\n          type: 'object',\n          properties: {},\n        };\n    return {\n      data: val || {},\n      definition: {\n        schema: type,\n      },\n    };\n  },\n\n  toolConfig: {\n    createByData: {\n      viewConfig: [\n        {\n          type: TypeEditorColumnType.Key,\n          visible: true,\n        },\n        {\n          type: TypeEditorColumnType.Type,\n          visible: true,\n        },\n        {\n          type: TypeEditorColumnType.Value,\n          visible: true,\n        },\n      ],\n      genDefaultValue: () => ({\n        data: {},\n        definition: {\n          schema: {\n            type: 'object',\n            properties: {},\n          },\n        },\n      }),\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/mode/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { type ModeValueConfig, type TypeEditorMode } from '../type';\nimport { typeDefinitionConfig } from './type-definition';\nimport { declareAssignConfig } from './declare-assign';\n\nexport const modeValueConfig: ModeValueConfig<TypeEditorMode, IJsonSchema>[] = [\n  declareAssignConfig,\n  typeDefinitionConfig,\n];\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/mode/type-definition.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { typeEditorUtils } from '../utils';\nimport { type ModeValueConfig } from '../type';\nimport { TypeEditorColumnType } from '../../../types';\n\nexport const typeDefinitionConfig: ModeValueConfig<'type-definition', IJsonSchema> = {\n  mode: 'type-definition',\n  convertSchemaToValue: (val) => val,\n  convertValueToSchema: (val) => val,\n  commonValueToSubmitValue: (val) => {\n    if (val) {\n      return typeEditorUtils.valueToTypeSchema(val);\n    }\n    return {\n      type: 'object',\n      properties: {},\n    };\n  },\n\n  toolConfig: {\n    createByData: {\n      viewConfig: [\n        {\n          type: TypeEditorColumnType.Key,\n          visible: true,\n        },\n        {\n          type: TypeEditorColumnType.Type,\n          visible: true,\n        },\n      ],\n      genDefaultValue() {\n        return {\n          type: 'object',\n          properties: {},\n        };\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const ErrorMsgContainer = styled.div`\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  transform: translateY(100%);\n  width: calc(100% - 6px);\n`;\n\nexport const EditCellContainer = styled.div<{\n  error?: boolean;\n  blink?: boolean;\n}>`\n  position: absolute;\n  width: 100%;\n  left: 0;\n  top: 0;\n  background-color: var(--semi-color-bg-0);\n  border: 1px solid var(--semi-color-focus-border);\n  height: 38px;\n  display: flex;\n  align-items: center;\n  transition: background-color 200ms;\n  box-sizing: border-box;\n  ${(props) => (props.error ? 'border: 1px solid var(--semi-color-danger);' : '')}\n  ${(props) => (props.blink ? 'background-color: rgba(238, 245, 40) !important;' : '')}\n`;\n\nexport const DragRowContainer = styled.tr<{\n  dragging?: boolean;\n}>`\n  opacity: ${(props) => (props.dragging ? 0.5 : 1)};\n`;\n\nexport const EditorTableHeader = styled.th`\n  border-right: 1px solid var(--semi-color-border);\n  border-bottom-width: 1px !important;\n  height: 37px;\n  box-sizing: border-box;\n  font-size: 12px;\n`;\n\nexport const EditorTableCell = styled.td`\n  border-right: 1px solid var(--semi-color-border) !important;\n  position: relative;\n\n  padding: 8px !important;\n  height: 36px;\n`;\n\nexport const EditorTable = styled.table`\n  min-width: 600px;\n  border-left-width: 1px;\n  border-top-width: 1px;\n  box-sizing: border-box;\n  border-color: var(--semi-color-border);\n  width: 100%;\n  position: relative;\n  border-style: solid;\n  border-bottom: none;\n  border-right: none;\n`;\n\nexport const EditorTableTitle = styled.div`\n  display: flex;\n  align-items: center;\n  cursor: pointer;\n`;\n\nexport const BaseIcon = styled.div`\n  width: 12px;\n  height: 12px;\n  flex-shrink: 0;\n`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/table.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable max-params */\n/* eslint-disable complexity */\n\nimport { HTML5Backend } from 'react-dnd-html5-backend';\nimport { DndProvider } from 'react-dnd';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport React from 'react';\n\nimport { isEqual } from 'lodash-es';\nimport classNames from 'classnames';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Space } from '@douyinfe/semi-ui';\n\nimport { TypeEditorRowData, TypeEditorColumnType } from '../../types';\nimport { TypeEditorOperationService, TypeEditorService } from '../../services';\nimport { TypeRegistryCreatorsAdapter, useService, useTypeDefinitionManager } from '../../contexts';\nimport { fixedTSForwardRef, typeEditorUtils } from './utils';\nimport { type TypeEditorMode, type TypeEditorProp, TypeEditorRef } from './type';\nimport { EditorTable } from './style';\nimport { useFormatter } from './hooks/formatter-value';\nimport { useActivePos } from './hooks';\nimport { Header } from './header';\nimport { DropTip } from './drop-tip';\nimport { ROOT_FIELD_ID } from './common';\nimport { EditCell } from './cell';\nimport { Body } from './body';\n\nconst TableInner = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>({\n  value,\n  onChange,\n  mode,\n  viewConfigs,\n  onInit,\n  forceUpdate,\n  onEditRowDataSource,\n  rootLevel = 0,\n  customEmptyNode,\n  tableClassName,\n  onError,\n  disableEditColumn,\n  readonly,\n  onPaste,\n  typeRegistryCreators,\n  onFieldChange,\n  getRootSchema,\n  onCustomSetValue,\n  extraConfig: originExtraConfig = {},\n}: TypeEditorProp<Mode, TypeSchema>) => {\n  const extraConfig = useMemo(\n    () => ({\n      ...originExtraConfig,\n    }),\n    [originExtraConfig]\n  );\n\n  const { deFormatter, formatter } = useFormatter<Mode, TypeSchema>({\n    mode,\n    extraConfig,\n  });\n\n  const typeSchema = useMemo(() => formatter(value), [formatter, value]);\n\n  const typeEditor = useService<TypeEditorService<TypeSchema>>(TypeEditorService);\n  const typeOperator = useService<TypeEditorOperationService<TypeSchema>>(\n    TypeEditorOperationService\n  );\n\n  const [tableDom, setTableDom] = useState<HTMLTableElement>();\n\n  const [initialSchema, setInitialSchema] = useState<TypeSchema>(() => {\n    const res = typeEditorUtils.clone(typeSchema) || typeEditorUtils.getInitialSchema<TypeSchema>();\n\n    typeOperator.storeState(res);\n\n    return res;\n  });\n\n  // const\n  const editor = useRef<TypeEditorRef<Mode, TypeSchema>>();\n\n  useEffect(() => {\n    // editor\n    const instance: TypeEditorRef<Mode, TypeSchema> = {\n      getService: () => typeEditor,\n      setValue(originNewVal) {\n        let newVal = originNewVal;\n\n        if (onCustomSetValue) {\n          newVal = onCustomSetValue(newVal);\n        }\n\n        const newSchema = formatter(newVal)!;\n\n        setInitialSchema(newSchema);\n\n        typeOperator.storeState(newSchema);\n\n        const final = typeEditorUtils.formateTypeSchema<TypeSchema>(newSchema, extraConfig);\n\n        const newValue = deFormatter(final)!;\n\n        if (onChange) {\n          onChange(newValue);\n        }\n      },\n      getContainer: () => tableDom,\n      getValue: () => {\n        const rootValue = deFormatter(typeEditor.rootTypeSchema);\n\n        return rootValue;\n      },\n      undo: () => {\n        typeOperator.undo();\n        typeEditor.onChange(typeOperator.getCurrentState(), {\n          storeState: false,\n        });\n      },\n      redo: () => {\n        typeOperator.redo();\n        typeEditor.onChange(typeOperator.getCurrentState(), {\n          storeState: false,\n        });\n      },\n      getOperator() {\n        return typeOperator;\n      },\n    };\n    editor.current = instance;\n  }, [typeEditor, onChange, tableDom, deFormatter, formatter, extraConfig, onCustomSetValue]);\n\n  useEffect(() => {\n    onInit?.(editor);\n  }, [onInit]);\n\n  /**\n   * 当前 rowData 的 children 是否不可见\n   * 定义为不可见的原因：默认可见\n   */\n  const [unOpenKeys, setUnOpenKeys] = useState<Record<string, boolean>>({});\n\n  useEffect(() => {\n    if (\n      !isEqual(typeSchema, initialSchema) &&\n      typeSchema &&\n      (forceUpdate || !typeEditorUtils.isTempState(initialSchema, extraConfig?.customValidateName))\n    ) {\n      setInitialSchema(typeEditorUtils.clone(typeSchema));\n    }\n  }, [typeSchema, extraConfig?.customValidateName]);\n\n  const typeService = useTypeDefinitionManager();\n\n  const displayColumn = useMemo(\n    () => viewConfigs.filter((v) => v.visible).map((v) => v.type),\n    [viewConfigs]\n  );\n\n  const { dataSource } = useMemo(() => {\n    const res: TypeEditorRowData<TypeSchema>[] = [];\n    let index = -1;\n\n    const dfs = (\n      schema: TypeSchema,\n      config: {\n        level: number;\n        parentId?: string;\n        key?: string;\n        parent?: TypeSchema;\n        canDefaultEditable?: true | string;\n        canValueEditable?: true | string;\n        path: string[];\n      }\n    ): void => {\n      const {\n        parentId,\n        level = -1,\n        key = ROOT_FIELD_ID,\n        path,\n        parent,\n        canValueEditable = true,\n        canDefaultEditable = true,\n      } = config;\n\n      const id = [parentId, key || new Date().valueOf().toString()].join('-');\n\n      const typeConfig = typeService.getTypeBySchema(schema);\n\n      const uid = parentId ? id : key;\n\n      const rowData: TypeEditorRowData<TypeSchema> = {\n        ...schema,\n        key,\n        index,\n        id: uid,\n        level,\n        self: schema,\n        parentId,\n        parent,\n        deepChildrenCount: 0,\n        isRequired: (parent?.required || []).includes(key),\n        childrenCount: 0,\n        disableEditColumn: [...(disableEditColumn || [])],\n        path,\n        extraConfig: { ...extraConfig },\n      };\n\n      index += 1;\n\n      typeEditor.dataSourceMap[rowData.id] = rowData;\n\n      res.push(rowData);\n\n      const customDefaultValidate =\n        extraConfig.customDefaultEditable && extraConfig.customDefaultEditable(rowData);\n\n      if (typeof customDefaultValidate === 'string') {\n        rowData.disableEditColumn!.push({\n          column: TypeEditorColumnType.Default,\n          reason: customDefaultValidate,\n        });\n      }\n\n      if (typeof canDefaultEditable === 'string' && level > rootLevel) {\n        rowData.disableEditColumn!.push({\n          column: TypeEditorColumnType.Default,\n          reason: canDefaultEditable,\n        });\n      }\n\n      if (typeof canValueEditable === 'string' && level > rootLevel) {\n        rowData.disableEditColumn!.push({\n          column: TypeEditorColumnType.Value,\n          reason: canValueEditable,\n        });\n      }\n\n      if (typeConfig) {\n        const children =\n          typeConfig.getTypeSchemaProperties && typeConfig.getTypeSchemaProperties(schema);\n\n        const childrenParent =\n          typeConfig.getPropertiesParent && typeConfig.getPropertiesParent(schema);\n        const childrenParentConfig = childrenParent && typeService.getTypeBySchema(childrenParent);\n\n        if (\n          !extraConfig.customDefaultEditable &&\n          typeof childrenParentConfig?.defaultEditable === 'string'\n        ) {\n          rowData.disableEditColumn!.push({\n            column: TypeEditorColumnType.Default,\n            reason: childrenParentConfig?.defaultEditable,\n          });\n        }\n\n        if (children) {\n          const originLen = res.length;\n          rowData.childrenCount = Object.keys(children).length;\n\n          // 如果不可见，不加子节点\n          if (unOpenKeys[uid]) {\n            return;\n          }\n\n          const parentPath = [...path, ...(typeConfig.getJsonPaths?.(schema) || [])];\n\n          let idx = 0;\n\n          const childCanValueEditable = typeConfig?.childrenValueEditable?.(schema);\n\n          Object.keys(children)\n            .map((k) => {\n              // 对不存在 index 的数据先进行修正，填上默认值\n              typeEditorUtils.fixFlowIndex(children[k], idx);\n              idx++;\n              return k;\n            })\n            .sort((k1, k2) => (children[k1].extra?.index || 0) - (children[k2].extra?.index || 0))\n            .forEach((k) => {\n              const canDefaultEditableFromParent = typeConfig.childrenDefaultEditable?.(schema);\n\n              dfs(children[k] as unknown as TypeSchema, {\n                key: k,\n                parentId: id,\n                parent: childrenParent as unknown as TypeSchema,\n                level: level + 1,\n                path: [...parentPath, k],\n                canDefaultEditable:\n                  typeof canDefaultEditableFromParent === 'string'\n                    ? canDefaultEditableFromParent\n                    : canDefaultEditable,\n                canValueEditable: childCanValueEditable,\n              });\n            });\n\n          rowData.deepChildrenCount = res.length - originLen;\n        } else {\n          if (\n            !extraConfig.customDefaultEditable &&\n            typeof typeConfig?.defaultEditable === 'string'\n          ) {\n            rowData.disableEditColumn!.push({\n              column: TypeEditorColumnType.Default,\n              reason: typeConfig?.defaultEditable,\n            });\n          }\n        }\n      }\n    };\n\n    if (initialSchema) {\n      dfs(initialSchema, { level: -1, path: [] });\n      // 不展示 root\n      res.shift();\n\n      const newData = onEditRowDataSource ? onEditRowDataSource(res) : res;\n\n      typeEditor.setDataSource(newData);\n\n      return {\n        dataSource: newData,\n      };\n    }\n\n    return {\n      dataSource: [],\n    };\n  }, [initialSchema, disableEditColumn, unOpenKeys, onEditRowDataSource]);\n\n  const activePos = useActivePos();\n\n  useEffect(() => {\n    typeEditor.rootTypeSchema = initialSchema;\n  }, [initialSchema]);\n\n  /**\n   * 修改单行数据\n   */\n  const handleTypeSchemaChange = useCallback(\n    (\n      type?: TypeSchema,\n      ctx: {\n        storeState?: boolean;\n      } = {}\n    ) => {\n      const { storeState = true } = ctx;\n\n      const newSchema = JSON.parse(JSON.stringify(type || initialSchema)) as TypeSchema;\n\n      setInitialSchema({ ...newSchema });\n\n      const final = typeEditorUtils.formateTypeSchema(newSchema, extraConfig);\n\n      if (storeState) {\n        typeOperator.storeState(newSchema);\n      }\n\n      const newValue = deFormatter(final)!;\n\n      if (onChange) {\n        onChange(newValue);\n      }\n    },\n    [initialSchema, deFormatter, extraConfig]\n  );\n\n  /**\n   * 添加新数据\n   */\n  const handleRowDataAdd = useCallback(\n    (id: string) => {\n      const rowData = typeEditor.dataSourceMap[id];\n\n      const currentSchema = rowData.self;\n\n      const config = typeService.getTypeBySchema(rowData.self);\n\n      if (currentSchema && config) {\n        let index = -1;\n        const parent = config.getPropertiesParent?.(currentSchema);\n        if (!parent) {\n          return;\n        }\n        if (!parent.properties) {\n          parent.properties = {};\n        }\n\n        Object.values(parent.properties).forEach((val) => {\n          if (!val.extra) {\n            val.extra = {\n              index: 0,\n            };\n          }\n\n          index = Math.max(index, val.extra.index || 0);\n        });\n\n        const [key, schema] = typeEditorUtils.genNewTypeSchema(index + 1);\n\n        parent.properties[key] = schema as IJsonSchema;\n\n        const newDataSource = typeEditor.getDataSource();\n\n        let addIndex = rowData.index + 1;\n        for (let i = rowData.index + 1, len = dataSource.length; i < len; i++) {\n          if (newDataSource[i].level > rowData.level) {\n            addIndex++;\n          } else {\n            break;\n          }\n        }\n\n        const newPos = {\n          x: 0,\n          y: addIndex,\n        };\n\n        typeEditor.setActivePos(newPos);\n\n        handleTypeSchemaChange();\n      }\n    },\n    [initialSchema, getRootSchema]\n  );\n\n  useEffect(() => {\n    typeEditor.columnViewConfig = viewConfigs.filter((v) => v.visible);\n    typeEditor.onChange = handleTypeSchemaChange;\n    typeEditor.onGlobalAdd = handleRowDataAdd;\n\n    typeEditor.typeRegistryCreators =\n      typeRegistryCreators as unknown as TypeRegistryCreatorsAdapter<TypeSchema>[];\n  }, [viewConfigs, handleTypeSchemaChange, typeRegistryCreators]);\n\n  /**\n   * 展开收起子字段\n   */\n  const handleChildrenVisibleChange = useCallback(\n    (rowDataId: string, newVal: boolean) => {\n      const newData = { ...unOpenKeys };\n\n      newData[rowDataId] = newVal;\n      setUnOpenKeys(newData);\n    },\n    [unOpenKeys]\n  );\n\n  return (\n    <>\n      <DndProvider backend={HTML5Backend}>\n        <Space vertical style={{ width: '100%' }}>\n          <div style={{ width: '100%' }}>\n            <EditorTable\n              ref={(dom) => setTableDom(dom as React.SetStateAction<HTMLTableElement | undefined>)}\n              className={classNames('semi-table', tableClassName)}\n            >\n              <Header\n                dataSourceMap={typeEditor.dataSourceMap}\n                value={initialSchema}\n                readonly={readonly}\n                onChange={handleTypeSchemaChange}\n                displayColumn={displayColumn}\n              />\n              <Body\n                viewConfigs={viewConfigs}\n                onFieldChange={onFieldChange}\n                onPaste={onPaste}\n                customEmptyNode={customEmptyNode}\n                readonly={readonly}\n                dataSourceMap={typeEditor.dataSourceMap}\n                unOpenKeys={unOpenKeys}\n                onError={onError}\n                onChildrenVisibleChange={handleChildrenVisibleChange}\n                onChange={handleTypeSchemaChange}\n                dataSource={dataSource}\n                displayColumn={displayColumn}\n              />\n            </EditorTable>\n          </div>\n        </Space>\n      </DndProvider>\n\n      {/** 当前编辑的单元格，单独渲染，减少表格的重复渲染 **/}\n      {activePos.x !== -1 && activePos.y !== -1 && tableDom && (\n        <EditCell\n          viewConfigs={viewConfigs}\n          unOpenKeys={unOpenKeys}\n          onPaste={onPaste}\n          onFieldChange={onFieldChange}\n          onError={onError}\n          onChildrenVisibleChange={handleChildrenVisibleChange}\n          onChange={handleTypeSchemaChange}\n          tableDom={tableDom}\n          dataSource={dataSource}\n          displayColumn={displayColumn}\n        />\n      )}\n      {/** 当前拖拽的提示辅助框 **/}\n      <DropTip dataSource={typeEditor.dataSourceMap} />\n    </>\n  );\n};\n\nexport const Table = fixedTSForwardRef(TableInner);\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/tool-bar.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Space, Tooltip } from '@douyinfe/semi-ui';\n\nimport {\n  ToolbarConfig,\n  ToolbarKey,\n  type TypeEditorMode,\n  type TypeEditorProp,\n  type TypeEditorRef,\n} from './type';\nimport { UndoRedo } from './tools/undo-redo';\nimport { CreateByData } from './tools/create-by-data';\n\nexport const ToolBar = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>({\n  mode,\n  editor,\n  toolbarConfig = [],\n}: TypeEditorProp<Mode, TypeSchema> & {\n  editor?: TypeEditorRef<Mode, TypeSchema>;\n}) => {\n  const config = useMemo(() => {\n    const res = new Map<string, ToolbarConfig>();\n\n    toolbarConfig.forEach((tool) => {\n      if (typeof tool === 'string') {\n        res.set(tool, { type: tool });\n      } else {\n        res.set(tool.type, tool);\n      }\n    });\n    return res;\n  }, [toolbarConfig]);\n\n  const importConfig = config.get(ToolbarKey.Import);\n\n  return (\n    <div style={{ width: '100%' }}>\n      {editor && (\n        <Space style={{ float: 'right' }}>\n          <>\n            {importConfig && (\n              <>\n                {importConfig.disabled ? (\n                  <Tooltip content={importConfig.disabled}>\n                    <CreateByData disabled mode={mode} editor={editor} />\n                  </Tooltip>\n                ) : (\n                  <CreateByData\n                    mode={mode}\n                    customInputRender={importConfig.customInputRender}\n                    editor={editor}\n                  />\n                )}\n              </>\n            )}\n          </>\n          {config.has(ToolbarKey.UndoRedo) && <UndoRedo editor={editor} />}\n        </Space>\n      )}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/tools/create-by-data.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { FC, useCallback, useMemo, useState } from 'react';\n\nimport { debounce } from 'lodash-es';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Button, Divider, Empty, Modal, Space, TextArea } from '@douyinfe/semi-ui';\nimport { IllustrationFailure, IllustrationFailureDark } from '@douyinfe/semi-illustrations';\n\nimport { typeEditorUtils } from '../utils';\nimport { TypeEditorTable } from '../type-editor';\nimport { type TypeEditorMode, type TypeEditorRef, type TypeEditorValue } from '../type';\nimport { modeValueConfig } from '../mode';\nimport { DataTransform } from './style';\n\nexport const CreateByData = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>({\n  mode,\n  disabled,\n  customInputRender: CustomInputRender,\n  editor,\n}: {\n  editor: TypeEditorRef<Mode, TypeSchema>;\n  mode: Mode;\n  disabled?: boolean;\n  customInputRender?: FC<{ value: string; onChange: (newVal: string) => void }>;\n}) => {\n  const modeConfig = useMemo(() => modeValueConfig.find((v) => v.mode === mode)!, [mode]);\n\n  const [visible, setVisible] = useState(false);\n\n  const [typeEditorValue, setTypeEditorValue] = useState<TypeEditorValue<Mode, TypeSchema>>(\n    modeConfig.toolConfig.createByData.genDefaultValue() as TypeEditorValue<Mode, TypeSchema>\n  );\n  const [errorEmpty, setErrorEmpty] = useState(false);\n\n  const handleClose = useCallback(() => {\n    setVisible(false);\n    setTypeEditorValue(\n      modeConfig.toolConfig.createByData.genDefaultValue() as TypeEditorValue<Mode, TypeSchema>\n    );\n  }, [modeConfig]);\n\n  const handleOk = useCallback(async () => {\n    if (editor) {\n      editor.setValue(typeEditorValue);\n      handleClose();\n    }\n  }, [handleClose, typeEditorValue, editor]);\n\n  const onChange = debounce((newVal: string) => {\n    const parsedValue = typeEditorUtils.jsonParse(newVal);\n    const error =\n      newVal && !(parsedValue && typeof parsedValue === 'object' && !Array.isArray(parsedValue));\n\n    if (!error) {\n      setTypeEditorValue(\n        modeConfig.commonValueToSubmitValue(parsedValue) as TypeEditorValue<Mode, TypeSchema>\n      );\n      setErrorEmpty(false);\n    } else if (parsedValue) {\n      setErrorEmpty(true);\n    }\n  }, 500);\n\n  return (\n    <div>\n      <Button disabled={disabled} size=\"small\" onClick={() => setVisible(true)}>\n        Import from JSON\n      </Button>\n      <Modal\n        okText={`Import`}\n        width={960}\n        okButtonProps={{}}\n        cancelText=\"Cancel\"\n        title=\"Import from JSON\"\n        visible={visible}\n        onOk={handleOk}\n        onCancel={handleClose}\n      >\n        <DataTransform>\n          <div style={{ height: '100%', flex: 1 }}>\n            {CustomInputRender ? (\n              <CustomInputRender value=\"{}\" onChange={onChange} />\n            ) : (\n              <TextArea defaultValue=\"{}\" onChange={onChange} />\n            )}\n          </div>\n          <Divider layout=\"vertical\" style={{ height: '100%' }} />\n          <Space align=\"start\" vertical style={{ height: '100%', flex: 1 }}>\n            <div style={{ height: '100%', flex: 1, overflowY: 'scroll' }}>\n              <TypeEditorTable\n                readonly\n                mode={mode}\n                forceUpdate\n                // TODO:\n                // tableClassName={s['tool-table']}\n                value={errorEmpty ? undefined : typeEditorValue}\n                customEmptyNode={\n                  errorEmpty ? (\n                    <Empty\n                      style={{ marginTop: 40 }}\n                      image={<IllustrationFailure style={{ width: 100, height: 100 }} />}\n                      darkModeImage={\n                        <IllustrationFailureDark style={{ width: 100, height: 100 }} />\n                      }\n                      description=\"Invalid value\"\n                    />\n                  ) : undefined\n                }\n                viewConfigs={modeConfig.toolConfig.createByData.viewConfig}\n              />\n            </div>\n          </Space>\n        </DataTransform>\n      </Modal>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/tools/index.module.less",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n.tool-table {\n  min-width: 450px;\n}\n\n.data-transform-container {\n  height: 406px;\n  display: flex;\n  gap: 4px;\n}\n\n.full-height {\n  height: 100%;\n}\n\n.full-fill {\n  flex: 1;\n}\n\n\n.type-editor-table-container {\n  overflow-y: scroll;\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/tools/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const DataTransform = styled.div`\n  height: 406px;\n  display: flex;\n  gap: 4px;\n`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/tools/undo-redo.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import { ShortcutsService, useIDEService } from '@flow-ide/client';\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Button, Tooltip } from '@douyinfe/semi-ui';\nimport { IconRedo, IconUndo } from '@douyinfe/semi-icons';\n// import { TypeEditorCommand, useMonitorData } from '@api-builder/base';\n\nimport { type TypeEditorMode, type TypeEditorRef } from '../type';\nimport { useMonitorData } from '../../../utils';\n\nexport const UndoRedo = <Mode extends TypeEditorMode, TypeSchema extends Partial<IJsonSchema>>({\n  editor,\n}: {\n  editor: TypeEditorRef<Mode, TypeSchema>;\n}) => {\n  const { data: canUndo } = useMonitorData(editor.getOperator()?.canUndo);\n  const { data: canRedo } = useMonitorData(editor.getOperator()?.canRedo);\n\n  return (\n    <>\n      <Tooltip content=\"Undo\">\n        <Button\n          disabled={!canUndo}\n          icon={<IconUndo />}\n          size=\"small\"\n          onClick={() => {\n            editor?.undo();\n          }}\n        />\n      </Tooltip>\n      <Tooltip content=\"Redo\">\n        <Button\n          size=\"small\"\n          icon={<IconRedo />}\n          disabled={!canRedo}\n          onClick={() => {\n            editor?.redo();\n          }}\n        />\n      </Tooltip>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/type-editor.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport styled from 'styled-components';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorProvider } from '../../contexts';\nimport { type TypeEditorMode, type TypeEditorProp } from './type';\nimport { Table } from './table';\nimport { TypeEditorListener } from './hooks';\n\nconst Container = styled.div`\n  position: relative;\n\n  display: flex;\n  flex-direction: column;\n  gap: 4px;\n\n  outline: none;\n\n  table {\n    table-layout: fixed;\n  }\n`;\n\nexport const TypeEditorTable = <\n  Mode extends TypeEditorMode,\n  TypeSchema extends Partial<IJsonSchema>\n>(\n  props: TypeEditorProp<Mode, TypeSchema>\n) => (\n  <TypeEditorProvider typeRegistryCreators={props.typeRegistryCreators}>\n    <TypeEditorListener configs={props.viewConfigs}>\n      <Container>\n        <Table {...props} />\n      </Container>\n    </TypeEditorListener>\n  </TypeEditorProvider>\n);\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\nimport { FC } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport {\n  type TypeEditorSpecialConfig,\n  type TypeEditorColumnViewConfig,\n  type TypeEditorRowData,\n  type TypeChangeContext,\n  type TypeEditorColumnType,\n  type TypeEditorSchema,\n  TypeEditorColumnConfig,\n} from '../../types';\nimport { TypeEditorOperationService, type TypeEditorService } from '../../services';\nimport { TypeRegistryCreatorsAdapter } from '../../contexts';\n\nexport type TypeEditorMode = 'type-definition' | 'declare-assign';\n\nexport interface DeclareAssignValueType<TypeSchema extends Partial<IJsonSchema>> {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  data: any;\n  definition: {\n    schema: TypeSchema;\n  };\n}\n\nexport type TypeEditorValue<\n  Mode extends TypeEditorMode,\n  TypeSchema extends Partial<IJsonSchema>\n> = Mode extends 'type-definition'\n  ? TypeEditorSchema<TypeSchema>\n  : DeclareAssignValueType<TypeSchema>;\n\nexport enum ToolbarKey {\n  Import = 'Import',\n  UndoRedo = 'UndoRedo',\n}\nexport type ToolbarConfig = {\n  /**\n   * 工具栏配置\n   */\n  type: ToolbarKey;\n  /**\n   * 是否禁用\n   */\n  disabled?: string;\n\n  /**\n   * 自定义输入渲染，仅 Import 使用\n   */\n  customInputRender?: FC<{\n    value: string;\n    onChange: (newVal: string) => void;\n  }>;\n};\n\nexport interface TypeEditorProp<\n  Mode extends TypeEditorMode,\n  TypeSchema extends Partial<IJsonSchema>\n> {\n  /**\n   * 菜单栏配置\n   */\n  toolbarConfig?: (ToolbarKey | ToolbarConfig)[];\n  /**\n   * type editor 模式类型\n   */\n  mode: Mode;\n\n  /**\n   * 只读态\n   */\n  readonly?: boolean;\n  /**\n   *\n   */\n  tableClassName?: string;\n\n  /**\n   *  各个 cell 的特化配置\n   */\n  extraConfig?: TypeEditorSpecialConfig<TypeSchema>;\n  /**\n   * 根节点层级\n   */\n  rootLevel?: number;\n\n  /**\n   * 获取全局 add 的 root schema\n   */\n  getRootSchema?: (schema: TypeSchema) => TypeSchema;\n  /**\n   *\n   */\n  typeRegistryCreators?: TypeRegistryCreatorsAdapter<IJsonSchema>[];\n\n  /**\n   * 每个列的配置\n   */\n  viewConfigs: (TypeEditorColumnViewConfig & {\n    config?: Partial<Omit<TypeEditorColumnConfig<TypeSchema>, 'type'>>;\n  })[];\n\n  /**\n   * 每次设置 DataSource 前调用，最后修改值的钩子\n   */\n  onEditRowDataSource?: (data: TypeEditorRowData<TypeSchema>[]) => TypeEditorRowData<TypeSchema>[];\n  /**\n   * 忽略报错强制更新\n   */\n  forceUpdate?: boolean;\n\n  /**\n   * onError\n   */\n  onError?: (msg?: string[]) => void;\n  /**\n   * value\n   */\n  value?: TypeEditorValue<Mode, TypeSchema>;\n  /**\n   * onChange\n   */\n  onChange?: (newValue: TypeEditorValue<Mode, TypeSchema>) => void;\n  /**\n   * onPaste\n   */\n  onPaste?: (typeSchema?: TypeSchema) => TypeSchema | undefined;\n\n  /**\n   * onInit\n   */\n  onInit?: (editor: React.MutableRefObject<TypeEditorRef<Mode, TypeSchema> | undefined>) => void;\n\n  /**\n   * 当具体某个 field change\n   */\n  onFieldChange?: (ctx: TypeChangeContext) => void;\n  /**\n   * 当执行 setValue\n   */\n  onCustomSetValue?: (\n    newValue: TypeEditorValue<Mode, TypeSchema>\n  ) => TypeEditorValue<Mode, TypeSchema>;\n\n  /**\n   * 自定义空状态\n   */\n  customEmptyNode?: React.ReactElement;\n\n  /**\n   * 不能编辑的列\n   * 和 TypeSchema 中 editable 的关系\n   * editable 为 false，会将 disableEditColumn 每个 column 都填上\n   */\n  disableEditColumn?: Array<{ column: TypeEditorColumnType; reason: string }>;\n}\n\nexport interface TypeEditorRef<\n  Mode extends TypeEditorMode,\n  TypeSchema extends Partial<IJsonSchema>\n> {\n  setValue: (newVal: TypeEditorValue<Mode, TypeSchema>) => void;\n  getValue: () => TypeEditorValue<Mode, TypeSchema> | undefined;\n  undo: () => void;\n  redo: () => void;\n  getService: () => TypeEditorService<TypeSchema> | undefined;\n  getOperator: () => TypeEditorOperationService<TypeSchema> | undefined;\n  getContainer: () => HTMLDivElement | undefined;\n}\n\nexport interface ModeValueConfig<\n  Mode extends TypeEditorMode,\n  TypeSchema extends Partial<IJsonSchema>\n> {\n  mode: Mode;\n  /**\n   * 提交值到 typeSchema\n   */\n  convertValueToSchema: (val: TypeEditorValue<Mode, TypeSchema>) => TypeSchema;\n  /**\n   * typeSchema 到提交值\n   */\n  convertSchemaToValue: (val: TypeSchema) => TypeEditorValue<Mode, TypeSchema>;\n  /**\n   * 常量值生成提交值\n   */\n  commonValueToSubmitValue: (\n    val: Record<string, unknown> | undefined\n  ) => TypeEditorValue<Mode, TypeSchema>;\n\n  toolConfig: {\n    createByData: {\n      viewConfig: TypeEditorColumnViewConfig[];\n      genDefaultValue: () => TypeEditorValue<Mode, TypeSchema>;\n    };\n  };\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-editor/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { forwardRef } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorSpecialConfig } from '../../types';\nimport { disableFixIndexFormatter, emptyKeyFormatter } from './formatter';\nimport { SUFFIX, COMPONENT_ID_PREFIX } from './common';\n\nconst genNewTypeSchema = <TypeSchema extends Partial<IJsonSchema>>(\n  index: number\n): [string, TypeSchema] => {\n  const newKey = genEmptyKey();\n\n  return [\n    newKey,\n    {\n      type: 'string',\n      extra: {\n        index,\n      },\n    } as TypeSchema,\n  ];\n};\n\nconst traverseIJsonSchema = <TypeSchema extends Partial<IJsonSchema>>(\n  root: TypeSchema | undefined,\n  cb: (type: TypeSchema) => void\n): void => {\n  if (root) {\n    cb(root);\n\n    if (root.items) {\n      traverseIJsonSchema(root.items as TypeSchema, cb);\n    }\n    if (root.additionalProperties) {\n      traverseIJsonSchema(root.additionalProperties as TypeSchema, cb);\n    }\n\n    if (root.properties) {\n      Object.values(root.properties).forEach((v) => {\n        traverseIJsonSchema(v as TypeSchema, cb);\n      });\n    }\n  }\n};\n\nexport const jsonParse = (jsonString?: string) => {\n  try {\n    return JSON.parse(jsonString || '');\n  } catch (error) {\n    // todo 兼容错误形态JSON\n    return undefined;\n  }\n};\n\nconst sortProperties = <TypeSchema extends Partial<IJsonSchema>>(typeSchema: TypeSchema) => {\n  const { properties = {} } = typeSchema;\n  const originKeys = Object.keys(properties);\n\n  const sortKeys = originKeys.sort(\n    (a, b) => (properties[a].extra?.index || 0) - (properties[b].extra?.index || 0)\n  );\n\n  for (let i = 0; i < sortKeys.length; i++) {\n    const key = sortKeys[i];\n\n    fixFlowIndex(properties[key]);\n\n    properties[key].extra!.index = i;\n  }\n};\n\nconst fixFlowIndex = <TypeSchema extends Partial<IJsonSchema>>(type: TypeSchema, idx = 0): void => {\n  if (!type) {\n    return;\n  }\n\n  if (!type.extra) {\n    type.extra = {};\n  }\n\n  if (type.extra.index === undefined) {\n    type.extra.index = idx;\n  }\n};\n\nconst getInitialSchema = <TypeSchema extends Partial<IJsonSchema>>(): TypeSchema => {\n  const res: IJsonSchema = {\n    type: 'object',\n    properties: {},\n  };\n  return res as TypeSchema;\n};\n\nconst clone = <T>(val: T): T => (val ? JSON.parse(JSON.stringify(val)) : val);\n\nconst isTempState = <TypeSchema extends Partial<IJsonSchema>>(\n  type: TypeSchema,\n  customValidateName?: (value: string) => string\n): boolean => {\n  let error = false;\n\n  traverseIJsonSchema(type, (c) => {\n    if (c.properties) {\n      Object.keys(c.properties).forEach((key) => {\n        const res = isEmptyKey(key) || customValidateName?.(key);\n        if (res) {\n          error = true;\n        }\n      });\n    }\n  });\n\n  return error;\n};\n\nconst genEmptyKey = () => SUFFIX + nanoid();\n\nconst isEmptyKey = (key: string) => key.startsWith(SUFFIX);\n\nconst formateKey = (key: string) => (isEmptyKey(key) ? '' : key);\n\nconst deFormateKey = (key: string, originKey?: string) => (!key ? originKey || genEmptyKey() : key);\n\nconst formateTypeSchema = <TypeSchema extends Partial<IJsonSchema>>(\n  typeSchema: TypeSchema,\n  config: TypeEditorSpecialConfig<TypeSchema>\n): TypeSchema => {\n  const newSchema = JSON.parse(JSON.stringify(typeSchema));\n\n  const formatters = [emptyKeyFormatter];\n  if (config.disableFixIndex) {\n    formatters.push(disableFixIndexFormatter);\n  }\n\n  traverseIJsonSchema(newSchema, (type) => {\n    formatters.forEach((formatter) => {\n      formatter(type);\n    });\n  });\n\n  return newSchema;\n};\n\nconst valueToTypeSchema = <TypeSchema extends Partial<IJsonSchema>>(value: unknown): TypeSchema => {\n  // return\n\n  switch (typeof value) {\n    case 'string': {\n      return {\n        type: 'string',\n      } as TypeSchema;\n    }\n    case 'bigint':\n    case 'number': {\n      return {\n        type: 'number',\n      } as TypeSchema;\n    }\n    case 'boolean': {\n      return {\n        type: 'boolean',\n      } as TypeSchema;\n    }\n    case 'object': {\n      if (value) {\n        if (Array.isArray(value)) {\n          return {\n            type: 'array',\n            items: valueToTypeSchema(value[0]),\n          } as TypeSchema;\n        } else {\n          const object: IJsonSchema = {\n            type: 'object',\n            properties: {},\n          };\n          Object.keys(value).forEach((k) => {\n            object.properties![k] = valueToTypeSchema((value as any)[k]);\n          });\n          return object as TypeSchema;\n        }\n      }\n      break;\n    }\n    default:\n      break;\n  }\n  return { type: 'string' } as TypeSchema;\n};\n\nexport const typeEditorUtils = {\n  genNewTypeSchema,\n  sortProperties,\n  traverseIJsonSchema,\n  fixFlowIndex,\n  genEmptyKey,\n  jsonParse,\n  clone,\n  formateTypeSchema,\n  isTempState,\n  valueToTypeSchema,\n  deFormateKey,\n  formateKey,\n  getInitialSchema,\n};\n\nexport function fixedTSForwardRef<T, P = object>(\n  render: (props: P, ref: React.Ref<T>) => JSX.Element\n): (props: P & React.RefAttributes<T>) => JSX.Element {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  return forwardRef(render as any) as any;\n}\n\nexport const getComponentId = (id: string): string => `${COMPONENT_ID_PREFIX}-${id}`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/cascader-v2/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Popover } from '@douyinfe/semi-ui';\n\nimport { TypeSelectorRef, type Props } from '../type';\nimport { FlowSchemaInitCtx } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport { TypeSearchPanel } from './type-search';\nimport { TypeCascader } from './type-cascader';\nimport { Trigger } from './trigger';\nimport { CascaderContainer, CustomCascaderContainer } from './style';\n\nexport const CascaderV2 = (\n  props: Props<IJsonSchema> & {\n    onInit?: (typeEditorRef: TypeSelectorRef) => void;\n  }\n) => {\n  const {\n    triggerRender,\n    disabled,\n    defaultOpen,\n    onInit,\n    value,\n    onDropdownVisibleChange,\n    getPopupContainer = () => document.body,\n  } = props;\n\n  const triggerRef = useRef<HTMLDivElement>(null);\n\n  const typeSelectorRef = useRef<TypeSelectorRef>(null);\n\n  const [searchValue, setSearchValue] = useState('');\n\n  const typeService = useTypeDefinitionManager();\n\n  useEffect(() => {\n    if (typeSelectorRef.current) {\n      onInit?.(typeSelectorRef.current);\n    }\n  }, [typeSelectorRef.current, onInit]);\n\n  const [rePosKey, setReposKey] = useState(0);\n  const [context, setContext] = useState<FlowSchemaInitCtx>(\n    (value && typeService.getTypeBySchema(value)?.typeCascaderConfig?.generateInitCtx?.(value)) ||\n      {}\n  );\n\n  const [visible, setVisible] = useState(defaultOpen);\n\n  if (disabled) {\n    return (\n      <>\n        {triggerRender ? (\n          <CustomCascaderContainer>{triggerRender()}</CustomCascaderContainer>\n        ) : (\n          <CascaderContainer>\n            <Trigger\n              typeSelectorRef={typeSelectorRef}\n              triggerRef={triggerRef}\n              searchValue={searchValue}\n              onSearchChange={setSearchValue}\n              ctx={context}\n              {...props}\n            />\n          </CascaderContainer>\n        )}\n      </>\n    );\n  }\n\n  return (\n    <>\n      <Popover\n        rePosKey={rePosKey}\n        visible={visible}\n        onVisibleChange={(v) => {\n          setVisible(v);\n          if (onDropdownVisibleChange) {\n            onDropdownVisibleChange(v);\n          }\n          if (v) {\n            setReposKey((key) => key + 1);\n          }\n        }}\n        autoAdjustOverflow\n        trigger=\"click\"\n        position=\"bottomLeft\"\n        getPopupContainer={getPopupContainer}\n        content={\n          !searchValue ? (\n            <TypeCascader\n              ref={typeSelectorRef}\n              onContextChange={setContext}\n              onRePos={() => setReposKey((pre) => pre + 1)}\n              {...props}\n            />\n          ) : (\n            <TypeSearchPanel\n              onSearchChange={setSearchValue}\n              ref={typeSelectorRef}\n              triggerRef={triggerRef}\n              query={searchValue}\n              {...props}\n            />\n          )\n        }\n      >\n        {triggerRender ? (\n          <CustomCascaderContainer>{triggerRender()}</CustomCascaderContainer>\n        ) : (\n          <CascaderContainer>\n            <Trigger\n              triggerRef={triggerRef}\n              searchValue={searchValue}\n              onSearchChange={setSearchValue}\n              typeSelectorRef={typeSelectorRef}\n              ctx={context}\n              {...props}\n            />\n          </CascaderContainer>\n        )}\n      </Popover>\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/cascader-v2/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled, { createGlobalStyle } from 'styled-components';\nimport { Typography } from '@douyinfe/semi-ui';\n\nexport const StyledFullContainer = styled.div`\n  width: 100%;\n  height: 100%;\n`;\n\nexport const CustomCascaderContainer = styled.span`\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 100%;\n  height: 100%;\n`;\n\nexport const CascaderContainer = styled(StyledFullContainer)`\n  position: relative;\n`;\n\nexport const TriggerText = styled.div`\n  display: flex;\n  width: 100%;\n`;\n\nexport const CascaderDropdown = styled.div`\n  display: flex;\n  width: 100%;\n  height: 100%;\n`;\n\nexport const TriggerGlobalStyle = createGlobalStyle`\n  .semi-cascader-selection {\n    font-size: 12px !important;\n\n    svg {\n      width: 12px;\n      height: 12px;\n    }\n  }\n`;\n\nexport const CascaderOptionItem = styled.li<{ focus?: boolean }>`\n  ${(props) => (props.focus ? 'background-color: var(--semi-color-fill-0)' : '')}\n`;\n\nexport const DropdownGlobalStyle = createGlobalStyle`\n  .semi-cascader-option-lists {\n    max-width: 510px;\n    overflow-x: auto;\n    height: auto;\n\n\n    .semi-cascader-option-list {\n    width: 150px;\n    flex-shrink: 0;\n    border-left: none;\n    border-right: 1px solid var(--semi-color-fill-0);\n\n    max-height: 50vh;\n\n    ::-webkit-scrollbar {\n      width: 0;\n      height: 0;\n    }\n\n    .semi-cascader-option {\n      font-size: 12px !important;\n      width: 100%;\n      padding: 6px 12px;\n      cursor: pointer;\n      box-sizing: border-box;\n\n      svg {\n        width: 12px;\n        height: 12px;\n      }\n    }\n  }\n\n\n    .semi-cascader-option-disabled {\n      cursor: not-allowed;\n    }\n\n  }\n\n  .semi-cascader-option-icon {\n    margin-right:8px;\n  }\n\n  .semi-cascader-option-icon-empty {\n    margin-right: 0;\n  }\n\n`;\n\nexport const StyledSearchList = styled.ul`\n  &::-webkit-scrollbar {\n    display: none;\n  }\n  width: 100%;\n  height: 100%;\n`;\n\nexport const TypeSearchText = styled(Typography.Text)`\n  padding: 8px 12px;\n`;\n\nexport const TextGlobalStyle = createGlobalStyle`\n  .semi-typography {\n    color: unset !important;\n  }\n\n`;\nexport const SearchText = styled.span<{\n  disabled?: boolean;\n}>`\n  display: inline-flex;\n  align-items: center;\n  width: 100%;\n  gap: 8px;\n  cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};\n  ${(props) => (props.disabled ? 'color: var(--semi-color-disabled-text);' : '')};\n`;\n\nexport const SearchIcon = styled.span`\n  display: inline-flex;\n  align-items: center;\n\n  svg {\n    width: 12px;\n    height: 12px;\n  }\n`;\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/cascader-v2/trigger.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo, useState } from 'react';\n\nimport classNames from 'classnames';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Input } from '@douyinfe/semi-ui';\nimport { IconChevronDown } from '@douyinfe/semi-icons';\n\nimport { TypeSelectorRef, type Props } from '../type';\nimport { useTypeSelectorHotKey } from '../hooks/hot-key';\nimport { FlowSchemaInitCtx } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport { TriggerGlobalStyle, StyledFullContainer, TriggerText } from './style';\n\nexport const Trigger = ({\n  value: originValue,\n  ctx,\n  typeSelectorRef,\n  triggerRef,\n  searchValue,\n  onSearchChange,\n}: Props<IJsonSchema> & {\n  ctx: FlowSchemaInitCtx;\n  searchValue: string;\n  triggerRef: React.RefObject<HTMLDivElement | null>;\n  typeSelectorRef: React.MutableRefObject<TypeSelectorRef | null>;\n  onSearchChange: (query: string) => void;\n}) => {\n  const typeService = useTypeDefinitionManager();\n\n  const value = useMemo(() => {\n    if (!originValue) {\n      return;\n    }\n    const type = typeService.getTypeBySchema(originValue);\n\n    return type?.getStringValueByTypeSchema?.(originValue) || '';\n  }, [originValue]);\n\n  const [focus, setFocus] = useState(false);\n\n  const hotkeys = useTypeSelectorHotKey(typeSelectorRef);\n\n  const reverseLabel = useMemo(() => {\n    if (!value || !originValue) {\n      return;\n    }\n\n    const def = typeService.getTypeBySchema(originValue);\n\n    return def ? def.getDisplayLabel(originValue) : <>{value}</>;\n  }, [originValue, value]);\n\n  return (\n    <StyledFullContainer\n      ref={triggerRef as React.RefObject<HTMLDivElement>}\n      className={classNames(\n        'semi-cascader semi-cascader-focus semi-cascader-single semi-cascader-filterable'\n      )}\n    >\n      <TriggerGlobalStyle />\n      <div className=\"semi-cascader-selection\">\n        <div className=\"semi-cascader-search-wrapper\">\n          {!searchValue && (\n            <TriggerText\n              style={{\n                opacity: focus ? 0.5 : 1,\n              }}\n              className={classNames('semi-cascader-selection-text-inactive')}\n            >\n              <>{reverseLabel}</>\n            </TriggerText>\n          )}\n\n          <div className=\"semi-input-wrapper semi-input-wrapper-focus semi-input-wrapper-default\">\n            <Input\n              autoFocus\n              onFocus={() => {\n                setFocus(true);\n              }}\n              onKeyDown={(e) => {\n                const hotKey = hotkeys.find((item) => item.matcher(e));\n                hotKey?.callback();\n                if (hotKey?.preventDefault) {\n                  e.preventDefault();\n                }\n              }}\n              value={searchValue}\n              onChange={(newVal) => {\n                onSearchChange(newVal);\n              }}\n              className=\"semi-cascader-input\"\n              onBlur={() => {\n                setFocus(false);\n              }}\n            />\n          </div>\n        </div>\n      </div>\n      <div className=\"semi-cascader-arrow\">\n        <IconChevronDown />\n      </div>\n    </StyledFullContainer>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/cascader-v2/type-cascader.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { type FC, forwardRef, useCallback, useMemo, useState } from 'react';\n\nimport classNames from 'classnames';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Tooltip } from '@douyinfe/semi-ui';\nimport { IconCheckboxTick, IconChevronRight } from '@douyinfe/semi-icons';\n\nimport { typeSelectorUtils } from '../utils';\nimport { TypeSelectorRef, type CascaderOption, type Props } from '../type';\nimport { useTypeTransform } from '../hooks/option-value';\nimport { useFocusItemCascader } from '../hooks/focus-item';\nimport { useCascaderRootTypes } from '../hooks';\nimport { FlowSchemaInitCtx, TypeEditorRegistry } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport { CascaderDropdown, CascaderOptionItem, DropdownGlobalStyle } from './style';\ninterface OptionListProps {\n  options: Array<CascaderOption>;\n\n  /**\n   * 蓝色背景，用于父类型展开/收起\n   */\n  activeType?: string;\n  /**\n   * 蓝色字体，用于表示选中的值\n   */\n  selectValue?: string;\n  /**\n   * 灰色背景，当前 focus 的类型\n   */\n  focusValue?: string;\n  onCollapse?: (val: string) => void;\n  onSelect?: (\n    value: string,\n    source?: Parameters<Required<Props<IJsonSchema>>['onChange']>[1]['source']\n  ) => void;\n}\n\nconst OptionList: FC<OptionListProps> = ({\n  options,\n  activeType,\n  selectValue,\n  onCollapse,\n  onSelect,\n  focusValue,\n}) => (\n  <ul className=\"semi-cascader-option-list\">\n    {options.map((opt) => {\n      const child = (\n        <CascaderOptionItem\n          focus={focusValue === opt.value}\n          onClick={\n            !opt.disabled\n              ? () =>\n                  opt.isLeaf\n                    ? onSelect?.(\n                        opt.value,\n                        opt.source as Parameters<\n                          Required<Props<IJsonSchema>>['onChange']\n                        >[1]['source']\n                      )\n                    : onCollapse?.(opt.type)\n              : undefined\n          }\n          key={opt.value}\n          className={classNames(\n            'semi-cascader-option',\n            activeType === opt.type && 'semi-cascader-option-active',\n            selectValue === opt.value && 'semi-cascader-option-select',\n\n            opt.disabled && 'semi-cascader-option-disabled'\n          )}\n        >\n          <span className=\"semi-cascader-option-label\">\n            {selectValue === opt.value ? (\n              <IconCheckboxTick\n                className={classNames('semi-cascader-option-icon')}\n                style={{\n                  color: selectValue === opt.value ? 'var(--semi-color-primary)' : undefined,\n                }}\n              />\n            ) : (\n              <span className=\"semi-cascader-option-icon semi-cascader-option-icon-empty\" />\n            )}\n            {typeof opt.label === 'string' ? <span>{opt.label}</span> : opt.label}\n          </span>\n          {!opt.isLeaf && <IconChevronRight />}\n        </CascaderOptionItem>\n      );\n\n      return opt.disabled ? <Tooltip content={opt.disabled}>{child}</Tooltip> : child;\n    })}\n  </ul>\n);\n\nconst useGenerateCascaderTypes = () => {\n  const typeService = useTypeDefinitionManager();\n\n  const generateCascaderTypes = useCallback(\n    (value?: string) => {\n      const res: string[] = [];\n\n      const types = value?.split('-') || [];\n      const arr = types.splice(0, types.length - 1) || [];\n\n      while (arr.length > 0) {\n        const type = arr.shift();\n        if (type) {\n          const config = type ? typeService.getTypeByName(type) : undefined;\n\n          if (config?.customChildOptionValue) {\n            const extras = config.customChildOptionValue();\n            arr.splice(0, extras.length);\n          }\n          res.push(type);\n        }\n      }\n\n      return res;\n    },\n    [typeService]\n  );\n\n  return generateCascaderTypes;\n};\n\nconst generateCustomPanelType = (value?: string) => (value?.split('-') || []).pop() || '';\n\n// eslint-disable-next-line react/display-name\nexport const TypeCascader = forwardRef<\n  TypeSelectorRef,\n  Props<IJsonSchema> & {\n    onContextChange: (ctx: FlowSchemaInitCtx) => void;\n    onRePos: () => void;\n  }\n>(({ value: originValue, onChange, onContextChange, onRePos, disableTypes = [] }, ref) => {\n  const typeService = useTypeDefinitionManager();\n\n  const {\n    convertOptionValueToModeValue,\n    convertValueToOptionValue,\n\n    getModeOptionChildrenType,\n  } = useTypeTransform();\n\n  const customDisableType = useMemo(() => {\n    const map = new Map<string, string>();\n\n    disableTypes.forEach((v) => {\n      map.set(v.type, v.reason);\n    });\n    return map;\n  }, [disableTypes]);\n\n  const rootTypes = useCascaderRootTypes(customDisableType);\n\n  const generateCascaderTypes = useGenerateCascaderTypes();\n\n  const value = useMemo(() => convertValueToOptionValue(originValue), [originValue]);\n\n  const [cascaderTypes, setCascaderLTypes] = useState<string[]>(generateCascaderTypes(value));\n\n  const [customPanelType, setCustomPanelType] = useState(generateCustomPanelType(value));\n\n  const handleChange = useCallback(\n    (\n      newOptionValue: string,\n      source: Parameters<Required<Props<IJsonSchema>>['onChange']>[1]['source'] = 'type-selector'\n    ) => {\n      if (newOptionValue === value) {\n        setCascaderLTypes(generateCascaderTypes(newOptionValue));\n        setCustomPanelType(generateCustomPanelType(newOptionValue));\n        onRePos();\n\n        return;\n      }\n      const newValue = convertOptionValueToModeValue(newOptionValue);\n\n      if (onChange) {\n        onChange(newValue as IJsonSchema, {\n          source,\n        });\n      }\n\n      setCascaderLTypes(generateCascaderTypes(newOptionValue));\n\n      setCustomPanelType(generateCustomPanelType(newOptionValue));\n\n      const newCtx =\n        (newValue &&\n          typeService.getTypeBySchema(newValue)?.typeCascaderConfig?.generateInitCtx?.(newValue)) ||\n        {};\n\n      onContextChange(newCtx);\n    },\n    [onChange, onContextChange, value]\n  );\n\n  const renderData = useMemo(\n    () =>\n      cascaderTypes.map((item, level) => {\n        const parentDef = typeService.getTypeByName(item);\n\n        const childrenType = getModeOptionChildrenType(\n          parentDef as TypeEditorRegistry<IJsonSchema>,\n          {\n            parentType: item,\n            level: level + 1,\n            parentTypes: [...cascaderTypes].splice(0, level),\n          }\n        );\n\n        const prefix = [...cascaderTypes]\n          .splice(0, level + 1)\n          .map((type) => {\n            const config = typeService.getTypeByName(type);\n            if (config?.customChildOptionValue) {\n              return [type, config.customChildOptionValue()];\n            }\n            return type;\n          })\n          .flat()\n          .join('-');\n\n        const options: CascaderOption[] = childrenType\n          .map((child) => {\n            const def = typeService.getTypeByName(child.type);\n            if (def) {\n              return typeSelectorUtils.definitionToCascaderOption({\n                config: def,\n                customDisableType,\n                prefix,\n                // parentConfig: parentDef,\n                level: level + 1,\n                disabled: child.disabled,\n                parentType: item,\n                parentTypes: [...cascaderTypes].splice(0, level + 1),\n              });\n            }\n          })\n          .filter(Boolean) as CascaderOption[];\n\n        return {\n          item,\n          options,\n        };\n      }),\n    [cascaderTypes, customDisableType]\n  );\n\n  const handleCollapse = useCallback(\n    (type: string, level: number) => {\n      const newCascaderTypes = [...cascaderTypes];\n\n      if (cascaderTypes[level] !== type) {\n        newCascaderTypes.splice(level);\n        newCascaderTypes.push(type);\n      } else {\n        newCascaderTypes.splice(level);\n      }\n      setCustomPanelType('');\n      onRePos();\n      setCascaderLTypes(newCascaderTypes);\n    },\n    [cascaderTypes]\n  );\n\n  const { focusValue } = useFocusItemCascader({\n    rootTypes,\n    onCollapse: handleCollapse,\n    renderData,\n    cascaderTypes,\n    onChange: (v) => handleChange(v, 'type-selector'),\n    ref,\n  });\n\n  return (\n    <CascaderDropdown>\n      <DropdownGlobalStyle />\n      <div className=\"semi-cascader-option-lists\">\n        <OptionList\n          activeType={cascaderTypes[0]}\n          selectValue={value}\n          options={rootTypes}\n          focusValue={focusValue}\n          onSelect={handleChange}\n          onCollapse={(type) => handleCollapse(type, 0)}\n        />\n\n        {renderData.map(({ item, options }, level) => (\n          <OptionList\n            onSelect={handleChange}\n            key={item + level}\n            focusValue={focusValue}\n            activeType={cascaderTypes[level + 1]}\n            selectValue={value}\n            options={options}\n            onCollapse={(type) => handleCollapse(type, level + 1)}\n          />\n        ))}\n\n        {(originValue &&\n          typeService.getTypeByName(customPanelType)?.typeCascaderConfig?.customCascaderPanel?.({\n            typeSchema: originValue,\n            onChange: ((newVal: IJsonSchema) => {\n              onChange?.(newVal, {\n                source: 'custom-panel',\n              });\n              const newCtx =\n                typeService\n                  .getTypeBySchema(newVal)\n                  ?.typeCascaderConfig?.generateInitCtx?.(newVal) || {};\n\n              onContextChange(newCtx);\n            }) as (typeSchema: Partial<IJsonSchema>) => void,\n          })) ||\n          null}\n      </div>\n    </CascaderDropdown>\n  );\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/cascader-v2/type-search.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, {\n  type FC,\n  forwardRef,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from 'react';\n\nimport classNames from 'classnames';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Typography } from '@douyinfe/semi-ui';\n\nimport { type SearchResultItem, type Props, TypeSelectorRef } from '../type';\nimport { useHighlightKeyword } from '../hooks/use-hight-light';\nimport { useTypeTransform } from '../hooks/option-value';\nimport { useFocusItemSearch } from '../hooks/focus-item';\nimport { TypeEditorRegistry } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\nimport { SearchIcon, SearchText, StyledSearchList, TextGlobalStyle, TypeSearchText } from './style';\n\nconst defaultDeep = 2;\n\ninterface CacheItem {\n  /** array-array-string **/\n  key: string;\n  type: string;\n  disabled?: string;\n}\n\nconst SearchItem: FC<{\n  item: SearchResultItem;\n  query: string;\n  focusValue: string;\n  selectValue: string;\n  onSelect: (item: SearchResultItem, value: IJsonSchema) => void;\n}> = ({ item, query, focusValue, onSelect, selectValue }) => {\n  const typeService = useTypeDefinitionManager();\n\n  const { convertOptionValueToModeValue } = useTypeTransform();\n\n  const config = typeService.getTypeByName(item.type);\n\n  const typeSchema = useMemo(\n    () => convertOptionValueToModeValue(item.value) as IJsonSchema,\n    [item.value]\n  );\n\n  const text = useMemo(() => config?.getDisplayText(typeSchema), [typeSchema, config]);\n\n  const label = useHighlightKeyword(\n    text || '',\n    query,\n    {\n      color: 'var(--semi-color-primary)',\n      fontWeight: 600,\n    },\n    item.value\n  );\n\n  return (\n    <li\n      key={item.value}\n      onClick={() => (item.disabled ? undefined : onSelect(item, typeSchema))}\n      style={{\n        minWidth: 'unset',\n        background: focusValue === item.value ? 'var(--semi-color-fill-0)' : undefined,\n      }}\n      className={classNames(\n        'semi-cascader-option',\n        item.disabled && 'semi-cascader-option-disabled',\n        selectValue === item.value && 'semi-cascader-option-select'\n      )}\n    >\n      <TextGlobalStyle />\n      <SearchText disabled={!!item.disabled}>\n        <SearchIcon>{config?.getDisplayIcon?.(typeSchema)}</SearchIcon>\n        <Typography.Text ellipsis={{ showTooltip: true }}>\n          <span>{label}</span>\n        </Typography.Text>\n      </SearchText>\n    </li>\n  );\n};\n// eslint-disable-next-line react/display-name\nexport const TypeSearchPanel = forwardRef<\n  TypeSelectorRef,\n  Props<IJsonSchema> & {\n    query: string;\n    triggerRef: React.RefObject<HTMLDivElement | null>;\n    onSearchChange: (query: string) => void;\n  }\n>(({ query, disableTypes = [], onChange, triggerRef, value, onSearchChange }, ref) => {\n  const typeService = useTypeDefinitionManager();\n  const {\n    convertValueToOptionValue,\n    checkHasChildren,\n    convertOptionValueToModeValue,\n    getModeOptionChildrenType,\n  } = useTypeTransform();\n\n  const customDisableType = useMemo(() => {\n    const map = new Map<string, string>();\n\n    disableTypes.forEach((v) => {\n      map.set(v.type, v.reason);\n    });\n    return map;\n  }, [disableTypes]);\n\n  const selectValue = useMemo(() => convertValueToOptionValue(value), [value]);\n\n  const [searchResult, setSearchResult] = useState<SearchResultItem[]>([]);\n\n  const levelCache = useRef<CacheItem[][]>([]);\n\n  const handleGenLevelInfo = useCallback(\n    (level: number) => {\n      for (let i = 0; i < level; i++) {\n        if (levelCache.current[i]) {\n          continue;\n        }\n\n        if (i === 0) {\n          const newRootTypes = typeService.getTypeRegistriesWithParentType();\n          levelCache.current[i] = newRootTypes.map((type) => ({\n            key: type.type,\n            type: type.type,\n            parentLabels: [],\n            disabled: customDisableType.get(type.type),\n          }));\n        } else {\n          const lastLevelCache = levelCache.current[i - 1];\n          const newCacheTypes: CacheItem[] = [];\n          levelCache.current[i] = newCacheTypes;\n          lastLevelCache.forEach((type) => {\n            // disabled 的就不用下钻了\n            if (type.disabled) {\n              return;\n            }\n            const config = typeService.getTypeByName(type.type);\n\n            const parentTypes = type.key.split('-');\n            if (\n              config &&\n              checkHasChildren(config as TypeEditorRegistry<IJsonSchema>, {\n                level: i,\n              })\n            ) {\n              const childrenTypes = getModeOptionChildrenType(\n                config as TypeEditorRegistry<IJsonSchema>,\n                {\n                  parentType: type.type,\n                  level: i,\n                  parentTypes,\n                }\n              );\n\n              childrenTypes.forEach((child) => {\n                const childConfig = typeService.getTypeByName(child.type);\n\n                newCacheTypes.push({\n                  type: child.type,\n                  key: [...parentTypes, child.type].join('-'),\n                  disabled:\n                    child.disabled ||\n                    (childConfig?.customDisabled\n                      ? childConfig.customDisabled({\n                          level: i + 1,\n                          parentType: type.type,\n                          parentTypes,\n                        })\n                      : undefined),\n                });\n              });\n            }\n          });\n        }\n      }\n    },\n    [customDisableType]\n  );\n\n  const handleGenSearchResult = useCallback(() => {\n    const len = levelCache.current.length;\n    if (!query) {\n      setSearchResult([]);\n      return;\n    }\n\n    const newSearchResult: SearchResultItem[] = [];\n    for (let i = 0; i < len; i++) {\n      const cacheTypes = levelCache.current[i] || [];\n\n      cacheTypes.forEach((type) => {\n        const config = typeService.getTypeByName(type.type);\n\n        if (config && config.label.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) > -1) {\n          newSearchResult.push({\n            value: type.key,\n            icon: config.icon,\n            disabled: type.disabled,\n            level: i,\n            type: type.type,\n          });\n        }\n      });\n    }\n\n    setSearchResult(newSearchResult);\n  }, [query]);\n\n  useEffect(() => {\n    handleGenLevelInfo(defaultDeep);\n  }, [handleGenLevelInfo]);\n\n  useEffect(() => {\n    handleGenSearchResult();\n  }, [query]);\n\n  const viewValue = useMemo(\n    () =>\n      searchResult.filter((item) => {\n        const config = typeService.getTypeByName(item.type);\n        return (\n          config &&\n          !checkHasChildren(config as TypeEditorRegistry<IJsonSchema>, {\n            level: item.level,\n          })\n        );\n      }),\n    [searchResult, typeService]\n  );\n\n  const { focusValue } = useFocusItemSearch({\n    ref,\n    viewValue,\n    onChange: (v) => {\n      const newVal = convertOptionValueToModeValue(v);\n      onChange?.(newVal as IJsonSchema, { source: 'custom-panel' });\n      onSearchChange('');\n    },\n  });\n\n  return (\n    <div\n      className={classNames('semi-cascader-option-lists')}\n      style={triggerRef.current ? { width: Math.max(triggerRef.current.clientWidth, 150) } : {}}\n    >\n      <StyledSearchList className={classNames('semi-cascader-option-list')}>\n        <>\n          {viewValue.length === 0 ? (\n            <TypeSearchText type=\"secondary\">No results.</TypeSearchText>\n          ) : (\n            <>\n              {viewValue.map((item) => (\n                <SearchItem\n                  selectValue={selectValue}\n                  key={item.value}\n                  query={query}\n                  focusValue={focusValue}\n                  item={item}\n                  onSelect={(_, v) => onChange?.(v, { source: 'type-selector' })}\n                />\n              ))}\n            </>\n          )}\n        </>\n      </StyledSearchList>\n    </div>\n  );\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/focus-item.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useImperativeHandle, useMemo, useState } from 'react';\n\nimport { noop } from 'lodash-es';\n\nimport { TypeSelectorRef, type CascaderOption, type SearchResultItem } from '../type';\n\ninterface Pos {\n  x: number;\n  y: number;\n}\n\nconst validatePos = (pos: Pos): boolean => pos.x >= 0 && pos.y >= 0;\n\nexport const useFocusItemCascader = ({\n  rootTypes,\n  renderData,\n  cascaderTypes,\n  ref,\n  onCollapse,\n  onChange,\n}: {\n  rootTypes: CascaderOption[];\n  cascaderTypes: string[];\n  onChange: (newVal: string) => void;\n  renderData: { item: string; options: CascaderOption[] }[];\n  ref: React.ForwardedRef<TypeSelectorRef>;\n  onCollapse: (type: string, level: number) => void;\n}) => {\n  const [focusPos, setFocusPos] = useState<Pos>({ x: -1, y: -1 });\n\n  const allOptions = useMemo(\n    () => [rootTypes, ...renderData.map((item) => item.options)],\n    [rootTypes, renderData]\n  );\n\n  const initPos = useCallback(() => {\n    setFocusPos({\n      x: 0,\n      y: 0,\n    });\n  }, []);\n\n  const focusItem = useMemo(() => allOptions[focusPos.x]?.[focusPos.y], [focusPos, allOptions]);\n\n  const focusValue = useMemo(\n    () => allOptions[focusPos.x]?.[focusPos.y]?.value,\n    [focusPos, allOptions]\n  );\n\n  useImperativeHandle(\n    ref,\n    (): TypeSelectorRef => ({\n      initFocusItem() {\n        initPos();\n      },\n      clearFocusItem() {\n        setFocusPos({\n          x: -1,\n          y: -1,\n        });\n      },\n      moveFocusItemUp() {\n        if (!validatePos(focusPos)) {\n          initPos();\n          return;\n        }\n        const { x, y } = focusPos;\n\n        if (!allOptions[x]?.[y]) {\n          return;\n        }\n\n        // 向上查找，直到找到一个 disabled not 的 item\n        let newY = (y - 1 + allOptions[x].length) % allOptions[x].length;\n        let item = allOptions[x]?.[newY];\n\n        while (item?.disabled && newY !== y) {\n          newY = (newY - 1 + allOptions[x].length) % allOptions[x].length;\n          item = allOptions[x]?.[newY];\n        }\n\n        if (newY !== y) {\n          setFocusPos({\n            x,\n            y: newY,\n          });\n        }\n      },\n      moveFocusItemDown() {\n        if (!validatePos(focusPos)) {\n          initPos();\n          return;\n        }\n\n        const { x, y } = focusPos;\n\n        if (!allOptions[x]?.[y]) {\n          return;\n        }\n\n        // 向上查找，直到找到一个 disabled not 的 item\n        let newY = (y + 1) % allOptions[x].length;\n        let item = allOptions[x]?.[newY];\n\n        while (item?.disabled && newY !== y) {\n          newY = (newY + 1) % allOptions[x].length;\n          item = allOptions[x]?.[newY];\n        }\n\n        if (newY !== y) {\n          setFocusPos({\n            x,\n            y: newY,\n          });\n        }\n      },\n      moveFocusItemLeft() {\n        if (!validatePos(focusPos)) {\n          initPos();\n          return;\n        }\n\n        const childrenPanelLen = cascaderTypes.length;\n\n        if (childrenPanelLen > 0) {\n          const lastParentType = cascaderTypes[childrenPanelLen - 1];\n\n          const newY = allOptions[childrenPanelLen - 1]?.findIndex(\n            (v) => v.type === lastParentType\n          );\n\n          onCollapse(lastParentType, childrenPanelLen - 1);\n\n          setFocusPos({\n            x: focusPos.x - 1,\n            y: newY || 0,\n          });\n        }\n      },\n      moveFocusItemRight() {\n        if (!validatePos(focusPos)) {\n          initPos();\n          return;\n        }\n\n        if (focusItem && !focusItem.isLeaf && cascaderTypes[focusPos.x] !== focusItem.type) {\n          onCollapse(focusItem.type, focusPos.x);\n          setFocusPos({\n            x: focusPos.x + 1,\n            y: 0,\n          });\n        }\n      },\n\n      selectFocusItem() {\n        if (!validatePos(focusPos)) {\n          return;\n        }\n        if (focusItem?.isLeaf) {\n          onChange(focusValue);\n        }\n      },\n    })\n  );\n\n  return {\n    focusValue,\n  };\n};\nexport const useFocusItemSearch = ({\n  ref,\n  onChange,\n  viewValue,\n}: {\n  viewValue: SearchResultItem[];\n  onChange: (newVal: string) => void;\n\n  ref: React.ForwardedRef<TypeSelectorRef>;\n}) => {\n  const [focusPos, setFocusPos] = useState(-1);\n\n  const focusValue = useMemo(() => viewValue[focusPos]?.value, [viewValue, focusPos]);\n\n  const focusItem = useMemo(() => viewValue[focusPos], [viewValue, focusPos]);\n\n  useImperativeHandle(ref, () => ({\n    selectFocusItem() {\n      if (!focusItem.disabled) {\n        onChange(focusValue);\n      }\n    },\n    moveFocusItemDown() {\n      if (focusPos < 0) {\n        setFocusPos(0);\n        return;\n      }\n\n      let newPos = (focusPos + 1) % viewValue.length;\n\n      while (viewValue[newPos]?.disabled && newPos !== focusPos) {\n        newPos = (newPos + 1) % viewValue.length;\n      }\n\n      setFocusPos(newPos);\n    },\n    moveFocusItemLeft: noop,\n    moveFocusItemRight: noop,\n    moveFocusItemUp() {\n      if (focusPos < 0) {\n        setFocusPos(0);\n        return;\n      }\n\n      let newPos = (focusPos - 1 + viewValue.length) % viewValue.length;\n\n      while (viewValue[newPos]?.disabled && newPos !== focusPos) {\n        newPos = (newPos - 1 + viewValue.length) % viewValue.length;\n      }\n\n      setFocusPos(newPos);\n    },\n    initFocusItem() {\n      setFocusPos(0);\n    },\n    clearFocusItem() {\n      setFocusPos(-1);\n    },\n  }));\n\n  return {\n    focusValue,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/hot-key.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { TypeSelectorRef } from '../type';\n\ninterface HotKeyConfig {\n  matcher: (event: React.KeyboardEvent) => boolean;\n  callback: () => void;\n  preventDefault?: boolean;\n}\n\nexport const useTypeSelectorHotKey = (selector: React.MutableRefObject<TypeSelectorRef | null>) => {\n  const hotKeyConfig: HotKeyConfig[] = useMemo(() => {\n    const res: HotKeyConfig[] = [\n      {\n        matcher: (e) => e.key === 'Enter',\n        callback: () => {\n          selector.current?.selectFocusItem();\n        },\n      },\n      {\n        matcher: (e) => e.key === 'ArrowUp',\n        callback: () => {\n          selector.current?.moveFocusItemUp();\n        },\n        preventDefault: true,\n      },\n\n      {\n        matcher: (e) => e.key === 'ArrowLeft',\n        callback: () => {\n          selector.current?.moveFocusItemLeft();\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => e.key === 'ArrowRight',\n        callback: () => {\n          selector.current?.moveFocusItemRight();\n        },\n        preventDefault: true,\n      },\n      {\n        matcher: (e) => e.key === 'ArrowDown',\n        callback: () => {\n          selector.current?.moveFocusItemDown();\n        },\n        preventDefault: true,\n      },\n    ];\n\n    return res;\n  }, []);\n\n  return hotKeyConfig;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useCascaderRootTypes } from './root-types';\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/option-value.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorRegistry } from '../../../types';\nimport { useTypeDefinitionManager } from '../../../contexts';\n\nexport const useTypeTransform = () => {\n  const typeService = useTypeDefinitionManager();\n\n  /**\n   * 根据提交值获取选项值\n   * 正常和 OptionValue 一致，不用特殊转化\n   * 但是在 IJsonSchema 的场景，这两个值会不同\n   * 级联场景也会不一样，因为 级联场景会 type-type\n   */\n  const convertValueToOptionValue = useMemo(\n    () => (originValue: IJsonSchema | undefined) => {\n      const type = originValue && typeService.getTypeBySchema(originValue);\n\n      return (originValue && type?.getStringValueByTypeSchema?.(originValue)) || '';\n    },\n    [typeService]\n  );\n\n  /**\n   * 根据选项值获取提交值\n   * 正常和 OptionValue 一致，不用特殊转化\n   * 但是在 IJsonSchema 的场景，这两个值会不同\n   * 级联场景也会不一样，因为 级联场景会 type-type\n   */\n  const convertOptionValueToModeValue = useMemo(\n    () => (optionValue: string | undefined) => {\n      const [root, ...rest] = (optionValue || '').split('-');\n\n      const rooType = typeService.getTypeByName(root);\n\n      if (rooType?.getTypeSchemaByStringValue) {\n        return rooType.getTypeSchemaByStringValue(rest.join('-'));\n      }\n\n      return rooType?.getDefaultSchema();\n    },\n    [typeService]\n  );\n\n  /**\n   * 判断是否有子类型\n   */\n  const checkHasChildren = useMemo(\n    () =>\n      (\n        typeDef: TypeEditorRegistry<IJsonSchema>,\n        ctx: {\n          level: number;\n        }\n      ): boolean =>\n        (typeDef?.getSupportedItemTypes && typeDef.getSupportedItemTypes(ctx).length !== 0) ||\n        !!typeDef.container,\n    [typeService]\n  );\n\n  const getModeOptionChildrenType = useMemo(\n    () =>\n      (\n        typeDef: TypeEditorRegistry<IJsonSchema> | undefined,\n        ctx: {\n          parentType: string;\n          level: number;\n          parentTypes?: string[];\n        }\n      ) => {\n        const getSupportType = (parentType = ''): TypeEditorRegistry<IJsonSchema>[] =>\n          typeService.getTypeRegistriesWithParentType(\n            parentType\n          ) as TypeEditorRegistry<IJsonSchema>[];\n\n        const support = new Set(getSupportType(ctx.parentType).map((v) => v.type));\n        return (\n          (typeDef?.getSupportedItemTypes && typeDef.getSupportedItemTypes(ctx)) ||\n          []\n        ).filter((v) => support.has(v.type));\n      },\n    [typeService]\n  );\n\n  return {\n    convertOptionValueToModeValue,\n    convertValueToOptionValue,\n    checkHasChildren,\n    getModeOptionChildrenType,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/root-types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { typeSelectorUtils } from '../utils';\nimport { type CascaderOption } from '../type';\nimport { useTypeDefinitionManager } from '../../../contexts';\n\nconst genId = (options: CascaderOption[]): string =>\n  options.map((v) => `${v.disabled}${v.label}`).join('-');\n\nexport const useCascaderRootTypes = (customDisableType: Map<string, string>) => {\n  const [rootTypes, setRootTypes] = useState<CascaderOption[]>([]);\n  const typeService = useTypeDefinitionManager();\n\n  useEffect(() => {\n    const init = () => {\n      const newRootTypes = typeService.getTypeRegistriesWithParentType().map((config) => {\n        const res = typeSelectorUtils.definitionToCascaderOption({\n          customDisableType,\n          config,\n          level: 0,\n          parentTypes: [],\n        });\n        return res;\n      });\n\n      if (genId(newRootTypes) !== genId(rootTypes)) {\n        setRootTypes(newRootTypes);\n      }\n    };\n    init();\n    const dispose = typeService.onTypeRegistryChange(init);\n    return () => {\n      dispose.dispose();\n    };\n  }, [rootTypes, customDisableType]);\n\n  return rootTypes;\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/hooks/use-hight-light.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { type CSSProperties } from 'react';\n/**\n * 搜索结果高亮\n */\nexport const useHighlightKeyword = (\n  label: string,\n  keyword?: string,\n  hightLightStyle: CSSProperties = {},\n  id?: string\n  // color = 'rgba(var(--semi-orange-5), 1)',\n): (string | React.JSX.Element)[] => {\n  if (label && keyword) {\n    return label.split(new RegExp(`(${keyword})`, 'gi')).map((c, i) =>\n      c.toLocaleLowerCase() === keyword.toLocaleLowerCase() ? (\n        <span\n          key={c + i + id}\n          style={{\n            color: 'rgba(var(--semi-orange-5), 1)',\n            ...hightLightStyle,\n          }}\n        >\n          {c}\n        </span>\n      ) : (\n        <span key={c + i + id}>{c}</span>\n      )\n    );\n  }\n  return [label];\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useState } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorProvider } from '../../contexts';\nimport { type Props } from './type';\nimport { CascaderV2 } from './cascader-v2';\n\nexport const TypeSelector = <TypeSchema extends Partial<IJsonSchema>>(props: Props<TypeSchema>) => {\n  const [init, setInit] = useState(false);\n  return (\n    <TypeEditorProvider\n      typeRegistryCreators={props.typeRegistryCreators}\n      onInit={() => setInit(true)}\n    >\n      {init ? <CascaderV2 {...(props as unknown as Props<IJsonSchema>)} /> : <></>}\n    </TypeEditorProvider>\n  );\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport {\n  CascaderProps,\n  type CascaderData as OriginCascaderData,\n} from '@douyinfe/semi-ui/lib/es/cascader';\n\nimport { DisableTypeInfo } from '../../types';\nimport { TypeRegistryCreatorsAdapter } from '../../contexts';\n\nexport interface Props<TypeSchema extends Partial<IJsonSchema>>\n  extends Omit<CascaderProps, 'value' | 'onChange' | 'triggerRender'> {\n  /**\n   *\n   */\n  value?: TypeSchema;\n\n  /**\n   * 禁用类型\n   */\n  disableTypes?: Array<DisableTypeInfo>;\n\n  onChange?: (\n    val: TypeSchema | undefined,\n    ctx: {\n      source: 'type-selector' | 'custom-panel';\n    }\n  ) => void;\n  triggerRender?: () => JSX.Element;\n  /**\n   *\n   */\n  typeRegistryCreators?: TypeRegistryCreatorsAdapter<TypeSchema>[];\n}\n\nexport type CascaderData = OriginCascaderData & {\n  originType: string;\n  text: string;\n  extra: {\n    label: string;\n    icon: React.JSX.Element;\n  };\n};\n\nexport interface CascaderOption {\n  label: string | JSX.Element;\n  value: string;\n  type: string;\n  disabled?: string;\n  source?: string;\n  isLeaf?: boolean;\n}\n\nexport interface SearchResultItem {\n  value: string;\n  type: string;\n  icon: JSX.Element;\n  level: number;\n  disabled?: string;\n}\n\nexport interface TypeSelectorRef {\n  /**\n   * 清除 item\n   */\n  clearFocusItem: () => void;\n  /**\n   * 初始化 item\n   */\n  initFocusItem: () => void;\n  /**\n   * 将当前激活的 item 向上移动\n   */\n  moveFocusItemUp: () => void;\n  /**\n   * 将当前激活的 item 向下移动\n   */\n  moveFocusItemDown: () => void;\n  /**\n   * 将当前激活的 item 向左移动，并关闭子项\n   */\n  moveFocusItemLeft: () => void;\n  /**\n   * 将当前激活的 item 向右移动，并展开子项\n   */\n  moveFocusItemRight: () => void;\n  /**\n   * 选择当前激活的 item\n   */\n  selectFocusItem: () => void;\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/components/type-selector/utils/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable max-params */\n\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { type CascaderOption } from '../type';\nimport { TypeEditorRegistry } from '../../../types';\n\n// import s from '../index.module.less';\n\nconst definitionToCascaderOption = <TypeSchema extends Partial<IJsonSchema>>({\n  config,\n  customDisableType = new Map(),\n  prefix,\n  parentTypes,\n  disabled,\n  parentType,\n  level,\n}: {\n  level: number;\n  config: TypeEditorRegistry<TypeSchema>;\n  parentType?: string;\n  customDisableType?: Map<string, string>;\n  parentTypes: string[];\n  prefix?: string;\n  disabled?: string;\n}): CascaderOption => {\n  const typeValue = config.type;\n\n  const optionValue = prefix ? [prefix, typeValue].join('-') : typeValue;\n\n  const label = (\n    <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>\n      {config.icon}\n      <span>{config.label}</span>\n    </div>\n  );\n\n  const customDisabled =\n    config.customDisabled && config.customDisabled({ level, parentType: parentType!, parentTypes });\n\n  const reason = disabled || customDisableType.get(typeValue) || customDisabled;\n\n  return {\n    disabled: reason,\n    value: optionValue,\n    label,\n    type: typeValue,\n    source: config.typeCascaderConfig?.unClosePanelAfterSelect ? 'custom-panel' : 'type-selector',\n    isLeaf: reason\n      ? true\n      : !(\n          (config.getSupportedItemTypes && config.getSupportedItemTypes({ level }).length !== 0) ||\n          config.container\n        ),\n  };\n};\n\nexport const typeSelectorUtils = {\n  definitionToCascaderOption,\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/contexts/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { createContext, useContext, useEffect, useMemo } from 'react';\n\nimport { Container, interfaces } from 'inversify';\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\n\nimport {\n  getTypeDefinitionAdapter,\n  ITypeDefinitionAdapter,\n  registryFormatter,\n} from '../utils/registry-adapter';\nimport { TypeEditorRegistry } from '../types';\nimport { defaultTypeRegistryCreators } from '../type-registry';\nimport { TypeEditorRegistryManager } from '../services/type-registry-manager';\nimport {\n  ClipboardService,\n  ShortcutsService,\n  TypeEditorOperationService,\n  TypeEditorService,\n} from '../services';\n\nexport type TypeRegistryCreatorsAdapter<TypeSchema extends Partial<IJsonSchema>> = (\n  param: Parameters<JsonSchemaTypeRegistryCreator<TypeSchema, TypeEditorRegistry<TypeSchema>>>[0] &\n    ITypeDefinitionAdapter<TypeSchema>\n) => ReturnType<JsonSchemaTypeRegistryCreator<TypeSchema, TypeEditorRegistry<TypeSchema>>>;\n\nexport const TypeEditorContext = createContext<{\n  /**\n   * @deprecated\n   */\n  typeRegistryCreators?: TypeRegistryCreatorsAdapter<IJsonSchema>[];\n}>({});\n\ninterface Context {\n  container: Container;\n}\n\nconst TypeContext = createContext<Context>({\n  container: new Container(),\n});\n\nexport function useService<T>(identifier: interfaces.ServiceIdentifier): T {\n  const container = useContext(TypeContext).container;\n\n  return container.get(identifier) as T;\n}\n\nexport const TypeEditorProvider = <TypeSchema extends Partial<IJsonSchema>>({\n  children,\n  typeRegistryCreators = [],\n  onInit,\n}: Parameters<\n  React.FunctionComponent<{\n    children: JSX.Element;\n    onInit?: () => void;\n    typeRegistryCreators?: TypeRegistryCreatorsAdapter<TypeSchema>[];\n  }>\n>[0]) => {\n  const container = useMemo(() => {\n    const res = new Container();\n\n    res.bind(TypeEditorService).toSelf().inSingletonScope();\n    res.bind(TypeEditorOperationService).toSelf().inSingletonScope();\n    res.bind(TypeEditorRegistryManager).toSelf().inSingletonScope();\n    res.bind(ShortcutsService).toSelf().inSingletonScope();\n    res.bind(ClipboardService).toSelf().inSingletonScope();\n\n    return res;\n  }, []);\n\n  useEffect(() => {\n    const typeManager =\n      container.get<TypeEditorRegistryManager<TypeSchema>>(TypeEditorRegistryManager);\n\n    [...defaultTypeRegistryCreators].forEach((creator) => {\n      typeManager.register(\n        creator as unknown as JsonSchemaTypeRegistryCreator<\n          TypeSchema,\n          TypeEditorRegistry<TypeSchema>\n        >\n      );\n    });\n  }, [container]);\n\n  useEffect(() => {\n    const typeManager =\n      container.get<TypeEditorRegistryManager<TypeSchema>>(TypeEditorRegistryManager);\n\n    const adapter = getTypeDefinitionAdapter(typeManager);\n\n    typeRegistryCreators.forEach((creator) => {\n      typeManager.register(creator({ typeManager, ...adapter }));\n    });\n\n    typeManager.getAllTypeRegistries().forEach((registry) => {\n      const res = registryFormatter(registry, typeManager);\n      typeManager.register(res);\n    });\n\n    typeManager.triggerChanges();\n    onInit?.();\n  }, [typeRegistryCreators, onInit, container]);\n\n  return <TypeContext.Provider value={{ container }}>{children}</TypeContext.Provider>;\n};\n\nexport const useTypeDefinitionManager = <TypeSchema extends Partial<IJsonSchema>>() =>\n  useService<TypeEditorRegistryManager<TypeSchema>>(TypeEditorRegistryManager);\n"
  },
  {
    "path": "packages/materials/type-editor/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// Import Table css\nimport '@douyinfe/semi-ui/lib/es/table';\n\nexport * from './types';\nexport { TypeEditorContext } from './contexts';\nexport { columnConfigs as typeEditorColumnConfigs } from './components/type-editor/columns';\nexport * from './components';\nexport * from './services/type-editor-service';\nexport * from './services/type-registry-manager';\nexport * from '@flowgram.ai/json-schema';\nexport * from './preset';\n"
  },
  {
    "path": "packages/materials/type-editor/src/preset/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ObjectTypeEditor } from './object-type-editor';\n"
  },
  {
    "path": "packages/materials/type-editor/src/preset/object-type-editor/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorColumnType, TypeEditorColumnViewConfig } from '../../types';\nimport { ToolbarKey, TypeEditor } from '../../components';\n\nconst defaultViewConfigs = [\n  {\n    type: TypeEditorColumnType.Key,\n    visible: true,\n  },\n  {\n    type: TypeEditorColumnType.Type,\n    visible: true,\n  },\n  {\n    type: TypeEditorColumnType.Description,\n    visible: true,\n  },\n  {\n    type: TypeEditorColumnType.Required,\n    visible: true,\n  },\n  {\n    type: TypeEditorColumnType.Default,\n    visible: true,\n  },\n  {\n    type: TypeEditorColumnType.Operate,\n    visible: true,\n  },\n];\n\ninterface PropsType {\n  value?: IJsonSchema;\n  onChange?: (value?: IJsonSchema) => void;\n  readonly?: boolean;\n  config?: {\n    rootKey?: string;\n    viewConfigs?: TypeEditorColumnViewConfig[];\n  };\n}\n\nexport function ObjectTypeEditor(props: PropsType) {\n  const { value, onChange, config, readonly } = props;\n\n  const { rootKey = 'outputs', viewConfigs = defaultViewConfigs } = config || {};\n\n  const wrapValue: IJsonSchema = useMemo(\n    () => ({\n      type: 'object',\n      properties: { [rootKey]: value || { type: 'object' } },\n    }),\n    [value, rootKey]\n  );\n\n  const disableEditColumn = useMemo(() => {\n    const res: any[] = [];\n\n    if (readonly) {\n      viewConfigs.forEach((v) => {\n        res.push({\n          column: v.type,\n          reason: 'This field is not editable.',\n        });\n      });\n    }\n\n    return res;\n  }, [readonly, viewConfigs]);\n\n  return (\n    <div>\n      <TypeEditor\n        readonly={readonly}\n        mode=\"type-definition\"\n        toolbarConfig={[ToolbarKey.Import, ToolbarKey.UndoRedo]}\n        rootLevel={1}\n        value={wrapValue}\n        disableEditColumn={disableEditColumn}\n        onChange={(_v) => onChange?.(_v?.properties?.[rootKey])}\n        onCustomSetValue={(newType) => ({\n          type: 'object',\n          properties: {\n            [rootKey]: newType,\n          },\n        })}\n        getRootSchema={(type) => type.properties![rootKey]}\n        viewConfigs={defaultViewConfigs}\n        onEditRowDataSource={(dataSource) => {\n          // 不允许该行编辑 key、required\n          if (dataSource[0]) {\n            dataSource[0].disableEditColumn = [\n              {\n                column: TypeEditorColumnType.Key,\n                reason: 'This field is not editable.',\n              },\n              {\n                column: TypeEditorColumnType.Type,\n                reason: 'This field is not editable.',\n              },\n              {\n                column: TypeEditorColumnType.Required,\n                reason: 'This field is not editable.',\n              },\n              {\n                column: TypeEditorColumnType.Default,\n                reason: 'This field is not editable.',\n              },\n              {\n                column: TypeEditorColumnType.Operate,\n                reason: 'This field is not editable.',\n              },\n            ];\n\n            dataSource[0].cannotDrag = true;\n          }\n\n          return dataSource;\n        }}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/clipboard-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Event, Emitter } from '@flowgram.ai/utils';\n\n@injectable()\nexport class ClipboardService {\n  public readonly onClipboardChangedEmitter = new Emitter<string>();\n\n  readonly onClipboardChanged: Event<string> = this.onClipboardChangedEmitter.event;\n\n  /**\n   * 读取浏览器数据\n   */\n  private get data(): Promise<string> {\n    return navigator.clipboard.readText();\n  }\n\n  private async saveReadData(): Promise<{\n    error?: string;\n    data?: string;\n  }> {\n    try {\n      const data = await this.data;\n      return {\n        data,\n      };\n    } catch (error) {\n      return {\n        error: error as string,\n      };\n    }\n  }\n\n  /**\n   * 设置剪切板数据\n   */\n  public async writeData(newStrData: string): Promise<void> {\n    const oldSaveData = await this.saveReadData();\n\n    // 读取错误可能是没有读取权限，此时不校验是否相等，直接写入剪切板\n    if (oldSaveData.error || oldSaveData.data !== newStrData) {\n      if (navigator.clipboard && window.isSecureContext) {\n        await navigator.clipboard.writeText(newStrData);\n        const event = document.createEvent('Event');\n        event.initEvent('onchange');\n        (event as unknown as { value: string }).value = newStrData;\n        navigator.clipboard.dispatchEvent(event);\n      } else {\n        const textarea = document.createElement('textarea');\n        textarea.value = newStrData;\n\n        // 视区以外渲染 dom，无法 display none，否则无文本 copy\n        textarea.style.display = 'absolute';\n        textarea.style.left = '-99999999px';\n\n        document.body.prepend(textarea);\n\n        // highlight the content of the textarea element\n        textarea.select();\n\n        try {\n          document.execCommand('copy');\n        } catch (err) {\n          console.log(err);\n        } finally {\n          textarea.remove();\n        }\n      }\n\n      this.onClipboardChangedEmitter.fire(newStrData);\n    }\n  }\n\n  /**\n   * 获取剪切板数据\n   */\n\n  public async readData(): Promise<string> {\n    const res = await this.saveReadData();\n    if (res.error) {\n      throw Error(res.error);\n    }\n    return res.data || '';\n  }\n\n  public clearData(): void {\n    this.writeData('');\n  }\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable import/no-unresolved */\nimport 'reflect-metadata';\nexport * from './type-editor-service';\nexport * from './shortcut-service';\nexport * from './clipboard-service';\nexport * from './type-operation-service';\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/shortcut-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\n@injectable()\nexport class ShortcutsService {}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/type-editor-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { MonitorData } from '../utils';\nimport {\n  type TypeEditorColumnType,\n  type TypeEditorRowData,\n  TypeEditorDropInfo,\n  TypeEditorColumnConfig,\n  TypeEditorPos,\n  TypeEditorColumnViewConfig,\n  ShortcutContext,\n} from '../types';\nimport { TypeRegistryCreatorsAdapter } from '../contexts';\nimport { ROOT_FIELD_ID } from '../components/type-editor/common';\nimport { TypeEditorRegistryManager } from './type-registry-manager';\nimport { ClipboardService } from './clipboard-service';\n\n@injectable()\nexport class TypeEditorService<TypeSchema extends Partial<IJsonSchema>> {\n  private _configs: Map<TypeEditorColumnType, TypeEditorColumnConfig<TypeSchema>> = new Map();\n\n  private _activePos: TypeEditorPos = { x: -1, y: -1 };\n\n  // -1 为 header\n  private _dropInfo: TypeEditorDropInfo = {\n    rowDataId: '',\n    indent: -1,\n    index: -2,\n  };\n\n  public errorMsgs = new MonitorData<{ pos: TypeEditorPos; msg?: string }[]>([]);\n\n  public editValue: unknown;\n\n  public onChange: (\n    typeSchema?: TypeSchema,\n    ctx?: {\n      storeState?: boolean;\n    }\n  ) => void;\n\n  public onRemoveEmptyLine: (id: string) => void;\n\n  public onGlobalAdd: ((id: string) => void) | undefined;\n\n  public typeRegistryCreators?: TypeRegistryCreatorsAdapter<TypeSchema>[];\n\n  private dataSource: TypeEditorRowData<TypeSchema>[] = [];\n\n  public dataSourceMap: Record<string, TypeEditorRowData<TypeSchema>> = {};\n\n  public dataSourceTouchedMap: Record<string, boolean> = {};\n\n  public blink = new MonitorData(false);\n\n  public columnViewConfig: TypeEditorColumnViewConfig[] = [];\n\n  public onActivePosChange = new Emitter<TypeEditorPos>();\n\n  public onDropInfoChange = new Emitter<TypeEditorDropInfo>();\n\n  @inject(ClipboardService)\n  public clipboard: ClipboardService;\n\n  @inject(TypeEditorRegistryManager)\n  public typeDefinition: TypeEditorRegistryManager<TypeSchema>;\n\n  public rootTypeSchema: TypeSchema;\n\n  public setErrorMsg = (pos: TypeEditorPos, msg?: string) => {\n    const newMsgs = [...this.errorMsgs.data];\n    const item = newMsgs.find((v) => v.pos.x === pos.x && v.pos.y === pos.y);\n    if (item) {\n      item.msg = msg;\n    } else {\n      newMsgs.push({ pos, msg });\n    }\n    this.errorMsgs.update(newMsgs);\n  };\n\n  public refreshErrorMsgAfterRemove = (index: number) => {\n    // 删除被删去那行的 errorMsgs\n    const newMsgs = this.errorMsgs.data.filter((msg) => msg.pos.y !== index);\n\n    newMsgs.forEach((msg) => {\n      if (msg.pos.y > index) {\n        msg.pos.y = msg.pos.y - 1;\n      }\n    });\n\n    this.errorMsgs.update(newMsgs);\n  };\n\n  public checkActivePosError = () => {\n    const pos = this.activePos;\n\n    return !!this.errorMsgs.data.find((v) => v.pos.x === pos.x && v.pos.y === pos.y && v.msg);\n  };\n\n  public setEditValue = (val: unknown) => {\n    this.editValue = val;\n  };\n\n  public registerConfigs(\n    config: TypeEditorColumnConfig<TypeSchema> | TypeEditorColumnConfig<TypeSchema>[]\n  ): void {\n    const configs = Array.isArray(config) ? config : [config];\n\n    configs.map((c) => {\n      this._configs.set(c.type, c);\n    });\n  }\n\n  public addConfigProps(\n    type: TypeEditorColumnType,\n    config: Partial<Omit<TypeEditorColumnConfig<TypeSchema>, 'type'>>\n  ): void {\n    const configByType = this.getConfigByType(type);\n\n    if (!configByType) {\n      return;\n    }\n\n    const newConfig = {\n      ...configByType,\n      ...config,\n    };\n\n    this._configs.set(type, newConfig);\n  }\n\n  public getConfigs = (): TypeEditorColumnConfig<TypeSchema>[] =>\n    Array.from(this._configs.values());\n\n  public getConfigByType(\n    type: TypeEditorColumnType\n  ): TypeEditorColumnConfig<TypeSchema> | undefined {\n    return this._configs.get(type);\n  }\n\n  public triggerShortcutEvent(\n    event: 'enter' | 'tab' | 'left' | 'right' | 'up' | 'down' | 'copy' | 'paste' | 'delete'\n  ): void {\n    const column = this.columnViewConfig[this.activePos.x];\n\n    const columnConfig = this.getConfigByType(column?.type);\n    if (!columnConfig) {\n      return;\n    }\n\n    const ctx: ShortcutContext<TypeSchema> = {\n      value: this.editValue,\n      rowData: this.dataSource[this.activePos.y],\n      onRemoveEmptyLine: this.onRemoveEmptyLine,\n      onChange: this.onChange,\n      typeEditor: this,\n      typeDefinitionService: this.typeDefinition,\n    };\n\n    switch (event) {\n      case 'enter': {\n        columnConfig.shortcuts?.onEnter?.(ctx);\n        return;\n      }\n      case 'tab': {\n        columnConfig.shortcuts?.onTab?.(ctx);\n        return;\n      }\n      case 'down': {\n        columnConfig.shortcuts?.onDown?.(ctx);\n        return;\n      }\n      case 'up': {\n        columnConfig.shortcuts?.onUp?.(ctx);\n        return;\n      }\n      case 'left': {\n        columnConfig.shortcuts?.onLeft?.(ctx);\n        return;\n      }\n      case 'right': {\n        columnConfig.shortcuts?.onRight?.(ctx);\n        return;\n      }\n      case 'copy': {\n        columnConfig.shortcuts?.onCopy?.(ctx);\n        return;\n      }\n      case 'paste': {\n        columnConfig.shortcuts?.onPaste?.(ctx);\n        return;\n      }\n      case 'delete': {\n        columnConfig.shortcuts?.onDelete?.(ctx);\n        return;\n      }\n\n      default: {\n        return;\n      }\n    }\n  }\n\n  public get activePos(): TypeEditorPos {\n    return this._activePos;\n  }\n\n  private checkRowDataColumnCanEdit = (\n    rowData: TypeEditorRowData<TypeSchema>,\n    column: TypeEditorColumnType\n  ): boolean =>\n    !(rowData.disableEditColumn || []).map((v) => v.column).includes(column) &&\n    this.getConfigByType(column)?.focusable !== false;\n\n  /**\n   * 获取可编辑的下一列/上一列\n   */\n  private getCanEditColumn(originPos: TypeEditorPos, direction: 'next' | 'last'): TypeEditorPos {\n    const newX =\n      (originPos.x + this.columnViewConfig.length + (direction === 'next' ? 1 : -1)) %\n      this.columnViewConfig.length;\n\n    const newPos = {\n      y: originPos.y,\n      x: newX,\n    };\n\n    if (\n      this.checkRowDataColumnCanEdit(\n        this.dataSource[newPos.y],\n        this.columnViewConfig[newPos.x].type\n      )\n    ) {\n      return newPos;\n    }\n\n    return this.getCanEditColumn(newPos, direction);\n  }\n\n  /**\n   * 获取可编辑的下一行/上一行\n   */\n  private getCanEditLine(originPos: TypeEditorPos, direction: 'next' | 'last'): TypeEditorPos {\n    const newY =\n      (originPos.y + this.dataSource.length + (direction === 'next' ? 1 : -1)) %\n      this.dataSource.length;\n\n    const newPos = {\n      y: newY,\n      x: originPos.x,\n    };\n\n    if (\n      this.checkRowDataColumnCanEdit(\n        this.dataSource[newPos.y],\n        this.columnViewConfig[newPos.x].type\n      )\n    ) {\n      return newPos;\n    }\n\n    return this.getCanEditLine(newPos, direction);\n  }\n\n  /**\n   * 获取下一个可编辑的\n   */\n  private getNextEditItem = (pos: TypeEditorPos): TypeEditorPos => {\n    const newPos = { ...pos };\n\n    if (newPos.x === this.columnViewConfig.length - 1) {\n      newPos.y = (1 + newPos.y) % this.dataSource.length;\n      newPos.x = 0;\n    } else {\n      newPos.x = newPos.x + 1;\n    }\n\n    if (\n      this.checkRowDataColumnCanEdit(\n        this.dataSource[newPos.y],\n        this.columnViewConfig[newPos.x].type\n      )\n    ) {\n      return newPos;\n    }\n\n    return this.getNextEditItem(newPos);\n  };\n\n  public moveActivePosToNextLine(): void {\n    const newPos = this.getCanEditLine(this.activePos, 'next');\n\n    this.setActivePos(newPos);\n  }\n\n  public moveActivePosToNextLineWithAddLine(rowData: TypeEditorRowData<TypeSchema>): void {\n    const newPos = { ...this.activePos };\n\n    if (!rowData.parentId) {\n      return;\n    }\n\n    const parentData = this.dataSourceMap[rowData.parentId] || this.dataSourceMap[ROOT_FIELD_ID];\n\n    const id = this.dataSourceMap[rowData.parentId] ? rowData.parentId : ROOT_FIELD_ID;\n    const addChild = parentData.index + parentData.deepChildrenCount === rowData.index;\n\n    if (addChild) {\n      if (this.onGlobalAdd) {\n        this.onGlobalAdd(id);\n        newPos.y = newPos.y + 1;\n      } else {\n        newPos.y = -1;\n      }\n    } else {\n      newPos.y = newPos.y + 1;\n    }\n    this.setActivePos(newPos);\n  }\n\n  public moveActivePosToLastLine(): void {\n    const newPos = this.getCanEditLine(this.activePos, 'last');\n\n    this.setActivePos(newPos);\n  }\n\n  public moveActivePosToLastColumn(): void {\n    const newPos = this.getCanEditColumn(this.activePos, 'last');\n    this.setActivePos(newPos);\n  }\n\n  public moveActivePosToNextColumn(): void {\n    const newPos = this.getCanEditColumn(this.activePos, 'next');\n\n    this.setActivePos(newPos);\n  }\n\n  public moveActivePosToNextItem(): void {\n    const newPos = this.getNextEditItem(this.activePos);\n\n    this.setActivePos(newPos);\n  }\n\n  public setActivePos(pos: TypeEditorPos): void {\n    if (this.checkActivePosError()) {\n      return;\n    }\n    this._activePos = pos;\n\n    this.onActivePosChange.fire(this._activePos);\n  }\n\n  public clearActivePos(): void {\n    this._activePos = { x: -1, y: -1 };\n    this.onActivePosChange.fire(this._activePos);\n  }\n\n  public setDataSource(newData: TypeEditorRowData<TypeSchema>[]): void {\n    this.dataSource = newData;\n  }\n\n  public getDataSource(): TypeEditorRowData<TypeSchema>[] {\n    return this.dataSource;\n  }\n\n  public setColumnViewConfig(config: TypeEditorColumnViewConfig[]): void {\n    this.columnViewConfig = config;\n  }\n\n  public get dropInfo(): TypeEditorDropInfo {\n    return this._dropInfo;\n  }\n\n  public setDropInfo(dropInfo: TypeEditorDropInfo): void {\n    if (\n      dropInfo.indent === this.dropInfo.indent &&\n      this.dropInfo.rowDataId === dropInfo.rowDataId &&\n      this.dropInfo.index === dropInfo.index\n    ) {\n      return;\n    }\n    this._dropInfo = dropInfo;\n    this.onDropInfoChange.fire(dropInfo);\n  }\n\n  public clearDropInfo(): void {\n    this.setDropInfo({ rowDataId: '', indent: -1, index: -2 });\n  }\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/type-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isEqual } from 'lodash-es';\nimport { injectable } from 'inversify';\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { MonitorData } from '../utils';\n\ninterface StackItem {\n  id: string;\n  value: string;\n}\n\n// 操作注册\n@injectable()\nexport class TypeEditorOperationService<TypeSchema extends Partial<IJsonSchema>> {\n  public undoStack: StackItem[] = [];\n\n  public redoStack: StackItem[] = [];\n\n  private _id_idx = 0;\n\n  private _getNewId(): string {\n    return `${this._id_idx++}`;\n  }\n\n  public _storeState = (value: TypeSchema) => {\n    if (this.redoStack.length > 0) {\n      this.redoStack.splice(0);\n    }\n\n    this.undoStack.push({\n      id: this._getNewId(),\n      value: JSON.stringify(value),\n    });\n    this.refreshUndoRedoStatus();\n  };\n\n  public canUndo = new MonitorData(false);\n\n  public canRedo = new MonitorData(false);\n\n  public constructor() {\n    this.refreshUndoRedoStatus();\n  }\n\n  public refreshUndoRedoStatus() {\n    this.canRedo.update(this.redoStack.length !== 0);\n    this.canUndo.update(this.undoStack.length > 1);\n  }\n\n  public getCurrentState(): TypeSchema | undefined {\n    const top = this.undoStack[this.undoStack.length - 1];\n\n    if (top) {\n      return JSON.parse(top.value);\n    }\n    return;\n  }\n\n  public clear(): void {\n    this.undoStack = [];\n    this.redoStack = [];\n  }\n\n  public storeState(value: TypeSchema): void {\n    if (isEqual(this.getCurrentState(), value)) {\n      return;\n    }\n\n    this._storeState(value);\n  }\n\n  public async undo(): Promise<void> {\n    const top = this.undoStack.pop();\n    if (top) {\n      this.redoStack.push(top);\n    }\n\n    this.refreshUndoRedoStatus();\n  }\n\n  public async redo(): Promise<void> {\n    const top = this.redoStack.pop();\n\n    if (top) {\n      this.undoStack.push(top);\n    }\n\n    this.refreshUndoRedoStatus();\n  }\n\n  public debugger(): void {\n    console.log('getCurrentState - debugger', this.getCurrentState());\n  }\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/type-registry-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { JsonSchemaTypeManager, IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorRegistry } from '../types';\n\nexport class TypeEditorRegistryManager<\n  TypeSchema extends Partial<IJsonSchema>\n> extends JsonSchemaTypeManager<TypeSchema, TypeEditorRegistry<TypeSchema>> {}\n"
  },
  {
    "path": "packages/materials/type-editor/src/services/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nexport const traverseIJsonSchema = (\n  root: Partial<IJsonSchema> | undefined,\n  cb: (type: Partial<IJsonSchema>) => void\n): void => {\n  if (root) {\n    cb(root);\n\n    if (root.items) {\n      traverseIJsonSchema(root.items, cb);\n    }\n    if (root.additionalProperties) {\n      traverseIJsonSchema(root.additionalProperties, cb);\n    }\n\n    if (root.properties) {\n      Object.values(root.properties).forEach((v) => {\n        traverseIJsonSchema(v, cb);\n      });\n    }\n  }\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/array.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { Space, Typography } from '@douyinfe/semi-ui';\n\nexport const arrayRegistryCreator: JsonSchemaTypeRegistryCreator = ({ typeManager }) => ({\n  type: 'array',\n\n  getDisplayLabel: (type: IJsonSchema) => {\n    const config = typeManager.getTypeBySchema(type);\n\n    return (\n      <Space style={{ width: '100%' }}>\n        {config?.getDisplayIcon(type) || config?.icon}\n        <div style={{ flex: 1, width: 0, display: 'flex' }}>\n          <Typography.Text size=\"small\" ellipsis={{ showTooltip: true }}>\n            {typeManager.getComplexText(type)}\n          </Typography.Text>\n        </div>\n      </Space>\n    );\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/boolean.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { Select } from '@douyinfe/semi-ui';\n\nimport { TypeInputContext } from '../types';\n\nexport const booleanRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'boolean',\n\n  getInputNode({ value, onChange, onSubmit }: TypeInputContext<IJsonSchema>): React.JSX.Element {\n    return (\n      <Select\n        style={{\n          width: '100%',\n          height: '100%',\n          display: 'flex',\n          alignItems: 'center',\n          background: 'var(--semi-color-bg-0)',\n        }}\n        optionList={[\n          {\n            value: 1,\n            label: 'True',\n          },\n          {\n            value: 0,\n            label: 'False',\n          },\n        ]}\n        className={'flow-type-select'}\n        value={Number(value)}\n        onSelect={(v) => {\n          onChange(Boolean(v));\n          onSubmit();\n        }}\n      />\n    );\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import { TypeDefinitionRegistryCreator } from '@flow-ide-editor/flow-schema-type-definitions';\n\nimport { JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\n\nimport { stringRegistryCreator } from './string';\nimport { objectRegistryCreator } from './object';\nimport { numberRegistryCreator } from './number';\nimport { integerRegistryCreator } from './integer';\nimport { booleanRegistryCreator } from './boolean';\nimport { arrayRegistryCreator } from './array';\n\nexport const defaultTypeRegistryCreators: JsonSchemaTypeRegistryCreator[] = [\n  stringRegistryCreator,\n  numberRegistryCreator,\n  integerRegistryCreator,\n  booleanRegistryCreator,\n  objectRegistryCreator,\n  arrayRegistryCreator,\n];\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/integer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { InputNumber } from '@douyinfe/semi-ui';\n\nimport { TypeInputContext } from '../types';\n\nexport const integerRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'integer',\n  getInputNode({ value, onChange, onSubmit }: TypeInputContext<IJsonSchema>): React.JSX.Element {\n    return (\n      <InputNumber\n        autoFocus\n        value={value}\n        style={{ width: '100%', height: '100%' }}\n        onChange={onChange}\n        onBlur={onSubmit}\n      />\n    );\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/number.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { InputNumber } from '@douyinfe/semi-ui';\n\nimport { TypeInputContext } from '../types';\n\nexport const numberRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'number',\n  getInputNode({ value, onChange, onSubmit }: TypeInputContext<IJsonSchema>): React.JSX.Element {\n    return (\n      <InputNumber\n        autoFocus\n        value={value}\n        style={{ width: '100%', height: '100%' }}\n        onChange={onChange}\n        onBlur={onSubmit}\n      />\n    );\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/object.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { Typography } from '@douyinfe/semi-ui';\n\nexport const objectRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'object',\n  getInputNode: (): React.JSX.Element => (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        padding: '0 8px',\n      }}\n    >\n      <Typography.Text>Not Supported</Typography.Text>\n    </div>\n  ),\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/type-registry/string.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '@flowgram.ai/json-schema';\nimport { Input } from '@douyinfe/semi-ui';\n\nimport { TypeInputContext } from '../types';\n\nexport const stringRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'string',\n  getInputNode: ({ value, onChange, onSubmit }: TypeInputContext<IJsonSchema>) => (\n    <Input\n      style={{\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n      }}\n      autoFocus\n      value={value}\n      onChange={onChange}\n      onBlur={onSubmit}\n    />\n  ),\n});\n"
  },
  {
    "path": "packages/materials/type-editor/src/types/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './type-editor';\nexport * from './registry';\n"
  },
  {
    "path": "packages/materials/type-editor/src/types/registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema, JsonSchemaTypeRegistry } from '@flowgram.ai/json-schema';\n\nexport interface TypeInputConfig<TypeSchema extends Partial<IJsonSchema>> {\n  canEnter?: boolean;\n\n  getProps?: (typeSchema: TypeSchema) => Record<string, unknown>;\n}\n\nexport interface FlowSchemaInitCtx {\n  enum?: string[];\n}\n\nexport interface TypeInputContext<TypeSchema extends Partial<IJsonSchema>> {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value?: any;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  onChange: (newVal: any) => void;\n  type: TypeSchema;\n  onSubmit: () => void;\n}\nexport interface TypeCascaderConfig<TypeSchema extends Partial<IJsonSchema>> {\n  /**\n   * 自定义 CascaderPanel\n   */\n  customCascaderPanel?: (ctx: {\n    typeSchema: TypeSchema;\n    onChange: (typeSchema: TypeSchema) => void;\n    onFocus?: () => void;\n    onBlur?: () => void;\n  }) => JSX.Element;\n  /**\n   * 选中后是否不关闭面板\n   */\n  unClosePanelAfterSelect?: boolean;\n  /**\n   * 获取生成 schema 的 ctx\n   */\n  generateInitCtx?: (typeSchema: TypeSchema) => FlowSchemaInitCtx;\n}\n\nexport interface TypeEditorRegistry<TypeSchema extends Partial<IJsonSchema>>\n  extends JsonSchemaTypeRegistry<TypeSchema> {\n  /**\n   * 当前字段是否支持 default\n   */\n  defaultEditable?: true | string;\n  /**\n   * 自定义 disabled\n   */\n  customDisabled?: (ctx: { level: number; parentType: string; parentTypes: string[] }) => string;\n\n  /**\n   * typeInput 设置\n   */\n  typeInputConfig?: TypeInputConfig<TypeSchema>;\n  /**\n   * typeCascader 设置\n   */\n  typeCascaderConfig?: TypeCascaderConfig<TypeSchema>;\n\n  /**\n   * 从默认值上下文\n   */\n  formatDefault?: (val: any, type: IJsonSchema) => IJsonSchema;\n\n  /**\n   *\n   */\n  deFormatDefault?: (val: any) => any;\n\n  /**\n   * 子字段是否可以编辑 default\n   */\n  childrenDefaultEditable?: (type: TypeSchema) => true | string;\n  /**\n   * 子字段是否可以编辑 default\n   */\n  childrenValueEditable?: (type: TypeSchema) => true | string;\n\n  getInputNode?: (ctx: TypeInputContext<TypeSchema>) => JSX.Element;\n  /**\n   * 自定义生成子类型的 optionValue\n   */\n  customChildOptionValue?: () => string[];\n\n  /**\n   * @deprecated api 已废弃，能力仍保留，请优先使用或定义 getSupportedItemTypes\n   */\n  getItemTypes?: (ctx: {\n    level: number;\n    parentTypes?: string[];\n  }) => Array<{ type: string; disabled?: string }>;\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/types/type-editor.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, Ref } from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\n\nimport { TypeEditorRegistryManager } from '../services/type-registry-manager';\nimport { type ShortcutsService, type TypeEditorService } from '../services';\n\nexport interface TypeEditorPos {\n  x: number;\n  y: number;\n}\n\nexport interface TypeEditorDropInfo {\n  rowDataId: string;\n  indent: number;\n  index: number;\n}\n\nexport interface TypeChangeContext {\n  type: TypeEditorColumnType;\n  oldValue: unknown;\n  newValue: unknown;\n}\n\nexport interface EditorProps {\n  keyCheck: boolean;\n}\n\nexport interface RenderProps<TypeSchema extends Partial<IJsonSchema>> {\n  rowData: TypeEditorRowData<TypeSchema>;\n  readonly?: boolean;\n  onViewMode: () => void;\n  typeEditor: TypeEditorService<TypeSchema>;\n  onChildrenVisibleChange: (rowDataKey: string, newVal: boolean) => void;\n  onChange: () => void;\n  onPaste?: (typeSchema?: TypeSchema) => TypeSchema | undefined;\n  onFieldChange?: (ctx: TypeChangeContext) => void;\n  onEditMode: () => void;\n  dragSource?: Ref<HTMLSpanElement>;\n  error: boolean;\n  onError?: (msg: string[]) => void;\n  unOpenKeys: Record<string, boolean>;\n  config: Omit<TypeEditorColumnViewConfig, 'type' | 'visible'>;\n}\n\nexport interface ShortcutContext<TypeSchema extends Partial<IJsonSchema>> {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value: any;\n  rowData: TypeEditorRowData<TypeSchema>;\n  onChange: () => void;\n  onRemoveEmptyLine: (id: string) => void;\n  typeEditor: TypeEditorService<TypeSchema>;\n  onError?: (msg?: string) => void;\n  typeDefinitionService: TypeEditorRegistryManager<TypeSchema>;\n}\n\nexport interface InfoContext {\n  shortcuts: ShortcutsService;\n}\nexport interface TypeEditorColumnConfig<TypeSchema extends Partial<IJsonSchema>> {\n  /**\n   * type\n   */\n  type: TypeEditorColumnType;\n  /**\n   * 标题\n   */\n  label: string;\n  /**\n   * 百分比\n   */\n  width?: number;\n  /**\n   * 是否可 focus\n   */\n  focusable?: boolean;\n  /**\n   * label ❓ 提示\n   */\n  info?: () => string;\n  /**\n   * 只读态 render\n   */\n  viewRender?: FC<RenderProps<TypeSchema>>;\n  /**\n   * 编辑态 render\n   */\n  editRender?: FC<RenderProps<TypeSchema>>;\n  /**\n   * 是否自定义拖拽\n   */\n  customDrop?: boolean;\n\n  /**\n   * 校验该行是否存在错误\n   */\n  validateCell?: (\n    rowData: TypeEditorRowData<TypeSchema>,\n    extra: TypeEditorSpecialConfig<TypeSchema>\n  ) =>\n    | {\n        level: 'error' | 'warning';\n        msg?: string;\n      }\n    | undefined;\n\n  /**\n   * 快捷键响应\n   */\n  shortcuts?: {\n    onEnter?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onTab?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onUp?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onDown?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onLeft?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onRight?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onCopy?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onPaste?: (ctx: ShortcutContext<TypeSchema>) => void;\n    onDelete?: (ctx: ShortcutContext<TypeSchema>) => void;\n  };\n}\n\nexport interface TypeEditorColumnViewConfig {\n  /**\n   * 类型\n   */\n  type: TypeEditorColumnType;\n  /**\n   * 是否可见\n   */\n  visible: boolean;\n}\n\nexport interface DisableTypeInfo {\n  type: string;\n  reason: string;\n}\n\nexport interface TypeEditorColumnViewConfig {\n  /**\n   * 类型\n   */\n  type: TypeEditorColumnType;\n  /**\n   * 是否可见\n   */\n  visible: boolean;\n}\n\nexport enum TypeEditorColumnType {\n  /**\n   *\n   */\n  Key = 'key',\n  Type = 'type',\n  Required = 'required',\n  Description = 'description',\n  Default = 'default',\n  Operate = 'operate',\n  Value = 'value',\n  Private = 'private',\n}\n\nexport interface TypeEditorExtraInfo {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  value?: any;\n}\n\nexport type TypeEditorSchema<TypeSchema extends Partial<IJsonSchema>> = TypeSchema & {\n  extra?: TypeEditorExtraInfo;\n};\n\nexport interface TypeEditorSpecialConfig<TypeSchema extends Partial<IJsonSchema>> {\n  /**\n   * 默认值展示模式\n   *\n   * default 默认编辑模式\n   * server 为展示后端兜底默认值\n   */\n  defaultMode?: 'default' | 'server';\n  /**\n   * 支持自定义校验 Name 函数\n   */\n  customValidateName?: (name: string) => string;\n  /**\n   * 关闭自动修复 index\n   */\n  disableFixIndex?: boolean;\n\n  /**\n   * 是否可以编辑 key 的可见\n   */\n  editorVisible?: boolean | string;\n  /**\n   * 隐藏拖拽\n   */\n  hiddenDrag?: boolean;\n  /**\n   * 使用 extra 字段，而非 flow 字段\n   */\n  useExtra?: boolean;\n  /**\n   * type-selector 禁用类型\n   */\n  customDisabledTypes?: Array<DisableTypeInfo>;\n  /**\n   * 是否禁用 add\n   */\n  disabledAdd?: (rowData: TypeEditorRowData<TypeSchema>) => string;\n  /**\n   * 自定义默认值展示\n   */\n  customDefaultView?: (ctx: {\n    rowData: TypeEditorRowData<TypeSchema>;\n    value: unknown;\n    disabled?: string;\n    onChange: (value: unknown) => void;\n    onSubmit: (value: unknown) => void;\n  }) => JSX.Element;\n  /**\n   * 自定义 default 禁用规则\n   */\n  customDefaultEditable?: (rowData: TypeEditorRowData<TypeSchema>) => true | string;\n}\n\nexport type TypeEditorRowData<TypeSchema extends Partial<IJsonSchema>> = TypeSchema & {\n  /**\n   * 当前行的唯一值\n   */\n  id: string;\n  /**\n   * key 值\n   */\n  key: string;\n  /**\n   * 是否必填\n   */\n  isRequired: boolean;\n  /**\n   * 层数\n   */\n  level: number;\n  /**\n   * rowData 关联的 IJsonSchema\n   */\n  self: TypeEditorSchema<TypeSchema>;\n  /**\n   * 父节点 IJsonSchema\n   */\n  parent?: TypeEditorSchema<TypeSchema>;\n  /**\n   * 子节点个数，只包括子类型\n   */\n  childrenCount: number;\n  /**\n   * 子节点个数，包括子类型嵌套类型\n   */\n  deepChildrenCount: number;\n  /**\n   * 不能编辑的列\n   * 和 IJsonSchema 中 editable 的关系\n   * editable 为 false，会将 disableEditColumn 每个 column 都填上\n   */\n  disableEditColumn?: Array<{ column: TypeEditorColumnType; reason: string }>;\n  /**\n   * 是否不能拖拽\n   */\n  cannotDrag?: boolean;\n  /**\n   * 行\n   */\n  index: number;\n  /**\n   *\n   */\n  extraConfig: TypeEditorSpecialConfig<TypeSchema>;\n\n  /**\n   * 父节点 rowId\n   */\n  parentId?: string;\n  /**\n   * path\n   */\n  path: string[];\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './monitor-data';\n"
  },
  {
    "path": "packages/materials/type-editor/src/utils/monitor-data/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { MonitorData } from './monitor-data';\nexport { useMonitorData } from './use-monitor-data';\n"
  },
  {
    "path": "packages/materials/type-editor/src/utils/monitor-data/monitor-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter, type Event } from '@flowgram.ai/utils';\n\n/**\n * 定义用于在 service 的一些需要被监听 onChange 的 data\n * 搭配 useMonitorData 使用\n */\nexport class MonitorData<Type> {\n  private _data: Type;\n\n  protected readonly onDataChangeEmitter = new Emitter<{\n    prev: Type;\n    next: Type;\n  }>();\n\n  readonly onDataChange: Event<{\n    prev: Type;\n    next: Type;\n  }> = this.onDataChangeEmitter.event;\n\n  public get data(): Type {\n    return this._data;\n  }\n\n  public constructor(initialValue?: Type) {\n    if (initialValue !== undefined) {\n      this._data = initialValue;\n    }\n  }\n\n  public update(data: Type) {\n    const prev = this._data;\n    this._data = data;\n    this.onDataChangeEmitter.fire({\n      prev,\n      next: data,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/materials/type-editor/src/utils/monitor-data/use-monitor-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { type MonitorData } from './monitor-data';\n\nexport const useMonitorData = <Type>(\n  monitorData: MonitorData<Type> | undefined\n): { data: Type | undefined } => {\n  const [data, setData] = useState(monitorData?.data);\n\n  useEffect(() => {\n    const dispose = monitorData?.onDataChange(({ prev, next }) => {\n      setData(next);\n    });\n    return () => {\n      dispose?.dispose();\n    };\n  }, [monitorData]);\n\n  return {\n    data,\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/src/utils/registry-adapter.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport React from 'react';\n\nimport { IJsonSchema } from '@flowgram.ai/json-schema';\nimport { Space, Typography } from '@douyinfe/semi-ui';\n\nimport { TypeEditorRegistry } from '../types';\nimport { TypeEditorRegistryManager } from '../services/type-registry-manager';\n\nexport const registryFormatter = <TypeSchema extends Partial<IJsonSchema>>(\n  registry: Partial<TypeEditorRegistry<TypeSchema>>,\n  manager: TypeEditorRegistryManager<TypeSchema>\n): Partial<TypeEditorRegistry<TypeSchema>> => {\n  const res: Record<string, unknown> = {\n    ...registry,\n  };\n\n  const apiMap: Record<string, string> = {\n    getItemTypes: 'getSupportedItemTypes',\n    getIJsonSchemaByStringValue: 'getTypeSchemaByStringValue',\n    getStringValue: 'getStringValueByTypeSchema',\n    getIJsonSchemaProperties: 'getTypeSchemaProperties',\n    getIJsonSchemaPropertiesParent: 'getPropertiesParent',\n    getChildrenExtraJsonPaths: 'getJsonPaths',\n    getDefaultIJsonSchema: 'getDefaultSchema',\n  };\n\n  Object.keys(apiMap).forEach((api) => {\n    if (res[api]) {\n      res[apiMap[api]] = res[api];\n    }\n    if (res[apiMap[api]]) {\n      res[api] = res[apiMap[api]];\n    }\n  });\n\n  return {\n    ...res,\n    getDisplayLabel: (type: IJsonSchema) => (\n      <Space style={{ width: '100%' }}>\n        {manager?.getDisplayIcon(type as TypeSchema)}\n        <div style={{ flex: 1, width: 0, display: 'flex' }}>\n          <Typography.Text size=\"small\" ellipsis={{ showTooltip: true }}>\n            {manager.getComplexText(type as TypeSchema)}\n          </Typography.Text>\n        </div>\n      </Space>\n    ),\n    getIJsonSchemaDeepField: (type: TypeSchema) => manager.getTypeSchemaDeepChildField(type),\n  } as unknown as TypeEditorRegistry<TypeSchema>;\n};\n\ninterface ITypeDefinitionManager<TypeSchema extends Partial<IJsonSchema>> {\n  getDefinitionByIJsonSchema: (\n    typeSchema?: TypeSchema\n  ) => TypeEditorRegistry<TypeSchema> | undefined;\n  getAllTypeDefinitions: () => TypeEditorRegistry<TypeSchema>[];\n  getDefinitionByType: (type: string) => TypeEditorRegistry<TypeSchema> | undefined;\n  getUndefinedIJsonSchema: () => TypeSchema;\n}\n\nexport interface ITypeDefinitionAdapter<TypeSchema extends Partial<IJsonSchema>> {\n  /**\n   * @deprecated 兼容旧接口，已废弃，请使用 typeRegistryManager\n   */\n  typeDefinitionManager: ITypeDefinitionManager<TypeSchema>;\n  /**\n   * @deprecated 兼容旧接口，已废弃，请使用 typeRegistryManager 的 getComplexText 方法\n   */\n  utils: {\n    getComposedLabel: (type: TypeSchema) => string;\n  };\n}\n\nexport const getTypeDefinitionAdapter = <TypeSchema extends Partial<IJsonSchema>>(\n  manager: TypeEditorRegistryManager<TypeSchema>\n): ITypeDefinitionAdapter<TypeSchema> => {\n  const typeDefinitionManager: ITypeDefinitionManager<TypeSchema> = {\n    getDefinitionByIJsonSchema: (typeSchema?: TypeSchema) =>\n      typeSchema ? manager.getTypeBySchema(typeSchema) : undefined,\n    getAllTypeDefinitions: () => manager.getTypeRegistriesWithParentType(),\n    getDefinitionByType: (type) => manager.getTypeByName(type),\n    getUndefinedIJsonSchema: () => manager.getTypeByName('unknown')!.getDefaultSchema(),\n  };\n\n  return {\n    typeDefinitionManager,\n    utils: {\n      getComposedLabel: (type: TypeSchema) => manager.getComplexText(type),\n    },\n  };\n};\n"
  },
  {
    "path": "packages/materials/type-editor/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/materials/type-editor/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/materials/type-editor/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/create-form.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { FieldModel } from '@/core/field-model';\nimport { FieldArrayModel } from '@/core/field-array-model';\nimport { createForm } from '@/core/create-form';\n\ndescribe('createForm', () => {\n  it('should create form with auto initialization by default', () => {\n    const { form, control } = createForm();\n\n    expect(form).toBeDefined();\n    expect(control).toBeDefined();\n    expect(control._formModel.initialized).toBe(true);\n  });\n\n  it('should disableAutoInit work', async () => {\n    const { control } = createForm({ disableAutoInit: true });\n\n    expect(control._formModel.initialized).toBe(false);\n\n    control.init();\n    expect(control._formModel.initialized).toBe(true);\n  });\n\n  it('should create form with initial values', () => {\n    const initialValues = {\n      username: 'John',\n      email: 'john@example.com',\n      age: 30,\n    };\n    const { form } = createForm({ initialValues });\n\n    expect(form.initialValues).toEqual(initialValues);\n    expect(form.values).toEqual(initialValues);\n  });\n\n  it('should create form with validation', async () => {\n    const { form } = createForm({\n      initialValues: { username: '' },\n    });\n\n    // Validation should be callable\n    const errors = await form.validate();\n\n    // Without explicit validators, errors should be empty or undefined\n    expect(errors === undefined || Object.keys(errors).length === 0).toBe(true);\n  });\n\n  it('should create form with empty options', () => {\n    const { form, control } = createForm({});\n\n    expect(form).toBeDefined();\n    expect(control).toBeDefined();\n    expect(control._formModel.initialized).toBe(true);\n  });\n\n  it('should create form without options', () => {\n    const { form, control } = createForm();\n\n    expect(form).toBeDefined();\n    expect(control).toBeDefined();\n    expect(control._formModel.initialized).toBe(true);\n  });\n\n  describe('control.getField', () => {\n    it('should get field by name', () => {\n      const { form, control } = createForm({\n        initialValues: {\n          username: 'John',\n        },\n      });\n\n      // Create field first\n      control._formModel.createField('username');\n      const field = control.getField('username');\n\n      expect(field).toBeDefined();\n      expect(field!.name).toBe('username');\n      expect(field!.value).toBe('John');\n    });\n\n    it('should return undefined for non-existent field', () => {\n      const { control } = createForm();\n\n      const field = control.getField('nonexistent');\n\n      expect(field).toBeUndefined();\n    });\n\n    it('should get FieldArray when field is array', () => {\n      const { control } = createForm({\n        initialValues: {\n          users: [{ name: 'Alice' }, { name: 'Bob' }],\n        },\n      });\n\n      // Create field array\n      control._formModel.createFieldArray('users');\n      const fieldArray = control.getField('users');\n\n      expect(fieldArray).toBeDefined();\n      expect(fieldArray!.name).toBe('users');\n      expect(Array.isArray(fieldArray!.value)).toBe(true);\n    });\n\n    it('should return Field for regular field', () => {\n      const { control } = createForm({\n        initialValues: {\n          username: 'John',\n        },\n      });\n\n      control._formModel.createField('username');\n      const field = control.getField('username');\n\n      expect(field).toBeDefined();\n      expect(field!.name).toBe('username');\n      expect((field as any).onChange).toBeDefined();\n    });\n\n    it('should return FieldArray for array field with array methods', () => {\n      const { control } = createForm({\n        initialValues: {\n          users: [{ name: 'Alice' }],\n        },\n      });\n\n      control._formModel.createFieldArray('users');\n      const fieldArrayModel = control._formModel.getField('users');\n      expect(fieldArrayModel).toBeInstanceOf(FieldArrayModel);\n\n      const fieldArray = control.getField('users');\n\n      expect(fieldArray).toBeDefined();\n      expect((fieldArray as any).append).toBeDefined();\n      expect((fieldArray as any).remove).toBeDefined();\n      expect((fieldArray as any).swap).toBeDefined();\n      expect((fieldArray as any).move).toBeDefined();\n    });\n\n    it('should handle nested field names', () => {\n      const { control } = createForm({\n        initialValues: {\n          user: {\n            profile: {\n              name: 'Alice',\n            },\n          },\n        },\n      });\n\n      control._formModel.createField('user.profile.name');\n      const field = control.getField('user.profile.name');\n\n      expect(field).toBeDefined();\n      expect(field!.name).toBe('user.profile.name');\n      expect(field!.value).toBe('Alice');\n    });\n\n    it('should handle array index in field names', () => {\n      const { control } = createForm({\n        initialValues: {\n          users: [{ name: 'Alice' }, { name: 'Bob' }],\n        },\n      });\n\n      control._formModel.createField('users.0.name');\n      const field = control.getField('users.0.name');\n\n      expect(field).toBeDefined();\n      expect(field!.name).toBe('users.0.name');\n      expect(field!.value).toBe('Alice');\n    });\n  });\n\n  describe('control.init', () => {\n    it('should initialize form with new options', () => {\n      const { control } = createForm({ disableAutoInit: true });\n\n      expect(control._formModel.initialized).toBe(false);\n\n      control.init();\n\n      expect(control._formModel.initialized).toBe(true);\n    });\n\n    it('should reinitialize form', () => {\n      const { control } = createForm({\n        initialValues: { username: 'John' },\n      });\n\n      expect(control._formModel.initialized).toBe(true);\n      expect(control._formModel.initialValues).toEqual({ username: 'John' });\n\n      control._formModel.dispose();\n      control.init();\n\n      expect(control._formModel.initialized).toBe(true);\n    });\n  });\n\n  it('should expose _formModel on control', () => {\n    const { control } = createForm();\n\n    expect(control._formModel).toBeDefined();\n    expect(control._formModel.init).toBeDefined();\n    expect(control._formModel.createField).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/field-array-model.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { Errors, ValidateTrigger, Warnings } from '@/types';\nimport { FormModel } from '@/core/form-model';\nimport { type FieldArrayModel } from '@/core/field-array-model';\n\nimport { FeedbackLevel } from '../src/types';\n\ndescribe('FormArrayModel', () => {\n  let formModel = new FormModel();\n  describe('children', () => {\n    let arrayField: FieldArrayModel;\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n      // 创建数组\n      formModel.createFieldArray('arr');\n      const field = formModel.getField<FieldArrayModel>('arr');\n      field!.append('a');\n      field!.append('b');\n      field!.append('c');\n      arrayField = field!;\n    });\n\n    it('can get children', () => {\n      expect(arrayField.children.length).toBe(3);\n    });\n  });\n  describe('append & delete', () => {\n    let arrayField: FieldArrayModel;\n    let arrEffect = vi.fn();\n    let aEffect = vi.fn();\n    let bEffect = vi.fn();\n    let cEffect = vi.fn();\n    let appendEffect = vi.fn();\n    let deleteEffect = vi.fn();\n    let aValidate = vi.fn();\n    let bValidate = vi.fn();\n    let cValidate = vi.fn();\n\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n      arrEffect = vi.fn();\n      aEffect = vi.fn();\n      bEffect = vi.fn();\n      cEffect = vi.fn();\n      appendEffect = vi.fn();\n      deleteEffect = vi.fn();\n      aValidate = vi.fn();\n      bValidate = vi.fn();\n      cValidate = vi.fn();\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        validate: {\n          ['arr.0']: aValidate,\n          ['arr.1']: bValidate,\n          ['arr.2']: cValidate,\n        },\n      });\n      // 创建其他field, 用于测试其他元素不会被影响\n      formModel.createField('other');\n      // 创建数组\n      formModel.createFieldArray('arr');\n      const field = formModel.getField<FieldArrayModel>('arr');\n      const a = field!.append('a');\n      const b = field!.append('b');\n      const c = field!.append('c');\n      arrayField = field!;\n      arrayField.onValueChange(arrEffect);\n      arrayField.onAppend(appendEffect);\n      arrayField.onDelete(deleteEffect);\n\n      a.onValueChange(aEffect);\n      b.onValueChange(bEffect);\n      c.onValueChange(cEffect);\n    });\n\n    it('append', async () => {\n      vi.spyOn(arrayField, 'validate');\n\n      arrayField.append('d');\n\n      expect(arrayField.children.length).toBe(4);\n      expect(formModel.getField('other')).toBeDefined();\n      expect(arrEffect).toHaveBeenCalledTimes(1);\n      expect(appendEffect).toHaveBeenCalledTimes(1);\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n    });\n    it('should fire OnFormValueChange event for arr when append', () => {\n      vi.spyOn(formModel.onFormValuesInitEmitter, 'fire');\n      vi.spyOn(formModel.onFormValuesChangeEmitter, 'fire');\n\n      arrayField.append('d');\n\n      expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          arr: ['a', 'b', 'c', 'd'],\n        },\n        prevValues: {\n          arr: ['a', 'b', 'c'],\n        },\n        name: 'arr',\n        options: {\n          action: 'array-append',\n          indexes: [3],\n        },\n      });\n      expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          arr: ['a', 'b', 'c', 'd'],\n        },\n        prevValues: {\n          arr: ['a', 'b', 'c'],\n        },\n        name: 'arr.3',\n      });\n    });\n    it('delete first element', () => {\n      vi.spyOn(formModel.onFormValuesChangeEmitter, 'fire');\n      vi.spyOn(arrayField, 'validate');\n      vi.spyOn(arrayField.onValueChangeEmitter, 'fire');\n\n      arrayField.delete(0);\n\n      // assert value\n      expect(arrayField.children.length).toBe(2);\n      expect(arrayField.children[0].value).toBe('b');\n      expect(arrayField.children[1].value).toBe('c');\n      expect(formModel.getField('other')).toBeDefined();\n\n      // assert change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).toHaveBeenCalledTimes(1);\n      expect(bEffect).toHaveBeenCalledTimes(1);\n      expect(cEffect).toHaveBeenCalledTimes(1);\n      expect(deleteEffect).toHaveBeenCalledTimes(1);\n      expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          arr: ['b', 'c'],\n        },\n        prevValues: {\n          arr: ['a', 'b', 'c'],\n        },\n        name: 'arr',\n        options: {\n          action: 'array-splice',\n          indexes: [0],\n        },\n      });\n      expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n      expect(aValidate).not.toHaveBeenCalled();\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n    });\n    it('delete middle element', () => {\n      vi.spyOn(arrayField, 'validate');\n      vi.spyOn(arrayField.onValueChangeEmitter, 'fire');\n\n      arrayField.delete(1);\n\n      // assert values\n      expect(arrayField.children.length).toBe(2);\n      expect(arrayField.children[0].value).toBe('a');\n      expect(arrayField.children[1].value).toBe('c');\n      expect(formModel.getField('other')).toBeDefined();\n\n      // assert change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).not.toHaveBeenCalled();\n      expect(bEffect).toHaveBeenCalledTimes(1);\n      expect(cEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n    });\n    it('delete last element', () => {\n      arrayField.delete(2);\n      expect(arrayField.children.length).toBe(2);\n      expect(arrayField.children[0].value).toBe('a');\n      expect(arrayField.children[1].value).toBe('b');\n      expect(formModel.getField('other')).toBeDefined();\n      expect(arrEffect).toHaveBeenCalled();\n      expect(cEffect).toHaveBeenCalled();\n    });\n    it('delete element which has nested field', () => {\n      vi.spyOn(arrayField, 'validate');\n      const axField = formModel.createField('arr.0.x');\n      const bxField = formModel.createField('arr.1.x');\n\n      vi.spyOn(axField, 'validate');\n      vi.spyOn(bxField, 'validate');\n\n      formModel.setValueIn('arr.0', { x: 1 });\n      formModel.setValueIn('arr.1', { x: 2 });\n\n      expect(arrayField.value).toEqual([{ x: 1 }, { x: 2 }, 'c']);\n\n      arrayField.delete(0);\n\n      expect(arrayField.value).toEqual([{ x: 2 }, 'c']);\n\n      // assert change events\n      expect(aEffect).toHaveBeenCalledTimes(2); // setValueIn 触发一次， delete 触发一次\n      expect(bEffect).toHaveBeenCalledTimes(2); // setValueIn 触发一次， delete 触发一次\n      expect(cEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(aValidate).toHaveBeenCalledTimes(1); // setValueIn 触发一次， delete 不会触发\n      expect(bValidate).toHaveBeenCalledTimes(1); // setValueIn 触发一次， delete 不会触发\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(axField.validate).toHaveBeenCalledTimes(1); // setValueIn 触发一次， delete 不会触发\n      expect(bxField.validate).toHaveBeenCalledTimes(1); // setValueIn 触发一次， delete 不会触发\n    });\n    it('more elements delete', () => {\n      /**\n       * 数组为 [a,b,c,d]\n       * 删除 b\n       * 希望数组值为 [a,c,d]\n       * 希望formModel中的field也正确对应\n       */\n      arrayField.append('d');\n\n      vi.spyOn(arrayField, 'validate');\n\n      arrayField.delete(1);\n\n      // assert values\n      expect(arrayField.children.length).toBe(3);\n      expect(arrayField.children[0].value).toBe('a');\n      expect(arrayField.children[1].value).toBe('c');\n      expect(arrayField.children[2].value).toBe('d');\n      expect(formModel.getField('arr.2')?.value).toBe('d');\n      expect(formModel.getField('other')).toBeDefined();\n\n      // assert value change events\n      expect(arrEffect).toHaveBeenCalled();\n      expect(bEffect).toHaveBeenCalled();\n      expect(cEffect).toHaveBeenCalled();\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n    });\n  });\n  describe('_splice', () => {\n    let arrayField: FieldArrayModel;\n    let aEffect = vi.fn();\n    let bEffect = vi.fn();\n    let cEffect = vi.fn();\n    let dEffect = vi.fn();\n    let eEffect = vi.fn();\n    let aValidate = vi.fn();\n    let bValidate = vi.fn();\n    let cValidate = vi.fn();\n    let dValidate = vi.fn();\n    let eValidate = vi.fn();\n\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n      aEffect = vi.fn();\n      bEffect = vi.fn();\n      cEffect = vi.fn();\n      dEffect = vi.fn();\n      eEffect = vi.fn();\n      aValidate = vi.fn();\n      bValidate = vi.fn();\n      cValidate = vi.fn();\n      dValidate = vi.fn();\n      eValidate = vi.fn();\n      formModel.createFieldArray('arr');\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        validate: {\n          ['arr.0']: aValidate,\n          ['arr.1']: bValidate,\n          ['arr.2']: cValidate,\n          ['arr.3']: dValidate,\n          ['arr.4']: eValidate,\n        },\n      });\n      const field = formModel.getField<FieldArrayModel>('arr');\n      const aField = field!.append('a');\n      const bField = field!.append('b');\n      const cField = field!.append('c');\n      const dField = field!.append('d');\n      const eField = field!.append('e');\n\n      aField.onValueChange(aEffect);\n      bField.onValueChange(bEffect);\n      cField.onValueChange(cEffect);\n      dField.onValueChange(dEffect);\n      eField.onValueChange(eEffect);\n\n      arrayField = field!;\n\n      vi.spyOn(arrayField, 'validate');\n      vi.spyOn(arrayField.onValueChangeEmitter, 'fire');\n    });\n\n    it('should throw error when delete count exceeds array length', () => {\n      expect(() => {\n        arrayField._splice(0, 6);\n      }).toThrowError();\n    });\n\n    it('should throw error when delete in empty array', () => {\n      arrayField._splice(0, 5);\n\n      expect(() => {\n        arrayField._splice(0);\n      }).toThrowError();\n    });\n\n    it('splice first 2', () => {\n      arrayField._splice(0, 2);\n\n      // assert values\n      expect(arrayField.children.length).toBe(3);\n      expect(arrayField.children[0].value).toBe('c');\n      expect(arrayField.children[1].value).toBe('d');\n      expect(arrayField.children[2].value).toBe('e');\n\n      // assert value change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).toHaveBeenCalledTimes(1);\n      expect(bEffect).toHaveBeenCalledTimes(1);\n      expect(cEffect).toHaveBeenCalledTimes(1);\n      expect(dEffect).toHaveBeenCalledTimes(1);\n      expect(eEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n      expect(aValidate).not.toHaveBeenCalled();\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(dValidate).not.toHaveBeenCalled();\n      expect(eValidate).not.toHaveBeenCalled();\n    });\n    it('splice last 2', () => {\n      arrayField._splice(3, 2);\n\n      // assert values\n      expect(arrayField.children.length).toBe(3);\n      expect(arrayField.children[0].value).toBe('a');\n      expect(arrayField.children[1].value).toBe('b');\n      expect(arrayField.children[2].value).toBe('c');\n\n      // assert value change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).not.toHaveBeenCalled();\n      expect(bEffect).not.toHaveBeenCalled();\n      expect(cEffect).not.toHaveBeenCalled();\n      expect(dEffect).toHaveBeenCalledTimes(1);\n      expect(eEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n      expect(aValidate).not.toHaveBeenCalled();\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(dValidate).not.toHaveBeenCalled();\n      expect(eValidate).not.toHaveBeenCalled();\n    });\n    it('splice middle elements', () => {\n      arrayField._splice(1, 2);\n\n      // assert values\n      expect(arrayField.children.length).toBe(3);\n      expect(arrayField.children[0].value).toBe('a');\n      expect(arrayField.children[1].value).toBe('d');\n      expect(arrayField.children[2].value).toBe('e');\n\n      // assert value change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).not.toHaveBeenCalled();\n      expect(bEffect).toHaveBeenCalledTimes(1);\n      expect(cEffect).toHaveBeenCalledTimes(1);\n      expect(dEffect).toHaveBeenCalledTimes(1);\n      expect(eEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n      expect(aValidate).not.toHaveBeenCalled();\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(dValidate).not.toHaveBeenCalled();\n      expect(eValidate).not.toHaveBeenCalled();\n    });\n\n    it('splice all elements', () => {\n      arrayField._splice(0, 5);\n\n      expect(arrayField.children.length).toBe(0);\n      expect(arrayField.value).toEqual([]);\n\n      // assert value change events\n      expect(arrayField.onValueChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(aEffect).toHaveBeenCalledTimes(1);\n      expect(bEffect).toHaveBeenCalledTimes(1);\n      expect(cEffect).toHaveBeenCalledTimes(1);\n      expect(dEffect).toHaveBeenCalledTimes(1);\n      expect(eEffect).toHaveBeenCalledTimes(1);\n\n      // assert validate trigger\n      expect(arrayField.validate).toHaveBeenCalledTimes(1);\n      expect(aValidate).not.toHaveBeenCalled();\n      expect(bValidate).not.toHaveBeenCalled();\n      expect(cValidate).not.toHaveBeenCalled();\n      expect(dValidate).not.toHaveBeenCalled();\n      expect(eValidate).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('State check when _splice', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n    it('should keep state of rest fields after delete a prev field', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n      });\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n\n      // 设置第1项的state\n      const aFieldModel = formModel.getField('arr.1');\n      aFieldModel!.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'error' }],\n      } as unknown as Errors;\n      aFieldModel!.state.warnings = {\n        'arr.1': [{ name: 'arr.1', message: 'warning' }],\n      } as unknown as Warnings;\n\n      // 删除第0项\n      arrayField._splice(0);\n\n      // 原第一项变为第0项且他的state 被保留了， 且errors 中的路径标识也更新了\n      expect(formModel.getField('arr.0')!.state.errors).toEqual({\n        'arr.0': [{ name: 'arr.0', message: 'error' }],\n      });\n      expect(formModel.getField('arr.0')!.state.warnings).toEqual({\n        'arr.0': [{ name: 'arr.0', message: 'warning' }],\n      });\n      expect(formModel.getField('arr.2')).toBeUndefined();\n    });\n\n    it('should keep state of rest fields after delete a prev field, when nested field', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        initialValues: {\n          arr: [\n            { x: 1, y: 2 },\n            { x: 0, y: 0 },\n            { x: 0, y: 0 },\n          ],\n        },\n      });\n      formModel.createField('arr.0');\n      formModel.createField('arr.0.x');\n      formModel.createField('arr.0.y');\n      formModel.createField('arr.1');\n      formModel.createField('arr.1.x');\n      formModel.createField('arr.1.y');\n      formModel.createField('arr.2');\n      formModel.createField('arr.2.x');\n      formModel.createField('arr.2.y');\n\n      // 设置第1项的state\n      const aFieldModel = formModel.getField('arr.1.x');\n      aFieldModel!.state.errors = {\n        'arr.1.x': [{ name: 'arr.1.x', message: 'error' }],\n      } as unknown as Errors;\n\n      // 删除第0项\n      arrayField._splice(0);\n\n      // 原第一项变为第0项且他的state errors 被保留了， 且errors 中的路径标识也更新了\n      expect(formModel.getField('arr.0')!.state.errors).toEqual({\n        'arr.0.x': [{ name: 'arr.0.x', message: 'error' }],\n      });\n      expect(formModel.getField('arr.0.x')!.state.errors).toEqual({\n        'arr.0.x': [{ name: 'arr.0.x', message: 'error' }],\n      });\n      expect(formModel.getField('arr.2')).toBeUndefined();\n      expect(formModel.getField('arr.2.x')).toBeUndefined();\n      expect(formModel.getField('arr.2.y')).toBeUndefined();\n    });\n    it('should align errors and warnings state with existing field in fieldMap ', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        initialValues: {\n          arr: [\n            { x: 1, y: 2 },\n            { x: 0, y: 0 },\n            { x: 0, y: 0 },\n          ],\n        },\n      });\n      const field0 = formModel.createField('arr.0');\n      const field0x = formModel.createField('arr.0.x');\n      const field0y = formModel.createField('arr.0.y');\n      const field1 = formModel.createField('arr.1');\n      const field1x = formModel.createField('arr.1.x');\n      const field1y = formModel.createField('arr.1.y');\n      const field2 = formModel.createField('arr.2');\n      const field2x = formModel.createField('arr.2.x');\n      const field2y = formModel.createField('arr.2.y');\n\n      field0x.state.errors = {\n        'arr.0.x': [{ name: 'arr.0.x', message: 'error' }],\n      } as unknown as Errors;\n      field0x.bubbleState();\n\n      field1x.state.errors = {\n        'arr.1.x': [{ name: 'arr.1.x', message: 'error' }],\n      } as unknown as Errors;\n      field1x.bubbleState();\n\n      field2x.state.errors = {\n        'arr.2.x': [{ name: 'arr.2.x', message: 'error' }],\n      } as unknown as Errors;\n\n      // 删除第0项\n      arrayField._splice(0);\n\n      expect(formModel.state.errors['arr.0.x']).toEqual([{ name: 'arr.0.x', message: 'error' }]);\n      expect(formModel.state.errors['arr.1.x']).toEqual([{ name: 'arr.1.x', message: 'error' }]);\n      expect(formModel.state.errors['arr.2.x']).toBeUndefined();\n\n      expect(field0.state.errors['arr.0.x']).toEqual([{ name: 'arr.0.x', message: 'error' }]);\n      expect(field1.state.errors['arr.1.x']).toEqual([{ name: 'arr.1.x', message: 'error' }]);\n\n      expect(arrayField.state.errors['arr.0.x']).toEqual([{ name: 'arr.0.x', message: 'error' }]);\n      expect(arrayField.state.errors['arr.1.x']).toEqual([{ name: 'arr.1.x', message: 'error' }]);\n      expect(arrayField.state.errors['arr.2.x']).toBeUndefined();\n    });\n\n    it('should not keep previous error state when delete first elem in array then add back ', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        initialValues: {\n          arr: [{ x: 1, y: 2 }],\n        },\n      });\n      const field0 = formModel.createField('arr.0');\n      const field0x = formModel.createField('arr.0.x');\n      const field0y = formModel.createField('arr.0.y');\n\n      field0x.state.errors = {\n        'arr.0.x': [{ name: 'arr.0.x', message: 'error' }],\n      } as unknown as Errors;\n      field0x.bubbleState();\n\n      // 删除第0项\n      arrayField._splice(0);\n      expect(formModel.state.errors['arr.0.x']).toBeUndefined();\n      expect(arrayField.state.errors['arr.0.x']).toBeUndefined();\n      expect(formModel._fieldMap.get('arr.0')).toBeUndefined();\n\n      arrayField.append({ x: 1, y: 2 });\n      formModel.createField('arr.0.x');\n      expect(formModel._fieldMap.get('arr.0')).toBeDefined();\n      expect(formModel.state.errors['arr.0.x']).toBeUndefined();\n      expect(formModel._fieldMap.get('arr.0.x').state.errors).toBeUndefined();\n    });\n  });\n  describe('swap', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('can swap from 0 to middle index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const a = arrayField!.append('a');\n      const b = arrayField!.append('b');\n      const c = arrayField!.append('c');\n\n      formModel.init({});\n\n      a.state.errors = {\n        'arr.0': [{ name: 'arr.0', message: 'err0', level: FeedbackLevel.Error }],\n      };\n      b.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],\n      };\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.swap(0, 1);\n      expect(formModel.values).toEqual({ arr: ['b', 'a', 'c'] });\n      expect(formModel.getField('arr.0').state.errors).toEqual({\n        'arr.0': [{ name: 'arr.0', message: 'err1', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.1').state.errors).toEqual({\n        'arr.1': [{ name: 'arr.1', message: 'err0', level: FeedbackLevel.Error }],\n      });\n    });\n    it('can chained swap', () => {\n      const arrayField = formModel.createFieldArray('x.arr');\n      const a = arrayField!.append('a');\n      const b = arrayField!.append('b');\n      arrayField!.append('c');\n\n      formModel.init({});\n\n      a.state.errors = {\n        'arr.0': [{ name: 'arr.0', message: 'err0', level: FeedbackLevel.Error }],\n      };\n      b.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],\n      };\n\n      expect(a.name).toBe('x.arr.0');\n      expect(b.name).toBe('x.arr.1');\n      expect(formModel.values.x).toEqual({ arr: ['a', 'b', 'c'] });\n\n      arrayField.swap(1, 0);\n      expect(a.name).toBe('x.arr.1');\n      expect(b.name).toBe('x.arr.0');\n      expect(formModel.values.x).toEqual({ arr: ['b', 'a', 'c'] });\n\n      arrayField.swap(1, 0);\n      expect(a.name).toBe('x.arr.0');\n      expect(formModel.fieldMap.get('x.arr.0').name).toBe('x.arr.0');\n      expect(b.name).toBe('x.arr.1');\n      expect(formModel.fieldMap.get('x.arr.1').name).toBe('x.arr.1');\n      expect(formModel.values.x).toEqual({ arr: ['a', 'b', 'c'] });\n\n      arrayField.swap(1, 0);\n      expect(a.name).toBe('x.arr.1');\n      expect(formModel.fieldMap.get('x.arr.1').name).toBe('x.arr.1');\n      expect(b.name).toBe('x.arr.0');\n      expect(formModel.fieldMap.get('x.arr.0').name).toBe('x.arr.0');\n\n      expect(formModel.values.x).toEqual({ arr: ['b', 'a', 'c'] });\n    });\n\n    it('can swap from 0 to last index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const a = arrayField!.append('a');\n      const b = arrayField!.append('b');\n      const c = arrayField!.append('c');\n\n      formModel.init({});\n\n      a.state.errors = {\n        'arr.0': [{ name: 'arr.0', message: 'err0', level: FeedbackLevel.Error }],\n      };\n      c.state.errors = {\n        'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],\n      };\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.swap(0, 2);\n      expect(formModel.values).toEqual({ arr: ['c', 'b', 'a'] });\n      expect(formModel.getField('arr.0').state.errors).toEqual({\n        'arr.0': [{ name: 'arr.0', message: 'err2', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.2').state.errors).toEqual({\n        'arr.2': [{ name: 'arr.2', message: 'err0', level: FeedbackLevel.Error }],\n      });\n    });\n    it('can swap from middle index to last index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const a = arrayField!.append('a');\n      const b = arrayField!.append('b');\n      const c = arrayField!.append('c');\n\n      formModel.init({});\n\n      b.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],\n      };\n      c.state.errors = {\n        'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],\n      };\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.swap(1, 2);\n      expect(formModel.values).toEqual({ arr: ['a', 'c', 'b'] });\n      expect(formModel.getField('arr.1').state.errors).toEqual({\n        'arr.1': [{ name: 'arr.1', message: 'err2', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.2').state.errors).toEqual({\n        'arr.2': [{ name: 'arr.2', message: 'err1', level: FeedbackLevel.Error }],\n      });\n    });\n    it('can swap from middle index to another middle index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      const b = arrayField!.append('b');\n      const c = arrayField!.append('c');\n      arrayField!.append('d');\n\n      formModel.init({});\n\n      b.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],\n      };\n      c.state.errors = {\n        'arr.2': [{ name: 'arr.2', message: 'err2', level: FeedbackLevel.Error }],\n      };\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd'] });\n      arrayField.swap(1, 2);\n      expect(formModel.values).toEqual({ arr: ['a', 'c', 'b', 'd'] });\n      expect(formModel.getField('arr.1').state.errors).toEqual({\n        'arr.1': [{ name: 'arr.1', message: 'err2', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.2').state.errors).toEqual({\n        'arr.2': [{ name: 'arr.2', message: 'err1', level: FeedbackLevel.Error }],\n      });\n    });\n\n    it('can swap for nested array', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const a = arrayField!.append({ x: 'x0', y: 'y0' });\n      const b = arrayField!.append({ x: 'x1', y: 'y1' });\n      const ax = formModel.createField('arr.0.x');\n      const ay = formModel.createField('arr.0.y');\n      const bx = formModel.createField('arr.1.x');\n      const by = formModel.createField('arr.1.y');\n\n      formModel.init({});\n\n      ax.state.errors = {\n        'arr.0.x': [{ name: 'arr.0.x', message: 'err0x', level: FeedbackLevel.Error }],\n      };\n      bx.state.errors = {\n        'arr.1.x': [{ name: 'arr.1.x', message: 'err1x', level: FeedbackLevel.Error }],\n      };\n\n      expect(formModel.values).toEqual({\n        arr: [\n          { x: 'x0', y: 'y0' },\n          { x: 'x1', y: 'y1' },\n        ],\n      });\n      arrayField.swap(0, 1);\n      expect(formModel.values).toEqual({\n        arr: [\n          { x: 'x1', y: 'y1' },\n          { x: 'x0', y: 'y0' },\n        ],\n      });\n      expect(formModel.getField('arr.0.x').state.errors).toEqual({\n        'arr.0.x': [{ name: 'arr.0.x', message: 'err1x', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.1.x').state.errors).toEqual({\n        'arr.1.x': [{ name: 'arr.1.x', message: 'err0x', level: FeedbackLevel.Error }],\n      });\n\n      // assert form.state.errors\n      expect(formModel.state.errors['arr.0.x']).toEqual([\n        { name: 'arr.0.x', message: 'err1x', level: FeedbackLevel.Error },\n      ]);\n      expect(formModel.state.errors['arr.1.x']).toEqual([\n        { name: 'arr.1.x', message: 'err0x', level: FeedbackLevel.Error },\n      ]);\n    });\n\n    it('should have correct form.state.errors after swapping invalid field with valid field', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const a = arrayField!.append('a');\n      const b = arrayField!.append('b');\n      arrayField!.append('c');\n\n      formModel.init({});\n\n      b.state.errors = {\n        'arr.1': [{ name: 'arr.1', message: 'err1', level: FeedbackLevel.Error }],\n      };\n\n      arrayField.swap(0, 1);\n      expect(formModel.getField('arr.0').state.errors).toEqual({\n        'arr.0': [{ name: 'arr.0', message: 'err1', level: FeedbackLevel.Error }],\n      });\n      expect(formModel.getField('arr.1').state.errors).toEqual(undefined);\n    });\n\n    it('should trigger array effect and child effect', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const fieldA = arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n\n      const arrayEffect = vi.fn();\n      arrayField.onValueChange(arrayEffect);\n      const fieldAEffect = vi.fn();\n      fieldA.onValueChange(fieldAEffect);\n\n      formModel.init({});\n\n      arrayField.swap(1, 2);\n      expect(arrayEffect).toHaveBeenCalledOnce();\n      expect(fieldAEffect).toHaveBeenCalledOnce();\n    });\n  });\n  describe('move', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('should throw error when from or to exceeds bound', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n      formModel.init({});\n      expect(() => arrayField.move(-1, 1)).toThrowError();\n      expect(() => arrayField.move(1, -1)).toThrowError();\n      expect(() => arrayField.move(1, 3)).toThrowError();\n      expect(() => arrayField.move(3, 1)).toThrowError();\n    });\n\n    it('can move from 0 to middle index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.move(0, 1);\n      expect(formModel.values).toEqual({ arr: ['b', 'a', 'c'] });\n    });\n\n    it('can move from 0 to last index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.move(0, 2);\n      expect(formModel.values).toEqual({ arr: ['b', 'c', 'a'] });\n    });\n    it('can move from middle index to last index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c'] });\n      arrayField.move(1, 2);\n      expect(formModel.values).toEqual({ arr: ['a', 'c', 'b'] });\n    });\n    it('can move from middle index to another middle index', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n      arrayField!.append('d');\n\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd'] });\n      arrayField.move(1, 2);\n      expect(formModel.values).toEqual({ arr: ['a', 'c', 'b', 'd'] });\n    });\n\n    it('can move from middle index to another middle index with more elements', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n      arrayField!.append('d');\n      arrayField!.append('e');\n      arrayField!.append('f');\n\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd', 'e', 'f'] });\n      arrayField.move(1, 4);\n      expect(formModel.values).toEqual({ arr: ['a', 'c', 'd', 'e', 'b', 'f'] });\n    });\n    it('can move from middle index to another middle index with more elements when to is greater than from', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n      arrayField!.append('d');\n      arrayField!.append('e');\n      arrayField!.append('f');\n\n      formModel.init({});\n\n      expect(formModel.values).toEqual({ arr: ['a', 'b', 'c', 'd', 'e', 'f'] });\n      arrayField.move(4, 1);\n      expect(formModel.values).toEqual({ arr: ['a', 'e', 'b', 'c', 'd', 'f'] });\n    });\n\n    it('should trigger array effect and child effect', () => {\n      const arrayField = formModel.createFieldArray('arr');\n      const fieldA = arrayField!.append('a');\n      arrayField!.append('b');\n      arrayField!.append('c');\n\n      const arrayEffect = vi.fn();\n      arrayField.onValueChange(arrayEffect);\n      const fieldAEffect = vi.fn();\n      fieldA.onValueChange(fieldAEffect);\n\n      formModel.init({});\n\n      arrayField.move(1, 2);\n      expect(arrayEffect).toHaveBeenCalledOnce();\n      expect(fieldAEffect).toHaveBeenCalledOnce();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/field-model.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { Errors, FeedbackLevel, ValidateTrigger } from '@/types';\nimport { FormModel } from '@/core/form-model';\n\ndescribe('FieldModel', () => {\n  let formModel = new FormModel();\n  describe('state', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('can bubble', () => {\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n      const parentField = formModel.getField('parent')!;\n\n      childField.value = 1;\n      expect(childField.state.isTouched).toBe(true);\n      expect(parentField.state.isTouched).toBe(true);\n      expect(formModel.state.isTouched).toBe(true);\n    });\n\n    it('can bubble with array', () => {\n      formModel.createField('parent');\n      formModel.createField('parent.arr');\n      formModel.createField('parent.arr.1');\n      const arrChild = formModel.getField('parent.arr.1')!;\n      const arrField = formModel.getField('parent.arr')!;\n      const parentField = formModel.getField('parent')!;\n\n      arrChild.value = 1;\n      expect(arrChild.state.isTouched).toBe(true);\n      expect(arrField.state.isTouched).toBe(true);\n      expect(parentField.state.isTouched).toBe(true);\n      expect(formModel.state.isTouched).toBe(true);\n    });\n\n    it('do not set isTouched for init value set', () => {\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n      const parentField = formModel.getField('parent')!;\n\n      expect(childField.state.isTouched).toBe(false);\n      expect(parentField.state.isTouched).toBe(false);\n      expect(formModel.state.isTouched).toBe(false);\n    });\n  });\n  describe('validate', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('when validate func return only a message', async () => {\n      formModel.init({ validate: { 'parent.*': () => 'some message' } });\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n\n      expect(childField.state.errors).toBeUndefined();\n\n      await childField.validate();\n\n      expect(childField.state.errors?.['parent.child'][0].message).toBe('some message');\n      expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);\n    });\n\n    it('when validate func return a FieldWarning', async () => {\n      formModel.init({\n        validate: {\n          'parent.*': () => ({\n            level: FeedbackLevel.Warning,\n            message: 'some message',\n          }),\n        },\n      });\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n\n      expect(childField.state.errors).toBeUndefined();\n\n      await childField.validate();\n\n      expect(childField.state.warnings?.['parent.child'][0].message).toBe('some message');\n      expect(childField.state.warnings?.['parent.child'][0].level).toBe(FeedbackLevel.Warning);\n    });\n    it('when validate return a FormError', async () => {\n      formModel.init({\n        validate: {\n          'parent.*': () => ({\n            level: FeedbackLevel.Error,\n            message: 'some message',\n          }),\n        },\n      });\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n\n      expect(childField.state.errors?.length).toBeUndefined();\n\n      await childField.validate();\n\n      expect(childField.state.errors?.['parent.child'][0].message).toBe('some message');\n      expect(childField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);\n    });\n    it('should bubble errors to parent field', async () => {\n      formModel.init({\n        validate: {\n          'parent.*': () => ({\n            level: FeedbackLevel.Error,\n            message: 'some message',\n          }),\n        },\n      });\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n      const parentField = formModel.getField('parent')!;\n\n      await childField.validate();\n\n      expect(parentField.state.errors?.['parent.child'][0].message).toBe('some message');\n      expect(parentField.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);\n    });\n\n    it('should bubble errors to form', async () => {\n      formModel.init({\n        validate: {\n          'parent.*': () => ({\n            level: FeedbackLevel.Error,\n            message: 'some message',\n          }),\n        },\n      });\n      formModel.createField('parent');\n      formModel.createField('parent.child');\n      const childField = formModel.getField('parent.child')!;\n\n      await childField.validate();\n\n      expect(formModel.state.errors?.['parent.child'][0].message).toBe('some message');\n      expect(formModel.state.errors?.['parent.child'][0].level).toBe(FeedbackLevel.Error);\n    });\n\n    it('should correctly set and bubble invalid', async () => {\n      formModel.init({\n        validate: {\n          'parent.*': () => ({\n            level: FeedbackLevel.Error,\n            message: 'some message',\n          }),\n        },\n      });\n      const parent = formModel.createField('parent');\n      const child = formModel.createField('parent.child');\n\n      await child.validate();\n\n      expect(child.state.invalid).toBe(true);\n      expect(parent.state.invalid).toBe(true);\n      expect(formModel.state.invalid).toBe(true);\n    });\n\n    it('should validate self ancestors and child', async () => {\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n      });\n      const root = formModel.createField('root');\n      const l1 = formModel.createField('root.l1');\n      const l2 = formModel.createField('root.l1.l2');\n      const l3 = formModel.createField('root.l1.l2.l3');\n      const l4 = formModel.createField('root.l1.l2.l3.l4');\n      const other = formModel.createField('root.other');\n\n      vi.spyOn(root, 'validate');\n      vi.spyOn(l1, 'validate');\n      vi.spyOn(l2, 'validate');\n      vi.spyOn(l3, 'validate');\n      vi.spyOn(l4, 'validate');\n      vi.spyOn(other, 'validate');\n\n      formModel.setValueIn('root.l1.l2', 1);\n\n      expect(root.validate).toHaveBeenCalledTimes(1);\n      expect(l1.validate).toHaveBeenCalledTimes(1);\n      expect(l2.validate).toHaveBeenCalledTimes(1);\n      expect(l3.validate).toHaveBeenCalledTimes(1);\n      expect(l4.validate).toHaveBeenCalledTimes(1);\n      expect(other.validate).toHaveBeenCalledTimes(0);\n    });\n\n    it('should validate when multiple pattern match ', async () => {\n      const validate1 = vi.fn();\n      const validate2 = vi.fn();\n\n      formModel.init({\n        validateTrigger: ValidateTrigger.onChange,\n        validate: {\n          'a.*.input': validate1,\n          'a.1.input': validate2,\n        },\n        initialValues: {\n          a: [{ input: '0' }, { input: '1' }],\n        },\n      });\n      const root = formModel.createField('a');\n      const i0 = formModel.createField('a.0.input');\n      const i1 = formModel.createField('a.1.input');\n\n      formModel.setValueIn('a.1.input', 'xxx');\n\n      expect(validate1).toHaveBeenCalledTimes(1);\n      expect(validate2).toHaveBeenCalledTimes(1);\n    });\n\n    // 暂时注释了从 parent 触发validate 的能力，所以注释这个单测\n    // it('can trigger validate from parent', async () => {\n    //   formModel.init({\n    //     validate: {\n    //       'parent.child1': () => ({\n    //         level: FeedbackLevel.Error,\n    //         message: 'error',\n    //       }),\n    //       'parent.child2': () => ({\n    //         level: FeedbackLevel.Warning,\n    //         message: 'warning',\n    //       }),\n    //     },\n    //   });\n    //   const parent = formModel.createField('parent');\n    //   formModel.createField('parent.child1');\n    //   formModel.createField('parent.child2');\n    //\n    //   await parent.validate();\n    //\n    //   expect(formModel.state.errors?.['parent.child1'][0].message).toBe('error');\n    //   expect(formModel.state.warnings?.['parent.child2'][0].level).toBe('warning');\n    // });\n  });\n\n  describe('onValueChange', () => {\n    let formEffect = vi.fn();\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n      formEffect = vi.fn();\n      formModel.onFormValuesChange(formEffect);\n    });\n\n    it('should bubble value change', () => {\n      const parent = formModel.createField('parent');\n      const child1 = formModel.createField('parent.child1');\n\n      const childOnChange = vi.fn();\n      const parentOnChange = vi.fn();\n\n      child1.onValueChange(childOnChange);\n      parent.onValueChange(parentOnChange);\n\n      child1.value = 1;\n\n      expect(parentOnChange).toHaveBeenCalledTimes(1);\n      expect(childOnChange).toHaveBeenCalledTimes(1);\n      expect(formEffect).toHaveBeenCalledTimes(1);\n    });\n    it('should bubble value change in array when delete', () => {\n      const parent = formModel.createField('parent');\n      const arr = formModel.createFieldArray('parent.arr');\n      const item1 = formModel.createField('parent.arr.0');\n\n      const parentOnChange = vi.fn();\n      const arrOnChange = vi.fn();\n      const item1OnChange = vi.fn();\n\n      parent.onValueChange(parentOnChange);\n      arr.onValueChange(arrOnChange);\n      item1.onValueChange(item1OnChange);\n\n      formModel.setValueIn('parent.arr.0', 1);\n      arr.delete(0);\n\n      expect(item1OnChange).toHaveBeenCalledTimes(2);\n      expect(arrOnChange).toHaveBeenCalledTimes(2);\n      expect(parentOnChange).toHaveBeenCalledTimes(2);\n    });\n    it('should bubble value change in array when append', () => {\n      const parent = formModel.createField('parent');\n      const arr = formModel.createFieldArray('parent.arr');\n\n      const parentOnChange = vi.fn();\n      const arrOnChange = vi.fn();\n\n      parent.onValueChange(parentOnChange);\n      arr.onValueChange(arrOnChange);\n\n      arr.append('1');\n\n      expect(arrOnChange).toHaveBeenCalledTimes(1);\n      expect(parentOnChange).toHaveBeenCalledTimes(1);\n      expect(formEffect).toHaveBeenCalledTimes(1);\n    });\n\n    it('should not trigger child field change when array append', () => {\n      formModel.createField('parent');\n      const arr = formModel.createFieldArray('parent.arr');\n      const item0 = formModel.createField('parent.arr.0');\n      const item0x = formModel.createField('parent.arr.0.x');\n\n      const item0OnChange = vi.fn();\n      const item0xOnChange = vi.fn();\n\n      item0.onValueChange(item0OnChange);\n      item0x.onValueChange(item0xOnChange);\n\n      arr.append('1');\n\n      expect(item0OnChange).toHaveBeenCalledTimes(0);\n      expect(item0xOnChange).toHaveBeenCalledTimes(0);\n    });\n\n    it('should clear and fire change', () => {\n      const parent = formModel.createField('parent');\n      const child1 = formModel.createField('parent.child1');\n\n      const child1OnChange = vi.fn();\n      const parentOnChange = vi.fn();\n      child1.onValueChange(child1OnChange);\n      parent.onValueChange(parentOnChange);\n\n      formModel.setValueIn('parent.child1', 1);\n      child1.clear();\n\n      expect(child1OnChange).toHaveBeenCalledTimes(2);\n      expect(parentOnChange).toHaveBeenCalledTimes(2);\n      expect(formEffect).toHaveBeenCalledTimes(2);\n    });\n\n    it('should bubble change in array delete', () => {\n      const arr = formModel.createFieldArray('arr');\n      const child1 = formModel.createField('arr.0');\n\n      const childOnChange = vi.fn();\n      const arrOnChange = vi.fn();\n      child1.onValueChange(childOnChange);\n      arr.onValueChange(arrOnChange);\n\n      formModel.setValueIn('arr.0', 1);\n      arr.delete(0);\n\n      expect(childOnChange).toHaveBeenCalledTimes(2);\n      expect(arrOnChange).toHaveBeenCalledTimes(2);\n      // formModel.setValueIn 一次，arr.delete 中 arr 本身触发一次\n      expect(formEffect).toHaveBeenCalledTimes(2);\n    });\n\n    it('should bubble change in array append', () => {\n      const arr = formModel.createFieldArray('arr');\n      const item0 = formModel.createField('arr.0');\n\n      const item0OnChange = vi.fn();\n      const arrOnChange = vi.fn();\n\n      item0.onValueChange(item0OnChange);\n      arr.onValueChange(arrOnChange);\n\n      formModel.setValueIn('arr.0', 'a');\n      arr.append('b');\n\n      expect(item0OnChange).toHaveBeenCalledTimes(1);\n    });\n    it('should ignore unchanged items when array delete', () => {\n      const other = formModel.createField('other');\n      const parent = formModel.createField('parent');\n      const arr = formModel.createFieldArray('parent.arr');\n      const item0 = formModel.createField('parent.arr.0');\n      const item1 = formModel.createField('parent.arr.1');\n      const item2 = formModel.createField('parent.arr.2');\n      formModel.setValueIn('parent.arr', [1, 2, 3]);\n\n      const item0OnChange = vi.fn();\n      const item1OnChange = vi.fn();\n      const item2OnChange = vi.fn();\n      const arrOnChange = vi.fn();\n      const parentOnChange = vi.fn();\n      const otherOnChange = vi.fn();\n\n      item0.onValueChange(item0OnChange);\n      item1.onValueChange(item1OnChange);\n      item2.onValueChange(item2OnChange);\n      arr.onValueChange(arrOnChange);\n      parent.onValueChange(parentOnChange);\n      other.onValueChange(otherOnChange);\n\n      arr.delete(1);\n\n      expect(arrOnChange).toHaveBeenCalledTimes(1);\n      expect(parentOnChange).toHaveBeenCalledTimes(1);\n      expect(item0OnChange).not.toHaveBeenCalled();\n      expect(item1OnChange).toHaveBeenCalledTimes(1);\n      expect(item2OnChange).toHaveBeenCalledTimes(1);\n      expect(otherOnChange).not.toHaveBeenCalled();\n    });\n  });\n  describe('dispose', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('should correctly cleanup when field dispose', () => {\n      const parent = formModel.createField('parent');\n      const child1 = formModel.createField('parent.child1');\n\n      child1.state.errors = { 'parent.child1': 'errors' } as unknown as Errors;\n      child1.bubbleState();\n\n      expect(formModel.state.errors?.['parent.child1']).toEqual('errors');\n      expect(parent.state.errors?.['parent.child1']).toEqual('errors');\n\n      parent.dispose();\n\n      // Ref 'dispose' method in field-model.ts\n      // 1. expect state has been cleared\n      // expect(child1.state.errors).toBeUndefined();\n      // expect(parent.state.errors?.['parent.child1']).toBeUndefined();\n\n      // 2. expect field model has been cleared\n      expect(formModel.fieldMap.get('parent')).toBeUndefined();\n      expect(formModel.fieldMap.get('parent.child1')).toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/form-model.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { mapValues } from 'lodash-es';\n\nimport { ValidateTrigger } from '@/types';\nimport { FormModel } from '@/core/form-model';\n\ndescribe('FormModel', () => {\n  let formModel = new FormModel();\n  describe('validate trigger', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('do not validate when value change if validateTrigger is onBlur', async () => {\n      formModel.init({ validateTrigger: ValidateTrigger.onBlur });\n\n      const field = formModel.createField('x');\n      field.originalValidate = vi.fn();\n      vi.spyOn(field, 'originalValidate');\n\n      field.value = 'some value';\n\n      expect(field.originalValidate).not.toHaveBeenCalledOnce();\n    });\n\n    describe('delete field', () => {\n      beforeEach(() => {\n        formModel.dispose();\n        formModel = new FormModel();\n      });\n\n      it('validate onChange', async () => {\n        formModel.init({ initialValues: { parent: { child1: 1 } } });\n\n        formModel.createField('parent');\n        formModel.createField('parent.child1');\n        expect(formModel.values.parent?.child1).toBe(1);\n\n        formModel.deleteField('parent');\n\n        expect(formModel.values.parent?.child1).toBeUndefined();\n        expect(formModel.getField('parent')).toBeUndefined();\n        expect(formModel.getField('parent.child1')).toBeUndefined();\n      });\n    });\n  });\n  describe('FormModel.validate', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n    });\n\n    it('should run validate on all matched names', async () => {\n      formModel.init({\n        validate: {\n          'a.b.*': () => 'error',\n        },\n      });\n\n      const bField = formModel.createField('a.b');\n      const xField = formModel.createField('a.b.x');\n\n      formModel.setValueIn('a.b', { x: 1, y: 2 });\n\n      const results = await formModel.validate();\n\n      // 1. assert validate has been executed correctly\n      expect(results.length).toEqual(2);\n      expect(results[0].message).toEqual('error');\n      expect(results[0].name).toEqual('a.b.x');\n      expect(results[1].message).toEqual('error');\n      expect(results[1].name).toEqual('a.b.y');\n      // 2. assert form state has been set correctly\n      expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n      // 3. assert field state has been set correctly\n      expect(xField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n      // 4. assert field state has been bubbled to its parent\n      expect(bField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n    });\n    it('should run validate if multiple patterns match', async () => {\n      const mockValidate1 = vi.fn();\n      const mockValidate2 = vi.fn();\n      formModel.init({\n        validate: {\n          'a.b.*': mockValidate1,\n          'a.b.x': mockValidate2,\n        },\n      });\n\n      const bField = formModel.createField('a.b');\n      const xField = formModel.createField('a.b.x');\n\n      formModel.setValueIn('a.b', { x: 1, y: 2 });\n\n      formModel.validate();\n\n      expect(mockValidate1).toHaveBeenCalledTimes(2);\n      expect(mockValidate2).toHaveBeenCalledTimes(1);\n    });\n    it('should run validate correctly if multiple patterns match but multiple layer empty value exist', async () => {\n      const mockValidate1 = vi.fn();\n      const mockValidate2 = vi.fn();\n      formModel.init({\n        validate: {\n          'a.*.x': mockValidate1,\n          'a.b.x': mockValidate2,\n        },\n      });\n\n      const bField = formModel.createField('a.b');\n      const xField = formModel.createField('a.b.x');\n\n      formModel.setValueIn('a', {});\n\n      formModel.validate();\n\n      expect(mockValidate1).toHaveBeenCalledTimes(0);\n      expect(mockValidate2).toHaveBeenCalledTimes(1);\n    });\n    it('should correctly set form errors state when field does not exist', async () => {\n      formModel.init({\n        validate: {\n          'a.b.*': () => 'error',\n        },\n      });\n\n      formModel.setValueIn('a.b', { x: 1, y: 2 });\n      await formModel.validate();\n\n      expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n    });\n    it('should set form and field state correctly when run validate twice', async () => {\n      formModel.init({\n        validate: {\n          'a.b.*': ({ value }) => (typeof value === 'string' ? undefined : 'error'),\n        },\n      });\n\n      const bField = formModel.createField('a.b');\n      const xField = formModel.createField('a.b.x');\n\n      formModel.setValueIn('a.b', { x: 1, y: 2 });\n\n      let results = await formModel.validate();\n      // both x y is string, so 2 errors\n      expect(results.length).toEqual(2);\n      expect(formModel.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n      expect(formModel.state?.errors?.['a.b.y']?.[0].message).toEqual('error');\n      expect(xField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n      expect(bField.state?.errors?.['a.b.x']?.[0].message).toEqual('error');\n\n      formModel.setValueIn('a.b', { x: '1', y: '2' });\n\n      results = await formModel.validate();\n\n      expect(results.length).toEqual(0);\n      expect(formModel.state?.errors?.['a.b.x']).toEqual([]);\n      expect(formModel.state?.errors?.['a.b.y']).toEqual([]);\n    });\n    it('validate as dynamic function', async () => {\n      formModel.init({\n        initialValues: { a: 3, b: 'str' },\n        validate: (v, ctx) => {\n          expect(ctx).toEqual('context');\n          return mapValues(v, (value) => {\n            if (typeof value === 'string') {\n              return () => 'string error';\n            }\n            return () => 'num error';\n          });\n        },\n        context: 'context',\n      });\n      const fieldResult = await formModel.validateIn('a');\n      expect(fieldResult).toEqual(['num error']);\n      const results = await formModel.validate();\n      expect(results).toEqual([\n        { name: 'a', message: 'num error', level: 'error' },\n        { name: 'b', message: 'string error', level: 'error' },\n      ]);\n    });\n  });\n  describe('FormModel set/get values', () => {\n    beforeEach(() => {\n      formModel.dispose();\n      formModel = new FormModel();\n      vi.spyOn(formModel.onFormValuesInitEmitter, 'fire');\n      vi.spyOn(formModel.onFormValuesChangeEmitter, 'fire');\n      vi.spyOn(formModel.onFormValuesUpdatedEmitter, 'fire');\n    });\n    it('should set value for root path', () => {\n      formModel.init({\n        initialValues: {\n          a: 1,\n        },\n      });\n\n      formModel.values = { a: 2 };\n      expect(formModel.values).toEqual({ a: 2 });\n    });\n\n    it('should set initialValues and fire init and updated events', async () => {\n      formModel.init({\n        initialValues: {\n          a: 1,\n        },\n      });\n\n      expect(formModel.values).toEqual({ a: 1 });\n      expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 1,\n        },\n        name: '',\n      });\n      expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 1,\n        },\n        name: '',\n      });\n    });\n\n    it('should set initialValues in certain path and fire change', async () => {\n      formModel.init({\n        initialValues: {\n          a: 1,\n        },\n      });\n\n      formModel.setInitValueIn('b', 2);\n\n      expect(formModel.values).toEqual({ a: 1, b: 2 });\n      expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 1,\n          b: 2,\n        },\n        prevValues: {\n          a: 1,\n        },\n        name: 'b',\n      });\n      expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 1,\n          b: 2,\n        },\n        prevValues: {\n          a: 1,\n        },\n        name: 'b',\n      });\n    });\n    it('should not set initialValues in certain path if value exists', async () => {\n      formModel.init({\n        initialValues: {\n          a: 1,\n        },\n      });\n\n      formModel.setInitValueIn('a', 2);\n\n      expect(formModel.values).toEqual({ a: 1 });\n      // 仅在初始化时调用一次，setInitValueIn 没有调用\n      expect(formModel.onFormValuesInitEmitter.fire).toHaveBeenCalledTimes(1);\n      expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledTimes(1);\n    });\n    it('should set values in certain path and fire change and updated events', async () => {\n      formModel.init({\n        initialValues: {\n          a: 1,\n        },\n      });\n\n      formModel.setValueIn('a', 2);\n\n      expect(formModel.values).toEqual({ a: 2 });\n      // 仅在初始化时调用一次，setInitValueIn 没有调用\n      expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledTimes(1);\n      // 初始化一次，变更值一次，所以是两次\n      expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledTimes(2);\n      expect(formModel.onFormValuesChangeEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 2,\n        },\n        prevValues: {\n          a: 1,\n        },\n        name: 'a',\n      });\n      expect(formModel.onFormValuesUpdatedEmitter.fire).toHaveBeenCalledWith({\n        values: {\n          a: 2,\n        },\n        prevValues: {\n          a: 1,\n        },\n        name: 'a',\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/glob.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// test src/glob.ts\nimport { describe, expect, it } from 'vitest';\n\nimport { Glob } from '../src/utils/glob';\n\ndescribe('glob', () => {\n  describe('isMatch', () => {\n    it('* at the end', () => {\n      expect(Glob.isMatch('a.b.*', 'a.b.c')).toBe(true);\n      expect(Glob.isMatch('a.b.*', 'a.k.c')).toBe(false);\n    });\n    it('* at the start', () => {\n      expect(Glob.isMatch('*.b.c', 'a.b.c')).toBe(true);\n      expect(Glob.isMatch('*.b.c', 'a.b.x')).toBe(false);\n    });\n    it('multiple *', () => {\n      expect(Glob.isMatch('*.b.*', 'a.b.c')).toBe(true);\n      expect(Glob.isMatch('a.b.*', 'a.k.c')).toBe(false);\n    });\n    it('no *', () => {\n      expect(Glob.isMatch('a.b.c', 'a.b.c')).toBe(true);\n    });\n    it('length not match', () => {\n      expect(Glob.isMatch('a.b.*', 'a.b.c.c')).toBe(false);\n    });\n  });\n  describe('isMatchOrParent', () => {\n    it('* at the end', () => {\n      expect(Glob.isMatchOrParent('a.b.*', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('a.b.*', 'a.k.c')).toBe(false);\n    });\n    it('* at the start', () => {\n      expect(Glob.isMatchOrParent('*.b.c', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('*.b.c', 'a.b.x')).toBe(false);\n      expect(Glob.isMatchOrParent('*.b', 'a.b.x')).toBe(true);\n    });\n    it('multiple *', () => {\n      expect(Glob.isMatchOrParent('*.b.*', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('*.b.*', 'a.k.c')).toBe(false);\n    });\n    it('no *', () => {\n      expect(Glob.isMatchOrParent('a.b.c', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('a.b', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('a', 'a.b.c')).toBe(true);\n      expect(Glob.isMatchOrParent('', 'a.b.c.')).toBe(true);\n    });\n    it('length not match', () => {\n      expect(Glob.isMatchOrParent('a.b.*', 'a.b.c.c')).toBe(true);\n      expect(Glob.isMatchOrParent('a.b.c.d', 'a.b.c.')).toBe(false);\n    });\n  });\n  describe('getParentPathByPattern', () => {\n    it('should get parent path correctly', () => {\n      expect(Glob.getParentPathByPattern('a.b.*', 'a.b.c')).toBe('a.b.c');\n      expect(Glob.getParentPathByPattern('a.b.*', 'a.b.c.d')).toBe('a.b.c');\n      expect(Glob.getParentPathByPattern('a.b', 'a.b.c.d')).toBe('a.b');\n      expect(Glob.getParentPathByPattern('a.*.c', 'a.b.c.d')).toBe('a.b.c');\n    });\n  });\n  describe('findMatchPaths', () => {\n    it('return original path array if no *', () => {\n      const obj = { a: { b: { c: 1 } } };\n      expect(Glob.findMatchPaths(obj, 'a.b.c')).toEqual(['a.b.c']);\n    });\n    it('object: when * is in middle of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPaths(obj, 'a.*.c')).toEqual(['a.b.c']);\n    });\n    it('object:when * is at the end of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPaths(obj, 'a.*')).toEqual(['a.b']);\n    });\n    // 暂时不支持该场景，见glob.ts 中 143行说明\n    it('object: * 后面数据异构', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);\n    });\n    it('object:when * is at the start and end of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPaths(obj, '*.y.*')).toEqual(['x.y.z']);\n    });\n    it('array: when * is at the end of the path', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: { a: 1, b: 2 },\n          },\n          {\n            x: 10,\n            y: {\n              a: 10,\n              b: 20,\n            },\n          },\n        ],\n      };\n      expect(Glob.findMatchPaths(obj, 'arr.*')).toEqual(['arr.0', 'arr.1']);\n    });\n    it('array: when * is at the start of the path', () => {\n      const arr = [\n        {\n          x: 1,\n          y: { a: 1, b: 2 },\n        },\n        {\n          x: 10,\n          y: {\n            a: 10,\n            b: 20,\n          },\n        },\n      ];\n\n      expect(Glob.findMatchPaths(arr, '*')).toEqual(['0', '1']);\n    });\n    it('array: when * is in the middle of the path', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: { a: 1, b: 2 },\n          },\n          {\n            x: 10,\n            y: {\n              a: 10,\n              b: 20,\n            },\n          },\n        ],\n      };\n\n      expect(Glob.findMatchPaths(obj, 'arr.*.y')).toEqual(['arr.0.y', 'arr.1.y']);\n    });\n    it('array in array: when double * ', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: ['1', '2'],\n          },\n        ],\n      };\n\n      expect(Glob.findMatchPaths(obj, 'arr.*.y.*')).toEqual(['arr.0.y.0', 'arr.0.y.1']);\n    });\n    it('array in object: when double * ', () => {\n      const obj = {\n        x: 100,\n        y: {\n          arr: [1, 2],\n        },\n      };\n\n      expect(Glob.findMatchPaths(obj, 'y.*.*')).toEqual(['y.arr.0', 'y.arr.1']);\n    });\n    it('array in object: when double * start ', () => {\n      const obj = {\n        x: 100,\n        y: {\n          arr: [{ a: 1, b: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPaths(obj, '*.arr.*')).toEqual(['y.arr.0']);\n    });\n    it('when value after * is  empty string ', () => {\n      const obj = {\n        $$input_decorator$$: {\n          inputParameters: [{ name: '', input: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPaths(obj, '$$input_decorator$$.inputParameters.*.name')).toEqual([\n        '$$input_decorator$$.inputParameters.0.name',\n      ]);\n    });\n    it('when value after * is undefined ', () => {\n      const obj = {\n        x: {\n          arr: [{ name: undefined, input: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPaths(obj, 'x.arr.*.name')).toEqual(['x.arr.0.name']);\n    });\n    it('when value not directly after * is undefined ', () => {\n      const obj = {\n        x: {\n          arr: [{ name: { a: undefined }, input: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPaths(obj, 'x.arr.*.name.a')).toEqual(['x.arr.0.name.a']);\n    });\n  });\n  describe('splitPattern', () => {\n    it('should splict pattern correctly', () => {\n      expect(Glob.splitPattern('a.b.*.c.*.d')).toEqual(['a.b', '*', 'c', '*', 'd']);\n      expect(Glob.splitPattern('a.b.*.c.*')).toEqual(['a.b', '*', 'c', '*']);\n      expect(Glob.splitPattern('a.b.*.*.*.d')).toEqual(['a.b', '*', '*', '*', 'd']);\n      expect(Glob.splitPattern('*.*.c.*.d')).toEqual(['*', '*', 'c', '*', 'd']);\n    });\n  });\n  describe('getSubPaths', () => {\n    it('should get sub paths for valid object', () => {\n      const obj = {\n        a: {\n          b: {\n            x1: {\n              y1: 1,\n            },\n            x2: {\n              y2: 2,\n            },\n          },\n        },\n      };\n      expect(Glob.getSubPaths(['a.b'], obj)).toEqual(['a.b.x1', 'a.b.x2']);\n      expect(Glob.getSubPaths(['a.b', 'a.b.x1'], obj)).toEqual(['a.b.x1', 'a.b.x2', 'a.b.x1.y1']);\n    });\n    it('should get sub paths for array', () => {\n      const obj = {\n        a: {\n          b: {\n            x1: [1, 2],\n          },\n        },\n      };\n      expect(Glob.getSubPaths(['a.b.x1'], obj)).toEqual(['a.b.x1.0', 'a.b.x1.1']);\n    });\n    it('should get sub paths when root obj is array', () => {\n      const obj = [\n        {\n          x1: [1, 2],\n        },\n        {\n          x2: [1, 2],\n        },\n      ];\n      expect(Glob.getSubPaths(['0.x1'], obj)).toEqual(['0.x1.0', '0.x1.1']);\n    });\n    it('should return empty array when obj is not object nor array', () => {\n      expect(Glob.getSubPaths(['x.y'], 1)).toEqual([]);\n      expect(Glob.getSubPaths(['x.y'], 'x')).toEqual([]);\n      expect(Glob.getSubPaths(['x.y'], undefined)).toEqual([]);\n    });\n    it('should return empty array when obj has no value for given path', () => {\n      const obj = {\n        a: {\n          b: {\n            x1: [1, 2],\n          },\n        },\n      };\n      expect(Glob.getSubPaths(['a.b.c'], obj)).toEqual([]);\n    });\n  });\n  describe('findMatchPathsWithEmptyValue', () => {\n    it('return original path array if no *', () => {\n      const obj = { a: { b: { c: 1 } } };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.b.c')).toEqual(['a.b.c']);\n    });\n    it('return original path array if no * and value is empty on multiple layers', () => {\n      const obj = { a: {} };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.b.c')).toEqual(['a.b.c']);\n    });\n    it('return original path array if no * and value is empty on multiple layers', () => {\n      const obj = { a: { b: {} } };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.x.y')).toEqual(['a.x.y']);\n    });\n    it('return array with original path even if path does not exists, but the original path does not contain * ', () => {\n      const obj = { a: { b: { c: 1 } } };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.b.c.d')).toEqual(['a.b.c.d']);\n    });\n    it('return original path array if no * and path related value is undefined in object', () => {\n      const obj = { a: { b: { c: {} } } };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.b.c.d')).toEqual(['a.b.c.d']);\n    });\n    it('object: when * is in middle of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.*.c')).toEqual(['a.b.c']);\n    });\n    it('object:when * is at the end of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'a.*')).toEqual(['a.b']);\n    });\n    // 暂时不支持该场景，见glob.ts 中 143行说明\n    it('object: * 后面数据异构', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, '*.y')).toEqual(['a.y', 'x.y']);\n    });\n    it('object:when * is at the start and end of the path', () => {\n      const obj = {\n        a: { b: { c: 1 } },\n        x: { y: { z: 2 } },\n      };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, '*.y.*')).toEqual(['x.y.z']);\n    });\n    it('array: when * is at the end of the path', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: { a: 1, b: 2 },\n          },\n          {\n            x: 10,\n            y: {\n              a: 10,\n              b: 20,\n            },\n          },\n        ],\n      };\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'arr.*')).toEqual(['arr.0', 'arr.1']);\n    });\n    it('array: when * is at the start of the path', () => {\n      const arr = [\n        {\n          x: 1,\n          y: { a: 1, b: 2 },\n        },\n        {\n          x: 10,\n          y: {\n            a: 10,\n            b: 20,\n          },\n        },\n      ];\n\n      expect(Glob.findMatchPathsWithEmptyValue(arr, '*')).toEqual(['0', '1']);\n    });\n    it('array: when * is in the middle of the path', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: { a: 1, b: 2 },\n          },\n          {\n            x: 10,\n            y: {\n              a: 10,\n              b: 20,\n            },\n          },\n        ],\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'arr.*.y')).toEqual(['arr.0.y', 'arr.1.y']);\n    });\n    it('array: when data related to path is undefined', () => {\n      const obj = [{ a: 1 }, { a: 2, b: 3 }];\n      expect(Glob.findMatchPathsWithEmptyValue(obj, '*.b')).toEqual(['0.b', '1.b']);\n    });\n    it('array in array: when double * ', () => {\n      const obj = {\n        other: 100,\n        arr: [\n          {\n            x: 1,\n            y: ['1', '2'],\n          },\n        ],\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'arr.*.y.*')).toEqual([\n        'arr.0.y.0',\n        'arr.0.y.1',\n      ]);\n    });\n    it('array in object: when double * ', () => {\n      const obj = {\n        x: 100,\n        y: {\n          arr: [1, 2],\n        },\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'y.*.*')).toEqual(['y.arr.0', 'y.arr.1']);\n    });\n    it('array in object: when double * start ', () => {\n      const obj = {\n        x: 100,\n        y: {\n          arr: [{ a: 1, b: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, '*.arr.*')).toEqual(['y.arr.0']);\n    });\n    it('when value after * is  empty string ', () => {\n      const obj = {\n        $$input_decorator$$: {\n          inputParameters: [{ name: '', input: 2 }],\n        },\n      };\n\n      expect(\n        Glob.findMatchPathsWithEmptyValue(obj, '$$input_decorator$$.inputParameters.*.name')\n      ).toEqual(['$$input_decorator$$.inputParameters.0.name']);\n    });\n    it('when value after * is undefined ', () => {\n      const obj = {\n        x: {\n          arr: [{ name: undefined, input: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'x.arr.*.name')).toEqual(['x.arr.0.name']);\n    });\n    it('when value not directly after * is undefined ', () => {\n      const obj = {\n        x: {\n          arr: [{ name: { a: undefined }, input: 2 }],\n        },\n      };\n\n      expect(Glob.findMatchPathsWithEmptyValue(obj, 'x.arr.*.name.a')).toEqual(['x.arr.0.name.a']);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/object.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { getIn, isEmptyArray, isNaN, isPromise, shallowSetIn } from '../src/utils';\n\ndescribe('object', () => {\n  describe('isEmptyArray', () => {\n    it('returns true when an empty array is passed in', () => {\n      expect(isEmptyArray([])).toBe(true);\n    });\n    it('returns false when anything other than empty array is passed in', () => {\n      expect(isEmptyArray()).toBe(false);\n      expect(isEmptyArray(null)).toBe(false);\n      expect(isEmptyArray(123)).toBe(false);\n      expect(isEmptyArray('abc')).toBe(false);\n      expect(isEmptyArray({})).toBe(false);\n      expect(isEmptyArray({ a: 1 })).toBe(false);\n      expect(isEmptyArray(['abc'])).toBe(false);\n    });\n  });\n\n  describe('getIn', () => {\n    const obj = {\n      a: {\n        b: 2,\n        c: false,\n        d: null,\n      },\n      t: true,\n      s: 'a random string',\n    };\n\n    it('gets a value by array path', () => {\n      expect(getIn(obj, ['a', 'b'])).toBe(2);\n    });\n\n    it('gets a value by string path', () => {\n      expect(getIn(obj, 'a.b')).toBe(2);\n    });\n\n    it('return \"undefined\" if value was not found using given path', () => {\n      expect(getIn(obj, 'a.z')).toBeUndefined();\n    });\n\n    it('return \"undefined\" if value was not found using given path and an intermediate value is \"false\"', () => {\n      expect(getIn(obj, 'a.c.z')).toBeUndefined();\n    });\n\n    it('return \"undefined\" if value was not found using given path and an intermediate value is \"null\"', () => {\n      expect(getIn(obj, 'a.d.z')).toBeUndefined();\n    });\n\n    it('return \"undefined\" if value was not found using given path and an intermediate value is \"true\"', () => {\n      expect(getIn(obj, 't.z')).toBeUndefined();\n    });\n\n    it('return \"undefined\" if value was not found using given path and an intermediate value is a string', () => {\n      expect(getIn(obj, 's.z')).toBeUndefined();\n    });\n  });\n\n  describe('shallowSetIn', () => {\n    it('sets flat value', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'flat', 'value');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', flat: 'value' });\n    });\n\n    it('keep the same object if nothing is changed', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'x', 'y');\n      expect(obj).toBe(newObj);\n    });\n\n    it('keep key shen set undefined', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'x', undefined);\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: undefined });\n      expect(Object.keys(newObj)).toEqual(['x']);\n    });\n\n    it('sets nested value', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'nested.value', 'nested value');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', nested: { value: 'nested value' } });\n    });\n\n    it('updates nested value', () => {\n      const obj = { x: 'y', nested: { value: 'a' } };\n      const newObj = shallowSetIn(obj, 'nested.value', 'b');\n      expect(obj).toEqual({ x: 'y', nested: { value: 'a' } });\n      expect(newObj).toEqual({ x: 'y', nested: { value: 'b' } });\n    });\n\n    it('updates deep nested value', () => {\n      const obj = { x: 'y', twofoldly: { nested: { value: 'a' } } };\n      const newObj = shallowSetIn(obj, 'twofoldly.nested.value', 'b');\n      expect(obj.twofoldly.nested === newObj.twofoldly.nested).toEqual(false); // fails, same object still\n      expect(obj).toEqual({ x: 'y', twofoldly: { nested: { value: 'a' } } }); // fails, it's b here, too\n      expect(newObj).toEqual({ x: 'y', twofoldly: { nested: { value: 'b' } } }); // works ofc\n    });\n\n    it('shallow clone data along the update path', () => {\n      const obj = {\n        x: 'y',\n        twofoldly: { nested: ['a', { c: 'd' }] },\n        other: { nestedOther: 'o' },\n      };\n      const newObj = shallowSetIn(obj, 'twofoldly.nested.0', 'b');\n      // All new objects/arrays created along the update path.\n      expect(obj).not.toBe(newObj);\n      expect(obj.twofoldly).not.toBe(newObj.twofoldly);\n      expect(obj.twofoldly.nested).not.toBe(newObj.twofoldly.nested);\n      // All other objects/arrays copied, not cloned (retain same memory\n      // location).\n      expect(obj.other).toBe(newObj.other);\n      expect(obj.twofoldly.nested[1]).toBe(newObj.twofoldly.nested[1]);\n    });\n\n    it('sets new array', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'nested.0', 'value');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', nested: ['value'] });\n    });\n\n    it('sets new array when item is empty string', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'nested.0', '');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', nested: [''] });\n    });\n\n    it('sets new array when item is empty string', () => {\n      const obj = {};\n      const newObj = shallowSetIn(obj, 'nested.0', '');\n      expect(obj).toEqual({});\n      expect(newObj).toEqual({ nested: [''] });\n    });\n\n    it('updates nested array value', () => {\n      const obj = { x: 'y', nested: ['a'] };\n      const newObj = shallowSetIn(obj, 'nested[0]', 'b');\n      expect(obj).toEqual({ x: 'y', nested: ['a'] });\n      expect(newObj).toEqual({ x: 'y', nested: ['b'] });\n    });\n\n    it('adds new item to nested array', () => {\n      const obj = { x: 'y', nested: ['a'] };\n      const newObj = shallowSetIn(obj, 'nested.1', 'b');\n      expect(obj).toEqual({ x: 'y', nested: ['a'] });\n      expect(newObj).toEqual({ x: 'y', nested: ['a', 'b'] });\n    });\n\n    it('sticks to object with int key when defined', () => {\n      const obj = { x: 'y', nested: { 0: 'a' } };\n      const newObj = shallowSetIn(obj, 'nested.0', 'b');\n      expect(obj).toEqual({ x: 'y', nested: { 0: 'a' } });\n      expect(newObj).toEqual({ x: 'y', nested: { 0: 'b' } });\n    });\n\n    it('supports bracket path', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'nested[0]', 'value');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', nested: ['value'] });\n    });\n\n    it('supports path containing key of the object', () => {\n      const obj = { x: 'y' };\n      const newObj = shallowSetIn(obj, 'a.x.c', 'value');\n      expect(obj).toEqual({ x: 'y' });\n      expect(newObj).toEqual({ x: 'y', a: { x: { c: 'value' } } });\n    });\n\n    // This case is not used in form sdk for now，so we comment it.\n    // it('should keep class inheritance for the top level object', () => {\n    //   class TestClass {\n    //     constructor(public key: string, public setObj?: any) {}\n    //   }\n    //   const obj = new TestClass('value');\n    //   const newObj = shallowSetIn(obj, 'setObj.nested', 'shallowSetInValue');\n    //   expect(obj).toEqual(new TestClass('value'));\n    //   expect(newObj).toEqual({\n    //     key: 'value',\n    //     setObj: { nested: 'shallowSetInValue' },\n    //   });\n    //   expect(obj instanceof TestClass).toEqual(true);\n    //   expect(newObj instanceof TestClass).toEqual(true);\n    // });\n\n    it('can convert primitives to objects before setting', () => {\n      const obj = { x: [{ y: true }] };\n      const newObj = shallowSetIn(obj, 'x.0.y.z', true);\n      expect(obj).toEqual({ x: [{ y: true }] });\n      expect(newObj).toEqual({ x: [{ y: { z: true } }] });\n    });\n    it('set undefined value with unknown key', () => {\n      const obj = { a: '' };\n      let newObj = shallowSetIn(obj, 'a', undefined);\n      newObj = shallowSetIn(newObj, 'b', undefined);\n      expect(obj).toEqual({ a: '' });\n      expect(newObj).toEqual({ a: undefined, b: undefined });\n    });\n  });\n\n  describe('isPromise', () => {\n    it('verifies that a value is a promise', () => {\n      const alwaysResolve = (resolve: Function) => resolve();\n      const promise = new Promise(alwaysResolve);\n      expect(isPromise(promise)).toEqual(true);\n    });\n\n    it('verifies that a value is not a promise', () => {\n      const emptyObject = {};\n      const identity = (i: any) => i;\n      const foo = 'foo';\n      const answerToLife = 42;\n\n      expect(isPromise(emptyObject)).toEqual(false);\n      expect(isPromise(identity)).toEqual(false);\n      expect(isPromise(foo)).toEqual(false);\n      expect(isPromise(answerToLife)).toEqual(false);\n\n      expect(isPromise(undefined)).toEqual(false);\n      expect(isPromise(null)).toEqual(false);\n    });\n  });\n\n  describe('isNaN', () => {\n    it('correctly validate NaN', () => {\n      expect(isNaN(NaN)).toBe(true);\n    });\n\n    it('correctly validate not NaN', () => {\n      expect(isNaN(undefined)).toBe(false);\n      expect(isNaN(1)).toBe(false);\n      expect(isNaN('')).toBe(false);\n      expect(isNaN([])).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/path.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { Path } from '../src/core/path';\n\ndescribe('path', () => {\n  it('toString', () => {\n    expect(new Path('a.b.c').toString()).toEqual('a.b.c');\n    expect(new Path('a.b[0].c').toString()).toEqual('a.b.0.c');\n    expect(new Path(['a', 'b', 'c']).toString()).toEqual('a.b.c');\n  });\n  it('parent', () => {\n    expect(new Path('a.b.c').parent!.toString()).toEqual('a.b');\n    expect(new Path('a.b[0]').parent!.toString()).toEqual('a.b');\n    expect(new Path('a').parent).toEqual(undefined);\n  });\n  it('isChild', () => {\n    expect(new Path('a.b').isChild('a.b.c')).toEqual(true);\n    expect(new Path('a.b').isChild('a.b[0]')).toEqual(true);\n  });\n  it('concat', () => {\n    expect(new Path('a').concat('b').toString()).toEqual('a.b');\n    expect(new Path('a').concat('b.c').toString()).toEqual('a.b.c');\n    expect(new Path('a').concat(0).toString()).toEqual('a.0');\n    expect(() => {\n      new Path('a').concat({} as any);\n    }).toThrowError(/invalid param type/);\n  });\n  it('compareArrayPath', () => {\n    expect((Path.compareArrayPath(new Path('a.b.0'), new Path('a.b.1')) as number) < 0).toBe(true);\n    expect((Path.compareArrayPath(new Path('a.b.0'), new Path('a.b.2')) as number) < 0).toBe(true);\n    expect((Path.compareArrayPath(new Path('a.b.1'), new Path('a.b.0')) as number) < 0).toBe(false);\n    expect((Path.compareArrayPath(new Path('a.b.0'), new Path('a.b.0')) as number) === 0).toBe(\n      true\n    );\n    expect((Path.compareArrayPath(new Path('a.b.1'), new Path('a.b.0.x')) as number) < 0).toBe(\n      false\n    );\n    expect((Path.compareArrayPath(new Path('a.b.1.y'), new Path('a.b.0.x')) as number) < 0).toBe(\n      false\n    );\n    expect(() => Path.compareArrayPath(new Path('a.1'), new Path('a.b.0'))).toThrowError();\n    expect(() => Path.compareArrayPath(new Path(''), new Path(''))).toThrowError();\n    expect(() => Path.compareArrayPath(new Path('a.b.c'), new Path('a.b'))).toThrowError();\n  });\n  it('isChildOrGrandChild', () => {\n    expect(new Path('a.b').isChildOrGrandChild('a.b.c')).toEqual(true);\n    expect(new Path('a.b').isChildOrGrandChild('a.b[0]')).toEqual(true);\n    expect(new Path('a.b').isChildOrGrandChild('a.b.1')).toEqual(true);\n    expect(new Path('a.b').isChildOrGrandChild('a.b')).toEqual(false);\n    expect(new Path('a.b').isChildOrGrandChild('a')).toEqual(false);\n    expect(new Path('').isChildOrGrandChild('a')).toEqual(true);\n  });\n\n  it('replaceParent', () => {\n    expect(new Path('a.b.c.d').replaceParent(new Path('a.b'), new Path('x.y')).toString()).toEqual(\n      'x.y.c.d'\n    );\n    expect(new Path('a.b.0.d').replaceParent(new Path('a.b'), new Path('x.y')).toString()).toEqual(\n      'x.y.0.d'\n    );\n    expect(new Path('0.d').replaceParent(new Path(''), new Path('x.y')).toString()).toEqual(\n      'x.y.0.d'\n    );\n    expect(\n      new Path('a.b.c').replaceParent(new Path('a.b.c'), new Path('x.y.z')).toString()\n    ).toEqual('x.y.z');\n    expect(() =>\n      new Path('a.b.0.d').replaceParent(new Path('a1.b'), new Path('x.y')).toString()\n    ).toThrowError();\n\n    expect(() =>\n      new Path('a.0.d').replaceParent(new Path('a.0.d.e'), new Path('x.y')).toString()\n    ).toThrowError();\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/to-field-array.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { toFieldArray } from '@/core/to-field-array';\nimport { FormModel } from '@/core/form-model';\nimport { FieldArrayModel } from '@/core/field-array-model';\n\ndescribe('toFieldArray', () => {\n  let formModel: FormModel;\n  let fieldArrayModel: FieldArrayModel;\n\n  beforeEach(() => {\n    formModel = new FormModel();\n    formModel.init({});\n    formModel.createFieldArray('users');\n    fieldArrayModel = formModel.getField<FieldArrayModel>('users')!;\n  });\n\n  it('should convert FieldArrayModel to FieldArray', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    expect(fieldArray).toBeDefined();\n    expect(fieldArray.name).toBe('users');\n    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);\n  });\n\n  it('should expose key property from model id', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    expect(fieldArray.key).toBe(fieldArrayModel.id);\n  });\n\n  it('should expose name property from model path', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    expect(fieldArray.name).toBe('users');\n    expect(fieldArray.name).toBe(fieldArrayModel.path.toString());\n  });\n\n  it('should expose value property from model value', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);\n    expect(fieldArray.value).toBe(fieldArrayModel.value);\n  });\n\n  it('should update model value via onChange', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n    const newValue = [{ name: 'Charlie' }];\n\n    fieldArray.onChange(newValue);\n\n    expect(fieldArrayModel.value).toEqual(newValue);\n    expect(fieldArray.value).toEqual(newValue);\n  });\n\n  it('should map over array elements correctly', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    const mapped = fieldArray.map((field, index) => {\n      expect(field).toBeDefined();\n      expect(field.name).toBe(`users.${index}`);\n      return field.value;\n    });\n\n    expect(mapped).toHaveLength(2);\n    expect(mapped).toEqual([{ name: 'Alice' }, { name: 'Bob' }]);\n  });\n\n  it('should append new item and return Field', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n    const newItem = { name: 'Charlie' };\n\n    const newField = fieldArray.append(newItem);\n\n    expect(newField).toBeDefined();\n    expect(newField.name).toBe('users.0');\n    expect(newField.value).toEqual(newItem);\n    expect(fieldArrayModel.value).toEqual([newItem]);\n  });\n\n  it('should delete item by index', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    fieldArray.delete(1);\n\n    expect(fieldArrayModel.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);\n    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);\n  });\n\n  it('should remove item by index (same as delete)', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    fieldArray.remove(1);\n\n    expect(fieldArrayModel.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);\n    expect(fieldArray.value).toEqual([{ name: 'Alice' }, { name: 'Charlie' }]);\n  });\n\n  it('should swap items at two indices', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    fieldArray.swap(0, 2);\n\n    expect(fieldArrayModel.value).toEqual([\n      { name: 'Charlie' },\n      { name: 'Bob' },\n      { name: 'Alice' },\n    ]);\n    expect(fieldArray.value).toEqual([{ name: 'Charlie' }, { name: 'Bob' }, { name: 'Alice' }]);\n  });\n\n  it('should move item from one index to another', () => {\n    fieldArrayModel.value = [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Charlie' }];\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    fieldArray.move(0, 2);\n\n    expect(fieldArrayModel.value).toEqual([\n      { name: 'Bob' },\n      { name: 'Charlie' },\n      { name: 'Alice' },\n    ]);\n    expect(fieldArray.value).toEqual([{ name: 'Bob' }, { name: 'Charlie' }, { name: 'Alice' }]);\n  });\n\n  it('should hide _fieldModel property (non-enumerable)', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    // _fieldModel should exist but not be enumerable\n    expect((fieldArray as any)._fieldModel).toBe(fieldArrayModel);\n    expect(Object.keys(fieldArray)).not.toContain('_fieldModel');\n  });\n\n  it('should support complex nested operations', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    // Append multiple items\n    fieldArray.append({ name: 'Alice', age: 30 });\n    fieldArray.append({ name: 'Bob', age: 25 });\n    fieldArray.append({ name: 'Charlie', age: 35 });\n\n    expect(fieldArray.value).toHaveLength(3);\n\n    // Map and modify\n    const names = fieldArray.map((field) => field.value.name);\n    expect(names).toEqual(['Alice', 'Bob', 'Charlie']);\n\n    // Swap\n    fieldArray.swap(0, 1);\n    expect(fieldArray.value[0].name).toBe('Bob');\n    expect(fieldArray.value[1].name).toBe('Alice');\n\n    // Remove\n    fieldArray.remove(2);\n    expect(fieldArray.value).toHaveLength(2);\n\n    // Move\n    fieldArray.move(1, 0);\n    expect(fieldArray.value[0].name).toBe('Alice');\n    expect(fieldArray.value[1].name).toBe('Bob');\n  });\n\n  it('should work with empty array', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    // Value might be undefined or empty array initially\n    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);\n\n    const mapped = fieldArray.map((field) => field.value);\n    expect(Array.isArray(mapped)).toBe(true);\n    expect(mapped.length === 0 || mapped.length > 0).toBe(true);\n  });\n\n  it('should preserve reactivity through getters', () => {\n    const fieldArray = toFieldArray(fieldArrayModel);\n\n    // Initial value might be undefined or empty array\n    expect(fieldArray.value === undefined || Array.isArray(fieldArray.value)).toBe(true);\n\n    // Modify through model\n    fieldArrayModel.value = [{ name: 'Alice' }];\n\n    // Should reflect in fieldArray (getter)\n    expect(fieldArray.value).toEqual([{ name: 'Alice' }]);\n\n    // Modify through fieldArray\n    fieldArray.onChange([{ name: 'Bob' }]);\n\n    // Should reflect in model\n    expect(fieldArrayModel.value).toEqual([{ name: 'Bob' }]);\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/to-field.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { ValidateTrigger } from '@/types';\nimport { toField, toFieldState } from '@/core/to-field';\nimport { FormModel } from '@/core/form-model';\nimport { FieldModel } from '@/core/field-model';\n\ndescribe('toField', () => {\n  let formModel: FormModel;\n  let fieldModel: FieldModel;\n\n  beforeEach(() => {\n    formModel = new FormModel();\n    formModel.init({});\n    fieldModel = formModel.createField('username') as FieldModel;\n  });\n\n  it('should convert FieldModel to Field', () => {\n    const field = toField(fieldModel);\n\n    expect(field).toBeDefined();\n    expect(field.name).toBe('username');\n    expect(field.value).toBeUndefined();\n  });\n\n  it('should expose name property from model', () => {\n    const field = toField(fieldModel);\n\n    expect(field.name).toBe(fieldModel.name);\n  });\n\n  it('should expose value property from model', () => {\n    fieldModel.value = 'John';\n    const field = toField(fieldModel);\n\n    expect(field.value).toBe('John');\n    expect(field.value).toBe(fieldModel.value);\n  });\n\n  describe('onChange', () => {\n    it('should update model value with plain value', () => {\n      const field = toField(fieldModel);\n\n      field.onChange('Alice');\n\n      expect(fieldModel.value).toBe('Alice');\n      expect(field.value).toBe('Alice');\n    });\n\n    it('should handle React change event for input', () => {\n      const field = toField(fieldModel);\n      const mockEvent = {\n        target: {\n          value: 'Bob',\n        },\n      } as React.ChangeEvent<HTMLInputElement>;\n\n      field.onChange(mockEvent);\n\n      expect(fieldModel.value).toBe('Bob');\n    });\n\n    it('should handle React change event for checkbox (checked)', () => {\n      const field = toField(fieldModel);\n      const mockEvent = {\n        target: {\n          type: 'checkbox',\n          checked: true,\n          value: 'on',\n        },\n      } as React.ChangeEvent<HTMLInputElement>;\n\n      field.onChange(mockEvent);\n\n      expect(fieldModel.value).toBe(true);\n    });\n\n    it('should handle React change event for checkbox (unchecked)', () => {\n      const field = toField(fieldModel);\n      const mockEvent = {\n        target: {\n          type: 'checkbox',\n          checked: false,\n          value: 'on',\n        },\n      } as React.ChangeEvent<HTMLInputElement>;\n\n      field.onChange(mockEvent);\n\n      expect(fieldModel.value).toBe(false);\n    });\n\n    it('should handle numeric value', () => {\n      const field = toField(fieldModel);\n\n      field.onChange(42);\n\n      expect(fieldModel.value).toBe(42);\n    });\n\n    it('should handle object value', () => {\n      const field = toField(fieldModel);\n      const objValue = { name: 'test', value: 123 };\n\n      field.onChange(objValue);\n\n      expect(fieldModel.value).toEqual(objValue);\n    });\n\n    it('should handle array value', () => {\n      const field = toField(fieldModel);\n      const arrValue = ['a', 'b', 'c'];\n\n      field.onChange(arrValue);\n\n      expect(fieldModel.value).toEqual(arrValue);\n    });\n  });\n\n  describe('onBlur', () => {\n    it('should call validate when validateTrigger is onBlur', () => {\n      formModel.dispose();\n      formModel = new FormModel();\n      formModel.init({ validateTrigger: ValidateTrigger.onBlur });\n      fieldModel = formModel.createField('username') as FieldModel;\n\n      const validateSpy = vi.spyOn(fieldModel, 'validate');\n      const field = toField(fieldModel);\n\n      field.onBlur?.();\n\n      expect(validateSpy).toHaveBeenCalled();\n    });\n\n    it('should not trigger validation when validateTrigger is not onBlur', () => {\n      formModel.dispose();\n      formModel = new FormModel();\n      formModel.init({ validateTrigger: ValidateTrigger.onChange });\n      fieldModel = formModel.createField('username') as FieldModel;\n\n      const validateSpy = vi.spyOn(fieldModel, 'validate');\n      const field = toField(fieldModel);\n\n      field.onBlur?.();\n\n      expect(validateSpy).not.toHaveBeenCalled();\n    });\n\n    it('should not trigger validation when validateTrigger is onSubmit', () => {\n      formModel.dispose();\n      formModel = new FormModel();\n      formModel.init({ validateTrigger: ValidateTrigger.onSubmit });\n      fieldModel = formModel.createField('username') as FieldModel;\n\n      const validateSpy = vi.spyOn(fieldModel, 'validate');\n      const field = toField(fieldModel);\n\n      field.onBlur?.();\n\n      expect(validateSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('onFocus', () => {\n    it('should set isTouched to true', () => {\n      const field = toField(fieldModel);\n\n      expect(fieldModel.state.isTouched).toBe(false);\n\n      field.onFocus?.();\n\n      expect(fieldModel.state.isTouched).toBe(true);\n    });\n\n    it('should set isTouched only once', () => {\n      const field = toField(fieldModel);\n\n      field.onFocus?.();\n      expect(fieldModel.state.isTouched).toBe(true);\n\n      field.onFocus?.();\n      expect(fieldModel.state.isTouched).toBe(true);\n    });\n  });\n\n  it('should expose key property (non-enumerable)', () => {\n    const field = toField(fieldModel);\n\n    expect((field as any).key).toBe(fieldModel.id);\n    expect(Object.keys(field)).not.toContain('key');\n  });\n\n  it('should hide _fieldModel property (non-enumerable)', () => {\n    const field = toField(fieldModel);\n\n    expect((field as any)._fieldModel).toBe(fieldModel);\n    expect(Object.keys(field)).not.toContain('_fieldModel');\n  });\n\n  it('should preserve reactivity through getters', () => {\n    const field = toField(fieldModel);\n\n    expect(field.name).toBe('username');\n    expect(field.value).toBeUndefined();\n\n    fieldModel.value = 'NewValue';\n\n    expect(field.value).toBe('NewValue');\n  });\n});\n\ndescribe('toFieldState', () => {\n  let formModel: FormModel;\n  let fieldModel: FieldModel;\n\n  beforeEach(() => {\n    formModel = new FormModel();\n    formModel.init({});\n    fieldModel = formModel.createField('username') as FieldModel;\n  });\n\n  it('should convert FieldModelState to FieldState', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState).toBeDefined();\n    expect(fieldState.isTouched).toBe(false);\n    expect(fieldState.isDirty).toBe(false);\n    expect(fieldState.invalid).toBe(false);\n    expect(fieldState.isValidating).toBe(false);\n  });\n\n  it('should reflect isTouched state', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.isTouched).toBe(false);\n\n    fieldModel.state.isTouched = true;\n\n    expect(fieldState.isTouched).toBe(true);\n  });\n\n  it('should reflect isDirty state', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.isDirty).toBe(false);\n\n    // Manually set dirty state\n    fieldModel.state.isDirty = true;\n\n    expect(fieldState.isDirty).toBe(true);\n  });\n\n  it('should reflect invalid state', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.invalid).toBe(false);\n\n    fieldModel.state.invalid = true;\n\n    expect(fieldState.invalid).toBe(true);\n  });\n\n  it('should reflect isValidating state', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.isValidating).toBe(false);\n\n    fieldModel.state.isValidating = true;\n\n    expect(fieldState.isValidating).toBe(true);\n  });\n\n  it('should return errors as flat array', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.errors).toBeUndefined();\n\n    fieldModel.state.errors = {\n      validate1: ['Error 1', 'Error 2'],\n      validate2: ['Error 3'],\n    };\n\n    expect(fieldState.errors).toEqual(['Error 1', 'Error 2', 'Error 3']);\n  });\n\n  it('should return warnings as flat array', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.warnings).toBeUndefined();\n\n    fieldModel.state.warnings = {\n      validate1: ['Warning 1', 'Warning 2'],\n      validate2: ['Warning 3'],\n    };\n\n    expect(fieldState.warnings).toEqual(['Warning 1', 'Warning 2', 'Warning 3']);\n  });\n\n  it('should handle empty errors object', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    fieldModel.state.errors = {};\n\n    expect(fieldState.errors).toEqual([]);\n  });\n\n  it('should handle empty warnings object', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    fieldModel.state.warnings = {};\n\n    expect(fieldState.warnings).toEqual([]);\n  });\n\n  it('should preserve reactivity through getters', () => {\n    const fieldState = toFieldState(fieldModel.state);\n\n    expect(fieldState.isTouched).toBe(false);\n\n    fieldModel.state.isTouched = true;\n\n    expect(fieldState.isTouched).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/to-form.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\n\nimport { toForm, toFormState } from '@/core/to-form';\nimport { FormModel } from '@/core/form-model';\n\ndescribe('toForm', () => {\n  let formModel: FormModel;\n\n  beforeEach(() => {\n    formModel = new FormModel();\n    formModel.init({\n      initialValues: {\n        username: 'John',\n        email: 'john@example.com',\n        age: 30,\n      },\n    });\n  });\n\n  it('should convert FormModel to Form', () => {\n    const form = toForm(formModel);\n\n    expect(form).toBeDefined();\n    expect(form.initialValues).toEqual({\n      username: 'John',\n      email: 'john@example.com',\n      age: 30,\n    });\n  });\n\n  it('should expose initialValues from model', () => {\n    const form = toForm(formModel);\n\n    expect(form.initialValues).toBe(formModel.initialValues);\n  });\n\n  it('should expose values getter from model', () => {\n    const form = toForm(formModel);\n\n    expect(form.values).toEqual({\n      username: 'John',\n      email: 'john@example.com',\n      age: 30,\n    });\n    expect(form.values).toEqual(formModel.values);\n  });\n\n  it('should expose values setter to update model', () => {\n    const form = toForm(formModel);\n    const newValues = {\n      username: 'Alice',\n      email: 'alice@example.com',\n      age: 25,\n    };\n\n    form.values = newValues;\n\n    expect(formModel.values).toEqual(newValues);\n    expect(form.values).toEqual(newValues);\n  });\n\n  it('should expose state as FormState', () => {\n    const form = toForm(formModel);\n\n    expect(form.state).toBeDefined();\n    expect(form.state.isTouched).toBe(false);\n    expect(form.state.isDirty).toBe(false);\n    expect(form.state.invalid).toBe(false);\n    expect(form.state.isValidating).toBe(false);\n  });\n\n  describe('getValueIn', () => {\n    it('should get value by field name', () => {\n      const form = toForm(formModel);\n\n      expect(form.getValueIn('username')).toBe('John');\n      expect(form.getValueIn('email')).toBe('john@example.com');\n      expect(form.getValueIn('age')).toBe(30);\n    });\n\n    it('should get nested value by path', () => {\n      formModel.values = {\n        user: {\n          profile: {\n            name: 'Alice',\n            age: 25,\n          },\n        },\n      };\n      const form = toForm(formModel);\n\n      expect(form.getValueIn('user.profile.name')).toBe('Alice');\n      expect(form.getValueIn('user.profile.age')).toBe(25);\n    });\n\n    it('should get array value by index', () => {\n      formModel.values = {\n        users: [{ name: 'Alice' }, { name: 'Bob' }],\n      };\n      const form = toForm(formModel);\n\n      expect(form.getValueIn('users.0.name')).toBe('Alice');\n      expect(form.getValueIn('users.1.name')).toBe('Bob');\n    });\n\n    it('should return undefined for non-existent path', () => {\n      const form = toForm(formModel);\n\n      expect(form.getValueIn('nonexistent')).toBeUndefined();\n    });\n  });\n\n  describe('setValueIn', () => {\n    it('should set value by field name', () => {\n      const form = toForm(formModel);\n\n      form.setValueIn('username', 'Bob');\n\n      expect(formModel.values.username).toBe('Bob');\n      expect(form.values.username).toBe('Bob');\n    });\n\n    it('should set nested value by path', () => {\n      formModel.values = {\n        user: {\n          profile: {\n            name: 'Alice',\n            age: 25,\n          },\n        },\n      };\n      const form = toForm(formModel);\n\n      form.setValueIn('user.profile.name', 'Charlie');\n\n      expect(formModel.values.user.profile.name).toBe('Charlie');\n    });\n\n    it('should set array value by index', () => {\n      formModel.values = {\n        users: [{ name: 'Alice' }, { name: 'Bob' }],\n      };\n      const form = toForm(formModel);\n\n      form.setValueIn('users.0.name', 'Charlie');\n\n      expect(formModel.values.users[0].name).toBe('Charlie');\n    });\n\n    it('should create nested structure if not exists', () => {\n      formModel.values = {};\n      const form = toForm(formModel);\n\n      form.setValueIn('user.profile.name', 'Alice');\n\n      expect(formModel.values.user.profile.name).toBe('Alice');\n    });\n  });\n\n  describe('validate', () => {\n    it('should bind model validate method', () => {\n      const form = toForm(formModel);\n\n      expect(form.validate).toBeDefined();\n      expect(typeof form.validate).toBe('function');\n    });\n\n    it('should call form validate method', async () => {\n      const form = toForm(formModel);\n\n      // Validate should be callable\n      const result = await form.validate();\n\n      // Without validators, should return empty object or undefined\n      expect(result === undefined || Object.keys(result || {}).length === 0).toBe(true);\n    });\n  });\n\n  it('should hide _formModel property (non-enumerable)', () => {\n    const form = toForm(formModel);\n\n    expect((form as any)._formModel).toBe(formModel);\n    expect(Object.keys(form)).not.toContain('_formModel');\n  });\n\n  it('should preserve reactivity through getters', () => {\n    const form = toForm(formModel);\n\n    expect(form.values.username).toBe('John');\n\n    formModel.values = { username: 'Alice' };\n\n    expect(form.values.username).toBe('Alice');\n  });\n\n  it('should work with empty initialValues', () => {\n    const emptyFormModel = new FormModel();\n    emptyFormModel.init({});\n    const form = toForm(emptyFormModel);\n\n    expect(form.initialValues).toBeUndefined();\n    expect(form.values).toBeUndefined();\n  });\n});\n\ndescribe('toFormState', () => {\n  let formModel: FormModel;\n\n  beforeEach(() => {\n    formModel = new FormModel();\n    formModel.init({\n      initialValues: {\n        username: 'John',\n        email: 'john@example.com',\n      },\n    });\n  });\n\n  it('should convert FormModelState to FormState', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState).toBeDefined();\n    expect(formState.isTouched).toBe(false);\n    expect(formState.isDirty).toBe(false);\n    expect(formState.invalid).toBe(false);\n    expect(formState.isValidating).toBe(false);\n  });\n\n  it('should reflect isTouched state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.isTouched).toBe(false);\n\n    formModel.state.isTouched = true;\n\n    expect(formState.isTouched).toBe(true);\n  });\n\n  it('should reflect isDirty state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.isDirty).toBe(false);\n\n    // Manually set dirty state\n    formModel.state.isDirty = true;\n\n    expect(formState.isDirty).toBe(true);\n  });\n\n  it('should reflect invalid state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.invalid).toBe(false);\n\n    formModel.state.invalid = true;\n\n    expect(formState.invalid).toBe(true);\n  });\n\n  it('should reflect isValidating state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.isValidating).toBe(false);\n\n    formModel.state.isValidating = true;\n\n    expect(formState.isValidating).toBe(true);\n  });\n\n  it('should expose errors from model state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.errors).toBeUndefined();\n\n    formModel.state.errors = {\n      username: 'Username is required',\n      email: 'Invalid email format',\n    };\n\n    expect(formState.errors).toEqual({\n      username: 'Username is required',\n      email: 'Invalid email format',\n    });\n  });\n\n  it('should expose warnings from model state', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.warnings).toBeUndefined();\n\n    formModel.state.warnings = {\n      username: 'Username should be longer',\n      email: 'Consider using a different email',\n    };\n\n    expect(formState.warnings).toEqual({\n      username: 'Username should be longer',\n      email: 'Consider using a different email',\n    });\n  });\n\n  it('should preserve reactivity through getters', () => {\n    const formState = toFormState(formModel.state);\n\n    expect(formState.isTouched).toBe(false);\n\n    formModel.state.isTouched = true;\n\n    expect(formState.isTouched).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/utils.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { Errors } from '@/types';\nimport { FieldEventUtils, mergeFeedbacks } from '@/core/utils';\n\ndescribe('core/utils', () => {\n  describe('mergeFeedbacks', () => {\n    it('should merge when some key in source is empty array', () => {\n      const origin = {\n        a: ['error'],\n        b: ['error'],\n      } as unknown as Errors;\n      const source = {\n        a: [],\n      } as unknown as Errors;\n\n      const result = mergeFeedbacks(origin, source);\n      expect(result).toEqual({\n        a: [],\n        b: ['error'],\n      });\n    });\n    it('should merge when some key in source is undefined', () => {\n      const origin = {\n        a: ['error'],\n        b: ['error'],\n      } as unknown as Errors;\n      const source = {\n        a: undefined,\n      } as unknown as Errors;\n\n      const result = mergeFeedbacks(origin, source);\n      expect(result).toEqual({\n        a: undefined,\n        b: ['error'],\n      });\n    });\n  });\n  describe('FieldEventUtils.shouldTriggerFieldChangeEvent', () => {\n    it('array append: should not trigger for all array child or grand child', () => {\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-append',\n              indexes: [0],\n            },\n          },\n          'arr.0',\n        ),\n      ).toBe(false);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-append',\n              indexes: [0],\n            },\n          },\n          'arr.0.x',\n        ),\n      ).toBe(false);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-append',\n              indexes: [0],\n            },\n          },\n          'arr',\n        ),\n      ).toBe(true);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'p.arr',\n            options: {\n              action: 'array-append',\n              indexes: [0],\n            },\n          },\n          'p',\n        ),\n      ).toBe(true);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: '',\n            options: {\n              action: 'array-append',\n              indexes: [0],\n            },\n          },\n          '0',\n        ),\n      ).toBe(false);\n    });\n    it('array splice: should not trigger for array child or grand child  only when index < first spliced index', () => {\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [0],\n            },\n          },\n          'arr.0',\n        ),\n      ).toBe(true);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [0],\n            },\n          },\n          'arr.1',\n        ),\n      ).toBe(true);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [1],\n            },\n          },\n          'arr.0',\n        ),\n      ).toBe(false);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [1, 2],\n            },\n          },\n          'arr.1',\n        ),\n      ).toBe(true);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [4, 5],\n            },\n          },\n          'arr.1',\n        ),\n      ).toBe(false);\n      expect(\n        FieldEventUtils.shouldTriggerFieldChangeEvent(\n          {\n            values: {},\n            prevValues: {},\n            name: 'arr',\n            options: {\n              action: 'array-splice',\n              indexes: [],\n            },\n          },\n          'arr.1',\n        ),\n      ).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/__tests__/validate.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { hasError } from '../src/utils/validate';\nimport { FeedbackLevel, FieldError } from '../src/types';\n\ndescribe('utils/validate', () => {\n  describe('hasError', () => {\n    it('should return false when errors is empty', () => {\n      expect(hasError({ xxx: [] })).toBe(false);\n      expect(hasError({ xxx: undefined })).toBe(false);\n      expect(hasError({})).toBe(false);\n      expect(hasError({ aaa: [], bbb: [] })).toBe(false);\n      expect(hasError({ aaa: undefined, bbb: [] })).toBe(false);\n    });\n    it('should return true when errors is not empty', () => {\n      const mockError: FieldError = { name: 'xxx', level: FeedbackLevel.Error, message: 'err' };\n      expect(hasError({ xxx: [mockError] })).toBe(true);\n      expect(hasError({ aaa: [mockError], bbb: [mockError] })).toBe(true);\n      expect(hasError({ aaa: undefined, bbb: [mockError] })).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/node-engine/form/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/form\",\n  \"version\": \"0.1.8\",\n  \"description\": \"form\",\n  \"keywords\": [\n    \"flow\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/reactive\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"fast-equals\": \"^2.0.0\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@testing-library/react\": \"^12\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/constants.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FieldModelState } from './types/field';\nimport { FormModelState } from './types';\n\nexport const DEFAULT_FIELD_STATE: FieldModelState = {\n  invalid: false,\n  isDirty: false,\n  isTouched: false,\n  isValidating: false,\n};\nexport const DEFAULT_FORM_STATE: FormModelState = {\n  invalid: false,\n  isDirty: false,\n  isTouched: false,\n  isValidating: false,\n};\n\nexport function createFormModelState(initialState?: Partial<FormModelState>) {\n  if (!initialState) {\n    return { ...DEFAULT_FORM_STATE };\n  }\n  return { ...DEFAULT_FORM_STATE, ...initialState };\n}\n\nexport function createFieldModelState(initialState?: Partial<FieldModelState>): FieldModelState {\n  if (!initialState) {\n    return { ...DEFAULT_FIELD_STATE };\n  }\n  return { ...DEFAULT_FIELD_STATE, ...initialState };\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/create-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CreateFormReturn, FormOptions } from '../types/form';\nimport { Field, FieldArray, FieldName, FieldValue } from '../types';\nimport { toForm } from './to-form';\nimport { toFieldArray } from './to-field-array';\nimport { toField } from './to-field';\nimport { FormModel } from './form-model';\nimport { FieldModel } from './field-model';\nimport { FieldArrayModel } from './field-array-model';\n\n// export interface CreateFormOptions<TValues = any> extends FormOptions<TValues> {\n//   parentContainer?: interfaces.Container;\n// }\n\nexport type CreateFormOptions<T = any> = FormOptions<T> & {\n  /**\n   * 为 true 时，createForm 不会对form 初始化， 用户需要手动调用 control.init()\n   * 该配置主要为了解决，用户需要去监听一些form 的初始化事件，那么他需要再配置完监听后再初始化。\n   * 该配置默认为 false\n   **/\n  disableAutoInit?: boolean;\n};\n\nexport function createForm<TValues>(\n  options?: CreateFormOptions<TValues>\n): CreateFormReturn<TValues> {\n  const { disableAutoInit = false, ...formOptions } = options || {};\n  const formModel = new FormModel();\n\n  if (!disableAutoInit) {\n    formModel.init(formOptions || {});\n  }\n\n  return {\n    form: toForm(formModel),\n    control: {\n      _formModel: formModel,\n      getField: <\n        TFieldValue = FieldValue,\n        TFieldModel extends Field<TFieldValue> | FieldArray<TFieldValue> = Field\n      >(\n        name: FieldName\n      ) => {\n        const fieldModel = formModel.getField(name);\n        if (fieldModel) {\n          return fieldModel instanceof FieldArrayModel\n            ? toFieldArray<TFieldValue>(fieldModel as unknown as FieldArrayModel<TFieldValue>)\n            : toField<TFieldValue>(fieldModel as unknown as FieldModel<TFieldValue>);\n        }\n      },\n      init: () => formModel.init(formOptions || {}),\n    },\n  };\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/field-array-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { FieldValue } from '../types';\nimport { Path } from './path';\nimport { FieldModel } from './field-model';\n\nexport class FieldArrayModel<TValue = FieldValue> extends FieldModel<Array<TValue>> {\n  protected onAppendEmitter = new Emitter<{\n    index: number;\n    value: TValue | undefined;\n    arrayValue: Array<TValue>;\n  }>();\n\n  readonly onAppend = this.onAppendEmitter.event;\n\n  protected onDeleteEmitter = new Emitter<{\n    arrayValue: Array<TValue> | undefined;\n    index: number;\n  }>();\n\n  readonly onDelete = this.onDeleteEmitter.event;\n\n  get children() {\n    const fields: FieldModel[] = [];\n    this.form.fieldMap.forEach((field, name: string) => {\n      if (this.path.isChild(name)) {\n        fields.push(field);\n      }\n    });\n\n    // 按 index 排序\n    return fields.sort((f1, f2) => {\n      const p1 = f1.path.value;\n      const p2 = f2.path.value;\n      const i1 = parseInt(p1[p1.length - 1]);\n      const i2 = parseInt(p2[p2.length - 1]);\n      return i1 - i2;\n    });\n  }\n\n  map<T>(cb: (f: FieldModel, index: number, arr: FieldModel[]) => T) {\n    const fields = (this.value || []).map((v: TValue, i: number) => {\n      const pathString = this.path.concat(i).toString();\n      let field = this.form.getField(pathString);\n      if (!field) {\n        field = this.form.createField(pathString);\n      }\n      return field;\n    });\n    return fields.map(cb);\n  }\n\n  append(value?: TValue) {\n    const curLength = this.value?.length || 0;\n    const newElemPath = this.path.concat(curLength).toString();\n    const newElemField = this.form.createField(newElemPath);\n    const newArrayValue = this.value ? [...this.value, value] : [value];\n\n    const prevFormValues = this.form.values;\n\n    // 设置新的数组值并触发事件\n    this.form.store.setIn(new Path(this.name), newArrayValue);\n    this.form.fireOnFormValuesChange({\n      values: this.form.values,\n      prevValues: prevFormValues,\n      name: this.name,\n      options: {\n        action: 'array-append',\n        indexes: [curLength],\n      },\n    });\n    // 触发新元素的初始值变更\n    this.form.fireOnFormValuesInit({\n      values: this.form.values,\n      prevValues: prevFormValues,\n      name: newElemPath,\n    });\n\n    this.onAppendEmitter.fire({\n      value,\n      arrayValue: this.value as Array<TValue>,\n      index: this.value!.length - 1,\n    });\n    return newElemField;\n  }\n\n  /**\n   * Delete the element in given index and delete the corresponding FieldModel as well\n   * @param index\n   */\n  delete(index: number) {\n    // const field = this.form.getField(name);\n    // if (!field) {\n    //   throw new Error(\n    //     `[Form] Error in FieldArrayModel.delete: delete failed, no field found for name ${name}`,\n    //   );\n    // }\n    // const index = field.path.getArrayIndex(this.path);\n    this._splice(index, 1);\n\n    this.onDeleteEmitter.fire({ arrayValue: this.value, index });\n  }\n\n  _splice(start: number, deleteCount = 1) {\n    if (start < 0 || deleteCount < 0) {\n      throw new Error(\n        `[Form] Error in FieldArrayModel.splice: Invalid Params, start and deleteCount should > 0`\n      );\n    }\n\n    if (!this.value || this.value.length === 0 || deleteCount > this.value.length) {\n      throw new Error(\n        `[Form] Error in FieldArrayModel.splice: delete count exceeds array length, tried to delete ${deleteCount} elements, but array length is ${\n          this.value?.length || 0\n        }`\n      );\n    }\n    const oldFormValues = this.form.values;\n\n    const tempValue = [...this.value];\n    tempValue.splice(start, deleteCount);\n\n    // 设置数组值并触发事件\n    this.form.store.setIn(new Path(this.name), tempValue);\n\n    this.form.fireOnFormValuesChange({\n      values: this.form.values,\n      prevValues: oldFormValues,\n      name: this.name,\n      options: {\n        action: 'array-splice',\n        indexes: Array.from({ length: deleteCount }, (_, i) => i + start),\n      },\n    });\n\n    const children = this.children;\n\n    // 如果要删除的元素都在数组末端， 直接删除\n    if (start + deleteCount >= children.length) {\n      for (let i = start; i < children.length; i++) {\n        this.form.disposeField(children[i].name);\n      }\n    }\n\n    const toDispose: FieldModel[] = [];\n    const newFieldMap = new Map<string, FieldModel>(this.form.fieldMap);\n\n    const recursiveHandleChildField = (field: FieldModel, index: number) => {\n      if (field.children?.length) {\n        field.children.forEach((cField) => {\n          recursiveHandleChildField(cField, index);\n        });\n      }\n      // start 以前的项不变\n      if (index < start) {\n        newFieldMap.set(field.name, field);\n      }\n      // 要删除的项， 放入toDispose\n      else if (index < start + deleteCount) {\n        toDispose.push(field);\n      }\n      // 剩余的项 index 向前移动 {deleteCount} 位， 并触发变更事件\n      else {\n        const originName = field.name;\n        const targetName = field.path\n          .replaceParent(this.path.concat(index), this.path.concat(index - deleteCount))\n          .toString();\n        newFieldMap.set(targetName, field);\n        if (!field.children.length) {\n          field.updateNameForLeafState(targetName);\n          field.bubbleState();\n        }\n        field.name = targetName;\n\n        // 最后 {deleteCount} 项，需要fire 被变更为undefined， 并从 newMap 中删除\n        if (index > children.length - deleteCount - 1) {\n          newFieldMap.delete(originName);\n        }\n      }\n    };\n\n    // 对数组所有子项做删除或 index 移动操作\n    children.map((field, index) => {\n      recursiveHandleChildField(field, index);\n    });\n\n    toDispose.forEach((f) => {\n      f.dispose();\n    });\n    this.form.fieldMap = newFieldMap;\n    this.form.alignStateWithFieldMap();\n  }\n\n  swap(from: number, to: number) {\n    if (!this.value) {\n      return;\n    }\n\n    if (from < 0 || to < 0 || from > this.value.length - 1 || to > this.value.length - 1) {\n      throw new Error(\n        `[Form]: FieldArrayModel.swap Error: invalid params 'form' and 'to', form=${from} to=${to}. expect the value between 0 to ${\n          length - 1\n        }`\n      );\n    }\n\n    const oldFormValues = this.form.values;\n    const tempValue = [...this.value];\n\n    const fromValue = tempValue[from];\n    const toValue = tempValue[to];\n\n    tempValue[to] = fromValue;\n    tempValue[from] = toValue;\n\n    this.form.store.setIn(this.path, tempValue);\n    this.form.fireOnFormValuesChange({\n      values: this.form.values,\n      prevValues: oldFormValues,\n      name: this.name,\n      options: {\n        action: 'array-swap',\n        indexes: [from, to],\n      },\n    });\n\n    // swap related FieldModels\n    const newFieldMap = new Map<string, FieldModel>(this.form.fieldMap);\n\n    const fromFields = this.findAllFieldsAt(from);\n    const toFields = this.findAllFieldsAt(to);\n    const fromRootPath = this.getPathAt(from);\n    const toRootPath = this.getPathAt(to);\n    const leafFieldsModified: FieldModel[] = [];\n    fromFields.forEach((f) => {\n      const newName = f.path.replaceParent(fromRootPath, toRootPath).toString();\n      f.name = newName;\n      if (!f.children.length) {\n        f.updateNameForLeafState(newName);\n        leafFieldsModified.push(f);\n      }\n      newFieldMap.set(newName, f);\n    });\n    toFields.forEach((f) => {\n      const newName = f.path.replaceParent(toRootPath, fromRootPath).toString();\n      f.name = newName;\n      if (!f.children.length) {\n        f.updateNameForLeafState(newName);\n      }\n      newFieldMap.set(newName, f);\n      leafFieldsModified.push(f);\n    });\n    this.form.fieldMap = newFieldMap;\n    leafFieldsModified.forEach((f) => f.bubbleState());\n    this.form.alignStateWithFieldMap();\n  }\n\n  move(from: number, to: number) {\n    if (!this.value) {\n      return;\n    }\n\n    if (from < 0 || to < 0 || from > this.value.length - 1 || to > this.value.length - 1) {\n      throw new Error(\n        `[Form]: FieldArrayModel.move Error: invalid params 'form' and 'to', form=${from} to=${to}. expect the value between 0 to ${\n          length - 1\n        }`\n      );\n    }\n\n    const tempValue = [...this.value];\n\n    const fromValue = tempValue[from];\n\n    tempValue.splice(from, 1);\n    tempValue.splice(to, 0, fromValue);\n\n    this.form.setValueIn(this.name, tempValue);\n\n    // todo(fix): should move fields in order to make sure fields' state is also moved\n  }\n\n  protected insertAt(index: number, value: TValue) {\n    if (!this.value) {\n      return;\n    }\n\n    if (index < 0 || index > this.value.length) {\n      throw new Error(`[Form]: FieldArrayModel.insertAt Error: index exceeds array boundary`);\n    }\n\n    const tempValue = [...this.value];\n    tempValue.splice(index, 0, value);\n    this.form.setValueIn(this.name, tempValue);\n\n    // todo: should move field in order to make sure field state is also moved\n  }\n\n  /**\n   * get element path at given index\n   * @param index\n   * @protected\n   */\n  protected getPathAt(index: number) {\n    return this.path.concat(index);\n  }\n\n  /**\n   * find all fields including child and grandchild fields at given index.\n   * @param index\n   * @protected\n   */\n  protected findAllFieldsAt(index: number) {\n    const rootPath = this.getPathAt(index);\n    const rootPathString = rootPath.toString();\n\n    const res: FieldModel[] = this.form.fieldMap.get(rootPathString)\n      ? [this.form.fieldMap.get(rootPathString)!]\n      : [];\n\n    this.form.fieldMap.forEach((field, fieldName) => {\n      if (rootPath.isChildOrGrandChild(fieldName)) {\n        res.push(field);\n      }\n    });\n    return res;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/field-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { get, groupBy, some } from 'lodash-es';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport { ReactiveState } from '@flowgram.ai/reactive';\n\nimport { toFeedback } from '../utils/validate';\nimport { FieldModelState, FieldName, FieldValue, Ref } from '../types/field';\nimport {\n  Errors,\n  FeedbackLevel,\n  FieldError,\n  FieldWarning,\n  Validate,\n  ValidateTrigger,\n  Warnings,\n} from '../types';\nimport { createFieldModelState, DEFAULT_FIELD_STATE } from '../constants';\nimport {\n  clearFeedbacks,\n  FieldEventUtils,\n  mergeFeedbacks,\n  shouldValidate,\n  updateFeedbacksName,\n} from './utils';\nimport { Path } from './path';\nimport { FormModel } from './form-model';\n\ninterface OnValueChangePayload<TValue> {\n  value: TValue | undefined;\n  prevValue: TValue | undefined;\n  formValues: any;\n  prevFormValues: any;\n}\n\nexport class FieldModel<TValue extends FieldValue = FieldValue> implements Disposable {\n  readonly onValueChangeEmitter = new Emitter<OnValueChangePayload<TValue>>();\n\n  readonly form: FormModel;\n\n  readonly id: string;\n\n  readonly onValueChange = this.onValueChangeEmitter.event;\n\n  protected toDispose = new DisposableCollection();\n\n  protected _ref?: Ref;\n\n  protected _path: Path;\n\n  protected _state: ReactiveState<FieldModelState> = new ReactiveState<FieldModelState>(\n    createFieldModelState()\n  );\n\n  /**\n   * @deprecated\n   * 原用于直接给field 设置validate 逻辑，现将该逻辑放到form._options.validate 中设置，该字段暂时弃用\n   */\n  originalValidate?: Validate;\n\n  protected _renderCount: number = 0;\n\n  constructor(path: Path, form: FormModel) {\n    this._path = path;\n    this.form = form;\n    this.id = nanoid();\n\n    const changeDisposable = this.form.onFormValuesChange((payload) => {\n      const { values, prevValues } = payload;\n      if (FieldEventUtils.shouldTriggerFieldChangeEvent(payload, this.name)) {\n        this.onValueChangeEmitter.fire({\n          value: get(values, this.name),\n          prevValue: get(prevValues, this.name),\n          formValues: values,\n          prevFormValues: prevValues,\n        });\n        if (\n          shouldValidate(ValidateTrigger.onChange, this.form.validationTrigger) &&\n          FieldEventUtils.shouldTriggerFieldValidateWhenChange(payload, this.name)\n        ) {\n          this.validate();\n        }\n      }\n    });\n    this.toDispose.push(changeDisposable);\n\n    // if (shouldValidate(ValidateTrigger.onChange, this.form.validationTrigger)) {\n    //   const validateDisposable = this.form.onFormValuesChange(({ name, values, prevValues }) => {\n    //     /**\n    //      * Field 值变更时，所有 ancestor 以及所有child 和 grand child 的校验都要触发\n    //      */\n    //     if (Glob.isMatchOrParent(this.name, name) || Glob.isMatchOrParent(name, this.name)) {\n    //       this.validate();\n    //     }\n    //   });\n    //   this.toDispose.push(validateDisposable);\n    // }\n\n    this.toDispose.push(this.onValueChangeEmitter);\n\n    this.initState();\n  }\n\n  protected _mount: boolean = false;\n\n  get renderCount() {\n    return this._renderCount;\n  }\n\n  set renderCount(n: number) {\n    this._renderCount = n;\n  }\n\n  private initState() {\n    const initialErrors = get(this.form.state.errors, this.name);\n    const initialWarnings = get(this.form.state.warnings, this.name);\n\n    if (initialErrors) {\n      this.state.errors = {\n        [this.name]: initialErrors,\n      };\n    }\n    if (initialWarnings) {\n      this.state.warnings = {\n        [this.name]: initialWarnings,\n      };\n    }\n  }\n\n  get path() {\n    return this._path;\n  }\n\n  get name() {\n    return this._path.toString();\n  }\n\n  set name(name: FieldName) {\n    this._path = new Path(name);\n  }\n\n  get ref() {\n    return this._ref;\n  }\n\n  set ref(ref: Ref | undefined) {\n    this._ref = ref;\n  }\n\n  get state() {\n    return this._state.value;\n  }\n\n  get reactiveState() {\n    return this._state;\n  }\n\n  get value() {\n    return this.form.getValueIn(this.name);\n  }\n\n  set value(value: TValue | undefined) {\n    this.form.setValueIn(this.name, value);\n    if (!this.state.isTouched) {\n      this.state.isTouched = true;\n      this.bubbleState();\n    }\n  }\n\n  updateNameForLeafState(newName: string) {\n    const { errors, warnings } = this.state;\n    const nameInErrors = errors ? Object.keys(errors)?.[0] : undefined;\n    if (nameInErrors && errors?.[nameInErrors] && nameInErrors !== newName) {\n      this.state.errors = {\n        [newName]: errors?.[nameInErrors]\n          ? updateFeedbacksName(errors?.[nameInErrors], newName)\n          : errors?.[nameInErrors],\n      };\n    }\n    const nameInWarnings = warnings ? Object.keys(warnings)?.[0] : undefined;\n    if (nameInWarnings && warnings?.[nameInWarnings] && nameInWarnings !== newName) {\n      this.state.warnings = {\n        [newName]: warnings?.[nameInWarnings]\n          ? updateFeedbacksName(warnings?.[nameInWarnings], newName)\n          : warnings?.[nameInWarnings],\n      };\n    }\n  }\n\n  // recursiveUpdateName(name: FieldName) {\n  //   if (this.children?.length) {\n  //     this.children.forEach(c => {\n  //       c.recursiveUpdateName(c.path.replaceParent(this.path, new Path(name)).toString());\n  //     });\n  //   } else {\n  //     this.updateNameForLeafState(name);\n  //     this.bubbleState();\n  //   }\n  //   this.name = name;\n  // }\n\n  /**\n   * @deprecated\n   * @param validate\n   * @param from\n   */\n  updateValidate(validate: Validate | undefined, from?: 'ui') {\n    if (from === 'ui') {\n      // todo(heyuan):暂时逻辑: 只在没有全局配置校验时来自ui 的validate 才生效。 后续需要支持多validate合并， ui 和全局的都需要生效\n      if (!this.originalValidate) {\n        this.originalValidate = validate;\n      }\n    } else {\n      this.originalValidate = validate;\n    }\n  }\n\n  bubbleState() {\n    const { errors, warnings } = this.state;\n\n    if (this.parent) {\n      this.parent.state.isTouched = some(\n        this.parent.children.map((c) => c.state.isTouched),\n        Boolean\n      );\n      this.parent.state.invalid = some(\n        this.parent.children.map((c) => c.state.invalid),\n        Boolean\n      );\n      this.parent.state.isDirty = some(\n        this.parent.children.map((c) => c.state.isDirty),\n        Boolean\n      );\n      this.parent.state.isValidating = some(\n        this.parent.children.map((c) => c.state.isValidating),\n        Boolean\n      );\n      this.parent.state.errors = errors\n        ? mergeFeedbacks<Errors>(this.parent.state.errors, errors)\n        : clearFeedbacks(this.name, this.parent.state.errors);\n      this.parent.state.warnings = warnings\n        ? mergeFeedbacks<Warnings>(this.parent.state.warnings, warnings)\n        : clearFeedbacks(this.name, this.parent.state.warnings);\n\n      this.parent.bubbleState();\n      return;\n    }\n    // parent 不存在，则更新form state\n    this.form.state.isTouched = some(\n      this.form.fields.map((f) => f.state.isTouched),\n      Boolean\n    );\n    this.form.state.invalid = some(\n      this.form.fields.map((f) => f.state.invalid),\n      Boolean\n    );\n    this.form.state.isDirty = some(\n      this.form.fields.map((f) => f.state.isDirty),\n      Boolean\n    );\n    this.form.state.isValidating = some(\n      this.form.fields.map((f) => f.state.isValidating),\n      Boolean\n    );\n    this.form.state.errors = errors\n      ? mergeFeedbacks<Errors>(this.form.state.errors, errors)\n      : clearFeedbacks(this.name, this.form.state.errors);\n    this.form.state.warnings = warnings\n      ? mergeFeedbacks<Warnings>(this.form.state.warnings, warnings)\n      : clearFeedbacks(this.name, this.form.state.warnings);\n    // console.log('>>>> bubble state: ', this.form.state.errors, this.form.state.invalid, this.form.fields.map(f => f.state.invalid))\n  }\n\n  clearState() {\n    this.state.errors = DEFAULT_FIELD_STATE.errors;\n    this.state.warnings = DEFAULT_FIELD_STATE.warnings;\n    this.state.isTouched = DEFAULT_FIELD_STATE.isTouched;\n    this.state.isDirty = DEFAULT_FIELD_STATE.isDirty;\n    this.bubbleState();\n  }\n\n  get children(): FieldModel[] {\n    const res: FieldModel[] = [];\n    this.form.fieldMap.forEach((field, path: string) => {\n      if (this.path.isChild(path)) {\n        res.push(field);\n      }\n    });\n    return res;\n  }\n\n  get parent(): FieldModel | undefined {\n    const parentPath = this.path.parent;\n    if (!parentPath) {\n      return undefined;\n    }\n    return this.form.fieldMap.get(parentPath.toString());\n  }\n\n  clear() {\n    if (!this.value) {\n      return;\n    }\n    this.value = undefined;\n  }\n\n  async validate() {\n    // 以下代码由于导致arr 配置的校验不触发，暂时注释，支持对父节点配置校验逻辑\n    // const children = this.children;\n\n    // 如果是非叶子field, 执行children的校验。暂不支持在父级上配校验器\n    // if (children?.length) {\n    //   await Promise.all(this.children.map(c => c.validate()));\n    //   return;\n    // }\n    await this.validateSelf();\n  }\n\n  async validateSelf() {\n    this.state.isValidating = true;\n    this.bubbleState();\n    const { errors, warnings } = await this._runAsyncValidate();\n\n    if (errors?.length) {\n      this.state.errors = groupBy(errors, 'name');\n      this.state.invalid = true;\n    } else {\n      this.state.errors = { [this.name]: [] };\n      this.state.invalid = false;\n    }\n\n    if (warnings?.length) {\n      this.state.warnings = groupBy(warnings, 'name');\n    } else {\n      this.state.warnings = { [this.name]: [] };\n    }\n\n    this.state.isValidating = false;\n    this.bubbleState();\n    this.form.onValidateEmitter.fire(this.form.state);\n  }\n\n  protected async _runAsyncValidate(): Promise<{\n    errors?: FieldError[];\n    warnings?: FieldWarning[];\n  }> {\n    let errors: FieldError[] = [];\n    let warnings: FieldWarning[] = [];\n\n    const results = await this.form.validateIn(this.name);\n    if (!results?.length) {\n      return {};\n    } else {\n      const feedbacks = results.map((result) => toFeedback(result, this.name)).filter(Boolean) as (\n        | FieldError\n        | FieldWarning\n      )[];\n\n      if (!feedbacks?.length) {\n        return {};\n      }\n\n      const groupedFeedbacks = groupBy(feedbacks, 'level');\n\n      warnings = warnings.concat((groupedFeedbacks[FeedbackLevel.Warning] as FieldWarning[]) || []);\n      errors = errors.concat((groupedFeedbacks[FeedbackLevel.Error] as FieldError[]) || []);\n    }\n\n    return { errors, warnings };\n  }\n\n  updateState(s: Partial<FieldModel>) {\n    // todo\n  }\n\n  dispose() {\n    this.children.map((c) => c.dispose());\n    // Do not reset state when field disposed, since it will clear errors and warnings in form model as well.\n    // todo: remove following line and related ut after a few weeks test online\n    // this.clearState();\n    this.toDispose.dispose();\n    this.form.fieldMap.delete(this.path.toString());\n  }\n\n  onDispose(fn: () => void) {\n    this.toDispose.onDispose(fn);\n  }\n\n  get disposed() {\n    return this.toDispose.disposed;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/form-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { flatten, get } from 'lodash-es';\nimport { deepEqual } from 'fast-equals';\nimport { Disposable, Emitter } from '@flowgram.ai/utils';\nimport { ReactiveState } from '@flowgram.ai/reactive';\n\nimport { feedbackToFieldErrorsOrWarnings, hasError, toFeedback } from '../utils/validate';\nimport { Glob } from '../utils/glob';\nimport { keepValidKeys } from '../utils';\nimport {\n  FormModelState,\n  FormOptions,\n  FormState,\n  OnFormValuesChangePayload,\n  OnFormValuesInitPayload,\n  OnFormValuesUpdatedPayload,\n} from '../types/form';\nimport { FieldName, FieldValue } from '../types/field';\nimport { Errors, FeedbackLevel, FormValidateReturn, Validate, Warnings } from '../types';\nimport { createFormModelState } from '../constants';\nimport { getValidByErrors, mergeFeedbacks } from './utils';\nimport { Store } from './store';\nimport { Path } from './path';\nimport { FieldModel } from './field-model';\nimport { FieldArrayModel } from './field-array-model';\n\nexport class FormModel<TValues = any> implements Disposable {\n  protected _fieldMap: Map<string, FieldModel> = new Map();\n\n  readonly store = new Store();\n\n  protected _options: FormOptions = {};\n\n  protected onFieldModelCreateEmitter = new Emitter<FieldModel>();\n\n  readonly onFieldModelCreate = this.onFieldModelCreateEmitter.event;\n\n  readonly onFormValuesChangeEmitter = new Emitter<OnFormValuesChangePayload>();\n\n  readonly onFormValuesChange = this.onFormValuesChangeEmitter.event;\n\n  readonly onFormValuesInitEmitter = new Emitter<OnFormValuesInitPayload>();\n\n  readonly onFormValuesInit = this.onFormValuesInitEmitter.event;\n\n  readonly onFormValuesUpdatedEmitter = new Emitter<OnFormValuesUpdatedPayload>();\n\n  readonly onFormValuesUpdated = this.onFormValuesUpdatedEmitter.event;\n\n  readonly onValidateEmitter = new Emitter<FormModelState>();\n\n  readonly onValidate = this.onValidateEmitter.event;\n\n  protected _state: ReactiveState<FormModelState> = new ReactiveState<FormModelState>(\n    createFormModelState()\n  );\n\n  protected _initialized = false;\n\n  set fieldMap(map) {\n    this._fieldMap = map;\n  }\n\n  /**\n   * 表单初始值，初始化设置后不可修改\n   * @protected\n   */\n  // protected _initialValues?: TValues;\n\n  get fieldMap() {\n    return this._fieldMap;\n  }\n\n  get context() {\n    return this._options.context;\n  }\n\n  get initialValues() {\n    return this._options.initialValues;\n  }\n\n  get values() {\n    return this.store.values;\n  }\n\n  set values(v) {\n    const prevValues = this.values;\n    if (deepEqual(prevValues, v)) {\n      return;\n    }\n    this.store.values = v;\n    this.fireOnFormValuesChange({\n      values: this.values,\n      prevValues,\n      name: '',\n    });\n  }\n\n  get validationTrigger() {\n    return this._options.validateTrigger;\n  }\n\n  get state() {\n    return this._state.value;\n  }\n\n  get reactiveState() {\n    return this._state;\n  }\n\n  get fields(): FieldModel[] {\n    return Array.from(this.fieldMap.values());\n  }\n\n  updateState(state: Partial<FormState>) {\n    // todo\n  }\n\n  get initialized() {\n    return this._initialized;\n  }\n\n  fireOnFormValuesChange(payload: OnFormValuesChangePayload) {\n    this.onFormValuesChangeEmitter.fire(payload);\n    this.onFormValuesUpdatedEmitter.fire(payload);\n  }\n\n  fireOnFormValuesInit(payload: OnFormValuesInitPayload) {\n    this.onFormValuesInitEmitter.fire(payload);\n    this.onFormValuesUpdatedEmitter.fire(payload);\n  }\n\n  init(options: FormOptions<TValues>) {\n    this._options = options;\n    if (options.initialValues) {\n      const prevValues = this.store.values;\n      this.store.values = options.initialValues;\n      this.fireOnFormValuesInit({\n        values: options.initialValues,\n        prevValues,\n        name: '',\n      });\n    }\n    this._initialized = true;\n  }\n\n  createField<TValue = FieldValue>(name: FieldName, isArray?: boolean): FieldModel<TValue> {\n    const path = new Path(name);\n    const pathString = path.toString();\n\n    if (this.fieldMap.get(pathString)) {\n      return this.fieldMap.get(pathString)!;\n    }\n\n    // const fieldValue = value || get(this.initialValues, pathString);\n\n    const field: FieldModel = isArray\n      ? new FieldArrayModel(path, this)\n      : new FieldModel(path, this);\n\n    this.fieldMap.set(pathString, field);\n    field.onDispose(() => {\n      this.fieldMap.delete(pathString);\n    });\n    this.onFieldModelCreateEmitter.fire(field);\n\n    return field;\n  }\n\n  createFieldArray<TValue = FieldValue>(\n    name: FieldName,\n    value?: Array<TValue>\n  ): FieldArrayModel<TValue> {\n    return this.createField<Array<TValue>>(name, true) as FieldArrayModel<TValue>;\n  }\n\n  /**\n   * 销毁Field 模型和子模型,但不会删除field的值\n   * @param name\n   */\n  disposeField(name: string) {\n    const field = this.fieldMap.get(name);\n    if (field) {\n      field.dispose();\n    }\n  }\n\n  /**\n   * 删除field, 会删除值和 Field 模型， 以及对应的子模型\n   * @param name\n   */\n  deleteField(name: string) {\n    const field = this.fieldMap.get(name);\n    if (field) {\n      // 销毁值\n      field.clear();\n      // 销毁模型\n      field.dispose();\n    }\n  }\n\n  getField<TFieldModel extends FieldModel | FieldArrayModel = FieldModel>(\n    name: FieldName\n  ): TFieldModel | undefined {\n    return this.fieldMap.get(new Path(name).toString()) as TFieldModel | undefined;\n  }\n\n  getValueIn<TValue>(name: FieldName): TValue {\n    return this.store.getIn<TValue>(new Path(name));\n  }\n\n  setValueIn<TValue>(name: FieldName, value: TValue): void {\n    const prevValues = this.values;\n\n    this.store.setIn(new Path(name), value);\n\n    this.fireOnFormValuesChange({\n      values: this.values,\n      prevValues,\n      name,\n    });\n  }\n\n  setInitValueIn<TValue = any>(name: FieldName, value: TValue): void {\n    const path = new Path(name);\n    const prevValue = this.store.getIn(path);\n    if (prevValue === undefined) {\n      const prevValues = this.values;\n      this.store.setIn(new Path(name), value);\n      this.fireOnFormValuesInit({\n        values: this.values,\n        prevValues,\n        name,\n      });\n    }\n  }\n\n  validateDisabled = false;\n\n  clearValueIn(name: FieldName) {\n    this.setValueIn(name, undefined);\n  }\n\n  async validateIn(name: FieldName) {\n    if (this.validateDisabled) return [];\n    const validateOptions = this.getValidateOptions();\n    if (!validateOptions) {\n      return;\n    }\n\n    const validateKeys = Object.keys(validateOptions).filter((pattern) =>\n      Glob.isMatch(pattern, name)\n    );\n\n    const validatePromises = validateKeys.map(async (validateKey) => {\n      const validate = validateOptions![validateKey];\n\n      return validate({\n        value: this.getValueIn(name),\n        formValues: this.values,\n        context: this.context,\n        name,\n      });\n    });\n\n    return Promise.all(validatePromises);\n  }\n\n  protected getValidateOptions(): Record<string, Validate> | undefined {\n    const validate = this._options.validate;\n    if (typeof validate === 'function') {\n      return validate(this.values, this.context);\n    }\n    return validate;\n  }\n\n  async validate(): Promise<FormValidateReturn> {\n    if (this.validateDisabled) return [];\n    const validateOptions = this.getValidateOptions();\n    if (!validateOptions) {\n      return [];\n    }\n\n    const feedbacksArrPromises = Object.keys(validateOptions).map(async (nameRule) => {\n      const validate = validateOptions![nameRule];\n      const values = this.values;\n      const paths = Glob.findMatchPathsWithEmptyValue(values, nameRule);\n      return Promise.all(\n        paths.map(async (path) => {\n          const result = await validate({\n            value: get(values, path),\n            formValues: values,\n            context: this.context,\n            name: path,\n          });\n\n          const feedback = toFeedback(result, path);\n          const field = this.getField(path);\n\n          const errors = feedbackToFieldErrorsOrWarnings<Errors>(\n            path,\n            feedback?.level === FeedbackLevel.Error ? feedback : undefined\n          );\n          const warnings = feedbackToFieldErrorsOrWarnings<Warnings>(\n            path,\n            feedback?.level === FeedbackLevel.Warning ? feedback : undefined\n          );\n\n          if (field) {\n            field.state.errors = errors;\n            field.state.warnings = warnings;\n            field.state.invalid = hasError(errors);\n            field.bubbleState();\n          }\n\n          // 无论是否存在 field 都要保证 form 的state 被更新\n          this.state.errors = mergeFeedbacks(this.state.errors, errors);\n          this.state.warnings = mergeFeedbacks(this.state.warnings, warnings);\n\n          this.state.invalid = !getValidByErrors(this.state.errors);\n          return feedback;\n        })\n      );\n    });\n\n    this.state.isValidating = true;\n    const feedbacksArr = await Promise.all(feedbacksArrPromises);\n    this.state.isValidating = false;\n    this.onValidateEmitter.fire(this.state);\n\n    return flatten(feedbacksArr).filter(Boolean) as FormValidateReturn;\n  }\n\n  alignStateWithFieldMap() {\n    const keys = Array.from(this.fieldMap.keys());\n\n    if (this.state.errors) {\n      this.state.errors = keepValidKeys(this.state.errors, keys);\n    }\n    if (this.state.warnings) {\n      this.state.warnings = keepValidKeys(this.state.warnings, keys);\n    }\n    this.fieldMap.forEach((f) => {\n      if (f.state.errors) {\n        f.state.errors = keepValidKeys(f.state.errors, keys);\n      }\n      if (f.state.warnings) {\n        f.state.warnings = keepValidKeys(f.state.warnings, keys);\n      }\n    });\n  }\n\n  dispose() {\n    this.fieldMap.forEach((f) => f.dispose());\n    this.store.dispose();\n    this._initialized = false;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FormModel } from './form-model';\nexport { createForm, type CreateFormOptions } from './create-form';\nexport { FieldModel } from './field-model';\nexport { FieldArrayModel } from './field-array-model';\n\nexport { toField, toFieldState } from './to-field';\nexport { toFieldArray } from './to-field-array';\nexport { toForm, toFormState } from './to-form';\nexport { Path } from './path';\n"
  },
  {
    "path": "packages/node-engine/form/src/core/path.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { toPath } from 'lodash-es';\n\nexport class Path {\n  protected _path: string[] = [];\n\n  constructor(path: string | string[]) {\n    this._path = toPath(path);\n  }\n\n  get parent(): Path | undefined {\n    if (this._path.length < 2) {\n      return undefined;\n    }\n    return new Path(this._path.slice(0, -1));\n  }\n\n  toString(): string {\n    return this._path.join('.');\n  }\n\n  get value(): string[] {\n    return this._path;\n  }\n\n  /**\n   * 仅计直系child\n   * @param path\n   */\n  isChild(path: string) {\n    const target = new Path(path).value;\n    const self = this.value;\n\n    if (target.length - self.length !== 1) {\n      return false;\n    }\n\n    for (let i = 0; i < self.length; i++) {\n      if (target[i] !== self[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * 比较两个数组path大小\n   * 返回小于0则path1<path2, 大于0 则path1>path2, 等于0则相等\n   * @param path1\n   * @param path2\n   */\n  static compareArrayPath(path1: Path, path2: Path): number | void {\n    let i = 0;\n    while (path1.value[i] && path2.value[i]) {\n      const index1 = parseInt(path1.value[i]);\n      const index2 = parseInt(path2.value[i]);\n\n      if (!isNaN(index1) && !isNaN(index2)) {\n        return index1 - index2;\n      } else if (path1.value[i] !== path2.value[i]) {\n        throw new Error(\n          `[Form] Path.compareArrayPath invalid input Error: two path should refers to the same array, but got path1: ${path1.toString()}, path2: ${path2.toString()}`\n        );\n      }\n      i++;\n    }\n    throw new Error(\n      `[Form] Path.compareArrayPath invalid input Error: got path1: ${path1.toString()}, path2: ${path2.toString()}`\n    );\n  }\n\n  isChildOrGrandChild(path: string) {\n    const target = new Path(path).value;\n    const self = this.value;\n\n    if (target.length - self.length < 1) {\n      return false;\n    }\n\n    for (let i = 0; i < self.length; i++) {\n      if (target[i] !== self[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  getArrayIndex(parent: Path) {\n    return parseInt(this._path[parent.value.length]);\n  }\n\n  concat(name: number | string) {\n    if (typeof name === 'string' || typeof name === 'number') {\n      return new Path(this._path.concat(new Path(name.toString())._path));\n    }\n    throw new Error(\n      `[Form] Error in Path.concat: invalid param type, require number or string, but got ${typeof name}`\n    );\n  }\n\n  replaceParent(parent: Path, newParent: Path) {\n    if (parent.value.length > this.value.length) {\n      throw new Error(\n        `[Form] Error in Path.replaceParent: invalid parent param: ${parent}, parent length should not greater than current length.`\n      );\n    }\n    const rest = [];\n    for (let i = 0; i < this.value.length; i++) {\n      if (i < parent.value.length && parent.value[i] !== this.value[i]) {\n        throw new Error(\n          `[Form] Error in Path.replaceParent: invalid parent param: '${parent}' is not a parent of '${this.toString()}'`\n        );\n      }\n      if (i >= parent.value.length) {\n        rest.push(this.value[i]);\n      }\n    }\n\n    return new Path(newParent.value.concat(rest));\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get, clone, cloneDeep } from 'lodash-es';\n\nimport { shallowSetIn } from '../utils';\nimport { FieldValue } from '../types/field';\nimport { Path } from './path';\n\nexport class Store<TValues = FieldValue> {\n  protected _values: TValues;\n\n  get values(): TValues {\n    return clone(this._values);\n  }\n\n  set values(v) {\n    this._values = cloneDeep(v);\n  }\n\n  setIn<TValue = FieldValue>(path: Path, value: TValue): void {\n    // shallow clone set\n    this._values = shallowSetIn(this._values || {}, path.toString(), value);\n  }\n\n  getIn<TValue = FieldValue>(path: Path): TValue {\n    return get(this.values, path.value);\n  }\n\n  dispose() {}\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/to-field-array.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field, FieldArray } from '../types/field';\nimport { toField } from './to-field';\nimport { FieldArrayModel } from './field-array-model';\n\nexport function toFieldArray<TValue>(model: FieldArrayModel<TValue>): FieldArray<TValue> {\n  const res: FieldArray<TValue> = {\n    get key() {\n      return model.id;\n    },\n    get name() {\n      return model.path.toString();\n    },\n    get value() {\n      return model.value;\n    },\n    onChange: (value) => {\n      model.value = value;\n    },\n    map: <T = any>(cb: (f: Field<TValue>, index: number) => T) =>\n      model.map<T>((f, index) => cb(toField(f), index)),\n    append: (value) => toField<TValue>(model.append(value)),\n    /**\n     * @deprecated: use remove instead\n     * @param index\n     */\n    delete: (index: number) => model.delete(index),\n    remove: (index: number) => model.delete(index),\n    swap: (from: number, to: number) => model.swap(from, to),\n    move: (from: number, to: number) => model.move(from, to),\n  } as FieldArray<TValue>;\n\n  // Object.defineProperty(res, 'validate', {\n  //   enumerable: false,\n  //   get() {\n  //     return model.validate.bind(model);\n  //   },\n  // });\n\n  // 隐藏属性\n  Object.defineProperty(res, '_fieldModel', {\n    enumerable: false,\n    get() {\n      return model;\n    },\n  });\n  return res;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/to-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { isCheckBoxEvent, isReactChangeEvent } from '../utils';\nimport { Field, FieldModelState } from '../types/field';\nimport { ValidateTrigger } from '../types';\nimport { shouldValidate } from './utils';\nimport { FieldModel } from './field-model';\n\nexport function toField<TValue>(model: FieldModel): Field<TValue> {\n  const res: Field<TValue> = {\n    get name() {\n      return model.name;\n    },\n    get value() {\n      return model.value;\n    },\n    onChange: (e: unknown) => {\n      if (isReactChangeEvent(e)) {\n        model.value = isCheckBoxEvent(e)\n          ? e.target.checked\n          : (e as React.ChangeEvent<HTMLInputElement>).target.value;\n      } else {\n        model.value = e;\n      }\n    },\n    onBlur() {\n      if (shouldValidate(ValidateTrigger.onBlur, model.form.validationTrigger)) {\n        model.validate();\n      }\n    },\n    onFocus() {\n      model.state.isTouched = true;\n    },\n  } as Field<TValue>;\n\n  Object.defineProperty(res, 'key', {\n    enumerable: false,\n    get() {\n      return model.id;\n    },\n  });\n\n  Object.defineProperty(res, '_fieldModel', {\n    enumerable: false,\n    get() {\n      return model;\n    },\n  });\n  return res;\n}\n\nexport function toFieldState(modelState: FieldModelState) {\n  return {\n    get isTouched() {\n      return modelState.isTouched;\n    },\n    get invalid() {\n      return modelState.invalid;\n    },\n    get isDirty() {\n      return modelState.isDirty;\n    },\n    get isValidating() {\n      return modelState.isValidating;\n    },\n    get errors() {\n      if (modelState.errors) {\n        return Object.values(modelState.errors).reduce((acc, arr) => acc.concat(arr), []);\n      }\n      return;\n    },\n    get warnings() {\n      if (modelState.warnings) {\n        return Object.values(modelState.warnings).reduce((acc, arr) => acc.concat(arr), []);\n      }\n      return;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/to-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Form, FormModelState, FormState } from '../types/form';\nimport { FieldName, FieldValue } from '../types/field';\nimport { FormModel } from './form-model';\n\nexport function toForm<TValue>(model: FormModel): Form<TValue> {\n  const res = {\n    initialValues: model.initialValues,\n    get values() {\n      return model.values;\n    },\n    set values(v) {\n      model.values = v;\n    },\n    state: toFormState(model.state),\n    getValueIn: <TValue = FieldValue>(name: FieldName) => model.getValueIn(name),\n    setValueIn: <TValue>(name: FieldName, value: TValue) => model.setValueIn(name, value),\n    validate: model.validate.bind(model),\n  };\n\n  Object.defineProperty(res, '_formModel', {\n    enumerable: false,\n    get() {\n      return model;\n    },\n  });\n  return res as Form<TValue>;\n}\n\nexport function toFormState(modelState: FormModelState): FormState {\n  return {\n    get isTouched() {\n      return modelState.isTouched;\n    },\n    get invalid() {\n      return modelState.invalid;\n    },\n    get isDirty() {\n      return modelState.isDirty;\n    },\n    get isValidating() {\n      return modelState.isValidating;\n    },\n    // get dirtyFields() {\n    //   return modelState.dirtyFields;\n    // },\n    // get isLoading() {\n    //   return modelState.isLoading;\n    // },\n    // get touchedFields() {\n    //   return modelState.touchedFields;\n    // },\n    get errors() {\n      return modelState.errors;\n    },\n    get warnings() {\n      return modelState.warnings;\n    },\n  };\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/core/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isEmpty, isEqual } from 'lodash-es';\n\nimport { Glob } from '../utils';\nimport { Errors, Feedback, OnFormValuesChangePayload, ValidateTrigger, Warnings } from '../types';\nimport { Path } from './path';\n\nexport function updateFeedbacksName(feedbacks: Feedback<any>[], name: string) {\n  return (feedbacks || []).map((f) => ({\n    ...f,\n    name,\n  }));\n}\n\nexport function mergeFeedbacks<T extends Errors | Warnings>(origin?: T, source?: T) {\n  if (!source) {\n    return origin;\n  }\n  if (!origin) {\n    return { ...source };\n  }\n  const changed = Object.keys(source).some(\n    (sourceKey) => !isEqual(origin[sourceKey], source[sourceKey])\n  );\n\n  if (changed) {\n    return {\n      ...origin,\n      ...source,\n    };\n  }\n  return origin;\n}\n\nexport function clearFeedbacks<T extends Errors | Warnings>(name: string, origin?: T) {\n  if (!origin) {\n    return origin;\n  }\n  if (name in origin) {\n    delete origin[name];\n  }\n  return origin;\n}\n\nexport function shouldValidate(currentTrigger: ValidateTrigger, formTrigger?: ValidateTrigger) {\n  return currentTrigger === formTrigger;\n}\n\nexport function getValidByErrors(errors: Errors | undefined) {\n  return errors ? Object.keys(errors).every((name) => isEmpty(errors[name])) : true;\n}\n\nexport namespace FieldEventUtils {\n  export function shouldTriggerFieldChangeEvent(\n    payload: OnFormValuesChangePayload,\n    fieldName: string\n  ) {\n    const { name: changedName, options } = payload;\n\n    // 如果 Field 是 变更path 的 ancestor 则触发\n    if (Glob.isMatchOrParent(fieldName, changedName)) {\n      return true;\n    }\n\n    // 如果 Field 是 变更path 的 child 或 grandchild 有条件触发\n    if (new Path(changedName).isChildOrGrandChild(fieldName)) {\n      // 数组情况下部分子项不触发变更\n\n      // 1. 数组 append 触发的FormValuesChange 不需要触发其子 Field 的 onValueChange\n      if (options?.action === 'array-append') {\n        return !new Path(changedName).isChildOrGrandChild(fieldName);\n      }\n      // 2. 数组 splice 触发的FormValuesChange 无需触发第一个删除项前的所有子  Field 的 onValueChange\n      else if (options?.action === 'array-splice' && options?.indexes?.length) {\n        return (\n          (Path.compareArrayPath(\n            new Path(fieldName),\n            new Path(changedName).concat(options.indexes[0])\n          ) as number) >= 0\n        );\n      }\n\n      // 其余情况都需要触发\n      return true;\n    }\n    return false;\n  }\n\n  export function shouldTriggerFieldValidateWhenChange(\n    payload: OnFormValuesChangePayload,\n    fieldName: string\n  ) {\n    const { name: changedName, options } = payload;\n\n    if (options?.action === 'array-splice' || options?.action === 'array-swap') {\n      // const splicedIndexes = options?.indexes || [];\n      //\n      // const splicedPaths = splicedIndexes.map(index => new Path(changedName).concat(index));\n      // const removedPaths = Array.from({ length: splicedIndexes.length }, (_, i) =>\n      //   new Path(changedName).concat(prevValues[changedName].length - i - 1),\n      // );\n      //\n      // const ignoredPathOrParentPaths = [...splicedPaths, ...removedPaths];\n      // // const ignoredPathOrParentPaths = splicedPaths;\n      // if (\n      //   ignoredPathOrParentPaths.some(\n      //     path => path.toString() === fieldName || path.isChildOrGrandChild(fieldName),\n      //   )\n      // ) {\n      //   return false;\n      // }\n\n      // splice 和 swap 都属于数组跟级别的变更，仅需触发数组field的校验, 无需校验子项\n      return fieldName === changedName;\n    }\n\n    return FieldEventUtils.shouldTriggerFieldChangeEvent(payload, fieldName);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './react';\nexport type {\n  FormRenderProps,\n  FieldRenderProps,\n  FieldArrayRenderProps,\n  FieldState,\n  FormState,\n  Validate,\n  FormControl,\n  FieldName,\n  FieldError,\n  FieldWarning,\n  FormValidateReturn,\n  FieldValue,\n  FieldArray as IFieldArray,\n  Field as IField,\n  Form as IForm,\n  Errors,\n  Warnings,\n} from './types';\n\nexport { ValidateTrigger, FeedbackLevel } from './types';\nexport { createForm, type CreateFormOptions } from './core/create-form';\nexport { Glob } from './utils';\nexport * from './core';\n"
  },
  {
    "path": "packages/node-engine/form/src/react/context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const FormModelContext = React.createContext<any>({});\nexport const FieldModelContext = React.createContext<any>({});\n"
  },
  {
    "path": "packages/node-engine/form/src/react/field-array.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { isFunction } from 'lodash-es';\nimport { DisposableCollection, useRefresh } from '@flowgram.ai/utils';\nimport { useReadonlyReactiveState } from '@flowgram.ai/reactive';\n\nimport {\n  FieldArrayOptions,\n  FieldArrayRenderProps,\n  FieldModelState,\n  FieldName,\n  FieldValue,\n} from '../types/field';\nimport { FormModelState } from '../types';\nimport { toFieldArray } from '../core/to-field-array';\nimport { FieldArrayModel } from '../core/field-array-model';\nimport { toFieldState, toFormState } from '../core';\nimport { useFormModel } from './utils';\nimport { FieldModelContext } from './context';\n\nexport type FieldArrayProps<TValue> = FieldArrayOptions<TValue> & {\n  /**\n   * A React element or a render prop\n   */\n  children?: ((props: FieldArrayRenderProps<TValue>) => React.ReactElement) | React.ReactElement;\n  /**\n   * Dependencies of the current field. If a field name is given in deps, current field will re-render if the given field name data is updated\n   */\n  deps?: FieldName[];\n};\n\n/**\n * HOC That declare an array field, an FieldArray model will be created when it's rendered. Multiple FieldArray rendering with a same name will link to the same model, which means they shared data、 status and methods\n */\nexport function FieldArray<TValue extends FieldValue>({\n  name,\n  defaultValue,\n  deps,\n  render,\n  children,\n}: FieldArrayProps<TValue>): React.ReactElement {\n  const formModel = useFormModel();\n  const fieldModel =\n    formModel.getField<FieldArrayModel<TValue>>(name) ||\n    (formModel.createFieldArray(name) as FieldArrayModel<any>);\n\n  const field = React.useMemo(() => toFieldArray<TValue>(fieldModel), [fieldModel]);\n\n  const refresh = useRefresh();\n\n  const fieldModelState = useReadonlyReactiveState<FieldModelState>(fieldModel.reactiveState);\n  const formModelState = useReadonlyReactiveState<FormModelState>(formModel.reactiveState);\n\n  const fieldState = toFieldState(fieldModelState);\n  const formState = React.useMemo(() => toFormState(formModelState), [formModelState]);\n\n  React.useEffect(() => {\n    // 当 FieldArray 加上 key 且 key 变化时候会销毁 FieldModel\n    if (fieldModel.disposed) {\n      refresh();\n      return () => {};\n    }\n    fieldModel.renderCount = fieldModel.renderCount + 1;\n\n    if (!formModel.getValueIn(name) !== undefined && defaultValue !== undefined) {\n      formModel.setInitValueIn(name, defaultValue);\n      refresh();\n    }\n\n    const disposableCollection = new DisposableCollection();\n\n    disposableCollection.push(\n      fieldModel.onValueChange(() => {\n        refresh();\n      })\n    );\n\n    if (deps) {\n      deps.forEach((dep) => {\n        const disposable = formModel.getField(dep)?.onValueChange(() => {\n          refresh();\n        });\n        if (disposable) {\n          disposableCollection.push(disposable);\n        }\n      });\n    }\n    return () => {\n      disposableCollection.dispose();\n\n      if (fieldModel.renderCount > 1) {\n        fieldModel.renderCount = fieldModel.renderCount - 1;\n      } else {\n        const newFieldModel = formModel.getField(fieldModel.name);\n        if (newFieldModel === fieldModel) fieldModel.dispose();\n      }\n    };\n  }, [fieldModel]);\n\n  const renderInner = () => {\n    if (render && isFunction(render)) {\n      // @ts-ignore\n      return render({ field, fieldState, formState });\n    }\n\n    if (isFunction(children)) {\n      return children({ field, fieldState, formState });\n    }\n    return <>Invalid Array render</>;\n  };\n\n  if (fieldModel.disposed) return <></>;\n\n  return (\n    <FieldModelContext.Provider value={fieldModel}>{renderInner()}</FieldModelContext.Provider>\n  );\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { isFunction } from 'lodash-es';\nimport { DisposableCollection, useRefresh } from '@flowgram.ai/utils';\nimport { useReadonlyReactiveState } from '@flowgram.ai/reactive';\n\nimport { toField, toFieldState } from 'src/core/to-field';\nimport { FieldModelState, FieldName, FieldOptions, FieldRenderProps } from '../types/field';\nimport { FormModelState } from '../types';\nimport { toFormState } from '../core/to-form';\nimport { useFormModel } from './utils';\nimport { FieldModelContext } from './context';\n\nexport type FieldProps<TValue> = FieldOptions<TValue> & {\n  /**\n   * A React element or a render prop\n   */\n  children?: ((props: FieldRenderProps<TValue>) => React.ReactElement) | React.ReactElement;\n  /**\n   * Dependencies of the current field. If a field name is given in deps, current field will re-render if the given field name data is updated\n   */\n  deps?: FieldName[];\n};\n\n/**\n * HOC That declare a field, an Field model will be created it's rendered. Multiple Field rendering with a same name will link to the same model, which means they shared data、 status and methods\n */\nexport function Field<TValue>({\n  name,\n  defaultValue,\n  render,\n  children,\n  deps,\n}: FieldProps<TValue>): React.ReactElement {\n  const formModel = useFormModel();\n\n  const fieldModel = formModel.getField(name) || formModel.createField(name);\n  const field = React.useMemo(() => toField<TValue>(fieldModel), [fieldModel]);\n\n  const fieldModelState = useReadonlyReactiveState<FieldModelState>(fieldModel.reactiveState);\n  const formModelState = useReadonlyReactiveState<FormModelState>(formModel.reactiveState);\n\n  const fieldState = React.useMemo(() => toFieldState(fieldModelState), [fieldModelState]);\n  const formState = toFormState(formModelState);\n\n  const refresh = useRefresh();\n\n  React.useEffect(() => {\n    // 当 Field 加上 key 且 key 变化时候会销毁 FieldModel\n    if (fieldModel.disposed) {\n      refresh();\n      return () => {};\n    }\n    fieldModel.renderCount = fieldModel.renderCount + 1;\n\n    if (!formModel.getValueIn(name) !== undefined && defaultValue !== undefined) {\n      formModel.setInitValueIn(name, defaultValue);\n      refresh();\n    }\n\n    const disposableCollection = new DisposableCollection();\n\n    disposableCollection.push(\n      fieldModel.onValueChange(() => {\n        refresh();\n      })\n    );\n\n    if (deps) {\n      deps.forEach((dep) => {\n        const disposable = formModel.getField(dep)?.onValueChange(() => {\n          refresh();\n        });\n        if (disposable) {\n          disposableCollection.push(disposable);\n        }\n      });\n    }\n    return () => {\n      disposableCollection.dispose();\n\n      if (fieldModel.renderCount > 1) {\n        fieldModel.renderCount = fieldModel.renderCount - 1;\n      } else {\n        const newFieldModel = formModel.getField(fieldModel.name);\n        if (newFieldModel === fieldModel) fieldModel.dispose();\n      }\n    };\n  }, [fieldModel]);\n\n  const renderInner = () => {\n    if (render) {\n      return render({ field, fieldState, formState });\n    }\n\n    if (isFunction(children)) {\n      return children({ field, fieldState, formState });\n    }\n\n    return React.cloneElement(children as React.ReactElement, { ...field });\n  };\n  if (fieldModel.disposed) return <></>;\n\n  return (\n    <FieldModelContext.Provider value={fieldModel}>{renderInner()}</FieldModelContext.Provider>\n  );\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/form.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { Children, useEffect, useMemo } from 'react';\n\nimport { isFunction } from 'lodash-es';\n\nimport { toForm } from 'src/core/to-form';\nimport { FormControl, FormOptions, FormRenderProps } from '../types/form';\nimport { createForm } from '../core/create-form';\nimport { FormModelContext } from './context';\n\nexport type FormProps<TValues> = FormOptions & {\n  /**\n   * React children or child render prop\n   */\n  children?: ((props: FormRenderProps<TValues>) => React.ReactNode) | React.ReactNode;\n\n  /**\n   * If this prop is set to true, Form instance will be kept event thought<Form /> is destroyed.\n   * This means you can still use some form's api such as Form.validate and Form.setValueIn to handle pure data logic.\n   * @default false\n   */\n  keepModelOnUnMount?: boolean;\n\n  /**\n   * provide form instance from outside. if control is given Form will use the form instance in the control instead of creating one.\n   */\n  control?: FormControl<TValues>;\n};\n\n/**\n * `FormContentRender` allows you to write `useWatch` to `formMeta.render`\n */\nfunction FormContentRender(\n  props: { render: (props: FormRenderProps<any>) => React.ReactNode } & FormRenderProps<any>\n): JSX.Element {\n  const { form, render } = props;\n  return <>{render({ form })}</>;\n}\n/**\n * Hoc That init and provide Form instance. You can also provide form instance from outside by using control prop\n * @param props\n */\nexport function Form<TValues>(props: FormProps<TValues>) {\n  const { children, keepModelOnUnMount = false, control, ...restOptions } = props;\n  const { _formModel: formModel } = useMemo(\n    () => (control ? control : createForm(restOptions).control),\n    [control]\n  );\n\n  useEffect(\n    () => () => {\n      // 组件销毁时，销毁formModel\n      if (!keepModelOnUnMount) {\n        formModel.dispose();\n      }\n    },\n    []\n  );\n\n  const form = useMemo(() => toForm<TValues>(formModel), [formModel]);\n\n  return (\n    <FormModelContext.Provider value={formModel}>\n      {children ? (\n        isFunction(children) ? (\n          <FormContentRender form={form} render={children} />\n        ) : (\n          Children.only(children)\n        )\n      ) : null}\n    </FormModelContext.Provider>\n  );\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './field';\nexport * from './form';\nexport * from './use-form';\nexport * from './use-watch';\nexport * from './field-array';\nexport * from './use-field';\nexport * from './use-form-state';\nexport * from './use-field-validate';\nexport * from './use-current-field';\nexport * from './use-current-field-state';\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-current-field-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext, useMemo } from 'react';\n\nimport { useReadonlyReactiveState } from '@flowgram.ai/reactive';\n\nimport { FieldModelState, FieldState } from '../types';\nimport { toFieldState } from '../core';\nimport { FieldModelContext } from './context';\n\n/**\n * Get the current field state. It should be used in a child component of <Field />, otherwise it throws an error\n */\nexport function useCurrentFieldState(): FieldState {\n  const fieldModel = useContext(FieldModelContext);\n\n  if (!fieldModel) {\n    throw new Error(\n      `[Form] useCurrentField Error: field not found, make sure that you are using this hook in a child Component of a Field`\n    );\n  }\n\n  const fieldModelState = useReadonlyReactiveState<FieldModelState>(fieldModel.reactiveState);\n\n  return useMemo(() => toFieldState(fieldModelState), [fieldModelState]);\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-current-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { Field, FieldArray, FieldValue } from '../types';\nimport { toField } from '../core/to-field';\nimport { toFieldArray } from '../core';\nimport { FieldModelContext } from './context';\n\n/**\n * Get the current Field. It should be used in a child component of <Field />, otherwise it throws an error\n */\nexport function useCurrentField<\n  TFieldValue = FieldValue,\n  TField extends Field<TFieldValue> | FieldArray<TFieldValue> = Field<TFieldValue>\n>(): Field<TFieldValue> | FieldArray<TFieldValue> {\n  const fieldModel = useContext(FieldModelContext);\n\n  if (!fieldModel) {\n    throw new Error(\n      `[Form] useCurrentField Error: field not found, make sure that you are using this hook in a child Component of a Field`\n    );\n  }\n\n  return fieldModel.map\n    ? (toFieldArray<TFieldValue>(fieldModel) as unknown as FieldArray<TFieldValue>)\n    : (toField(fieldModel) as unknown as TField);\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-field-validate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useContext } from 'react';\n\nimport { FieldName } from '../types';\nimport { useFormModel } from './utils';\nimport { FieldModelContext } from './context';\n\n/**\n * Get validate method of a field with given name. the returned function could possibly do nothing if the field is not found.\n * The reason could be that the field is not rendered yet or the name given is wrong.\n * @param name\n */\nexport function useFieldValidate(name?: FieldName): () => void {\n  const currentFieldModel = useContext(FieldModelContext);\n  const formModel = useFormModel();\n\n  return useCallback(() => {\n    const fieldModel = name ? formModel.getField(name!) : currentFieldModel;\n    fieldModel?.validate();\n  }, [currentFieldModel]);\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext, useEffect } from 'react';\n\nimport { Disposable, useRefresh } from '@flowgram.ai/utils';\n\nimport { Field, FieldArray, FieldName, FieldValue } from '../types';\nimport { toField } from '../core/to-field';\nimport { toFieldArray } from '../core';\nimport { useFormModel } from './utils';\nimport { FieldModelContext } from './context';\n\n/**\n * @deprecated\n * `useField` is deprecated because its return relies on React render. if the Field is not rendered, the return would be\n * undefined. If you simply want to monitor the change of the value of a certain path, please use `useWatch(fieldName)`\n * @param name\n */\nexport function useField<\n  TFieldValue = FieldValue,\n  TField extends Field<TFieldValue> | FieldArray<TFieldValue> = Field<TFieldValue>\n>(name?: FieldName): TField | undefined {\n  const currentFieldModel = useContext(FieldModelContext);\n  const formModel = useFormModel();\n  const refresh = useRefresh();\n  const fieldModel = name ? formModel.getField(name!) : currentFieldModel;\n\n  useEffect(() => {\n    let disposable: Disposable;\n    if (fieldModel) {\n      disposable = fieldModel.onValueChange(() => refresh());\n    }\n    return () => {\n      disposable?.dispose();\n    };\n  }, [fieldModel]);\n\n  if (!fieldModel) {\n    return undefined;\n  }\n\n  if (fieldModel.map) {\n    return toFieldArray<TFieldValue>(fieldModel) as unknown as TField;\n  }\n\n  return toField(fieldModel) as unknown as TField;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-form-state.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useObserve } from '@flowgram.ai/reactive';\n\nimport { Form, FormControl, FormState } from '../types';\n\nexport function useFormState(control?: FormControl<any> | Form) {\n  // @ts-ignore\n  return useObserve<FormState>(control?._formModel.reactiveState.value || ({} as FormState));\n}\n\nexport function useFormErrors(control?: FormControl<any> | Form) {\n  // @ts-ignore\n  return useObserve<FormState>(control?._formModel.reactiveState.value || ({} as FormState))\n    ?.errors;\n}\n\nexport function useFormWarnings(control?: FormControl<any> | Form) {\n  // @ts-ignore\n  return useObserve<FormState>(control?._formModel.reactiveState.value || ({} as FormState))\n    ?.warnings;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Form } from '../types';\nimport { toForm } from '../core/to-form';\nimport { useFormModel } from './utils';\n\n/**\n * Get Form instance. It should be use in a child component of  <Form />\n */\nexport function useForm(): Form {\n  const formModel = useFormModel();\n  return toForm(formModel);\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/use-watch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/utils';\n\nimport { FieldName, FieldValue } from '../types';\nimport { useFormModel } from './utils';\n\n/**\n * Listen to the field data change and refresh the React component.\n * @param name the field's uniq name (path)\n */\nexport function useWatch<TValue = FieldValue>(name: FieldName): TValue {\n  const refresh = useRefresh();\n\n  const formModel = useFormModel();\n\n  if (!formModel) {\n    throw new Error('[Form] error in useWatch, formModel not found');\n  }\n\n  const value = formModel.getValueIn<TValue>(name);\n\n  useEffect(() => {\n    const disposable = formModel.onFormValuesUpdated(({ name: updatedName }) => {\n      if (updatedName === name) {\n        refresh();\n      }\n    });\n    return () => disposable.dispose();\n  }, [name, formModel]);\n\n  return value;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/react/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { FormModel } from '../core/form-model';\nimport { FormModelContext } from './context';\n\nexport function useFormModel(): FormModel {\n  return useContext<FormModel>(FormModelContext);\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/types/common.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type Context = any;\n"
  },
  {
    "path": "packages/node-engine/form/src/types/field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Errors, FieldError, FieldWarning, Warnings } from './validate';\nimport { FormState } from './form';\n\nexport type NativeFieldValue = string | number | boolean | null | undefined | unknown[];\n\nexport type FieldValue = any;\nexport type FieldArrayValue = Array<any> | undefined;\nexport type FieldName = string;\n\nexport type CustomElement = Partial<HTMLElement> & {\n  name: FieldName;\n  type?: string;\n  value?: any;\n  disabled?: boolean;\n  checked?: boolean;\n  options?: HTMLOptionsCollection;\n  files?: FileList | null;\n  focus?: () => void;\n};\n\nexport type FieldElement =\n  | HTMLInputElement\n  | HTMLSelectElement\n  | HTMLTextAreaElement\n  | CustomElement;\n\nexport type Ref = FieldElement;\n\n/**\n * Field render model, it's only available when Field is rendered\n */\nexport interface Field<\n  TFieldValue extends FieldValue = FieldValue,\n  E = React.ChangeEvent<any> | TFieldValue\n> {\n  /**\n   * Uniq key for the Field, you can use it for the child react component's uniq key.\n   */\n  key: string;\n  /**\n   * A function which sends the input's value to Field.\n   * It should be assigned to the onChange prop of the input component\n   * @param e It can be the new value of the field or the event sent by original dom input or checkbox component.\n   */\n  onChange: (e: E) => void;\n  /**\n   * The current value of Field\n   */\n  value: TFieldValue;\n  /**\n   * Field's name (path)\n   */\n  name: FieldName;\n  /**\n   * A function which sends the input's onFocus event to Field. It should be assigned to the input's onFocus prop.\n   */\n  onFocus?: () => void;\n  /**\n   * A function which sends the input's onBlur event to Field. It should be assigned to the input's onBlur prop.\n   */\n  onBlur?: () => void;\n}\n\n/**\n * FieldArray render model, it's only available when FieldArray is rendered\n */\nexport interface FieldArray<TFieldValue extends FieldValue = FieldValue>\n  extends Field<Array<TFieldValue> | undefined, Array<TFieldValue> | undefined> {\n  /**\n   * Same as native Array.map, the first param of the callback function is the child field of this FieldArray.\n   * @param cb callback function\n   */\n  map: <T = any>(cb: (f: Field<TFieldValue>, index: number) => T) => T[];\n  /**\n   * Append a value at the end of the array, it will create a new Field for this value as well.\n   * @param value the value to append\n   */\n  append: (value: TFieldValue) => Field<TFieldValue>;\n  /**\n   * @deprecated use remove instead\n   * Delete the value and the related field at certain index of the array.\n   * @param index the index of the element to delete\n   */\n  delete: (index: number) => void;\n  /**\n   * Delete the value and the related field at certain index of the array.\n   * @param index the index of the element to delete\n   */\n  remove: (index: number) => void;\n  /**\n   * Move an array element from one position to another.\n   * @param from from position\n   * @param to to position\n   */\n  move: (from: number, to: number) => void;\n  /**\n   * Swap the position of two elements of the array.\n   * @param from\n   * @param to\n   */\n  swap: (from: number, to: number) => void;\n}\n\nexport interface FieldOptions<TValue, TFormValues = any> {\n  /**\n   * Field's name(path), it should be uniq within a form instance.\n   * Two Fields Rendered with the same name will link to the same part of data and field status such as errors is shared.\n   */\n  name: FieldName;\n  /**\n   * Default value of the field. Please notice that Field is a render model, so this default value will only be set when\n   * the field is rendered. If you want to give a default value before field rendering, please set it in the Form's defaultValue.\n   */\n  defaultValue?: TValue;\n  /**\n   * This is a render prop. A function that returns a React element and provides the ability to attach events and value into the component.\n   * This simplifies integrating with external controlled components with non-standard prop names. Provides field、fieldState and formState, to the child component.\n   * @param props\n   */\n  render?: (props: FieldRenderProps<TValue>) => React.ReactElement;\n}\n\nexport interface FieldRenderProps<TValue> {\n  field: Field<TValue>;\n  fieldState: Readonly<FieldState>;\n  formState: Readonly<FormState>;\n}\n\nexport interface FieldArrayOptions<TValue> {\n  /**\n   * Field's name(path), it should be uniq within a form instance.\n   * Two Fields Rendered with the same name will link to the same part of data and field status such as errors is shared.\n   */\n  name: FieldName;\n  /**\n   * Default value of the field. Please notice that Field is a render model, so this default value will only be set when\n   * the field is rendered. If you want to give a default value before field rendering, please set it in the Form's initialValues.\n   */\n  defaultValue?: TValue[];\n  /**\n   * This is a render prop. A function that returns a React element and provides the ability to attach events and value into the component.\n   * This simplifies integrating with external controlled components with non-standard prop names. Provides field、fieldState and formState, to the child component.\n   * @param props\n   */\n  render?: (props: FieldArrayRenderProps<TValue>) => React.ReactElement;\n}\n\nexport interface FieldArrayRenderProps<TValue> {\n  field: FieldArray<TValue>;\n  fieldState: Readonly<FieldState>;\n  formState: Readonly<FormState>;\n}\n\nexport interface UseFieldReturn {}\n\nexport interface FieldState {\n  /**\n   * If field value is invalid\n   */\n  invalid: boolean;\n  /**\n   * If field input component is touched by user\n   */\n  isTouched: boolean;\n  /**\n   * If field current value is different from the initialValue.\n   */\n  isDirty: boolean;\n  /**\n   * If field is validating.\n   */\n  isValidating: boolean;\n  /**\n   * Field errors, empty array means there is no errors.\n   */\n  errors?: FieldError[];\n  /**\n   * Field warnings, empty array means there is no warnings.\n   */\n  warnings?: FieldWarning[];\n}\n\nexport interface FieldModelState extends Omit<FieldState, 'errors' | 'warnings'> {\n  errors?: Errors;\n  warnings?: Warnings;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/types/form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormModel } from '../core/form-model';\nimport { Errors, FormValidateReturn, Validate, ValidateTrigger, Warnings } from './validate';\nimport { Field, FieldArray, FieldName, FieldValue } from './field';\nimport { Context } from './common';\n\nexport interface FormState {\n  // isLoading: boolean;\n  /**\n   * If the form data is valid\n   */\n  invalid: boolean;\n  /**\n   * If the form data is different from the intialValues\n   */\n  isDirty: boolean;\n  /**\n   * If the form fields have been touched\n   */\n  isTouched: boolean;\n  /**\n   * If the form is during validation\n   */\n  isValidating: boolean;\n  /**\n   * Form errors\n   */\n  errors?: Errors;\n  /**\n   * Form warnings\n   */\n  warnings?: Warnings;\n}\n\nexport interface FormModelState extends Omit<FormState, 'errors' | 'warnings'> {\n  errors?: Errors;\n  warnings?: Warnings;\n}\n\nexport interface FormOptions<TValues = any> {\n  /**\n   * InitialValues of the form.\n   */\n  initialValues?: TValues;\n  /**\n   * When should the validation trigger, for example onChange or onBlur.\n   */\n  validateTrigger?: ValidateTrigger;\n  /**\n   * Form data's validation rules. It's a key value map, where the key is a pattern of data's path (or field name), the value is a validate function.\n   */\n  validate?:\n    | Record<string, Validate>\n    | ((value: TValues, ctx: Context) => Record<string, Validate>);\n  /**\n   * Custom context. It will be accessible via form instance or in validate function.\n   */\n  context?: Context;\n}\n\nexport interface Form<TValues = any> {\n  /**\n   * The initialValues of the form.\n   */\n  initialValues: TValues;\n  /**\n   * Form values. Returns a deep copy of the data in the store.\n   */\n  values: TValues;\n  /**\n   * Form state\n   */\n  state: FormState;\n\n  /**\n   * Get value in certain path\n   * @param name path\n   */\n  getValueIn<TValue = FieldValue>(name: FieldName): TValue;\n\n  /**\n   * Set value in certain path.\n   * It will trigger the re-rendering of the Field Component if a Field is related to this path\n   * @param name path\n   */\n  setValueIn<TValue>(name: FieldName, value: TValue): void;\n\n  /**\n   * Trigger validate for the whole form.\n   */\n  validate: () => Promise<FormValidateReturn>;\n}\n\nexport interface FormRenderProps<TValues> {\n  /**\n   * Form instance.\n   */\n  form: Form<TValues>;\n}\n\nexport interface FormControl<TValues> {\n  _formModel: FormModel<TValues>;\n  getField: <\n    TValue = FieldValue,\n    TField extends Field<TValue> | FieldArray<TValue> = Field<TValue>\n  >(\n    name: FieldName\n  ) => Field<TValue> | FieldArray<TValue> | undefined;\n  /** 手动初始化form */\n  init: () => void;\n}\n\nexport interface CreateFormReturn<TValues> {\n  form: Form<TValues>;\n  control: FormControl<TValues>;\n}\n\nexport interface OnFormValuesChangeOptions {\n  action?: 'array-append' | 'array-splice' | 'array-swap';\n  indexes?: number[];\n}\n\nexport interface OnFormValuesChangePayload {\n  values: FieldValue;\n  prevValues: FieldValue;\n  name: FieldName;\n  options?: OnFormValuesChangeOptions;\n}\n\nexport interface OnFormValuesInitPayload {\n  values: FieldValue;\n  prevValues: FieldValue;\n  name: FieldName;\n}\n\nexport interface OnFormValuesUpdatedPayload {\n  values: FieldValue;\n  prevValues: FieldValue;\n  name: FieldName;\n  options?: OnFormValuesChangeOptions;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/types/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './field';\nexport * from './form';\nexport * from './validate';\n"
  },
  {
    "path": "packages/node-engine/form/src/types/validate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MaybePromise } from '@flowgram.ai/utils';\n\nimport { FieldName } from './field';\nimport { Context } from './common';\n\nexport enum FeedbackLevel {\n  Error = 'error',\n  Warning = 'warning',\n}\n\nexport interface Feedback<FeedbackLevel> {\n  /**\n   * The data path (or field path) that generate this feedback\n   */\n  name: string;\n  /**\n   * The type of the feedback\n   */\n  type?: string;\n  /**\n   * Feedback level\n   */\n  level: FeedbackLevel;\n  /**\n   * Feedback message\n   */\n  message: string | React.ReactNode;\n}\n\nexport type FieldError = Feedback<FeedbackLevel.Error>;\nexport type FieldWarning = Feedback<FeedbackLevel.Warning>;\n\nexport type FormErrorOptions = Omit<FieldError, 'name'>;\nexport type FormWarningOptions = Omit<FieldWarning, 'name'>;\nexport type FeedbackOptions<FeedbackLevel> = Omit<Feedback<FeedbackLevel>, 'name'>;\n\nexport type Validate<TFieldValue = any, TFormValues = any> = (props: {\n  /**\n   * Value of the data to validate\n   */\n  value: TFieldValue;\n  /**\n   * Complete form values\n   */\n  formValues: TFormValues;\n  /**\n   * The path of the data we are validating\n   */\n  name: FieldName;\n  /**\n   * The custom context set when init form\n   */\n  context: Context;\n}) =>\n  | MaybePromise<string>\n  | MaybePromise<FormErrorOptions>\n  | MaybePromise<FormWarningOptions>\n  | MaybePromise<undefined>;\n\nexport function isFieldError(f: Feedback<any>): f is FieldError {\n  if (f.level === FeedbackLevel.Error) {\n    return true;\n  }\n  return false;\n}\n\nexport function isFieldWarning(f: Feedback<any>): f is FieldWarning {\n  if (f.level === FeedbackLevel.Warning) {\n    return true;\n  }\n  return false;\n}\n\nexport type Errors = Record<FieldName, FieldError[]>;\nexport type Warnings = Record<FieldName, FieldWarning[]>;\n\nexport enum ValidateTrigger {\n  onChange = 'onChange',\n  onBlur = 'onBlur',\n}\n\nexport type FormValidateReturn = (FieldError | FieldWarning)[];\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/dom.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport function isReactChangeEvent(e: unknown): e is React.ChangeEvent<HTMLInputElement> {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    'target' in e &&\n    typeof (e as React.ChangeEvent<any>).target === 'object'\n  );\n}\n\nexport function isCheckBoxEvent(e: unknown): e is React.ChangeEvent<HTMLInputElement> {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    'target' in e &&\n    typeof (e as React.ChangeEvent<HTMLInputElement>).target === 'object' &&\n    (e as React.ChangeEvent<HTMLInputElement>).target.type === 'checkbox'\n  );\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/event.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Emitter } from '@flowgram.ai/utils';\n\ninterface Payload<T> {\n  origin?: T;\n  current?: T;\n}\n\nexport class EmitterChain<T> {\n  protected emitter: Emitter<Payload<T>>;\n\n  constructor() {\n    this.emitter = new Emitter<Payload<T>>();\n  }\n\n  get event() {\n    return this.emitter.event;\n  }\n\n  _fire(current?: T, origin?: T) {\n    this.emitter.fire({ current, origin });\n  }\n\n  fire(current: T, next?: EmitterChain<T>) {\n    this._fire(current);\n    next?._fire(undefined, current);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/glob.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { flatten, get, isArray, isObject } from 'lodash-es';\n\nexport namespace Glob {\n  export const DIVIDER = '.';\n  export const ALL = '*';\n\n  // 仅支持一个通配符\n  export function isMatch(pattern: string, path: string) {\n    const patternArr = pattern.split(DIVIDER);\n    const pathArr = path.split(DIVIDER);\n    if (patternArr.length !== pathArr.length) {\n      return false;\n    }\n    return patternArr.every((pattern, index) => {\n      if (pattern === ALL) {\n        return true;\n      }\n      return pattern === pathArr[index];\n    });\n  }\n\n  /**\n   * 判断pattern 是否match pattern 或其parent\n   * @param pattern\n   * @param path\n   */\n  export function isMatchOrParent(pattern: string, path: string) {\n    if (pattern === '') {\n      return true;\n    }\n    const patternArr = pattern.split(DIVIDER);\n    const pathArr = path.split(DIVIDER);\n\n    if (patternArr.length > pathArr.length) {\n      return false;\n    }\n\n    for (let i = 0; i < patternArr.length; i++) {\n      if (patternArr[i] !== ALL && patternArr[i] !== pathArr[i]) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  /**\n   * 从 path 中提取出匹配pattern 的 parent path，包括是 path 自身\n   * 该方法默认 isMatchOrParent(pattern, path) 为 true, 不做为false 的错误处理。\n   * @param pattern\n   * @param path\n   */\n  export function getParentPathByPattern(pattern: string, path: string) {\n    const patternArr = pattern.split(DIVIDER);\n    const pathArr = path.split(DIVIDER);\n\n    return pathArr.slice(0, patternArr.length).join(DIVIDER);\n  }\n\n  function concatPath(p1: string | number, ...pathArr: (string | number)[]): string {\n    const p2 = pathArr.shift();\n    if (p2 === undefined) return p1.toString();\n    let resultPath = '';\n    if (p1 === '' && p2 === '') {\n      resultPath = '';\n    } else if (p1 !== '' && p2 === '') {\n      resultPath = p1.toString();\n    } else if (p1 === '' && p2 !== '') {\n      resultPath = p2.toString();\n    } else {\n      resultPath = `${p1}${DIVIDER}${p2}`;\n    }\n    if (pathArr.length > 0) {\n      return concatPath(resultPath, ...pathArr);\n    }\n    return resultPath;\n  }\n\n  /**\n   * 找到 obj 在给与 paths 下所有子path\n   * @param paths\n   * @param obj\n   * @private\n   */\n  export function getSubPaths(paths: string[], obj: any): string[] {\n    if (!obj || typeof obj !== 'object') {\n      return [];\n    }\n\n    return flatten(\n      paths.map((path) => {\n        const value = path === '' ? obj : get(obj, path);\n        if (isArray(value)) {\n          return value.map((_: any, index: number) => concatPath(path, index));\n        } else if (isObject(value)) {\n          return Object.keys(value).map((key) => concatPath(path, key));\n        }\n        return [];\n      })\n    );\n  }\n\n  /**\n   * 将带有通配符的 path pattern 分割。如 a.b.*.c.*.d, 会被分割成['a.b','*','c','*','d']\n   * @param pattern\n   * @private\n   */\n  export function splitPattern(pattern: string): string[] {\n    const parts = pattern.split(DIVIDER);\n    const res: string[] = [];\n\n    let i = 0;\n    let curPath: string[] = [];\n\n    while (i < parts.length) {\n      if (parts[i] === ALL) {\n        if (curPath.length) {\n          res.push(curPath.join(DIVIDER));\n        }\n        res.push(ALL);\n        curPath = [];\n      } else {\n        curPath.push(parts[i]);\n      }\n      i += 1;\n    }\n    if (curPath.length) {\n      res.push(curPath.join(DIVIDER));\n    }\n    return res;\n  }\n\n  /**\n   * Find all paths matched pattern in object. If withEmptyValue is true, it will include\n   * paths  whoes value is undefined.\n   * @param obj\n   * @param pattern\n   * @param withEmptyValue\n   */\n\n  export function findMatchPaths(obj: any, pattern: string, withEmptyValue?: boolean): string[] {\n    if (!obj || !pattern) {\n      return [];\n    }\n    const nextPaths: string[] = pattern.split(DIVIDER);\n    let curKey: string | undefined = nextPaths.shift();\n    let curPaths: string[] = [];\n    let curValue = obj;\n    while (curKey) {\n      let isObject = typeof curValue === 'object' && curValue !== null;\n      if (!isObject) return [];\n      // 匹配 *\n      if (curKey === ALL) {\n        const parentPath = curPaths.join(DIVIDER);\n        return flatten(\n          Object.keys(curValue).map((key) => {\n            if (nextPaths.length === 0) {\n              return concatPath(parentPath, key);\n            }\n            return findMatchPaths(curValue[key], `${nextPaths.join(DIVIDER)}`, withEmptyValue).map(\n              (p) => concatPath(parentPath, key, p)\n            );\n          })\n        );\n      }\n      // 找不到对应 key 则不匹配\n      if (!(curKey in curValue) && !withEmptyValue) {\n        return [];\n      }\n      curValue = curValue[curKey!];\n      curPaths.push(curKey);\n      curKey = nextPaths.shift();\n    }\n\n    return [pattern];\n\n    // const parts = splitPattern(pattern);\n    //\n    // let prePaths: string[] = [''];\n    // let curPath: string = '';\n    //\n    // for (let i in parts) {\n    //   const part = parts[i];\n    //   if (part === ALL) {\n    //     prePaths = getSubPaths(\n    //       prePaths.map(p => concatPath(p, curPath)),\n    //       obj,\n    //     );\n    //     curPath = '';\n    //   } else {\n    //     curPath = part;\n    //\n    //     /**\n    //      * 过滤掉后续path 值不存在的prePath\n    //      * 为什么： prePaths 是返回前一个通配符下所有的路径，但每个路径下的数据的field 可能不同\n    //      * 这会导致一些prePath 不存在后面所需的路径。如以下场景\n    //      * const obj = {\n    //      *   a: { b: { c: 1 } },\n    //      *   x: { y: { z: 2 } },\n    //      * };\n    //      * expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);\n    //      */\n    //\n    //     prePaths = prePaths.filter(p => {\n    //       const preValue = p ? get(obj, p) : obj;\n    //       if (typeof preValue === 'object') {\n    //         return curPath in preValue;\n    //       }\n    //       return true;\n    //     });\n    //   }\n    // }\n    //\n    // if (curPath) {\n    //   return prePaths.map(p => [p, curPath].join(DIVIDER));\n    // }\n    // return prePaths;\n  }\n\n  /**\n   * Find all paths matched pattern in object, including paths  whoes value is undefined.\n   * @param obj\n   * @param pattern\n   */\n  export function findMatchPathsWithEmptyValue(obj: any, pattern: string): string[] {\n    if (!pattern.includes('*')) {\n      return [pattern];\n    }\n    return findMatchPaths(obj, pattern, true);\n  }\n\n  // export function findMatchPathsWithEmptyValue(obj: any, pattern: string) {\n  //   const parts = splitPattern(pattern);\n  //\n  //   let prePaths: string[] = [''];\n  //   let curPath: string = '';\n  //\n  //   for (let i in parts) {\n  //     const part = parts[i];\n  //     if (part === ALL) {\n  //       prePaths = getSubPaths(\n  //         prePaths.map(p => concatPath(p, curPath)),\n  //         obj,\n  //       );\n  //       curPath = '';\n  //     } else {\n  //       curPath = part;\n  //\n  //       /**\n  //        * 过滤掉后续path 值不存在的prePath\n  //        * 为什么： prePaths 是返回前一个通配符下所有的路径，但每个路径下的数据的field 可能不同\n  //        * 这会导致一些prePath 不存在后面所需的路径。如以下场景\n  //        * const obj = {\n  //        *   a: { b: { c: 1 } },\n  //        *   x: { y: { z: 2 } },\n  //        * };\n  //        * expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);\n  //        */\n  //\n  //       // prePaths = prePaths.filter(p => {\n  //       //   const preValue = p ? get(obj, p) : obj;\n  //       //   if (typeof preValue === 'object') {\n  //       //     return curPath in preValue;\n  //       //   }\n  //       //   return true;\n  //       // });\n  //     }\n  //   }\n  //\n  //   if (curPath) {\n  //     return prePaths.map(p => [p, curPath].join(DIVIDER));\n  //   }\n  //   return prePaths;\n  // }\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './object';\nexport * from './dom';\nexport * from './glob';\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/object.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { clone, toPath } from 'lodash-es';\n\n/**\n * These functions are copied from Formik.\n * @see https://github.com/jaredpalmer/formik\n */\n\nexport const isEmptyArray = (value?: any) => Array.isArray(value) && value.length === 0;\n\n/** @private is the given object a Function? */\nexport const isFunction = (obj: any): obj is Function => typeof obj === 'function';\n\n/** @private is the given object an Object? */\nexport const isObject = (obj: any): obj is Object => obj !== null && typeof obj === 'object';\n\n/** @private is the given object an integer? */\nexport const isInteger = (obj: any): boolean => String(Math.floor(Number(obj))) === obj;\n\n/** @private is the given object a string? */\nexport const isString = (obj: any): obj is string =>\n  Object.prototype.toString.call(obj) === '[object String]';\n\n/** @private is the given object a NaN? */\n// eslint-disable-next-line no-self-compare\nexport const isNaN = (obj: any): boolean => obj !== obj;\n\n/** @private is the given object/value a promise? */\nexport const isPromise = (value: any): value is PromiseLike<any> =>\n  isObject(value) && isFunction(value.then);\n\n/**\n * Deeply get a value from an object via its path.\n */\nexport function getIn(obj: any, key: string | string[], def?: any, p: number = 0) {\n  const path = toPath(key);\n  while (obj && p < path.length) {\n    obj = obj[path[p++]];\n  }\n\n  // check if path is not in the end\n  if (p !== path.length && !obj) {\n    return def;\n  }\n\n  return obj === undefined ? def : obj;\n}\n\n/**\n * Deeply set a value from in object via its path. If the value at `path`\n * has changed, return a shallow copy of obj with `value` set at `path`.\n * If `value` has not changed, return the original `obj`.\n *\n * Existing objects / arrays along `path` are also shallow copied. Sibling\n * objects along path retain the same internal js reference. Since new\n * objects / arrays are only created along `path`, we can test if anything\n * changed in a nested structure by comparing the object's reference in\n * the old and new object, similar to how russian doll cache invalidation\n * works.\n */\nexport function shallowSetIn(obj: any, path: string, value: any): any {\n  let res: any = clone(obj); // this keeps inheritance when obj is a class\n  let resVal: any = res;\n  let i = 0;\n  let pathArray = toPath(path);\n\n  for (; i < pathArray.length - 1; i++) {\n    const currentPath: string = pathArray[i];\n    let currentObj: any = getIn(obj, pathArray.slice(0, i + 1));\n\n    if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {\n      resVal = resVal[currentPath] = clone(currentObj);\n    } else {\n      const nextPath: string = pathArray[i + 1];\n      resVal = resVal[currentPath] = isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};\n    }\n  }\n\n  // Return original object if new value is the same as current\n  //  `pathArray[i] in obj` is to supoort set undefined value with unknown key\n  if ((i === 0 ? obj : resVal)[pathArray[i]] === value && pathArray[i] in obj) {\n    return obj;\n  }\n\n  /**\n   * In Formik, they delete the key if the value is undefined. but here we keep the key with the undefined value.\n   * The reason that Formik tackle in this way is to fix the issue https://github.com/jaredpalmer/formik/issues/727\n   * Their fix is https://github.com/jaredpalmer/formik/issues/727, and we roll back to the code before this PR.\n   */\n  resVal[pathArray[i]] = value;\n  return res;\n}\n\nexport function keepValidKeys(obj: Record<string, any>, validKeys: string[]) {\n  const validKeysSet = new Set(validKeys);\n  const newObj: Record<string, any> = {};\n  Object.keys(obj).forEach((key) => {\n    if (validKeysSet.has(key)) {\n      newObj[key] = obj[key];\n    }\n  });\n  return newObj;\n}\n"
  },
  {
    "path": "packages/node-engine/form/src/utils/validate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Errors,\n  Feedback,\n  FeedbackLevel,\n  FieldError,\n  FieldName,\n  FieldWarning,\n  FormErrorOptions,\n  FormWarningOptions,\n} from '../types';\n\nexport function toFeedback(\n  result: string | FormErrorOptions | FormWarningOptions | undefined,\n  name: FieldName\n): FieldError | FieldWarning | undefined {\n  if (typeof result === 'string') {\n    return {\n      name,\n      message: result,\n      level: FeedbackLevel.Error,\n    };\n  } else if (result?.message) {\n    return {\n      ...result,\n      name,\n    };\n  }\n}\n\nexport function feedbackToFieldErrorsOrWarnings<T>(name: string, feedback?: Feedback<any>) {\n  return {\n    [name]: feedback ? [feedback] : [],\n  } as T;\n}\n\nexport const hasError = (errors: Errors) =>\n  Object.keys(errors).some((key) => errors[key]?.length > 0);\n"
  },
  {
    "path": "packages/node-engine/form/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\",\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"./__mocks__\",\n    \"./__tests__\"\n  ]\n}\n"
  },
  {
    "path": "packages/node-engine/form/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src')\n    },\n  },\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/__tests__/form-path-service.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { FormPathService } from '../src/form/services/form-path-service';\n\ndescribe('FormPathService', () => {\n  it('should parse array item path correctly', () => {\n    const path = 'a/b/2/description';\n    const result = FormPathService.parseArrayItemPath(path);\n\n    expect(result).toEqual({\n      itemIndex: 2,\n      arrayPath: 'a/b/[]',\n      itemMetaPath: 'a/b/[]/description',\n    });\n  });\n\n  it('should handle non-numeric item index correctly', () => {\n    const path = 'a/b';\n    const result = FormPathService.parseArrayItemPath(path);\n\n    expect(result).toBeNull();\n  });\n\n  it('should handle empty path correctly', () => {\n    const path = '';\n    const result = FormPathService.parseArrayItemPath(path);\n\n    expect(result).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/__tests__/form.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\ndescribe('form test', () => {\n  it('form test', () => {\n    // eslint-disable-next-line no-console\n    console.log('form test');\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/form-core\",\n  \"version\": \"0.1.8\",\n  \"description\": \"automation form core\",\n  \"keywords\": [\n    \"flow\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/client/create-node-container-modules.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NodeContainerModule } from '../node/node-container-module';\nimport { FormCoreContainerModule } from '../form';\nimport { ErrorContainerModule } from '../error';\n\nexport function createNodeContainerModules() {\n  return [NodeContainerModule, FormCoreContainerModule, ErrorContainerModule];\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/client/create-node-entity-datas.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityDataRegistry } from '@flowgram.ai/core';\n\nimport { FlowNodeFormData } from '../form';\nimport { FlowNodeErrorData } from '../error';\n\nexport function createNodeEntityDatas(): EntityDataRegistry[] {\n  return [FlowNodeFormData, FlowNodeErrorData];\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-node-container-modules';\nexport * from './node-render';\nexport * from './node-material-client';\nexport * from './create-node-entity-datas';\nexport * from '../form/client';\nexport * from '../error/client';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/client/node-material-client.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MATERIAL_KEY } from '../node/core-materials';\nimport { NodeManager, NodePlaceholderRender, Render } from '../node';\n\nexport function registerNodeErrorRender(nodeManager: NodeManager, render: Render): void {\n  nodeManager.registerMaterialRender(MATERIAL_KEY.NODE_ERROR_RENDER, render);\n}\n\nexport function registerNodePlaceholderRender(\n  nodeManager: NodeManager,\n  render: NodePlaceholderRender,\n): void {\n  nodeManager.registerMaterialRender(MATERIAL_KEY.NODE_PLACEHOLDER_RENDER, render);\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/client/node-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { memo, useCallback, useEffect } from 'react';\n\nimport { PlaygroundContext, useRefresh, useService, PluginContext } from '@flowgram.ai/core';\n\nimport { NodeEngineReactContext } from '../node-react/context/node-engine-react-context';\nimport { useNodeEngineContext } from '../node-react';\nimport { NodeManager } from '../node/node-manager';\nimport { PLUGIN_KEY } from '../node/core-plugins';\nimport { MATERIAL_KEY, type NodeRenderProps } from '../node';\nimport { getFormModel, isNodeFormReady } from '../form';\nimport { FlowNodeErrorData } from '../error/flow-node-error-data';\nimport { getNodeError } from '../error';\n\nconst PureNodeRender = ({ node }: NodeRenderProps) => {\n  const refresh = useRefresh();\n  const nodeErrorData = node.getData<FlowNodeErrorData>(FlowNodeErrorData);\n  const formModel = getFormModel(node);\n  const isNodeError = !!getNodeError(node);\n  const isFormReady = isNodeFormReady(node);\n  const playgroundContext = useService<PlaygroundContext>(PlaygroundContext);\n  const clientContext = useService<PluginContext>(PluginContext);\n  const nodeManager = useService<NodeManager>(NodeManager);\n  const nodeFormRender = nodeManager.getPluginRender(PLUGIN_KEY.FORM);\n  const nodeErrorRender = nodeManager.getPluginRender(PLUGIN_KEY.ERROR);\n  const nodePlaceholderRender = nodeManager.getMaterialRender(MATERIAL_KEY.NODE_PLACEHOLDER_RENDER);\n\n  const nodeEngineContext = useNodeEngineContext();\n\n  useEffect(() => {\n    const errorDisposable = nodeErrorData.onDataChange(() => {\n      refresh();\n    });\n    const formDisposable = formModel.onInitialized(() => {\n      refresh();\n    });\n    return () => {\n      errorDisposable.dispose();\n      formDisposable.dispose();\n    };\n  }, []);\n\n  const renderContent = useCallback(() => {\n    if (isNodeError) {\n      return nodeErrorRender!({ node, playgroundContext, clientContext });\n    }\n    if (!formModel.formMeta) {\n      return null;\n    }\n    if (isFormReady) {\n      return nodeFormRender!({ node, playgroundContext, clientContext });\n    }\n    return nodePlaceholderRender?.({ node, playgroundContext }) || null;\n  }, [\n    isNodeError,\n    isFormReady,\n    nodeErrorRender,\n    nodeFormRender,\n    nodePlaceholderRender,\n    node,\n    playgroundContext,\n  ]);\n\n  return (\n    <NodeEngineReactContext.Provider value={nodeEngineContext.json}>\n      {nodeManager.nodeRenderHoc(renderContent)()}\n    </NodeEngineReactContext.Provider>\n  );\n};\n\nexport const NodeRender = memo(PureNodeRender);\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/client.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FlowNodeErrorData } from './flow-node-error-data';\n\nexport function getNodeError(node: FlowNodeEntity) {\n  return node.getData<FlowNodeErrorData>(FlowNodeErrorData).getError();\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/error-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\nimport { bindContributions } from '@flowgram.ai/utils';\n\nimport { NodeContribution } from '../node';\nimport { ErrorNodeContribution } from './error-node-contribution';\n\nexport const ErrorContainerModule = new ContainerModule(bind => {\n  bindContributions(bind, ErrorNodeContribution, [NodeContribution]);\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/error-node-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\nimport { NodeContribution } from '../node';\nimport { NodeManager, PLUGIN_KEY } from '../node';\nimport { errorPluginRender } from './renders';\n\n@injectable()\nexport class ErrorNodeContribution implements NodeContribution {\n  onRegister(nodeManager: NodeManager) {\n    nodeManager.registerPluginRender(PLUGIN_KEY.ERROR, errorPluginRender);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/flow-node-error-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { EntityData } from '@flowgram.ai/core';\n\nexport interface ErrorData {\n  error: Error | null;\n}\n\nexport class FlowNodeErrorData extends EntityData {\n  static type = 'FlowNodeErrorData';\n\n  getDefaultData(): ErrorData {\n    return { error: null };\n  }\n\n  setError(e: ErrorData['error']) {\n    this.update({ error: e });\n  }\n\n  getError(): Error {\n    return this.data.error;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './flow-node-error-data';\nexport * from './types';\nexport * from './error-container-module';\nexport * from './client';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/renders/default-error-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { NodeErrorRenderProps } from '../types';\n\nconst ERROR_STYLE = {\n  color: '#f54a45',\n};\n\nexport const defaultErrorRender = ({ error }: NodeErrorRenderProps) => (\n  <div style={ERROR_STYLE}>{error.message}</div>\n);\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/renders/error-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useCallback, useEffect } from 'react';\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext, useRefresh, useService, PluginContext } from '@flowgram.ai/core';\n\nimport { FlowNodeErrorData } from '../flow-node-error-data';\nimport { MATERIAL_KEY, NodeManager, NodePluginRender } from '../../node';\nimport { defaultErrorRender } from './default-error-render';\n\ninterface NodeRenderProps {\n  node: FlowNodeEntity;\n  playgroundContext: PlaygroundContext;\n  clientContext: PluginContext;\n}\n\nexport const ErrorRender = ({ node, playgroundContext, clientContext }: NodeRenderProps) => {\n  const refresh = useRefresh();\n  const nodeErrorData = node.getData<FlowNodeErrorData>(FlowNodeErrorData);\n  const nodeError = nodeErrorData.getError();\n  const nodeManager = useService<NodeManager>(NodeManager);\n  const nodeErrorRender = nodeManager.getMaterialRender(MATERIAL_KEY.NODE_ERROR_RENDER);\n\n  const renderError = useCallback(() => {\n    if (!nodeErrorRender) {\n      return defaultErrorRender({\n        error: nodeError,\n        context: { node, playgroundContext, clientContext },\n      });\n    }\n    return nodeErrorRender({\n      error: nodeError,\n      context: { node, playgroundContext, clientContext },\n    });\n  }, [nodeError, node, playgroundContext, clientContext]);\n\n  useEffect(() => {\n    const disposable = nodeErrorData.onDataChange(() => {\n      refresh();\n    });\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  return nodeError ? renderError() : null;\n};\n\nexport const errorPluginRender: NodePluginRender = (props) => <ErrorRender {...props} />;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/renders/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './error-render';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/error/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { NodeContext, Render } from '../node';\n\nexport interface NodeErrorRenderProps {\n  error: Error;\n  context: NodeContext;\n}\n\nexport type NodeErrorRender = Render<NodeErrorRenderProps>;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/decorator-ability/decorator-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport class DecoratorAbility implements FormItemAbility {\n  static readonly type = 'decorator';\n\n  get type(): string {\n    return DecoratorAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/decorator-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './decorator-ability';\nexport * from './types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/decorator-ability/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemContext, FormItemFeedback } from '../../types';\nimport { SetterOrDecoratorContext } from '../../abilities/setter-ability';\n\nexport interface DecoratorAbilityOptions {\n  /**\n   * 已注册的decorator的唯一标识\n   */\n  key: string;\n}\n\nexport interface DecoratorComponentProps<CustomOptions = any>\n  extends FormItemFeedback,\n    FormItemContext {\n  readonly: boolean;\n  children?: any;\n  options: DecoratorAbilityOptions & CustomOptions;\n  context: SetterOrDecoratorContext;\n}\n\nexport interface DecoratorExtension {\n  key: string;\n  component: (props: DecoratorComponentProps) => any;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/default-ability/default-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport class DefaultAbility implements FormItemAbility {\n  static readonly type = 'default';\n\n  get type(): string {\n    return DefaultAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/default-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './default-ability';\nexport * from './types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/default-ability/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemContext, FormItemMaterialContext } from '../..';\n\nexport interface GetDefaultValueProps extends FormItemContext {\n  options: DefaultAbilityOptions;\n  context: FormItemMaterialContext;\n}\n\nexport interface DefaultAbilityOptions<T = any> {\n  getDefaultValue: (params: GetDefaultValueProps) => T;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/effect-ability/effect-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport class EffectAbility implements FormItemAbility {\n  static readonly type = 'effect';\n\n  get type(): string {\n    return EffectAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/effect-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './effect-ability';\nexport * from './types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/effect-ability/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemContext, FormItemEventName } from '../../types';\nimport { type FormItemMaterialContext } from '../../models/form-item-material-context';\n\nexport interface EffectAbilityOptions {\n  /**\n   * 已注册的effect 唯一标识\n   */\n  key?: string;\n  /**\n   * 触发 effect 的事件\n   */\n  event?: FormItemEventName;\n  /**\n   * 如果不使用已经注册的effect, 也支持直接写effect函数\n   */\n  effect?: EffectFunction;\n}\n\nexport interface EffectEvent {\n  target: any & { value: any };\n  currentTarget: any;\n  type: FormItemEventName;\n}\n\nexport interface EffectProps<CustomOptions = any, Event = EffectEvent> extends FormItemContext {\n  event: Event;\n  options: EffectAbilityOptions & CustomOptions;\n  context: FormItemMaterialContext;\n}\n\nexport type EffectFunction = (props: EffectProps) => void;\n\nexport interface EffectExtension {\n  key: string;\n  effect: EffectFunction;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './setter-ability';\nexport * from './decorator-ability';\nexport * from './visibility-ability';\nexport * from './effect-ability';\nexport * from './default-ability';\nexport * from './validation-ability';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/setter-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './setter-ability';\nexport * from './types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/setter-ability/setter-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport class SetterAbility implements FormItemAbility {\n  static readonly type = 'setter';\n\n  get type(): string {\n    return SetterAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/setter-ability/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FormItemContext, FormItemFeedback } from '../../types';\nimport { type FormItemMaterialContext } from '../../models/form-item-material-context';\nimport { ValidatorFunction } from '../../abilities/validation-ability';\n\nexport interface SetterAbilityOptions {\n  /**\n   * 已注册的setter的唯一标识\n   */\n  key: string;\n}\n\n/**\n * Setter context 是 FormItemMaterialContext 的外观\n * 基于外观设计模式设计，屏蔽了FormItemMaterialContext中一些setter不可见的接口\n * readonly: 对于setter 已经放在props 根级别，所以在这里屏蔽，防止干扰\n * getFormItemValueByPath: setter需通过表单联动方式获取其他表单项的值，不推荐是用这个方法，所以屏蔽\n */\nexport type SetterOrDecoratorContext = Omit<\n  FormItemMaterialContext,\n  'getFormItemValueByPath' | 'readonly'\n>;\n\nexport interface SetterComponentProps<T = any, CustomOptions = any>\n  extends FormItemFeedback,\n    FormItemContext {\n  value: T;\n  onChange: (v: T) => void;\n  /**\n   * 节点引擎全局readonly\n   */\n  readonly: boolean;\n  children?: any;\n  options: SetterAbilityOptions & CustomOptions;\n  context: SetterOrDecoratorContext;\n}\n\nexport interface SetterExtension {\n  key: string;\n  component: (props: SetterComponentProps) => any;\n  validator?: ValidatorFunction;\n}\n\nexport type SetterHoc = (\n  Component: React.JSXElementConstructor<SetterComponentProps>\n) => React.JSXElementConstructor<SetterComponentProps>;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/validation-ability/form-validate.types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// export type ValidatorFormats =\n//   | 'url'\n//   | 'email'\n//   | 'ipv6'\n//   | 'ipv4'\n//   | 'number'\n//   | 'integer'\n//   | 'idcard'\n//   | 'qq'\n//   | 'phone'\n//   | 'money'\n//   | 'zh'\n//   | 'date'\n//   | 'zip'\n//   | (string & {});\n//\n// export interface IValidatorRules<Context = any> {\n//   format?: ValidatorFormats;\n//   validator?: ValidatorFunction<Context>;\n//   required?: boolean;\n//   pattern?: RegExp | string;\n//   max?: number;\n//   maximum?: number;\n//   maxItems?: number;\n//   minItems?: number;\n//   maxLength?: number;\n//   minLength?: number;\n//   exclusiveMaximum?: number;\n//   exclusiveMinimum?: number;\n//   minimum?: number;\n//   min?: number;\n//   len?: number;\n//   whitespace?: boolean;\n//   enum?: any[];\n//   const?: any;\n//   multipleOf?: number;\n//   uniqueItems?: boolean;\n//   maxProperties?: number;\n//   minProperties?: number;\n//   message?: string;\n//\n//   [key: string]: any;\n// }\n//\n// export interface IValidateResult {\n//   type: 'error' | 'warning';\n//   message: string;\n// }\n//\n// export const isValidateResult = (obj: any): obj is IValidateResult =>\n//   Boolean(obj.type) && Boolean(obj.message);\n//\n// export type ValidatorFunctionResponse =\n//   | null\n//   | void\n//   | undefined\n//   | string\n//   | boolean\n//   | IValidateResult;\n//\n// export type ValidatorFunction<Context = any> = (\n//   value: any,\n//   ctx: Context,\n// ) => ValidatorFunctionResponse | Promise<ValidatorFunctionResponse>;\n//\n// export type Validator<Context = any> =\n//   | ValidatorFormats\n//   | ValidatorFunction<Context>\n//   | IValidatorRules;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/validation-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './validation-ability';\nexport * from './types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/validation-ability/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemMaterialContext } from '../../models';\n\n// 这里参照了rehaje 的validator function 返回格式\nexport interface IValidateResult {\n  type: 'error' | 'warning';\n  message: string;\n}\n\nexport type ValidatorFunctionResponse =\n  | null\n  | void\n  | undefined\n  | string\n  | boolean\n  | IValidateResult;\n\nexport interface ValidationAbilityOptions {\n  /**\n   * 已注册的validator唯一标识\n   */\n  key?: string;\n  /**\n   * 不使用已注册的validator 也支持在options中直接写validator\n   */\n  validator?: ValidatorFunction;\n}\n\nexport interface ValidatorProps<T = any, CustomOptions = any> {\n  value: T;\n  options: ValidationAbilityOptions & CustomOptions;\n  context: FormItemMaterialContext;\n}\n\nexport type ValidatorFunction = (props: ValidatorProps) => ValidatorFunctionResponse;\n\nexport interface ValidationExtension {\n  key: string;\n  validator: ValidatorFunction;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/validation-ability/validation-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport class ValidationAbility implements FormItemAbility {\n  static readonly type = 'validation';\n\n  get type(): string {\n    return ValidationAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/visibility-ability/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './visibility-ability';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/abilities/visibility-ability/visibility-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormItemAbility } from '../../models/form-item-ability';\n\nexport interface VisibilityAbilityOptions {\n  /**\n   * 是否隐藏\n   */\n  hidden: string | boolean;\n  /**\n   * 隐藏是否要清空表单值, 默认为false\n   */\n  clearWhenHidden?: boolean;\n}\n\nexport class VisibilityAbility implements FormItemAbility {\n  static readonly type = 'visibility';\n\n  get type(): string {\n    return VisibilityAbility.type;\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FlowNodeFormData } from '../flow-node-form-data';\nimport { FormModel } from '..';\n\nexport function isNodeFormReady(node: FlowNodeEntity) {\n  return node.getData<FlowNodeFormData>(FlowNodeFormData).getFormModel<FormModel>().initialized;\n}\n\nexport function getFormModel(node: FlowNodeEntity) {\n  return node.getData<FlowNodeFormData>(FlowNodeFormData).formModel;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/flow-node-form-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Disposable, Emitter } from '@flowgram.ai/utils';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport { FlowNodeErrorData } from '../error';\nimport { FormMetaOrFormMetaGenerator } from './types';\nimport { FormModel, type FormModelFactory } from './models';\n\ninterface Options {\n  formModelFactory: FormModelFactory;\n}\n\nexport interface DetailChangeEvent {\n  path: string;\n  oldValue: any;\n  value: any;\n  initialized: boolean;\n}\n\nexport interface OnFormValuesChangePayload {\n  values: any;\n  prevValues: any;\n  name: string;\n}\n\nexport class FlowNodeFormData extends EntityData {\n  static type = 'FlowNodeEntityFormData';\n\n  readonly formModel: FormModel;\n\n  protected flowNodeEntity: FlowNodeEntity;\n\n  /**\n   * @deprecated rehaje 版表单form Values change 事件\n   * @protected\n   */\n  protected onDetailChangeEmitter = new Emitter<DetailChangeEvent>();\n\n  /**\n   * @deprecated 该方法为旧版引擎（rehaje）表单数据变更事件, 新版节点引擎请使用\n   * this.getFormModel<FormModelV2>().onFormValuesChange.\n   * @protected\n   */\n  readonly onDetailChange = this.onDetailChangeEmitter.event;\n\n  constructor(entity: FlowNodeEntity, opts: Options) {\n    super(entity);\n\n    this.flowNodeEntity = entity;\n    this.formModel = opts.formModelFactory(entity);\n\n    this.toDispose.push(this.onDetailChangeEmitter);\n\n    this.toDispose.push(\n      Disposable.create(() => {\n        this.formModel.dispose();\n      })\n    );\n  }\n\n  getFormModel<TFormModel>(): TFormModel {\n    // @ts-ignore\n    return this.formModel as TFormModel;\n  }\n\n  getDefaultData(): any {\n    return {};\n  }\n\n  createForm(formMetaOrFormMetaGenerator: any, initialValue?: any): void {\n    const errorData = this.flowNodeEntity.getData<FlowNodeErrorData>(FlowNodeErrorData);\n\n    errorData.setError(null);\n    try {\n      this.formModel.init(formMetaOrFormMetaGenerator, initialValue);\n    } catch (e) {\n      errorData.setError(e as Error);\n    }\n  }\n\n  updateFormValues(value: any) {\n    this.formModel.updateFormValues(value);\n  }\n\n  recreateForm(formMetaOrFormMetaGenerator: FormMetaOrFormMetaGenerator, initialValue?: any): void {\n    this.createForm(formMetaOrFormMetaGenerator, initialValue);\n  }\n\n  toJSON(): any {\n    return this.formModel.toJSON();\n  }\n\n  dispose(): void {\n    super.dispose();\n  }\n\n  /**\n   * @deprecated rehaje 版表单form Values change 事件触发函数\n   * @protected\n   */\n  fireDetaiChange(detailChangeEvent: DetailChangeEvent) {\n    this.onDetailChangeEmitter.fire(detailChangeEvent);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/form-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FormManager } from './services/form-manager';\n\nexport const FormContribution = Symbol('FormContribution');\n\nexport interface FormContribution {\n  onRegister?(formManager: FormManager): void;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/form-core-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\nimport { bindContributions } from '@flowgram.ai/utils';\n\nimport { FormContextMaker } from './services/form-context-maker';\nimport { NodeContribution } from '../node';\nimport { FormManager, FormPathService } from './services';\nimport { FormNodeContribution } from './form-node-contribution';\n\nexport const FormCoreContainerModule = new ContainerModule((bind) => {\n  bind(FormManager).toSelf().inSingletonScope();\n  bind(FormPathService).toSelf().inSingletonScope();\n  bind(FormContextMaker).toSelf().inSingletonScope();\n  bindContributions(bind, FormNodeContribution, [NodeContribution]);\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/form-node-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\nimport { NodeContribution, NodeManager, PLUGIN_KEY } from '../node';\nimport { formPluginRender } from './form-render';\n\n@injectable()\nexport class FormNodeContribution implements NodeContribution {\n  onRegister(nodeManager: NodeManager) {\n    nodeManager.registerPluginRender(PLUGIN_KEY.FORM, formPluginRender);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/utils';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext } from '@flowgram.ai/core';\n\nimport { NodeContext } from '../node';\nimport { FormModel } from './models';\nimport { FlowNodeFormData } from './flow-node-form-data';\n\ninterface FormRenderProps {\n  node: FlowNodeEntity;\n  playgroundContext?: PlaygroundContext;\n}\n\nfunction getFormModelFromNode(node: FlowNodeEntity) {\n  return node.getData(FlowNodeFormData)?.getFormModel<FormModel>();\n}\n\nexport function FormRender({ node }: FormRenderProps): any {\n  const refresh = useRefresh();\n\n  const formModel = getFormModelFromNode(node);\n\n  useEffect(() => {\n    const disposable = formModel?.onInitialized(() => {\n      refresh();\n    });\n    return () => {\n      disposable.dispose();\n    };\n  }, [formModel]);\n\n  return formModel?.initialized ? formModel.render() : null;\n}\n\nexport const formPluginRender = (props: NodeContext) => <FormRender {...props} />;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './form-core-container-module';\nexport * from './form-contribution';\nexport * from './services';\nexport * from './models';\nexport * from './types';\nexport * from './abilities';\nexport * from './flow-node-form-data';\nexport * from './client';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-ability-extension-registry.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\nexport interface Extension {\n  key: string;\n}\n@injectable()\nexport class FormAbilityExtensionRegistry {\n  protected registry = new Map<string, Extension>();\n\n  register(extension: Extension): void {\n    this.registry.set(extension.key, extension);\n  }\n\n  get<T extends Extension>(key: string): T | undefined {\n    return this.registry.get(key) as T | undefined;\n  }\n\n  get objectMap(): Record<string, Extension> {\n    return Object.fromEntries(this.registry);\n  }\n\n  get collection(): Extension[] {\n    return Array.from(this.registry.values());\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-item-ability.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface FormItemAbility {\n  type: string;\n  /**\n   * 注册到formManager时钩子时调用\n   */\n  onAbilityRegister?: () => void;\n}\n\nexport interface AbilityClass {\n  type: string;\n\n  new (): FormItemAbility;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-item-material-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext } from '@flowgram.ai/core';\n\nimport { type FormModel, IFormItemMeta } from '..';\n\nexport interface FormItemMaterialContext {\n  /**\n   * 当前表单项的meta\n   */\n  meta: IFormItemMeta;\n  /**\n   * 当前表单项的路径\n   */\n  path: string;\n  /**\n   * 节点引擎全局readonly\n   */\n  readonly: boolean;\n  /**\n   * 通过路径获取表单项的值\n   * @param path 表单项在当前表单中的绝对路径，路径协议遵循glob\n   */\n  getFormItemValueByPath: <T>(path: string) => T;\n  /**\n   * 节点表单校验回调函数注册\n   */\n  onFormValidate: FormModel['onValidate'];\n  /**\n   * 获取Node模型\n   */\n  node: FlowNodeEntity;\n  /**\n   * 获取FormModel原始模型\n   */\n  form: FormModel;\n  /**\n   * 业务注入的全局context\n   */\n  playgroundContext: PlaygroundContext;\n  /**\n   * 数组场景下当前项的index\n   */\n  index?: number | undefined;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-item.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { FormItemDomRef, type IFormItemMeta } from '..';\nimport { type FormModel } from '.';\n\nexport abstract class FormItem {\n  readonly meta: IFormItemMeta;\n\n  readonly path: string;\n\n  readonly formModel: FormModel;\n\n  readonly onInitEventEmitter = new Emitter<FormItem>();\n\n  readonly onInit = this.onInitEventEmitter.event;\n\n  protected toDispose: DisposableCollection = new DisposableCollection();\n\n  readonly onDispose = this.toDispose.onDispose;\n\n  // todo(heyuan): 将dom 相关逻辑拆到form item插件里\n  private _domRef: FormItemDomRef;\n\n  protected constructor(meta: IFormItemMeta, path: string, formModel: FormModel) {\n    this.meta = meta;\n    this.path = path;\n    this.formModel = formModel;\n    this.toDispose.push(this.onInitEventEmitter);\n  }\n\n  abstract get value(): any;\n\n  abstract set value(value: any);\n\n  abstract validate(): void;\n\n  set domRef(domRef: FormItemDomRef) {\n    this._domRef = domRef;\n  }\n\n  get domRef() {\n    return this._domRef;\n  }\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-meta.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IFormItemMeta, type IFormMeta, type IFormMetaOptions } from '../types';\nimport { FormPathService } from '../services';\n\nexport interface FormMetaTraverseParams {\n  formItemMeta: IFormItemMeta;\n  parentPath?: string;\n  handle: (params: { formItemMeta: IFormItemMeta; path: string }) => any;\n}\n\nexport class FormMeta implements IFormMeta {\n  constructor(root: IFormItemMeta, options: IFormMetaOptions) {\n    this._root = root;\n    this._options = options;\n  }\n\n  protected _root: IFormItemMeta;\n\n  get root(): IFormItemMeta {\n    return this._root;\n  }\n\n  protected _options: IFormMetaOptions;\n\n  get options(): IFormMetaOptions {\n    return this._options;\n  }\n\n  static traverse({ formItemMeta, parentPath = '', handle }: FormMetaTraverseParams): void {\n    if (!formItemMeta) {\n      return;\n    }\n\n    const isRoot = !parentPath;\n\n    const path = isRoot\n      ? FormPathService.ROOT\n      : formItemMeta.name\n      ? FormPathService.join([parentPath, formItemMeta.name])\n      : parentPath;\n\n    handle({ formItemMeta, path });\n\n    if (formItemMeta.items) {\n      this.traverse({\n        formItemMeta: formItemMeta.items,\n        handle,\n        parentPath: FormPathService.toArrayPath(path),\n      });\n    }\n\n    if (formItemMeta.children && formItemMeta.children.length) {\n      formItemMeta.children.forEach((child) => {\n        this.traverse({ formItemMeta: child, handle, parentPath: path });\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/form-model.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { DisposableCollection, Event, MaybePromise } from '@flowgram.ai/utils';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FormFeedback, FormModelValid, IFormItem } from '../types';\nimport { FormManager } from '../services/form-manager';\nimport { type FormItem } from '.';\n\nexport type FormModelFactory = (entity: FlowNodeEntity) => FormModel;\nexport const FormModelFactory = Symbol('FormModelFactory');\nexport const FormModelEntity = Symbol('FormModelEntity');\n\n@injectable()\nexport abstract class FormModel {\n  readonly onValidate: Event<FormModel>;\n\n  readonly onValidChange: Event<FormModelValid>;\n\n  readonly onFeedbacksChange: Event<FormFeedback[]>;\n\n  readonly onInitialized: Event<FormModel>;\n\n  protected toDispose: DisposableCollection = new DisposableCollection();\n\n  /**\n   * @deprecated\n   * use `formModel.node` instead in FormModelV2\n   */\n  abstract get flowNodeEntity(): FlowNodeEntity;\n\n  /**\n   * @deprecated\n   */\n  abstract get formManager(): FormManager;\n\n  abstract get formMeta(): any;\n\n  abstract get initialized(): boolean;\n\n  abstract get valid(): FormModelValid;\n\n  abstract updateFormValues(value: any): void;\n\n  /**\n   * @deprecated\n   * use `formModel.getFieldIn` instead in FormModelV2 to get the model of a form field\n   * do not use this in FormModelV2 since  it only return an empty Map.\n   */\n  abstract get formItemPathMap(): Map<string, IFormItem>;\n\n  /**\n   * @deprecated\n   */\n  abstract clearValid(): void;\n\n  abstract validate(): Promise<boolean>;\n\n  abstract validateWithFeedbacks(): Promise<FormFeedback[]>;\n\n  abstract init(formMetaOrFormMetaGenerator: any, initialValue?: any): MaybePromise<void>;\n\n  abstract toJSON(): any;\n\n  /**\n   * @deprecated\n   * use `formModel.getField` instead in FormModelV2\n   */\n  abstract getFormItemByPath(path: string): FormItem | undefined;\n\n  /**\n   * @deprecated\n   * use `formModel.getFieldValue` instead in FormModelV2 to get the model of a form field by path\n   */\n  abstract getFormItemValueByPath<T = any>(path: string): any | undefined;\n\n  abstract render(): any;\n\n  abstract dispose(): void;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/models/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './form-model';\nexport * from './form-item';\nexport * from './form-meta';\nexport * from './form-ability-extension-registry';\nexport * from './form-item-ability';\nexport * from './form-item-material-context';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/services/form-context-maker.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { injectPlaygroundContext, PlaygroundContext } from '@flowgram.ai/core';\n\nimport { NodeEngineContext } from '../../node';\nimport { FormItem, FormItemMaterialContext } from '..';\n\n@injectable()\nexport class FormContextMaker {\n  @inject(NodeEngineContext) readonly nodeEngineContext: NodeEngineContext;\n\n  @injectPlaygroundContext() readonly playgroundContext: PlaygroundContext;\n\n  makeFormItemMaterialContext(\n    formItem: FormItem,\n    options?: { getIndex: () => number | undefined }\n  ): FormItemMaterialContext {\n    return {\n      meta: formItem.meta,\n      path: formItem.path,\n      readonly: this.nodeEngineContext.readonly,\n      getFormItemValueByPath: formItem.formModel.getFormItemValueByPath.bind(formItem.formModel),\n      onFormValidate: formItem.formModel.onValidate.bind(formItem.formModel),\n      form: formItem.formModel,\n      node: formItem.formModel.flowNodeEntity,\n      playgroundContext: this.playgroundContext,\n      index: options?.getIndex(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/services/form-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { mapValues } from 'lodash-es';\nimport { inject, injectable, multiInject, optional, postConstruct } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\nimport { injectPlaygroundContext, PlaygroundContext } from '@flowgram.ai/core';\n\nimport { AbilityClass, FormItemAbility } from '../models/form-item-ability';\nimport { FormAbilityExtensionRegistry, FormModel } from '../models';\nimport { FormContribution } from '../form-contribution';\nimport {\n  DecoratorAbility,\n  DecoratorExtension,\n  SetterAbility,\n  SetterExtension,\n  SetterHoc,\n} from '../abilities';\nimport { FormContextMaker, FormPathService } from './index';\n\n@injectable()\nexport class FormManager {\n  readonly abilityRegistry: Map<string, FormItemAbility> = new Map();\n\n  readonly setterHocs: SetterHoc[] = [];\n\n  readonly extensionRegistryMap: Map<string, FormAbilityExtensionRegistry> = new Map();\n\n  @inject(FormPathService) readonly pathManager: FormPathService;\n\n  @inject(FormContextMaker) readonly formContextMaker: FormContextMaker;\n\n  @injectPlaygroundContext() readonly playgroundContext: PlaygroundContext;\n\n  @multiInject(FormContribution) @optional() protected formContributions: FormContribution[] = [];\n\n  private readonly onFormModelWillInitEmitter = new Emitter<{\n    model: FormModel;\n    data: any;\n  }>();\n\n  readonly onFormModelWillInit = this.onFormModelWillInitEmitter.event;\n\n  get components(): Record<string, any> {\n    return mapValues(\n      this.extensionRegistryMap.get(SetterAbility.type)?.objectMap || {},\n      (setter: SetterExtension) => setter.component\n    );\n  }\n\n  get decorators(): Record<string, any> {\n    return mapValues(\n      this.extensionRegistryMap.get(DecoratorAbility.type)?.objectMap || {},\n      (decorator: DecoratorExtension) => decorator.component\n    );\n  }\n\n  registerAbilityExtension(type: string, extension: any): void {\n    if (!this.extensionRegistryMap.get(type)) {\n      this.extensionRegistryMap.set(type, new FormAbilityExtensionRegistry());\n    }\n    const registry = this.extensionRegistryMap.get(type);\n    if (!registry) {\n      return;\n    }\n    registry.register(extension);\n  }\n\n  getAbilityExtension(abilityType: string, extensionKey: string): any {\n    return this.extensionRegistryMap.get(abilityType)?.get(extensionKey);\n  }\n\n  registerAbility(Ability: AbilityClass): void {\n    const ability = new Ability();\n    this.abilityRegistry.set(ability.type, ability);\n  }\n\n  registerAbilities(Abilities: AbilityClass[]): void {\n    Abilities.forEach(this.registerAbility.bind(this));\n  }\n\n  getAbility<ExtendAbility>(type: string): (FormItemAbility & ExtendAbility) | undefined {\n    return this.abilityRegistry.get(type) as FormItemAbility & ExtendAbility;\n  }\n\n  /**\n   * @deprecated\n   * Setter Hoc and setter are no longer supported in NodeEngineV2\n   * @param hoc\n   */\n  registerSetterHoc(hoc: SetterHoc): void {\n    this.setterHocs.push(hoc);\n  }\n\n  fireFormModelWillInit(model: FormModel, data: any) {\n    this.onFormModelWillInitEmitter.fire({\n      model,\n      data,\n    });\n  }\n\n  dispose() {\n    this.onFormModelWillInitEmitter.dispose();\n  }\n\n  @postConstruct()\n  protected init(): void {\n    this.formContributions.forEach((contrib) => contrib.onRegister?.(this));\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/services/form-path-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\n@injectable()\nexport class FormPathService {\n  static readonly ROOT = '/';\n\n  static readonly DIVIDER = '/';\n\n  static readonly RELATIVE_PARENT = '..';\n\n  static readonly RELATIVE_CURRENT = '.';\n\n  static readonly ARRAY = '[]';\n\n  static normalize(path: string) {\n    if (path === FormPathService.ROOT) {\n      return path;\n    }\n    // 去掉末尾的斜杠\n    if (path.endsWith(FormPathService.DIVIDER)) {\n      path = path.slice(0, -1);\n    }\n    return path;\n  }\n\n  static join(paths: string[]): string {\n    if (paths[1].startsWith(FormPathService.ROOT)) {\n      throw new Error(\n        `FormPathService Error: join failed, invalid paths[1], paths[1]= ${paths[1]}`,\n      );\n    }\n    if (paths[0].endsWith(FormPathService.DIVIDER)) {\n      return `${paths[0]}${paths[1]}`;\n    }\n    return paths.join(FormPathService.DIVIDER);\n  }\n\n  static toArrayPath(path: string): string {\n    return FormPathService.join([path, FormPathService.ARRAY]);\n  }\n\n  static parseArrayItemPath(path: string) {\n    const names = path.split('/');\n\n    let i = 0;\n    while (i < names.length) {\n      const itemIndex = parseInt(names[i]);\n\n      if (!isNaN(itemIndex)) {\n        const arrayPath = FormPathService.toArrayPath(\n          names.slice(0, i).join(FormPathService.DIVIDER),\n        );\n        const restPath = names.slice(i + 1).join(FormPathService.DIVIDER);\n        const itemMetaPath = FormPathService.join([arrayPath, restPath]);\n        return { itemIndex, arrayPath, itemMetaPath };\n      }\n      i = i + 1;\n    }\n    return null;\n  }\n\n  simplify(path: string) {\n    const segments = path.split(FormPathService.DIVIDER);\n    const resSegments: string[] = [];\n\n    for (let i = 0; i < segments.length; i++) {\n      if (!segments[i]) {\n        throw new Error('FormPathService: join failed');\n      }\n\n      if (segments[i] === FormPathService.RELATIVE_CURRENT) {\n        // eslint-disable-next-line no-continue\n        continue;\n      }\n\n      if (segments[i] === FormPathService.RELATIVE_PARENT) {\n        resSegments.pop();\n      }\n      resSegments.push(segments[i]);\n    }\n    return resSegments.join(FormPathService.DIVIDER);\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './form-path-service';\nexport * from './form-manager';\nexport * from './form-context-maker';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/types/form-ability.types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext } from '@flowgram.ai/core';\nimport { MaybePromise } from '@flowgram.ai/utils';\n\nimport { type IFormItem } from './form-model.types';\nimport { IFormItemMeta } from './form-meta.types';\n\nexport interface FormItemAbilityMeta<Options = any> {\n  type: string;\n  options: Options;\n}\n\n/**\n * @deprecated\n */\nexport interface FormItemContext {\n  /**\n   * @deprecated Use context.node instead\n   */\n  formItemMeta: IFormItemMeta;\n  /**\n   * @deprecated\n   */\n  formItem: IFormItem;\n  /**\n   * @deprecated Use context.node instead\n   */\n  flowNodeEntity: FlowNodeEntity;\n  /**\n   * @deprecated Use context.playgroundContext instead\n   */\n  playgroundContext: PlaygroundContext;\n}\n\nexport interface FormItemHookParams extends FormItemContext {\n  formItem: IFormItem;\n}\n\nexport interface FormItemHooks<T> {\n  /**\n   * FormItem初始化钩子\n   */\n  onInit?: (params: FormItemHookParams & T) => void;\n  /**\n   * FormItem提交时钩子\n   */\n  onSubmit?: (params: FormItemHookParams & T) => void;\n  /**\n   * FormItem克隆时钩子\n   */\n  onClone?: (params: FormItemHookParams & T) => MaybePromise<void>;\n  /**\n   * 克隆后执行的逻辑\n   */\n  afterClone?: (params: FormItemHookParams & T) => void;\n  /**\n   * FormItem全局校验时钩子\n   */\n  onValidate?: (params: FormItemHookParams & T) => void;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/types/form-meta.types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MaybePromise } from '@flowgram.ai/utils';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext, PluginContext } from '@flowgram.ai/core';\n\nimport { type FormItemAbilityMeta } from './form-ability.types';\n\nexport type FormDataTypeName =\n  | 'string'\n  | 'number'\n  | 'integer'\n  | 'boolean'\n  | 'object'\n  | 'array'\n  | 'null';\n\nexport type FormDataType =\n  | string //\n  | number\n  | boolean\n  | FormDataObject\n  | DataArray\n  | null;\n\nexport interface FormDataObject {\n  [key: string]: FormDataType;\n}\n\nexport type DataArray = Array<FormDataType>;\n\nexport const FORM_VOID = 'form-void' as const;\n\nexport interface TreeNode<T> {\n  name: string;\n  children?: TreeNode<T>[];\n}\n\nexport interface IFormItemMeta extends TreeNode<IFormItemMeta> {\n  /**\n   * 表单项名称\n   */\n  name: string;\n  /**\n   * 数据类型\n   */\n  type: FormDataTypeName | typeof FORM_VOID;\n  /**\n   * 枚举值\n   */\n  enum?: FormDataType[];\n  /**\n   * 数组类型item的数据类型描述\n   */\n  items?: IFormItemMeta;\n  /**\n   * 表单项标题\n   */\n  title?: string;\n  /**\n   * 表单项描述\n   */\n  description?: string;\n  /**\n   * 表单项默认值\n   */\n  default?: FormDataType;\n  /**\n   * 是否必填\n   */\n  required?: boolean;\n  /**\n   * 扩展能力\n   */\n  abilities?: FormItemAbilityMeta[];\n\n  /**\n   * 子表单项\n   */\n  children?: IFormItemMeta[];\n}\n\nexport interface IFormMeta {\n  /**\n   * 表单树结构root\n   */\n  root?: IFormItemMeta;\n  /**\n   * 表单全局配置\n   */\n  options?: IFormMetaOptions;\n}\n\nexport interface NodeFormContext {\n  node: FlowNodeEntity;\n  playgroundContext: PlaygroundContext;\n  clientContext: PluginContext & Record<string, any>;\n}\n\nexport interface IFormMetaOptions {\n  formatOnInit?: (value: any, context: NodeFormContext) => any;\n\n  formatOnSubmit?: (value: any, context: NodeFormContext) => any;\n\n  [key: string]: any;\n}\n\nexport interface FormMetaGeneratorParams<PlaygroundContext, FormValue = any> {\n  node: FlowNodeEntity;\n  playgroundContext: PlaygroundContext;\n  initialValue?: FormValue;\n}\n\nexport type FormMetaGenerator<PlaygroundContext = any, FormValue = any> = (\n  params: FormMetaGeneratorParams<FormValue, FormValue>\n) => MaybePromise<IFormMeta>;\n\nexport type FormMetaOrFormMetaGenerator = FormMetaGenerator | IFormMeta;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/types/form-model.types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface IFormItem<T = any> {\n  value: T;\n}\n\nexport enum FormItemEventName {\n  onFormValueChange = 'onFormValueChange',\n  onFormItemInit = 'onFormItemInit',\n}\n\nexport type FormModelValid = boolean | null;\n\nexport type FeedbackStatus = 'error' | 'warning' | 'pending';\nexport type FeedbackText = string;\n\nexport interface FormItemFeedback {\n  feedbackStatus?: FeedbackStatus;\n  feedbackText?: FeedbackText;\n}\n\nexport interface FormFeedback {\n  feedbackStatus?: FeedbackStatus;\n  feedbackText?: FeedbackText;\n  path: string;\n}\n\nexport interface FormItemDomRef {\n  current: HTMLElement | null;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/form/types/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './form-model.types';\nexport * from './form-meta.types';\nexport * from './form-ability.types';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node';\nexport * from './error';\nexport * from './form';\nexport * from './client';\nexport * from './node-react';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/core-materials.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const MATERIAL_KEY = {\n  NODE_ERROR_RENDER: 'node_error_render',\n  NODE_PLACEHOLDER_RENDER: 'node_placeholder_render',\n};\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/core-plugins.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const PLUGIN_KEY = {\n  FORM: 'Plugin_Form',\n  ERROR: 'Plugin_Error',\n};\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeContribution } from './node-contribution';\nexport * from './node-container-module';\nexport * from './node-manager';\nexport * from './types';\nexport * from './core-plugins';\nexport * from './core-materials';\nexport * from './node-engine';\nexport * from './node-engine-context';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/node-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { NodeManager } from './node-manager';\nimport { NodeEngineContext } from './node-engine-context';\nimport { NodeEngine } from './node-engine';\n\nexport const NodeContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {\n  bind(NodeEngine).toSelf().inSingletonScope();\n  bind(NodeManager).toSelf().inSingletonScope();\n  bind(NodeEngineContext).toSelf().inSingletonScope();\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/node-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type NodeManager } from './node-manager';\n\nexport const NodeContribution = Symbol('NodeContribution');\n\nexport interface NodeContribution {\n  onRegister?(nodeManager: NodeManager): void;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/node-engine-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nexport interface INodeEngineContext {\n  readonly: boolean;\n}\n\n/**\n * NodeEngineContext 在 Node Engine 中为全局单例, 它的作用是让Node之间共享数据。\n * context 分为内置context(如 readonly) 和 自定义context(业务可以按需注入)\n */\n@injectable()\nexport class NodeEngineContext {\n  static DEFAULT_READONLY = false;\n\n  static DEFAULT_JSON = { readonly: NodeEngineContext.DEFAULT_READONLY };\n\n  readonly onChangeEmitter = new Emitter<NodeEngineContext>();\n\n  readonly onChange = this.onChangeEmitter.event;\n\n  private _readonly: boolean = NodeEngineContext.DEFAULT_READONLY;\n\n  private _json: INodeEngineContext = NodeEngineContext.DEFAULT_JSON;\n\n  get json(): INodeEngineContext {\n    return this._json;\n  }\n\n  get readonly(): boolean {\n    return this._readonly;\n  }\n\n  set readonly(value: boolean) {\n    this._readonly = value;\n    this.fireChange();\n  }\n\n  private fireChange(): void {\n    this.updateJSON();\n    this.onChangeEmitter.fire(this);\n  }\n\n  private updateJSON(): void {\n    this._json = {\n      readonly: this._readonly,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/node-engine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\n\nimport { NodeManager } from './node-manager';\nimport { NodeEngineContext } from './node-engine-context';\n\n@injectable()\nexport class NodeEngine {\n  @inject(NodeManager) nodeManager: NodeManager;\n\n  @inject(NodeEngineContext) context: NodeEngineContext;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/node-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { flow } from 'lodash-es';\nimport { injectable, multiInject, optional, postConstruct } from 'inversify';\n\nimport { NodeErrorRenderProps } from '../error';\nimport { NodePluginRender, NodeRenderHoc, Render } from './types';\nimport { NodeContribution } from './node-contribution';\n\nexport enum MaterialRenderKey {\n  CustomNodeError = 'Material_CustomNodeError',\n}\n\n@injectable()\nexport class NodeManager {\n  readonly materialRenderRegistry: Map<string, Render> = new Map();\n\n  readonly pluginRenderRegistry: Map<string, Render> = new Map();\n\n  readonly nodeRenderHocs: NodeRenderHoc[] = [];\n\n  @multiInject(NodeContribution) @optional() protected nodeContributions: NodeContribution[] = [];\n\n  registerMaterialRender(key: string, render: Render) {\n    this.materialRenderRegistry.set(key, render);\n  }\n\n  getMaterialRender(key: string): Render | undefined {\n    return this.materialRenderRegistry.get(key);\n  }\n\n  registerPluginRender(key: string, render: NodePluginRender): void {\n    this.pluginRenderRegistry.set(key, render);\n  }\n\n  getPluginRender(key: string): NodePluginRender | undefined {\n    return this.pluginRenderRegistry.get(key);\n  }\n\n  registerNodeErrorRender(render: Render<NodeErrorRenderProps>) {\n    this.registerMaterialRender(MaterialRenderKey.CustomNodeError, render);\n  }\n\n  get nodeRenderHoc() {\n    return flow(this.nodeRenderHocs);\n  }\n\n  registerNodeRenderHoc(hoc: NodeRenderHoc) {\n    this.nodeRenderHocs.push(hoc);\n  }\n\n  get nodeErrorRender() {\n    return this.materialRenderRegistry.get(MaterialRenderKey.CustomNodeError);\n  }\n\n  @postConstruct()\n  protected init(): void {\n    this.nodeContributions.forEach((contrib) => contrib.onRegister?.(this));\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { NodeFormContext } from '../form';\n\n/**\n * @deprecated\n * use `NodeFormContext` instead\n */\nexport type NodeContext = NodeFormContext;\n\nexport type Render<T = any> = (props: T) => any;\n\nexport type NodePluginRender = Render<NodeFormContext>;\n\nexport type NodePlaceholderRender = Render<NodeFormContext>;\n\nexport interface NodeRenderProps {\n  node: FlowNodeEntity;\n}\n\nexport type NodeRenderHoc = (\n  Component: React.JSXElementConstructor<NodeRenderProps>\n) => React.JSXElementConstructor<NodeRenderProps>;\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node-engine-react-context';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/context/node-engine-react-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { INodeEngineContext, NodeEngineContext } from '../../node';\n\nexport const NodeEngineReactContext = React.createContext<INodeEngineContext>(\n  NodeEngineContext.DEFAULT_JSON,\n);\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './use-node-engine-context';\nexport * from './use-form-Item';\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/hooks/use-form-Item.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { useEntityFromContext, useRefresh } from '@flowgram.ai/core';\n\nimport { FlowNodeFormData, FormModel, IFormItem } from '../../form';\n\nexport function useFormItem(path: string): IFormItem | undefined {\n  const refresh = useRefresh();\n  const node = useEntityFromContext<FlowNodeEntity>();\n  const formData = node.getData<FlowNodeFormData>(FlowNodeFormData);\n  const formItem = formData.getFormModel<FormModel>().getFormItemByPath(path);\n\n  useEffect(() => {\n    const disposable = formData.onDataChange(() => {\n      refresh();\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  return formItem;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/hooks/use-node-engine-context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useService, useRefresh } from '@flowgram.ai/core';\n\nimport { NodeEngineContext } from '../../node';\n\nexport function useNodeEngineContext(): NodeEngineContext {\n  const refresh = useRefresh();\n  const nodeEngineContext = useService<NodeEngineContext>(NodeEngineContext);\n\n  useEffect(() => {\n    const disposable = nodeEngineContext.onChange(() => {\n      refresh();\n    });\n\n    return () => {\n      disposable.dispose();\n    };\n  }, []);\n\n  return nodeEngineContext;\n}\n"
  },
  {
    "path": "packages/node-engine/form-core/src/node-react/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './hooks';\nexport * from './context';\n"
  },
  {
    "path": "packages/node-engine/form-core/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./src\",\n    \"baseUrl\": \"./\",\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n\n"
  },
  {
    "path": "packages/node-engine/form-core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/node-engine/form-core/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/node-engine/node/__tests__/form-effects.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { DataEvent } from '../src/types';\nimport { FormModelV2 } from '../src/form-model-v2';\n\ndescribe('FormModelV2 effects', () => {\n  const node = {\n    getService: vi.fn().mockReturnValue({}),\n    getData: vi.fn().mockReturnValue({ fireChange: vi.fn() }),\n  } as unknown as FlowNodeEntity;\n\n  let formModelV2 = new FormModelV2(node);\n\n  beforeEach(() => {\n    formModelV2.dispose();\n    formModelV2 = new FormModelV2(node);\n  });\n\n  it('should trigger init effects when initialValues exists', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should trigger init effects when formatOnInit return value', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      formatOnInit: () => ({ a: { b: 1 } }),\n      effect: {\n        'a.b': [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should trigger value change effects', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    formModelV2.setValueIn('a', 2);\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should trigger onValueInitOrChange effects when form defaultValue init', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should trigger onValueInitOrChange effects when field defaultValue init', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    formModelV2.nativeFormModel?.setInitValueIn('a', 2);\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should trigger child onValueInit effects when field defaultValue init', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        'a.b.c': [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    formModelV2.nativeFormModel?.setInitValueIn('a', { b: { c: 1 } });\n    expect(mockEffect).toHaveBeenCalledOnce();\n  });\n  it('should not trigger child onValueInit effects when field defaultValue init but child path has no value', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        'a.b.c': [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    formModelV2.nativeFormModel?.setInitValueIn('a', 2);\n    expect(mockEffect).not.toHaveBeenCalled();\n  });\n  it('should trigger onValueInitOrChange effects when value change', () => {\n    const mockEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    formModelV2.setValueIn('a', 2);\n    expect(mockEffect).toHaveBeenCalledOnce();\n\n    formModelV2.setValueIn('a', {});\n    expect(mockEffect).toHaveBeenCalledTimes(2);\n\n    formModelV2.setValueIn('a.b', 2);\n    expect(mockEffect).toHaveBeenCalledTimes(3);\n  });\n  it('should trigger single item init effect when array append', () => {\n    const mockArrItemEffect = vi.fn();\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        ['arr.*']: [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockArrItemEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta);\n    const arrModel = formModelV2.nativeFormModel?.createFieldArray('arr');\n    arrModel?.append(1);\n    arrModel?.append(2);\n    expect(mockArrItemEffect).toHaveBeenCalledTimes(2);\n  });\n  it('should trigger value change effects return when value change', () => {\n    const mockEffectReturn = vi.fn();\n    const mockEffect = vi.fn(() => mockEffectReturn);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    formModelV2.setValueIn('a', 2);\n    formModelV2.setValueIn('a', 3);\n    expect(mockEffect).toHaveBeenCalledTimes(2);\n    expect(mockEffectReturn).toHaveBeenCalledOnce();\n  });\n  it('should trigger onValueInitOrChange effects return when value init', () => {\n    const mockEffectReturn = vi.fn();\n    const mockEffect = vi.fn(() => mockEffectReturn);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    formModelV2.setValueIn('a', 2);\n    expect(mockEffectReturn).toHaveBeenCalledOnce();\n  });\n  it('should trigger onValueInitOrChange effects return when value init and change', () => {\n    const mockEffectReturn = vi.fn();\n    const mockEffect = vi.fn(() => mockEffectReturn);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1 });\n    formModelV2.setValueIn('a', 2);\n    formModelV2.setValueIn('a', 3);\n    // 第一次setValue，触发 init 时记录的return， 第二次setValue 触发 第一次setValue时记录的return， 共2次\n    expect(mockEffectReturn).toHaveBeenCalledTimes(2);\n  });\n  it('should update effect return function each time init or change the value', () => {\n    const mockEffectReturn = vi.fn().mockReturnValueOnce(1).mockReturnValueOnce(2);\n    const mockEffect = vi.fn(() => mockEffectReturn);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        ['arr.*.var']: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { arr: [] });\n    const form = formModelV2.nativeFormModel!;\n    const arrayField = form.createFieldArray('arr');\n    arrayField!.append({ var: 'x' });\n    form.setValueIn('arr.0.var', 'y');\n\n    formModelV2.dispose();\n\n    expect(mockEffectReturn).toHaveNthReturnedWith(1, 1);\n    expect(mockEffectReturn).toHaveNthReturnedWith(2, 2);\n  });\n  it('should trigger effects when setValueIn called in parent name', () => {\n    const mockInitEffectReturn = vi.fn();\n    const mockInitEffect = vi.fn(() => mockInitEffectReturn);\n\n    const mockInitOrChangeEffectReturn = vi.fn();\n    const mockInitOrChangeEffect = vi.fn(() => mockInitOrChangeEffectReturn);\n\n    const mockChangeEffectReturn = vi.fn();\n    const mockChangeEffect = vi.fn(() => mockChangeEffectReturn);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        'inputsValues.*': [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockInitEffect,\n          },\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockInitOrChangeEffect,\n          },\n          {\n            event: DataEvent.onValueChange,\n            effect: mockChangeEffect,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { inputsValues: { a: 1 } });\n    expect(mockInitEffect).toHaveBeenCalledTimes(1);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(1);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(0);\n\n    formModelV2.setValueIn('inputsValues', { a: 2 });\n    expect(mockInitEffect).toHaveBeenCalledTimes(1);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(2);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(1);\n\n    formModelV2.setValueIn('inputsValues', { b: 3 });\n    expect(mockInitEffect).toHaveBeenCalledTimes(2);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(4);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(2);\n\n    formModelV2.setValueIn('inputsValues', { b: 4 });\n    expect(mockInitEffect).toHaveBeenCalledTimes(2);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(5);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(3);\n\n    formModelV2.setValueIn('inputsValues', { a: 1, b: 4 });\n    expect(mockInitEffect).toHaveBeenCalledTimes(3);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(6);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(3);\n\n    formModelV2.setValueIn('inputsValues', {});\n    expect(mockInitEffect).toHaveBeenCalledTimes(3);\n    expect(mockInitOrChangeEffect).toHaveBeenCalledTimes(8);\n    expect(mockChangeEffect).toHaveBeenCalledTimes(5);\n  });\n  it('should trigger all effects return when formModel dispose', () => {\n    const mockEffectReturn1 = vi.fn();\n    const mockEffect1 = vi.fn(() => mockEffectReturn1);\n    const mockEffectReturn2 = vi.fn();\n    const mockEffect2 = vi.fn(() => mockEffectReturn2);\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffect1,\n          },\n        ],\n        b: [\n          {\n            event: DataEvent.onValueInit,\n            effect: mockEffect2,\n          },\n        ],\n      },\n    };\n    formModelV2.init(formMeta, { a: 1, b: 2 });\n\n    formModelV2.dispose();\n\n    expect(mockEffectReturn1).toHaveBeenCalledTimes(1);\n    expect(mockEffectReturn2).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/node/__tests__/form-model-v2.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FormMeta } from '../src/types';\nimport { FormModelV2 } from '../src/form-model-v2';\n\ndescribe('FormModelV2', () => {\n  const node = {\n    getService: vi.fn().mockReturnValue({}),\n    getData: vi.fn().mockReturnValue({ fireChange: vi.fn() }),\n  } as unknown as FlowNodeEntity;\n\n  let formModelV2 = new FormModelV2(node);\n\n  beforeEach(() => {\n    formModelV2.dispose();\n    formModelV2 = new FormModelV2(node);\n  });\n\n  describe('v1 apis', () => {\n    it('getFormItemValueByPath', () => {\n      const formMeta = {\n        render: vi.fn(),\n      };\n      formModelV2.init(formMeta, {\n        a: 1,\n        b: 2,\n      });\n\n      expect(formModelV2.getFormItemValueByPath('/a')).toBe(1);\n      expect(formModelV2.getFormItemValueByPath('/b')).toBe(2);\n      expect(formModelV2.getFormItemValueByPath('/')).toEqual({ a: 1, b: 2 });\n    });\n    it('getFormItemByPath when path is /', () => {\n      const formMeta = {\n        render: vi.fn(),\n      };\n      formModelV2.init(formMeta, {\n        a: 1,\n        b: 2,\n      });\n\n      const formItem = formModelV2.getFormItemByPath('/');\n      expect(formItem?.value).toEqual({\n        a: 1,\n        b: 2,\n      });\n\n      formItem!.value = { a: 3, b: 4 };\n\n      expect(formItem?.value).toEqual({\n        a: 3,\n        b: 4,\n      });\n    });\n  });\n\n  describe('onFormValueChangeIn', () => {\n    beforeEach(() => {\n      formModelV2.dispose();\n      formModelV2 = new FormModelV2(node);\n    });\n\n    it('should trigger callback when value change', () => {\n      const mockCallback = vi.fn();\n      const formMeta = {\n        render: vi.fn(),\n      } as unknown as FormMeta;\n      formModelV2.init(formMeta, { a: 1 });\n      formModelV2.onFormValueChangeIn('a', mockCallback);\n      formModelV2.setValueIn('a', 2);\n\n      expect(mockCallback).toHaveBeenCalledOnce();\n    });\n    it('should throw error when formModel is not initialized', () => {\n      expect(() => formModelV2.onFormValueChangeIn('a', vi.fn())).toThrowError();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/node/__tests__/form-plugins.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it, vi } from 'vitest';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { DataEvent, FormMeta } from '../src/types';\nimport { defineFormPluginCreator } from '../src/form-plugin';\nimport { FormModelV2 } from '../src/form-model-v2';\n\ndescribe('FormModelV2 plugins', () => {\n  const node = {\n    getService: vi.fn().mockReturnValue({}),\n    getData: vi.fn().mockReturnValue({ fireChange: vi.fn() }),\n  } as unknown as FlowNodeEntity;\n\n  let formModelV2 = new FormModelV2(node);\n\n  beforeEach(() => {\n    formModelV2.dispose();\n    formModelV2 = new FormModelV2(node);\n  });\n\n  it('should call onInit when formModel init', () => {\n    const mockInit = vi.fn();\n    const plugin = defineFormPluginCreator({\n      name: 'test',\n      onInit: mockInit,\n    })({ opt1: 1 });\n    const formMeta = {\n      render: vi.fn(),\n      plugins: [plugin],\n    } as unknown as FormMeta;\n    formModelV2.init(formMeta);\n\n    expect(mockInit).toHaveBeenCalledOnce();\n    expect(mockInit).toHaveBeenCalledWith(\n      { formModel: formModelV2, ...formModelV2.nodeContext },\n      { opt1: 1 }\n    );\n  });\n\n  it('should call onDispose when formModel dispose', () => {\n    const mockDispose = vi.fn();\n    const plugin = defineFormPluginCreator({\n      name: 'test',\n      onDispose: mockDispose,\n    })({ opt1: 1 });\n    const formMeta = {\n      render: vi.fn(),\n      plugins: [plugin],\n    } as unknown as FormMeta;\n    formModelV2.init(formMeta);\n    formModelV2.dispose();\n\n    expect(mockDispose).toHaveBeenCalledOnce();\n    expect(mockDispose).toHaveBeenCalledWith(\n      { formModel: formModelV2, ...formModelV2.nodeContext },\n      { opt1: 1 }\n    );\n  });\n\n  it('should call effects when corresponding events trigger', () => {\n    const mockEffectPlugin = vi.fn();\n    const mockEffectOrigin = vi.fn();\n\n    const plugin = defineFormPluginCreator({\n      name: 'test',\n      onSetupFormMeta(ctx, opts) {\n        ctx.mergeEffect({\n          a: [\n            {\n              event: DataEvent.onValueInitOrChange,\n              effect: mockEffectPlugin,\n            },\n          ],\n        });\n      },\n    })({ opt1: 1 });\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        a: [\n          {\n            event: DataEvent.onValueInitOrChange,\n            effect: mockEffectOrigin,\n          },\n        ],\n      },\n      plugins: [plugin],\n    } as unknown as FormMeta;\n\n    formModelV2.init(formMeta, { a: 0 });\n\n    expect(mockEffectPlugin).toHaveBeenCalledOnce();\n    expect(mockEffectOrigin).toHaveBeenCalledOnce();\n  });\n\n  it('should call effects when corresponding events trigger: array case', () => {\n    const mockEffectPluginArrStar = vi.fn();\n    const mockEffectOriginArrStar = vi.fn();\n    const mockEffectPluginOther = vi.fn();\n\n    const plugin = defineFormPluginCreator({\n      name: 'test',\n      onSetupFormMeta(ctx, opts) {\n        ctx.mergeEffect({\n          'arr.*': [\n            {\n              event: DataEvent.onValueChange,\n              effect: mockEffectPluginArrStar,\n            },\n          ],\n          other: [\n            {\n              event: DataEvent.onValueChange,\n              effect: mockEffectPluginOther,\n            },\n          ],\n        });\n      },\n    })({ opt1: 1 });\n\n    const formMeta = {\n      render: vi.fn(),\n      effect: {\n        'arr.*': [\n          {\n            event: DataEvent.onValueChange,\n            effect: mockEffectOriginArrStar,\n          },\n        ],\n      },\n      plugins: [plugin],\n    } as unknown as FormMeta;\n\n    formModelV2.init(formMeta, { arr: [0], other: 1 });\n    expect(mockEffectOriginArrStar).not.toHaveBeenCalled();\n    expect(mockEffectPluginArrStar).not.toHaveBeenCalled();\n    expect(mockEffectPluginOther).not.toHaveBeenCalled();\n\n    formModelV2.setValueIn('arr.0', 2);\n    formModelV2.setValueIn('other', 2);\n\n    expect(mockEffectOriginArrStar).toHaveBeenCalledOnce();\n    expect(mockEffectPluginArrStar).toHaveBeenCalledOnce();\n    expect(mockEffectPluginOther).toHaveBeenCalledOnce();\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/node/__tests__/glob.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// test src/glob.ts\nimport { describe, expect, it } from 'vitest';\nimport { Glob } from '@flowgram.ai/form';\n\ndescribe('glob', () => {\n  it('return original path array if no *', () => {\n    const obj = { a: { b: { c: 1 } } };\n    expect(Glob.findMatchPaths(obj, 'a.b.c')).toEqual(['a.b.c']);\n  });\n  it('object: when * is in middle of the path', () => {\n    const obj = {\n      a: { b: { c: 1 } },\n      x: { y: { z: 2 } },\n    };\n    expect(Glob.findMatchPaths(obj, 'a.*.c')).toEqual(['a.b.c']);\n  });\n  it('object:when * is at the end of the path', () => {\n    const obj = {\n      a: { b: { c: 1 } },\n      x: { y: { z: 2 } },\n    };\n    expect(Glob.findMatchPaths(obj, 'a.*')).toEqual(['a.b']);\n  });\n  it('object:when * is at the start of the path', () => {\n    const obj = {\n      a: { b: { c: 1 } },\n      x: { y: { z: 2 } },\n    };\n    expect(Glob.findMatchPaths(obj, '*.y')).toEqual(['x.y']);\n  });\n  it('array: when * is at the end of the path', () => {\n    const obj = {\n      other: 100,\n      arr: [\n        {\n          x: 1,\n          y: { a: 1, b: 2 },\n        },\n        {\n          x: 10,\n          y: {\n            a: 10,\n            b: 20,\n          },\n        },\n      ],\n    };\n    expect(Glob.findMatchPaths(obj, 'arr.*')).toEqual(['arr.0', 'arr.1']);\n  });\n  it('array: when * is at the start of the path', () => {\n    const arr = [\n      {\n        x: 1,\n        y: { a: 1, b: 2 },\n      },\n      {\n        x: 10,\n        y: {\n          a: 10,\n          b: 20,\n        },\n      },\n    ];\n\n    expect(Glob.findMatchPaths(arr, '*')).toEqual(['0', '1']);\n  });\n  it('array: when * is in the middle of the path', () => {\n    const obj = {\n      other: 100,\n      arr: [\n        {\n          x: 1,\n          y: { a: 1, b: 2 },\n        },\n        {\n          x: 10,\n          y: {\n            a: 10,\n            b: 20,\n          },\n        },\n      ],\n    };\n\n    expect(Glob.findMatchPaths(obj, 'arr.*.y')).toEqual(['arr.0.y', 'arr.1.y']);\n  });\n});\n"
  },
  {
    "path": "packages/node-engine/node/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/node-engine/node/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/node\",\n  \"version\": \"0.1.8\",\n  \"description\": \"automation form core\",\n  \"keywords\": [\n    \"flow\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/form\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/form-model-v2.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get, groupBy, isEmpty, isNil, mapKeys, uniq } from 'lodash-es';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport {\n  FlowNodeFormData,\n  FormFeedback,\n  FormItem,\n  FormManager,\n  FormModel,\n  FormModelValid,\n  IFormItem,\n  NodeFormContext,\n  OnFormValuesChangePayload,\n} from '@flowgram.ai/form-core';\nimport {\n  createForm,\n  FieldArrayModel,\n  FieldName,\n  FieldValue,\n  type FormControl,\n  FormModel as NativeFormModel,\n  FormValidateReturn,\n  Glob,\n  IField,\n  IFieldArray,\n  toForm,\n} from '@flowgram.ai/form';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext, PluginContext } from '@flowgram.ai/core';\n\nimport {\n  convertGlobPath,\n  findMatchedInMap,\n  formFeedbacksToNodeCoreFormFeedbacks,\n  mergeEffectReturn,\n  runAndDeleteEffectReturn,\n} from './utils';\nimport {\n  DataEvent,\n  Effect,\n  EffectOptions,\n  EffectReturn,\n  FormMeta,\n  onFormValueChangeInPayload,\n} from './types';\nimport { renderForm } from './form-render';\nimport { FormPlugin } from './form-plugin';\n\nconst DEFAULT = {\n  // Different formModel should have different reference\n  EFFECT_MAP: () => ({}),\n  EFFECT_RETURN_MAP: () =>\n    new Map([\n      [DataEvent.onValueInitOrChange, {}],\n      [DataEvent.onValueChange, {}],\n      [DataEvent.onValueInit, {}],\n      [DataEvent.onArrayAppend, {}],\n      [DataEvent.onArrayDelete, {}],\n    ]),\n  FORM_FEEDBACKS: () => [],\n  VALID: null,\n};\n\nexport class FormModelV2 extends FormModel implements Disposable {\n  protected effectMap: Record<string, EffectOptions[]> = DEFAULT.EFFECT_MAP();\n\n  protected effectReturnMap: Map<DataEvent, Record<string, EffectReturn>> =\n    DEFAULT.EFFECT_RETURN_MAP();\n\n  protected plugins: FormPlugin[] = [];\n\n  protected node: FlowNodeEntity;\n\n  protected formFeedbacks: FormValidateReturn | undefined = DEFAULT.FORM_FEEDBACKS();\n\n  protected onInitializedEmitter = new Emitter<FormModel>();\n\n  protected onValidateEmitter = new Emitter<FormModel>();\n\n  readonly onValidate = this.onValidateEmitter.event;\n\n  readonly onInitialized = this.onInitializedEmitter.event;\n\n  protected onDisposeEmitter = new Emitter<void>();\n\n  readonly onDispose = this.onDisposeEmitter.event;\n\n  protected toDispose = new DisposableCollection();\n\n  protected onFormValuesChangeEmitter = new Emitter<OnFormValuesChangePayload>();\n\n  readonly onFormValuesChange = this.onFormValuesChangeEmitter.event;\n\n  protected onValidChangeEmitter = new Emitter<FormModelValid>();\n\n  readonly onValidChange = this.onValidChangeEmitter.event;\n\n  protected onFeedbacksChangeEmitter = new Emitter<FormFeedback[]>();\n\n  readonly onFeedbacksChange = this.onFeedbacksChangeEmitter.event;\n\n  constructor(node: FlowNodeEntity) {\n    super();\n    this.node = node;\n    this.toDispose.pushAll([\n      this.onInitializedEmitter,\n      this.onValidateEmitter,\n      this.onValidChangeEmitter,\n      this.onFeedbacksChangeEmitter,\n      this.onFormValuesChangeEmitter,\n    ]);\n  }\n\n  protected _valid: FormModelValid = DEFAULT.VALID;\n\n  get valid(): FormModelValid {\n    return this._valid;\n  }\n\n  private set valid(valid: FormModelValid) {\n    this._valid = valid;\n    this.onValidChangeEmitter.fire(valid);\n  }\n\n  get flowNodeEntity() {\n    return this.node;\n  }\n\n  get formManager() {\n    return this.node.getService(FormManager);\n  }\n\n  protected _formControl?: FormControl<any>;\n\n  get formControl() {\n    return this._formControl;\n  }\n\n  protected _formMeta: FormMeta;\n\n  get formMeta(): FormMeta {\n    return this._formMeta || (this.node.getNodeRegistry().formMeta as FormMeta);\n  }\n\n  get values() {\n    return this.nativeFormModel?.values;\n  }\n\n  protected _feedbacks: FormFeedback[] = [];\n\n  get feedbacks(): FormFeedback[] {\n    return this._feedbacks;\n  }\n\n  updateFormValues(value: any) {\n    if (this.nativeFormModel) {\n      const finalValue = this.formMeta.formatOnInit\n        ? this.formMeta.formatOnInit(value, this.nodeContext)\n        : value;\n      this.nativeFormModel.values = finalValue;\n    }\n  }\n\n  private set feedbacks(feedbacks: FormFeedback[]) {\n    this._feedbacks = feedbacks;\n    this.onFeedbacksChangeEmitter.fire(feedbacks);\n  }\n\n  get formItemPathMap(): Map<string, IFormItem> {\n    return new Map<string, IFormItem>();\n  }\n\n  protected _initialized: boolean = false;\n\n  get initialized(): boolean {\n    return this._initialized;\n  }\n\n  get nodeContext(): NodeFormContext {\n    return {\n      node: this.node,\n      playgroundContext: this.node.getService(PlaygroundContext),\n      clientContext: this.node.getService(PluginContext),\n    };\n  }\n\n  get nativeFormModel(): NativeFormModel | undefined {\n    return this._formControl?._formModel;\n  }\n\n  render() {\n    return renderForm(this);\n  }\n\n  initPlugins(plugins: FormPlugin[]) {\n    if (!plugins.length) {\n      return;\n    }\n\n    this.plugins = plugins;\n    plugins.forEach((plugin) => {\n      plugin.init(this);\n    });\n  }\n\n  init(formMeta: FormMeta, rawInitialValues?: any) {\n    /* 透传 onFormValuesChange 事件给 FlowNodeFormData */\n    const formData = this.node.getData<FlowNodeFormData>(FlowNodeFormData);\n    this.onFormValuesChange(() => {\n      this._valid = null;\n      formData.fireChange();\n    });\n\n    (formMeta.plugins || [])?.forEach((_plugin) => {\n      if (_plugin.setupFormMeta) {\n        formMeta = _plugin.setupFormMeta(formMeta, this.nodeContext);\n      }\n    });\n\n    this._formMeta = formMeta;\n\n    const { validateTrigger, validate, effect } = formMeta;\n    if (effect) {\n      this.effectMap = effect;\n    }\n\n    // 计算初始值: defaultValues 是默认表单值，不需要被format, 而rawInitialValues 是用户创建form 时传入的初始值，可能不同于表单数据格式，需要被format\n    const defaultValues =\n      typeof formMeta.defaultValues === 'function'\n        ? formMeta.defaultValues(this.nodeContext)\n        : formMeta.defaultValues;\n\n    const initialValues = formMeta.formatOnInit\n      ? formMeta.formatOnInit(rawInitialValues, this.nodeContext)\n      : rawInitialValues;\n\n    // 初始化底层表单\n    const { control } = createForm({\n      initialValues: initialValues || defaultValues,\n      validateTrigger,\n      context: this.nodeContext,\n      validate: validate,\n      disableAutoInit: true,\n    });\n\n    this._formControl = control;\n    const nativeFormModel = control._formModel;\n    this.toDispose.push(nativeFormModel);\n\n    // forward onFormValuesChange event\n    nativeFormModel.onFormValuesChange((props) => {\n      this.onFormValuesChangeEmitter.fire(props);\n    });\n\n    if (formMeta.plugins) {\n      this.initPlugins(formMeta.plugins);\n    }\n\n    // Form 数据变更时触发对应的effect\n    nativeFormModel.onFormValuesChange(({ values, prevValues, name, options }) => {\n      Object.keys(this.effectMap).forEach((pattern) => {\n        // 找到匹配 pattern 的数据路径\n        const paths = uniq([\n          ...Glob.findMatchPaths(values, pattern),\n          ...Glob.findMatchPaths(prevValues, pattern),\n        ]).filter(\n          (path) =>\n            // trigger effect by compare if value changed\n            get(values, path) !== get(prevValues, path)\n        );\n\n        if (Glob.isMatchOrParent(pattern, name)) {\n          const currentName = Glob.getParentPathByPattern(pattern, name);\n          if (!paths.includes(currentName)) {\n            // trigger effect anyway\n            paths.push(currentName);\n          }\n        }\n\n        const effectOptionsArr = this.effectMap[pattern];\n\n        paths.forEach((path) => {\n          let eventList = [DataEvent.onValueChange, DataEvent.onValueInitOrChange];\n          const isPrevNil = isNil(get(prevValues, path));\n\n          if (isPrevNil) {\n            // HACK: For array append, onFormValuesInit will auto triggered for array[index]\n            if (options?.action === 'array-append' && Glob.isMatch(`${name}.*`, path)) {\n              eventList = [];\n            } else {\n              eventList = [DataEvent.onValueInit, DataEvent.onValueInitOrChange];\n            }\n          }\n\n          // 对触发 init 事件的 name 或他的字 path 触发 effect\n          runAndDeleteEffectReturn(this.effectReturnMap, path, eventList);\n\n          // 执行该事件配置下所有 onValueChange 事件的 effect\n          effectOptionsArr.forEach(({ effect, event }: EffectOptions) => {\n            if (eventList.includes(event)) {\n              // 执行 effect\n              const effectReturn = (effect as Effect)({\n                name: path,\n                value: get(values, path),\n                prevValue: get(prevValues, path),\n                formValues: values,\n                form: toForm(this.nativeFormModel!),\n                context: this.nodeContext,\n              });\n\n              // 更新 effect return\n              if (\n                effectReturn &&\n                typeof effectReturn === 'function' &&\n                this.effectReturnMap.has(event)\n              ) {\n                const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;\n                eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);\n              }\n            }\n          });\n        });\n      });\n    });\n\n    // Form 数据初始化时触发对应的 effect\n    nativeFormModel.onFormValuesInit(({ values, name, prevValues }) => {\n      Object.keys(this.effectMap).forEach((pattern) => {\n        // 找到匹配 pattern 的数据路径\n        const paths = Glob.findMatchPaths(values, pattern);\n\n        // 获取配置在该 pattern上的所有effect配置\n        const effectOptionsArr = this.effectMap[pattern];\n\n        paths.forEach((path) => {\n          if (Glob.isMatchOrParent(name, path) || name === path) {\n            // 对触发 init 事件的 name 或他的字 path 触发 effect\n            runAndDeleteEffectReturn(this.effectReturnMap, path, [\n              DataEvent.onValueInit,\n              DataEvent.onValueInitOrChange,\n            ]);\n\n            effectOptionsArr.forEach(({ event, effect }: EffectOptions) => {\n              if (event === DataEvent.onValueInit || event === DataEvent.onValueInitOrChange) {\n                const effectReturn = (effect as Effect)({\n                  name: path,\n                  value: get(values, path),\n                  formValues: values,\n                  prevValue: get(prevValues, path),\n                  form: toForm(this.nativeFormModel!),\n                  context: this.nodeContext,\n                });\n\n                // 更新 effect return\n                if (\n                  effectReturn &&\n                  typeof effectReturn === 'function' &&\n                  this.effectReturnMap.has(event)\n                ) {\n                  const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;\n                  eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);\n                }\n              }\n            });\n          }\n        });\n      });\n    });\n\n    // 为 Field 添加 effect, 主要针对array\n    nativeFormModel.onFieldModelCreate((field) => {\n      // register effect\n      const effectOptionsArr = findMatchedInMap<EffectOptions[]>(field, this.effectMap);\n      if (effectOptionsArr?.length) {\n        // 按事件聚合\n        const eventMap = groupBy(effectOptionsArr, 'event');\n\n        mapKeys(eventMap, (optionsArr, event) => {\n          const combinedEffect = (props: any) => {\n            // 该事件下执行所有effect\n            optionsArr.forEach(({ effect }) =>\n              effect({\n                ...props,\n                formValues: nativeFormModel.values,\n                form: toForm(this.nativeFormModel!),\n                context: this.nodeContext,\n              })\n            );\n          };\n\n          switch (event) {\n            case DataEvent.onArrayAppend:\n              if (field instanceof FieldArrayModel) {\n                (field as FieldArrayModel).onAppend(combinedEffect);\n              }\n              break;\n            case DataEvent.onArrayDelete:\n              if (field instanceof FieldArrayModel) {\n                (field as FieldArrayModel).onDelete(combinedEffect);\n              }\n              break;\n          }\n        });\n      }\n    });\n\n    // 手动初始化form\n    this._formControl.init();\n\n    this._initialized = true;\n\n    this.onInitializedEmitter.fire(this);\n\n    this.onDispose(() => {\n      this._initialized = false;\n      this.effectMap = {};\n      nativeFormModel.dispose();\n    });\n  }\n\n  toJSON() {\n    if (this.formMeta.formatOnSubmit) {\n      return this.formMeta.formatOnSubmit(this.nativeFormModel?.values, this.nodeContext);\n    }\n    return this.nativeFormModel?.values;\n  }\n\n  clearValid() {}\n\n  async validate() {\n    this.formFeedbacks = await this.nativeFormModel?.validate();\n    this.valid = isEmpty(this.formFeedbacks?.filter((f) => f.level === 'error'));\n    this.onValidateEmitter.fire(this);\n    return this.valid;\n  }\n\n  getValues<T = any>(): T | undefined {\n    return this._formControl?._formModel.values;\n  }\n\n  getField<\n    TValue = FieldValue,\n    TField extends IFieldArray<TValue> | IField<TValue> = IField<TValue>\n  >(name: FieldName): TField | undefined {\n    let finalName = name.includes('/') ? convertGlobPath(name) : name;\n\n    return this.formControl?.getField<TValue, TField>(finalName) as TField;\n  }\n\n  getValueIn<TValue>(name: FieldName): TValue | undefined {\n    let finalName = name.includes('/') ? convertGlobPath(name) : name;\n\n    return this.nativeFormModel?.getValueIn(finalName);\n  }\n\n  setValueIn(name: FieldName, value: any) {\n    let finalName = name.includes('/') ? convertGlobPath(name) : name;\n\n    this.nativeFormModel?.setValueIn(finalName, value);\n  }\n\n  /**\n   * 监听表单某个路径下的值变化\n   * @param name 路径\n   * @param callback 回调函数\n   */\n  onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(\n    name: FieldName,\n    callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void\n  ): Disposable {\n    if (!this._initialized) {\n      throw new Error(\n        `[NodeEngine] FormModel Error: onFormValueChangeIn can not be called before initialized`\n      );\n    }\n\n    return this.formControl!._formModel.onFormValuesChange(\n      ({ name: changedName, values, prevValues }) => {\n        if (changedName === name) {\n          callback({\n            value: get(values, name),\n            prevValue: get(prevValues, name),\n            formValues: values,\n            prevFormValues: prevValues,\n          });\n        }\n      }\n    );\n  }\n\n  /**\n   * @deprecated 该方法用于兼容 V1 版本 FormModel接口，如果确定是FormModelV2 请使用 FormModel.getValueIn\n   * @param path glob path\n   */\n  getFormItemValueByPath(globPath: string) {\n    if (!globPath) {\n      return;\n    }\n    if (globPath === '/') {\n      return this._formControl?._formModel.values;\n    }\n    const name = convertGlobPath(globPath);\n    return this.getValueIn(name!);\n  }\n\n  async validateWithFeedbacks(): Promise<FormFeedback[]> {\n    await this.validate();\n    return formFeedbacksToNodeCoreFormFeedbacks(this.formFeedbacks!);\n  }\n\n  /**\n   * @deprecated 该方法用于兼容 V1 版本 FormModel接口，如果确定是FormModelV2, 请使用FormModel.getValueIn 和 FormModel.setValueIn\n   * @param path glob path\n   */\n  getFormItemByPath(path: string): FormItem | undefined {\n    if (!this.nativeFormModel) {\n      return;\n    }\n\n    const that = this;\n\n    if (path === '/') {\n      return {\n        get value() {\n          return that.nativeFormModel!.values;\n        },\n        set value(v) {\n          that.nativeFormModel!.values = v;\n        },\n      } as FormItem;\n    }\n\n    const name = convertGlobPath(path);\n    const formItemValue = that.getValueIn(name!);\n    return {\n      get value() {\n        return formItemValue;\n      },\n      set value(v) {\n        that.setValueIn(name, v);\n      },\n    } as FormItem;\n  }\n\n  dispose(): void {\n    this.onDisposeEmitter.fire();\n\n    // 执行所有effect return\n    this.effectReturnMap.forEach((eventMap) => {\n      Object.values(eventMap).forEach((effectReturn) => {\n        effectReturn();\n      });\n    });\n\n    this.effectMap = DEFAULT.EFFECT_MAP();\n    this.effectReturnMap = DEFAULT.EFFECT_RETURN_MAP();\n\n    this.plugins.forEach((p) => {\n      p.dispose();\n    });\n\n    this.plugins = [];\n\n    this.formFeedbacks = DEFAULT.FORM_FEEDBACKS();\n    this._valid = DEFAULT.VALID;\n\n    this._formControl = undefined;\n    this._initialized = false;\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/form-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nanoid } from 'nanoid';\nimport { Disposable } from '@flowgram.ai/utils';\nimport { type NodeFormContext } from '@flowgram.ai/form-core';\n\nimport { mergeEffectMap } from './utils';\nimport { type FormMeta, type FormPluginCtx, type FormPluginSetupMetaCtx } from './types';\nimport { FormModelV2 } from './form-model-v2';\n\nexport interface FormPluginConfig<Opts = any> {\n  /**\n   * form plugin name, for debug use\n   */\n  name?: string;\n\n  /**\n   * setup formMeta\n   * @param ctx\n   * @returns\n   */\n  onSetupFormMeta?: (ctx: FormPluginSetupMetaCtx, opts: Opts) => void;\n\n  /**\n   * FormModel 初始化时执行\n   * @param ctx\n   */\n  onInit?: (ctx: FormPluginCtx, opts: Opts) => void;\n\n  /**\n   * FormModel 销毁时执行\n   */\n  onDispose?: (ctx: FormPluginCtx, opts: Opts) => void;\n}\n\nexport class FormPlugin<Opts = any> implements Disposable {\n  readonly name: string;\n\n  readonly pluginId: string;\n\n  readonly config: FormPluginConfig;\n\n  readonly opts?: Opts;\n\n  protected _formModel: FormModelV2;\n\n  constructor(config: FormPluginConfig, opts?: Opts) {\n    this.name = config?.name || '';\n    this.pluginId = `${this.name}__${nanoid()}`;\n    this.config = config;\n\n    this.opts = opts;\n  }\n\n  get formModel(): FormModelV2 {\n    return this._formModel;\n  }\n\n  get ctx(): { formModel: FormModelV2 } & NodeFormContext {\n    return {\n      formModel: this.formModel,\n      ...this.formModel.nodeContext,\n    };\n  }\n\n  setupFormMeta(formMeta: FormMeta, nodeContext: NodeFormContext): FormMeta {\n    const nextFormMeta: FormMeta = {\n      ...formMeta,\n    };\n\n    this.config.onSetupFormMeta?.(\n      {\n        mergeEffect: (effect) => {\n          nextFormMeta.effect = mergeEffectMap(nextFormMeta.effect || {}, effect);\n        },\n        mergeValidate: (validate) => {\n          nextFormMeta.validate = {\n            ...(nextFormMeta.validate || {}),\n            ...validate,\n          };\n        },\n        addFormatOnInit: (formatOnInit) => {\n          if (!nextFormMeta.formatOnInit) {\n            nextFormMeta.formatOnInit = formatOnInit;\n            return;\n          }\n          const legacyFormatOnInit = nextFormMeta.formatOnInit;\n          nextFormMeta.formatOnInit = (v, c) => formatOnInit?.(legacyFormatOnInit(v, c), c);\n        },\n        addFormatOnSubmit: (formatOnSubmit) => {\n          if (!nextFormMeta.formatOnSubmit) {\n            nextFormMeta.formatOnSubmit = formatOnSubmit;\n            return;\n          }\n          const legacyFormatOnSubmit = nextFormMeta.formatOnSubmit;\n          nextFormMeta.formatOnSubmit = (v, c) => formatOnSubmit?.(legacyFormatOnSubmit(v, c), c);\n        },\n        ...nodeContext,\n      },\n      this.opts\n    );\n\n    return nextFormMeta;\n  }\n\n  init(formModel: FormModelV2) {\n    this._formModel = formModel;\n    this.config?.onInit?.(this.ctx, this.opts);\n  }\n\n  dispose() {\n    if (this.config?.onDispose) {\n      this.config?.onDispose(this.ctx, this.opts);\n    }\n  }\n}\n\nexport type FormPluginCreator<Opts> = (opts: Opts) => FormPlugin<Opts>;\n\nexport function defineFormPluginCreator<Opts>(\n  config: FormPluginConfig<Opts>\n): FormPluginCreator<Opts> {\n  return function (opts: Opts) {\n    return new FormPlugin(config, opts);\n  };\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Form } from '@flowgram.ai/form';\n\nimport { FormModelV2 } from './form-model-v2';\n\ninterface FormRenderProps {\n  formModel: FormModelV2;\n}\n\nconst FormRender = ({ formModel }: FormRenderProps) =>\n  formModel?.formControl ? (\n    <>\n      <Form control={formModel?.formControl} keepModelOnUnMount>\n        {formModel.formMeta.render}\n      </Form>\n    </>\n  ) : null;\n\nexport function renderForm(formModel: FormModelV2) {\n  return <FormRender formModel={formModel} />;\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/get-node-form.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { Disposable, Event } from '@flowgram.ai/utils';\nimport { FlowNodeFormData, NodeRender, OnFormValuesChangePayload } from '@flowgram.ai/form-core';\nimport { FieldName, FieldValue, FormState } from '@flowgram.ai/form';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { onFormValueChangeInPayload } from './types';\nimport { FormModelV2 } from './form-model-v2';\n\nexport interface NodeFormProps<TValues> {\n  /**\n   * The initialValues of the form.\n   */\n  initialValues: TValues;\n  /**\n   * Form values. Returns a deep copy of the data in the store.\n   */\n  values: TValues;\n  /**\n   * Form state\n   */\n  state: FormState;\n  /**\n   * Get value in certain path\n   * @param name path\n   */\n  getValueIn<TValue = FieldValue>(name: FieldName): TValue;\n\n  /**\n   * Set value in certain path.\n   * It will trigger the re-rendering of the Field Component if a Field is related to this path\n   * @param name path\n   */\n  setValueIn<TValue>(name: FieldName, value: TValue): void;\n  /**\n   * set form values\n   */\n  updateFormValues(values: any): void;\n  /**\n   * Render form\n   */\n  render: () => React.ReactNode;\n  /**\n   * Form value change event\n   */\n  onFormValuesChange: Event<OnFormValuesChangePayload>;\n  /**\n   * Trigger form validate\n   */\n  validate: () => Promise<boolean>;\n  /**\n   * Form validate event\n   */\n  onValidate: Event<FormState>;\n  /**\n   * Form field value change event\n   */\n  onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(\n    name: FieldName,\n    callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void\n  ): Disposable;\n}\n\n/**\n * Use `node.form` instead\n * @deprecated\n * @param node\n */\nexport function getNodeForm<TValues = FieldValue>(\n  node: FlowNodeEntity\n): NodeFormProps<TValues> | undefined {\n  const formModel = node.getData<FlowNodeFormData>(FlowNodeFormData)?.getFormModel<FormModelV2>();\n  const nativeFormModel = formModel?.nativeFormModel;\n\n  if (!formModel || !nativeFormModel) return undefined;\n\n  const result: NodeFormProps<TValues> = {\n    initialValues: nativeFormModel.initialValues,\n    get values() {\n      return nativeFormModel.values;\n    },\n\n    state: nativeFormModel.state,\n    getValueIn: (name: FieldName) => nativeFormModel.getValueIn(name),\n    setValueIn: (name: FieldName, value: any) => nativeFormModel.setValueIn(name, value),\n    updateFormValues: (values: any) => {\n      formModel.updateFormValues(values);\n    },\n    render: () => <NodeRender node={node} />,\n    onFormValuesChange: formModel.onFormValuesChange.bind(formModel),\n    onFormValueChangeIn: formModel.onFormValueChangeIn.bind(formModel),\n    onValidate: formModel.nativeFormModel.onValidate,\n    validate: formModel.validate.bind(formModel),\n  };\n\n  Object.defineProperty(result, '_formModel', {\n    enumerable: false,\n    get() {\n      return formModel;\n    },\n  });\n  return result;\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/helpers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeFormData } from '@flowgram.ai/form-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { DataEvent } from './types';\nimport { FormModelV2 } from './form-model-v2';\n\nexport function getFormModel(node: FlowNodeEntity) {\n  // @ts-ignore\n  return node.getData<FlowNodeFormData>(FlowNodeFormData)?.formModel as FormModelV2;\n}\n\nexport function isFormV2(node: FlowNodeEntity) {\n  return !!node.getNodeRegistry().formMeta?.render;\n}\n\nexport function createEffectOptions<T>(\n  event: DataEvent,\n  effect: T\n): { effect: T; event: DataEvent } {\n  return {\n    event,\n    effect,\n  };\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/hooks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/utils';\nimport { FlowNodeFormData } from '@flowgram.ai/form-core';\nimport { Errors, Warnings } from '@flowgram.ai/form';\nimport { FormState, useFormErrors, useFormState, useFormWarnings } from '@flowgram.ai/form';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FormModelV2 } from './form-model-v2';\n\n/**\n * Listen to Form's values and refresh the React component.\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useWatchFormValues<T = any>(node: FlowNodeEntity): T | undefined {\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const disposable = formModel.nativeFormModel?.onFormValuesChange(() => {\n      refresh();\n    });\n    return () => disposable?.dispose();\n  }, [formModel.nativeFormModel]);\n\n  return formModel.getValues<T>();\n}\n\n/**\n * Listen to Form's value in a certain path and refresh the React component.\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useWatchFormValueIn<T = any>(node: FlowNodeEntity, name: string): T | undefined {\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const disposable = formModel.nativeFormModel?.onFormValuesChange(({ name: changedName }) => {\n      if (name === changedName) {\n        refresh();\n      }\n    });\n\n    return () => disposable?.dispose();\n  }, []);\n\n  return formModel.getValueIn<T>(name);\n}\n\n/**\n * Listen to FormModel's initialization and refresh React component.\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useInitializedFormModel(node: FlowNodeEntity) {\n  const formModel = node.getData(FlowNodeFormData).getFormModel<FormModelV2>();\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    const disposable = formModel.onInitialized(() => {\n      refresh();\n    });\n    return () => disposable.dispose();\n  }, [formModel]);\n\n  return formModel;\n}\n\n/**\n * Get Form's state, Form State is a proxy, it will refresh the React component when the value you accessed changed\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useWatchFormState(node: FlowNodeEntity): FormState | undefined {\n  const formModel = useInitializedFormModel(node);\n  return useFormState(formModel.formControl);\n}\n\n/**\n * Get Form's errors, Form errors is a proxy, it will refresh the React component when the value you accessed changed\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useWatchFormErrors(node: FlowNodeEntity): Errors | undefined {\n  const formModel = useInitializedFormModel(node);\n  return useFormErrors(formModel.formControl);\n}\n\n/**\n * Get Form's warnings, Form warnings is a proxy, it will refresh the React component when the value you accessed changed\n * By providing related node, you can use this hook outside the Form Component.\n * @param node\n */\nexport function useWatchFormWarnings(node: FlowNodeEntity): Warnings | undefined {\n  const formModel = useInitializedFormModel(node);\n  return useFormWarnings(formModel.formControl);\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './types';\nexport * from './form-model-v2';\nexport { isFormV2, createEffectOptions } from './helpers';\nexport * from './hooks';\nexport * from './form-plugin';\nexport { type NodeFormProps, getNodeForm } from './get-node-form';\n"
  },
  {
    "path": "packages/node-engine/node/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { FormModel, IFormMeta, NodeContext } from '@flowgram.ai/form-core';\nimport { FieldName, FieldValue } from '@flowgram.ai/form';\nimport {\n  FormRenderProps,\n  IForm,\n  Validate as FormValidate,\n  ValidateTrigger,\n} from '@flowgram.ai/form';\n\nimport { FormPlugin } from './form-plugin';\nimport { FormModelV2 } from './form-model-v2';\n\nexport interface Node {}\n\nexport interface Flow {}\n\nexport type Validate<TFieldValue = any, TFormValues = any> = (props: {\n  value: TFieldValue;\n  formValues: TFormValues;\n  context: NodeContext;\n  name: FieldName;\n}) => ReturnType<FormValidate<TFieldValue, TFormValues>>;\n\nexport enum DataEvent {\n  /* When value change */\n  onValueChange = 'onValueChange',\n  /**\n   * When value Init，it triggers when\n   * - defaultValue is configured in formMeta, it will trigger when form is initializing.\n   * - defaultValue is configured in Field, it will trigger when this Field is initializing if no initial value is set to this field.\n   */\n  onValueInit = 'onValueInit',\n  /**\n   * When Value Init or change\n   */\n  onValueInitOrChange = 'onValueInitOrChange',\n  /* It will trigger when ArrayField.append is called. It relies on ArrayField's rendering. If ArrayField is possibly not rendered in your case, please avoid using this event */\n  onArrayAppend = 'onArrayAppend',\n  /* It will trigger when ArrayField.delete is called. It relies on ArrayField's rendering. If ArrayField is possibly not rendered in your case, please avoid using this event */\n  onArrayDelete = 'onArrayDelete',\n}\n\nexport type EffectReturn = () => void;\n\nexport interface EffectFuncProps<TFieldValue = any, TFormValues = any> {\n  name: FieldName;\n  value: TFieldValue;\n  prevValue?: TFieldValue;\n  formValues: TFormValues;\n  form: IForm;\n  context: NodeContext;\n}\n\nexport type Effect<TFieldValue = any, TFormValues = any> = (\n  props: EffectFuncProps<TFieldValue, TFormValues>\n) => void | EffectReturn;\n\nexport type ArrayAppendEffect<TFieldValue = any, TFormValues = any> = (props: {\n  index: number;\n  value: TFieldValue;\n  arrayValues: Array<TFieldValue>;\n  formValues: TFormValues;\n  form: IForm;\n  context: NodeContext;\n}) => void | EffectReturn;\n\nexport type ArrayDeleteEffect<TFieldValue = any, TFormValues = any> = (props: {\n  index: number;\n  arrayValue: Array<TFieldValue>;\n  formValues: TFormValues;\n  form: IForm;\n  context: NodeContext;\n}) => void | EffectReturn;\n\nexport type EffectOptions =\n  | { effect: Effect; event: DataEvent }\n  | { effect: ArrayAppendEffect; event: DataEvent }\n  | { effect: ArrayDeleteEffect; event: DataEvent };\n\nexport interface FormMeta<TValues = any> {\n  /**\n   * The render method of the node form content. <Form /> is already integrated, so you don't need to wrap your components with <Form />\n   * @param props\n   */\n  render: (props: FormRenderProps<any>) => React.ReactElement;\n  /**\n   * When to trigger the validation.\n   */\n  validateTrigger?: ValidateTrigger;\n  /**\n   * Form data's validation rules. It's a key value map, where the key is a pattern of data's path (or field name), the value is a validate function.\n   */\n  validate?:\n    | Record<FieldName, Validate>\n    | ((values: TValues, ctx: NodeContext) => Record<FieldName, Validate>);\n  /**\n   * Form data's effects. It's a key value map, where the key is a pattern of data's path (or field name), the value is an array of effect configuration.\n   */\n  effect?: Record<FieldName, EffectOptions[]>;\n  /**\n   * Form data's complete default value. it will not be sent to formatOnInit, but used directly as form's value when needed.\n   */\n  defaultValues?: TValues | ((context: NodeContext) => TValues);\n  /**\n   * This function is to format the value when initiate the form, the returned value will be used as the initial value of the form.\n   * @param value value input to node as initialValue.\n   * @param context\n   */\n  formatOnInit?: (value: any, context: NodeContext) => any;\n  /**\n   * This function is to format the value when FormModel.toJSON is called, the returned value will be used as the final value to be saved .\n   * @param value value sent by form before format.\n   * @param context\n   */\n  formatOnSubmit?: (value: any, context: NodeContext) => any;\n  /**\n   * Form's plugins\n   */\n  plugins?: FormPlugin[];\n}\n\nexport function isFormModelV2(fm: FormModel | FormModelV2): fm is FormModelV2 {\n  return 'onFormValuesChange' in fm;\n}\n\nexport function isFormMetaV2(formMeta: IFormMeta | FormMeta) {\n  return 'render' in formMeta;\n}\n\nexport type FormPluginCtx = {\n  formModel: FormModelV2;\n} & NodeContext;\n\nexport type FormPluginSetupMetaCtx = {\n  mergeEffect: (effect: Record<string, EffectOptions[]>) => void;\n  mergeValidate: (validate: Record<FieldName, Validate>) => void;\n  addFormatOnInit: (formatOnInit: FormMeta['formatOnInit']) => void;\n  addFormatOnSubmit: (formatOnSubmit: FormMeta['formatOnSubmit']) => void;\n} & NodeContext;\n\nexport interface onFormValueChangeInPayload<TValue = FieldValue, TFormValues = FieldValue> {\n  value: TValue;\n  prevValue: TValue;\n  formValues: TFormValues;\n  prevFormValues: TFormValues;\n}\n"
  },
  {
    "path": "packages/node-engine/node/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { find, mergeWith } from 'lodash-es';\nimport { FormFeedback, FormPathService } from '@flowgram.ai/form-core';\nimport type { FieldError, FieldWarning, FormValidateReturn } from '@flowgram.ai/form';\nimport { type FieldModel, FieldName } from '@flowgram.ai/form';\n\nimport { DataEvent, EffectOptions, EffectReturn } from './types';\n\nexport function findMatchedInMap<T = any>(\n  field: FieldModel<any>,\n  validateMap: Record<FieldName, T> | undefined\n): T | undefined {\n  if (!validateMap) {\n    return;\n  }\n  if (validateMap[field.name]) {\n    return validateMap[field.name];\n  }\n\n  const found = find(Object.keys(validateMap), (key) => {\n    if (key.startsWith('regex:')) {\n      const regex = RegExp(key.split(':')[1]);\n      return regex.test(field.name);\n    }\n    return false;\n  });\n\n  if (found) {\n    return validateMap[found];\n  }\n}\n\nexport function formFeedbacksToNodeCoreFormFeedbacks(\n  formFeedbacks: FormValidateReturn\n): FormFeedback[] {\n  return formFeedbacks.map(\n    (f: FieldError | FieldWarning) =>\n      ({\n        feedbackStatus: f.level,\n        feedbackText: f.message,\n        path: f.name,\n      } as FormFeedback)\n  );\n}\n\nexport function convertGlobPath(path: string) {\n  if (path.startsWith('/')) {\n    const parts = FormPathService.normalize(path).slice(1).split('/');\n    return parts.join('.');\n  }\n  return path;\n}\n\nexport function mergeEffectMap(\n  origin: Record<string, EffectOptions[]>,\n  source: Record<string, EffectOptions[]>\n) {\n  return mergeWith(origin, source, function (objValue: EffectOptions[], srcValue: EffectOptions[]) {\n    return (objValue || []).concat(srcValue);\n  });\n}\n\nexport function mergeEffectReturn(origin?: EffectReturn, source?: EffectReturn): EffectReturn {\n  return () => {\n    origin?.();\n    source?.();\n  };\n}\n\nexport function runAndDeleteEffectReturn(\n  effectReturnMap: Map<DataEvent, Record<string, EffectReturn>>,\n  name: string,\n  events: DataEvent[]\n) {\n  events.forEach((event) => {\n    const eventMap = effectReturnMap.get(event);\n    if (eventMap?.[name]) {\n      eventMap[name]();\n      delete eventMap[name];\n    }\n  });\n}\n"
  },
  {
    "path": "packages/node-engine/node/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"rootDir\": \"./\",\n    \"baseUrl\": \"./\",\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\",\n    \"./__mocks__\",\n    \"./__tests__\"\n  ]\n}\n\n"
  },
  {
    "path": "packages/node-engine/node/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport {defineConfig} from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n  resolve: {\n    alias: {\n      '@': path.resolve(__dirname, './src')\n    },\n  },\n});\n"
  },
  {
    "path": "packages/node-engine/node/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/background-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/background-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/background-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/background-plugin/src/background-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { domUtils } from '@flowgram.ai/utils';\nimport { Layer, observeEntity, PlaygroundConfigEntity, SCALE_WIDTH } from '@flowgram.ai/core';\n\ninterface BackgroundScaleUnit {\n  realSize: number;\n  renderSize: number;\n  zoom: number;\n}\n\nconst PATTERN_PREFIX = 'gedit-background-pattern-';\nconst DEFAULT_RENDER_SIZE = 20;\nconst DEFAULT_DOT_SIZE = 1;\nlet id = 0;\n\nexport const BackgroundConfig = Symbol('BackgroundConfig');\nexport interface BackgroundLayerOptions {\n  /** 网格间距，默认 20px */\n  gridSize?: number;\n  /** 点的大小，默认 1px */\n  dotSize?: number;\n  /** 点的颜色，默认 \"#eceeef\" */\n  dotColor?: string;\n  /** 点的透明度，默认 0.5 */\n  dotOpacity?: number;\n  /** 背景颜色，默认透明 */\n  backgroundColor?: string;\n  /** 点的填充颜色，默认与stroke颜色相同 */\n  dotFillColor?: string;\n  /** Logo 配置 */\n  logo?: {\n    /** Logo 文本内容 */\n    text?: string;\n    /** Logo 图片 URL */\n    imageUrl?: string;\n    /** Logo 位置，默认 'center' */\n    position?: 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n    /** Logo 大小，默认 'medium' */\n    size?: 'small' | 'medium' | 'large' | number;\n    /** Logo 透明度，默认 0.1 */\n    opacity?: number;\n    /** Logo 颜色（仅文本），默认 \"#cccccc\" */\n    color?: string;\n    /** Logo 字体大小（仅文本），默认根据 size 计算 */\n    fontSize?: number;\n    /** Logo 字体家族（仅文本），默认 'Arial, sans-serif' */\n    fontFamily?: string;\n    /** Logo 字体粗细（仅文本），默认 'normal' */\n    fontWeight?: 'normal' | 'bold' | 'lighter' | number;\n    /** 自定义偏移 */\n    offset?: { x: number; y: number };\n    /** 新拟态（Neumorphism）效果配置 */\n    neumorphism?: {\n      /** 是否启用新拟态效果，默认 false */\n      enabled?: boolean;\n      /** 主要文字颜色，应该与背景色接近，默认自动计算 */\n      textColor?: string;\n      /** 亮色阴影颜色，默认自动计算（背景色的亮色版本） */\n      lightShadowColor?: string;\n      /** 暗色阴影颜色，默认自动计算（背景色的暗色版本） */\n      darkShadowColor?: string;\n      /** 阴影偏移距离，默认 6 */\n      shadowOffset?: number;\n      /** 阴影模糊半径，默认 12 */\n      shadowBlur?: number;\n      /** 效果强度（0-1），影响阴影的透明度，默认 0.3 */\n      intensity?: number;\n      /** 凸起效果（true）还是凹陷效果（false），默认 true */\n      raised?: boolean;\n    };\n  };\n}\n\n/**\n * dot 网格背景\n */\nexport class BackgroundLayer extends Layer<BackgroundLayerOptions> {\n  static type = 'WorkflowBackgroundLayer';\n\n  @observeEntity(PlaygroundConfigEntity)\n  protected playgroundConfigEntity: PlaygroundConfigEntity;\n\n  private _patternId = `${PATTERN_PREFIX}${id++}`;\n\n  node = domUtils.createDivWithClass('gedit-flow-background-layer');\n\n  grid: HTMLElement = document.createElement('div');\n\n  /**\n   * 获取网格大小配置\n   */\n  private get gridSize(): number {\n    return this.options.gridSize ?? DEFAULT_RENDER_SIZE;\n  }\n\n  /**\n   * 获取点大小配置\n   */\n  private get dotSize(): number {\n    return this.options.dotSize ?? DEFAULT_DOT_SIZE;\n  }\n\n  /**\n   * 获取点颜色配置\n   */\n  private get dotColor(): string {\n    return this.options.dotColor ?? '#eceeef';\n  }\n\n  /**\n   * 获取点透明度配置\n   */\n  private get dotOpacity(): number {\n    return this.options.dotOpacity ?? 0.5;\n  }\n\n  /**\n   * 获取背景颜色配置\n   */\n  private get backgroundColor(): string {\n    return this.options.backgroundColor ?? 'transparent';\n  }\n\n  /**\n   * 获取点填充颜色配置\n   */\n  private get dotFillColor(): string {\n    return this.options.dotFillColor ?? this.dotColor;\n  }\n\n  /**\n   * 获取Logo配置\n   */\n  private get logoConfig() {\n    return this.options.logo;\n  }\n\n  /**\n   * 当前缩放比\n   */\n  get zoom(): number {\n    return this.config.finalScale;\n  }\n\n  onReady() {\n    const { firstChild } = this.pipelineNode;\n    // 背景插入到最下边\n    this.pipelineNode.insertBefore(this.node, firstChild);\n    // 初始化设置最大 200% 最小 10% 缩放\n    this.playgroundConfigEntity.updateConfig({\n      minZoom: 0.1,\n      maxZoom: 2,\n    });\n    // 确保点的位置在线条的下方\n    this.grid.style.zIndex = '-1';\n    this.grid.style.position = 'relative';\n    this.node.appendChild(this.grid);\n    this.grid.className = 'gedit-grid-svg';\n\n    // 设置背景颜色\n    if (this.backgroundColor !== 'transparent') {\n      this.node.style.backgroundColor = this.backgroundColor;\n    }\n  }\n\n  /**\n   * 最小单元格大小\n   */\n  getScaleUnit(): BackgroundScaleUnit {\n    const { zoom } = this;\n\n    return {\n      realSize: this.gridSize, // 使用配置的网格大小\n      renderSize: Math.round(this.gridSize * zoom * 100) / 100, // 一个单元格渲染的大小值\n      zoom, // 缩放比\n    };\n  }\n\n  /**\n   * 绘制\n   */\n  autorun(): void {\n    const playgroundConfig = this.playgroundConfigEntity.config;\n    const scaleUnit = this.getScaleUnit();\n    const mod = scaleUnit.renderSize * 10;\n    const viewBoxWidth = playgroundConfig.width + mod * 2;\n    const viewBoxHeight = playgroundConfig.height + mod * 2;\n    const { scrollX } = playgroundConfig;\n    const { scrollY } = playgroundConfig;\n    const scrollXDelta = this.getScrollDelta(scrollX, mod);\n    const scrollYDelta = this.getScrollDelta(scrollY, mod);\n    domUtils.setStyle(this.node, {\n      left: scrollX - SCALE_WIDTH,\n      top: scrollY - SCALE_WIDTH,\n    });\n    this.drawGrid(scaleUnit, viewBoxWidth, viewBoxHeight);\n    // 设置网格\n    this.setSVGStyle(this.grid, {\n      width: viewBoxWidth,\n      height: viewBoxHeight,\n      left: SCALE_WIDTH - scrollXDelta - mod,\n      top: SCALE_WIDTH - scrollYDelta - mod,\n    });\n  }\n\n  /**\n   * 计算Logo位置\n   */\n  private calculateLogoPosition(\n    viewBoxWidth: number,\n    viewBoxHeight: number\n  ): { x: number; y: number } {\n    if (!this.logoConfig) return { x: 0, y: 0 };\n\n    const { position = 'center', offset = { x: 0, y: 0 } } = this.logoConfig;\n    const playgroundConfig = this.playgroundConfigEntity.config;\n    const scaleUnit = this.getScaleUnit();\n    const mod = scaleUnit.renderSize * 10;\n\n    // 计算SVG内的相对位置，使Logo相对于可视区域固定\n    const { scrollX, scrollY } = playgroundConfig;\n    const scrollXDelta = this.getScrollDelta(scrollX, mod);\n    const scrollYDelta = this.getScrollDelta(scrollY, mod);\n\n    // 可视区域的基准点（相对于SVG坐标系）\n    const visibleLeft = mod + scrollXDelta;\n    const visibleTop = mod + scrollYDelta;\n    const visibleCenterX = visibleLeft + playgroundConfig.width / 2;\n    const visibleCenterY = visibleTop + playgroundConfig.height / 2;\n\n    let x = 0,\n      y = 0;\n\n    switch (position) {\n      case 'center':\n        x = visibleCenterX;\n        y = visibleCenterY;\n        break;\n      case 'top-left':\n        x = visibleLeft + 100;\n        y = visibleTop + 100;\n        break;\n      case 'top-right':\n        x = visibleLeft + playgroundConfig.width - 100;\n        y = visibleTop + 100;\n        break;\n      case 'bottom-left':\n        x = visibleLeft + 100;\n        y = visibleTop + playgroundConfig.height - 100;\n        break;\n      case 'bottom-right':\n        x = visibleLeft + playgroundConfig.width - 100;\n        y = visibleTop + playgroundConfig.height - 100;\n        break;\n    }\n\n    return { x: x + offset.x, y: y + offset.y };\n  }\n\n  /**\n   * 获取Logo大小\n   */\n  private getLogoSize(): number {\n    if (!this.logoConfig) return 0;\n\n    const { size = 'medium' } = this.logoConfig;\n\n    if (typeof size === 'number') {\n      return size;\n    }\n\n    switch (size) {\n      case 'small':\n        return 24;\n      case 'medium':\n        return 48;\n      case 'large':\n        return 72;\n      default:\n        return 48;\n    }\n  }\n\n  /**\n   * 颜色工具函数：将十六进制颜色转换为RGB\n   */\n  private hexToRgb(hex: string): { r: number; g: number; b: number } | null {\n    const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n    return result\n      ? {\n          r: parseInt(result[1], 16),\n          g: parseInt(result[2], 16),\n          b: parseInt(result[3], 16),\n        }\n      : null;\n  }\n\n  /**\n   * 颜色工具函数：调整颜色亮度\n   */\n  private adjustBrightness(hex: string, percent: number): string {\n    const rgb = this.hexToRgb(hex);\n    if (!rgb) return hex;\n\n    const adjust = (value: number) => {\n      const adjusted = Math.round(value + (255 - value) * percent);\n      return Math.max(0, Math.min(255, adjusted));\n    };\n\n    return `#${adjust(rgb.r).toString(16).padStart(2, '0')}${adjust(rgb.g)\n      .toString(16)\n      .padStart(2, '0')}${adjust(rgb.b).toString(16).padStart(2, '0')}`;\n  }\n\n  /**\n   * 生成新拟态阴影滤镜\n   */\n  private generateNeumorphismFilter(\n    filterId: string,\n    lightShadow: string,\n    darkShadow: string,\n    offset: number,\n    blur: number,\n    intensity: number,\n    raised: boolean\n  ): string {\n    const lightOffset = raised ? -offset : offset;\n    const darkOffset = raised ? offset : -offset;\n\n    return `\n      <defs>\n        <filter id=\"${filterId}\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n          <feDropShadow dx=\"${lightOffset}\" dy=\"${lightOffset}\" stdDeviation=\"${blur}\" flood-color=\"${lightShadow}\" flood-opacity=\"${intensity}\"/>\n          <feDropShadow dx=\"${darkOffset}\" dy=\"${darkOffset}\" stdDeviation=\"${blur}\" flood-color=\"${darkShadow}\" flood-opacity=\"${intensity}\"/>\n        </filter>\n      </defs>`;\n  }\n\n  /**\n   * 绘制Logo SVG内容\n   */\n  private generateLogoSVG(viewBoxWidth: number, viewBoxHeight: number): string {\n    if (!this.logoConfig) return '';\n\n    const {\n      text,\n      imageUrl,\n      opacity = 0.1,\n      color = '#cccccc',\n      fontSize,\n      fontFamily = 'Arial, sans-serif',\n      fontWeight = 'normal',\n      neumorphism,\n    } = this.logoConfig;\n    const position = this.calculateLogoPosition(viewBoxWidth, viewBoxHeight);\n    const logoSize = this.getLogoSize();\n\n    let logoSVG = '';\n\n    if (imageUrl) {\n      // 图片Logo（暂不支持3D效果）\n      logoSVG = `\n        <image\n          href=\"${imageUrl}\"\n          x=\"${position.x - logoSize / 2}\"\n          y=\"${position.y - logoSize / 2}\"\n          width=\"${logoSize}\"\n          height=\"${logoSize}\"\n          opacity=\"${opacity}\"\n        />`;\n    } else if (text) {\n      // 文本Logo\n      const actualFontSize = fontSize ?? Math.max(logoSize / 2, 12);\n\n      // 检查是否启用新拟态效果\n      if (neumorphism?.enabled) {\n        const {\n          textColor,\n          lightShadowColor,\n          darkShadowColor,\n          shadowOffset = 6,\n          shadowBlur = 12,\n          intensity = 0.3,\n          raised = true,\n        } = neumorphism;\n\n        // 自动计算颜色（如果未提供）\n        const bgColor = this.backgroundColor !== 'transparent' ? this.backgroundColor : '#f0f0f0';\n        const finalTextColor = textColor || bgColor;\n        const finalLightShadow = lightShadowColor || this.adjustBrightness(bgColor, 0.2);\n        const finalDarkShadow = darkShadowColor || this.adjustBrightness(bgColor, -0.2);\n\n        const filterId = `neumorphism-${this._patternId}`;\n\n        // 添加新拟态滤镜定义\n        logoSVG += this.generateNeumorphismFilter(\n          filterId,\n          finalLightShadow,\n          finalDarkShadow,\n          shadowOffset,\n          shadowBlur,\n          intensity,\n          raised\n        );\n\n        // 创建新拟态文本\n        logoSVG += `\n          <text\n            x=\"${position.x}\"\n            y=\"${position.y}\"\n            font-family=\"${fontFamily}\"\n            font-size=\"${actualFontSize}\"\n            font-weight=\"${fontWeight}\"\n            fill=\"${finalTextColor}\"\n            opacity=\"${opacity}\"\n            text-anchor=\"middle\"\n            dominant-baseline=\"middle\"\n            filter=\"url(#${filterId})\"\n          >${text}</text>`;\n      } else {\n        // 普通文本（无3D效果）\n        logoSVG = `\n          <text\n            x=\"${position.x}\"\n            y=\"${position.y}\"\n            font-family=\"${fontFamily}\"\n            font-size=\"${actualFontSize}\"\n            font-weight=\"${fontWeight}\"\n            fill=\"${color}\"\n            opacity=\"${opacity}\"\n            text-anchor=\"middle\"\n            dominant-baseline=\"middle\"\n          >${text}</text>`;\n      }\n    }\n\n    return logoSVG;\n  }\n\n  /**\n   * 绘制网格\n   */\n  protected drawGrid(unit: BackgroundScaleUnit, viewBoxWidth: number, viewBoxHeight: number): void {\n    const minor = unit.renderSize;\n    if (!this.grid) {\n      return;\n    }\n    const patternSize = this.dotSize * this.zoom;\n\n    // 构建SVG内容，根据是否有背景颜色决定是否添加背景矩形\n    let svgContent = `<svg width=\"100%\" height=\"100%\">`;\n\n    // 如果设置了背景颜色，先绘制背景矩形\n    if (this.backgroundColor !== 'transparent') {\n      svgContent += `<rect width=\"100%\" height=\"100%\" fill=\"${this.backgroundColor}\"/>`;\n    }\n\n    // 添加点阵图案\n    // 构建圆圈属性，保持与原始实现的兼容性\n    const circleAttributes = [\n      `cx=\"${patternSize}\"`,\n      `cy=\"${patternSize}\"`,\n      `r=\"${patternSize}\"`,\n      `stroke=\"${this.dotColor}\"`,\n      // 只有当 dotFillColor 被明确设置且与 dotColor 不同时才添加 fill 属性\n      this.options.dotFillColor && this.dotFillColor !== this.dotColor\n        ? `fill=\"${this.dotFillColor}\"`\n        : '',\n      `fill-opacity=\"${this.dotOpacity}\"`,\n    ]\n      .filter(Boolean)\n      .join(' ');\n\n    svgContent += `\n      <pattern id=\"${this._patternId}\" width=\"${minor}\" height=\"${minor}\" patternUnits=\"userSpaceOnUse\">\n        <circle ${circleAttributes} />\n      </pattern>\n      <rect width=\"100%\" height=\"100%\" fill=\"url(#${this._patternId})\"/>`;\n\n    // 添加Logo\n    const logoSVG = this.generateLogoSVG(viewBoxWidth, viewBoxHeight);\n    if (logoSVG) {\n      svgContent += logoSVG;\n    }\n\n    svgContent += `</svg>`;\n\n    this.grid.innerHTML = svgContent;\n  }\n\n  protected setSVGStyle(\n    svgElement: HTMLElement | undefined,\n    style: { width: number; height: number; left: number; top: number }\n  ): void {\n    if (!svgElement) {\n      return;\n    }\n\n    svgElement.style.width = `${style.width}px`;\n    svgElement.style.height = `${style.height}px`;\n    svgElement.style.left = `${style.left}px`;\n    svgElement.style.top = `${style.top}px`;\n  }\n\n  /**\n   * 获取相对滚动距离\n   * @param realScroll\n   * @param mod\n   */\n  protected getScrollDelta(realScroll: number, mod: number): number {\n    // 正向滚动不用补差\n    if (realScroll >= 0) {\n      return realScroll % mod;\n    }\n    return mod - (Math.abs(realScroll) % mod);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/background-plugin/src/create-background-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { BackgroundConfig, BackgroundLayer, BackgroundLayerOptions } from './background-layer';\n\n/**\n * 点位背景插件\n */\nexport const createBackgroundPlugin = definePluginCreator<BackgroundLayerOptions>({\n  onBind: (bindConfig, opts) => {\n    bindConfig.bind(BackgroundConfig).toConstantValue(opts);\n  },\n  onInit: (ctx, opts) => {\n    ctx.playground.registerLayer(BackgroundLayer, opts);\n  },\n});\n"
  },
  {
    "path": "packages/plugins/background-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './background-layer';\nexport * from './create-background-plugin';\n"
  },
  {
    "path": "packages/plugins/background-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n}\n"
  },
  {
    "path": "packages/plugins/background-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/background-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/export-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/export-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/export-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"nanoid\": \"^5.0.9\",\n    \"modern-screenshot\": \"4.6.7\",\n    \"lodash-es\": \"^4.17.21\",\n    \"js-yaml\": \"^4.1.1\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\",\n    \"@types/js-yaml\": \"^4.0.9\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum FlowDownloadFormat {\n  JSON = 'json',\n  YAML = 'yaml',\n  PNG = 'png',\n  JPEG = 'jpeg',\n  SVG = 'svg',\n}\n\nexport const FlowImageFormats = [\n  FlowDownloadFormat.PNG,\n  FlowDownloadFormat.JPEG,\n  FlowDownloadFormat.SVG,\n];\n\nexport const FlowDataFormats = [FlowDownloadFormat.JSON, FlowDownloadFormat.YAML];\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/create-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { CreateDownloadPluginOptions } from './type';\nimport { WorkflowExportImageService } from './export-image-service';\nimport { FlowDownloadService } from './download-service';\n\nexport const createDownloadPlugin = definePluginCreator<CreateDownloadPluginOptions>({\n  onBind: ({ bind }) => {\n    bind(WorkflowExportImageService).toSelf().inSingletonScope();\n    bind(FlowDownloadService).toSelf().inSingletonScope();\n  },\n  onInit: (ctx: PluginContext, opts: CreateDownloadPluginOptions) => {\n    ctx.get(FlowDownloadService).init(opts);\n  },\n  onDispose: (ctx: PluginContext) => {\n    ctx.get(FlowDownloadService).dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/download-service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowDownloadService } from './service';\nexport { DownloadServiceOptions } from './type';\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/download-service/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport { nanoid } from 'nanoid';\nimport { inject, injectable } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport { FlowDocument } from '@flowgram.ai/document';\n\nimport type { DownloadServiceOptions, WorkflowDownloadParams } from './type';\nimport { WorkflowExportImageService } from '../export-image-service';\nimport { FlowDataFormats, FlowDownloadFormat, FlowImageFormats } from '../constant';\n\n@injectable()\nexport class FlowDownloadService {\n  @inject(FlowDocument) private readonly document: FlowDocument;\n\n  @inject(WorkflowExportImageService)\n  private readonly exportImageService: WorkflowExportImageService;\n\n  private toDispose: DisposableCollection = new DisposableCollection();\n\n  public downloading = false;\n\n  private onDownloadingChangeEmitter = new Emitter<boolean>();\n\n  private options: DownloadServiceOptions = {};\n\n  public onDownloadingChange = this.onDownloadingChangeEmitter.event;\n\n  public init(options?: Partial<DownloadServiceOptions>) {\n    this.options = options ?? {};\n    this.toDispose.push(this.onDownloadingChangeEmitter);\n  }\n\n  public dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  public async download(params: WorkflowDownloadParams): Promise<void> {\n    if (this.downloading) {\n      return;\n    }\n\n    const { format } = params;\n\n    if (FlowImageFormats.includes(format)) {\n      await this.handleImageDownload(format);\n    } else if (FlowDataFormats.includes(format)) {\n      await this.handleDataDownload(format);\n    }\n  }\n\n  public setDownloading(value: boolean) {\n    this.downloading = value;\n    this.onDownloadingChangeEmitter.fire(value);\n  }\n\n  private async handleImageDownload(format: FlowDownloadFormat): Promise<void> {\n    this.setDownloading(true);\n    try {\n      await this.downloadImage(format);\n    } finally {\n      this.setDownloading(false);\n    }\n  }\n\n  private async handleDataDownload(format: FlowDownloadFormat): Promise<void> {\n    this.setDownloading(true);\n    try {\n      await this.downloadData(format);\n    } finally {\n      this.setDownloading(false);\n    }\n  }\n\n  private async downloadData(format: FlowDownloadFormat): Promise<void> {\n    const json = this.document.toJSON();\n    const { content, mimeType } = await this.formatDataContent(json, format);\n\n    const blob = new Blob([content], { type: mimeType });\n    const url = URL.createObjectURL(blob);\n    const filename = this.getFileName(format);\n\n    this.downloadFile(url, filename);\n    URL.revokeObjectURL(url);\n  }\n\n  private async formatDataContent(\n    json: unknown,\n    format: FlowDownloadFormat\n  ): Promise<{ content: string; mimeType: string }> {\n    if (format === FlowDownloadFormat.YAML) {\n      const yaml = await import('js-yaml');\n      return {\n        content: yaml.dump(json, {\n          indent: 2,\n          lineWidth: -1,\n          noRefs: true,\n        }),\n        mimeType: 'application/x-yaml',\n      };\n    }\n\n    return {\n      content: JSON.stringify(json, null, 2),\n      mimeType: 'application/json',\n    };\n  }\n\n  private async downloadImage(format: FlowDownloadFormat): Promise<void> {\n    const imageUrl = await this.exportImageService.export({\n      format,\n      watermarkSVG: this.options.watermarkSVG,\n    });\n    if (!imageUrl) {\n      return;\n    }\n\n    const filename = this.getFileName(format);\n    this.downloadFile(imageUrl, filename);\n  }\n\n  private getFileName(format: FlowDownloadFormat): string {\n    if (this.options.getFilename) {\n      return this.options.getFilename(format);\n    }\n    return `flowgram-${nanoid(5)}.${format}`;\n  }\n\n  private downloadFile(href: string, filename: string): void {\n    const link = document.createElement('a');\n    link.href = href;\n    link.download = filename;\n    document.body.appendChild(link);\n    link.click();\n    document.body.removeChild(link);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/download-service/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDownloadFormat } from '../constant';\n\nexport interface WorkflowDownloadParams {\n  format: FlowDownloadFormat;\n}\n\nexport interface DownloadServiceOptions {\n  getFilename?: (format: FlowDownloadFormat) => string;\n  watermarkSVG?: string;\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/export-image-service/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst USER_AGENT = navigator?.userAgent ?? '';\nexport const IN_CHROME = USER_AGENT.includes('Chrome');\nexport const IN_SAFARI = USER_AGENT.includes('AppleWebKit') && !IN_CHROME;\nexport const IN_FIREFOX = USER_AGENT.includes('Firefox');\n\nexport const EXPORT_IMAGE_WATERMARK_SVG = `\n<svg width=\"201\" height=\"133\" viewBox=\"0 0 201 133\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n    <image id=\"-\" x=\"0\" y=\"0\" width=\"200\" height=\"133\" xlink:href=\"data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAMgAAACFCAYAAAAenrcsAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAyKADAAQAAAABAAAAhQAAAAA7OKFtAAAej0lEQVR4Ae1dCZhUxbWuurd7ZoABAiiuIBAElSSuCBGB2UBxS2LCAIPDMhBMNMa4PuOCDYm4EE1ATQIBZxhgZoDo0yAqyiysgmJ8LrihIIuKArINzNJ9q95/bk8P3TPdTS+3b9PfV/Vxu+pWnTrn1F91arvzcTjzC1lZLgfv1ednXPKxksnLUNTerzj6JJgwzr7hjFdJIf9ZvaBgS/RMrK4hef60ynGc81FSsgvBvW18EqRA/Z3g9xoX2pwKV/aX8fGLv/bILVvS2n697+dQ7Cpw64nHEQ9XzlgdOnKLpskXF+QOXRMPLyvquqTU2r21YQRjvIAxcRnjPDMuvjROGY1TVuXWxJw/9h+8zccPed4weOLSU53MsxiUw3x5Vsecsz92OeKYuWxZvmE170j4jXlk1WmGwasBx/mR0EdNw1mDJuWtFQ/nzY+6rkUVCl+vxoDRFoNdH4tYBrDBgFnhydAKywYPPhBQYNPLjE2buqQxTwnG6XUJE8nZ7Uf7X/GMi2PKIyGDiua3d3LPykQaB8nBjP3ovkzPvUg2Gybl2xEKZ65sZ3j4toQZBzVCsnTB+LzR06on2dGmljLGvbH6YqbpVchPiHGQPIyRa/V68c6E6uqMlvIT/T7zvffaOZmxIqHG4W3krHZvb/gdJU0DSZNt7sbgvTjRDWziPyNvcsV5NsnyipGSN9Y5KvAS53YqMq0FM2bTahUZtTVUtO3AlurfmIXi2xZHpk5Pw9CWRkZqIVV97d0w0QEWcgzNSrJZT23Y0Fv76R1L24DqntCU1pcYhvwf67mG5jhy2spOmAAStyS3Es3bCg+7q1V2AjM+X7XmJizLvRIoIpC1ZMOnbN7sDMxM3Fvx9u1YsaS941ST92rpBxrIIslI7Aucjc1yVcd1cIxGWafD2Tsaemto+SBr+ETGBcbxm8goLaNKP3bwaL5l3E7AaN+e3QOxM293AjJrizmboOm683RruUbATUpH+q69HSKgtITEEOwnljCKgglufbpGQW4FaScrmETDQ5f8ymjo46HlOj8rnvox1nVqwvCY55AYGcRczeOus++gLqUes6IxVsTNod242i2PDuzpMcITfTXBbW8fKZkUodGjo2ooBJKDgDKQ5OCupKYIAspAUqSjlJrJQUAZSHJwV1JTBAFlICnSUUrN5CCgDCQ5uCupKYKAMpAU6SilZnIQUAaSHNyV1BRBQBlIinSUUjM5CCgDSQ7uSmqKIKAMJEU6SqmZHASUgSQHdyU1RRBQBpIiHaXUTA4CykCSg7uSmiIIKANJkY5SaiYHAWUgycFdSU0RBJSBpEhHKTWTg4AykOTgrqSmCALKQFKko5SayUFAGUhycFdSUwQBZSAp0lFKzeQgoAwkObgrqSmCgDKQFOkopWZyEFAGkhzcldQUQUAZSIp0lFIzOQgoA0kO7kpqiiCgDCRFOkqpmRwElIEkB3clNUUQUAaSIh2l1EwOArb56ICjxUPwfrRS01i5kI6PO9c5DtrVZPhG/AiyXob8vnBLcG4C5R7E/+q+GvyX61z7MIFyWrFGG2fAkVYuZ3Ig/tf1RLbxewjfyLhcJwXf0EqRBGUYHvmilqEPZB5jOHww/grOdGxxacGzJywqaHL6mKCmmWzXnVLryIXzzsZEComE9+hpVbmCyRLQnh0JfcQ08HIr04zRy+4bdijiOgkiLKxcW8gMMQueIK3zGcK5mzOxoFfu0JvJuWWCVI+Y7V82bRiDfpwHHxoJdauXeAPh7LXq5wrgspexEbfNTq871vVcTYiBcA19EF713q7qtnUXc7kSAvjIpUt1Y2uHrrpbdzrrG46UPXZds2dWuIIml9QXRNwjYQjhAnrukqk5NxPJbbO3pu85tKuHg/GeHkN872DatnJX9r4w1eMqKly5sqvO0jp7hJTpvHHv/KuvphmeFa5e3ZM1ss1Ido5LgLeyR+ParQvyBs+lV3LgKevEWZ40Z7outdpe61ftdiWoD2dv3NihgRv94F3qJ1zjO4Wsf/fey7P3kB5/2bjmAsn195BM2E4owQYiv6suHms6s8wd90IX4ajfCq8rAbMaBtfCLkf0yVavLqOmV10opXwd4DV7esJss7CN1KaUuLLrXS6pfcSrvkN5FzwxB/Dccb7Uertc2Z5fulb10jlfC2Zn+jGUmAz+Z8nDOTP98uJOZlVXO7ob2jz4Xhzvzwx4Tu2lG4+6srM9hZWrc+DFfRV0xL84gmTPLhw+lLy+8vGvr7lCcFmN9HH/hJzt9jD3ZeV5ed/GIaVV1SfefnMQF4L6MGCVwHbyobv7X/EItu3yL2+9OVZKsahVZYsyEnpIl0z7vamny6UJvX53S+OgMgziwv3t3XMsao/JhrZR4Pt/eGk2DirA3rzwmCa/HulamuZyYZvA5XSzQjw/XB9JxjHysTc6wjhIpr9xEGeOc8kTo6avmhqPGP+6aBvv7tE+aGkcRIOy6ds8fDalF+YOrcKZ5CVKxxGO/sBz1HRICoPLhnGsA6/jxkGMJTvbIZ17xq3aFNdk46/jo2+v7QXjqEFegHEQDdr9p5mb1t9H6dqj9c/DUBKyAyH+iTMQzmtriscsISE5O8+dgQheSoMH9PeErEkVPwpeGl3ulCmbnejE/w1ZS8pOmtblFipfOjWPBtLhkLQRFAghPyYyrUEnIw/pghmdapnX26LKtefD2M8LpR4uIn5buHItOWcl6ywNRRdh/otPX3NNw0hyYyfYi2HryPpnw5ZHUeiQOukdeuvE2Z3EDitlPQx0YRSsoyINrUBUbIIQS3aoKZfDAODfOjBgK4Dew+bDjMwdwH+H3bzUoPzmMs1XrjE4cTTzOVzVmd7qiA7l2Bs386D379kBzj3wndfMJ1AuvWFwPYEt1mxzFcEWCVk/bk0VQQ6Xu5Y9nF3rpZSXtKxB+pibG2qexjuMfryqzpySKB9eE812IKY8k9aXptiXbqI16yGP6ngaUGryBB2JQFslLCEg6PIqvG/SpPwYoAYURfMCrrQqsrbV6y7CNB0wAZh9hTLiTiLwL39y1ZohUNF8pxirqldVFNK72Rzk+dJUt7mJJi32HcjwGLIrtZVqEw3xp6XjeOCnPLlpbZ+7Bgz+DKvmcpSPP15mXSpxBsKw+6WAUYghSN3dKlCbmwPntGwHLt1mIahMcPBiViCQmkAzk953+qWRbwJJ6XBBMuc77GVa0Y6B0zFsgcJRhy6T/IivEI1tE9AeKvCq5iOhGDK9VJg18Ip0s+jWxFSBBr7ZWn/mVNd8px9vuilFVbxBslMoITTPMcbSmjKjj7gU5gQghdHBp3soLtCB1DiDyklnn04Um8Gvc6i19JBxUJAow2GtqRa9n0gaaKTTNFiHxo8aAcbj5WnFb8IMBO3z+rSm242JZVVQNsdfYVg97ZePZ3E2SgrHGtZWYtPZlO81McYML5iynZ+bbBNhydoJPx4o1njaAMxB4bcCnNddKt+pXw7pqO3H9Lg6EabOpcM+rUTo3l3o0oCrY7N90N0bpEhPz+hZJ92N1B5mACHfuKV20pPm1+6mtptNJpw8Xi6krZGmTcPbFC8q9OuHQRMZ4+JzbzItrnOB5Jp5yaIZ8hvDN5qbZJh92JSmBUzgKtjhkAO4oeFOzew2b5Mw7XlnSK+maLkZHKR2k+p6U9vpleDJYBptz/sCV0QITZH3Ba+S1R+tq6MbLAbjgA/1xISEGQja0yn31xV9Kv81+jPNcOcL3Uk3HEFXEkwb78ttn71QU+PyDYOYWzvitldea39K+h4wCOn/HSv3H8xrSez92PSqHjELw4r3vqOqG+rvwFbvGYyKn4bihfn049L7rtwZqjya/ML16x9ixzwwkOAB2HtYmraMSrH+XNZqdAWvFjQXA38QFfR4c/VnXwzKWQdeVwYjNMevFHfNH5r1brDyaPOe3LjhCZwl54eqB8Oppps6l4S77bc2eC+DQhHHkY+xkrDAhUcsJu6VpeP3A8BJANe3JvgL/VZyxy+tMA5i+urT19AOfZS/AP80Zq9PDLGvhPJG/bn6bkSZlI41OAQ3B2rF1Jwy8P5HUD6cf6M5ZG7QshgyFw4atBcD9w+hqupSjls0ZMg3mATQv/KOUHSR5GMKyZ5QveE8mlAMzX0T6oS41OCbOrqPzY2EZyQ0dw284jnQBcUTZ59327bvlE98Mt98sw+iTpROREikgUBfeUnuxDICldUUFyzQNQ2zGZuDgfQBnjexYt8t69mPaubnf040VoWlD+au0aTWHzI2HecpaeV+/HCnhouWufIbR7qqM6UQ5lXhcZoYUlLcN3J61aVUEx8Lb8Ue71oMKrq73wf5n2B1fMhdJ35c/oCl3whk6fChszTJ6CD+Nh6awCGWV+LTwBULhmeV451tH5RNNz3nUzrWgDY4hIFreKy2Zbm5O9J5eh8I+zv41Xt5ym/JWNt0ajuYbrtilROs3j0DBtFtYy4wrIAeW3DAXIUpdmLt0YYrbu3Xr9a18ZUOUpOVwepalZfgD4Wmmga6bkp1yRiaEewO/MZHXjs9Q7TJTDPErhJ8ICQFrndtbtuGH6KBdYFFCn2vc/268qlZb1rELyo2IzdsaMM6djSW9evX6KtYuGr1JJgNzcBBLj58VFHFS+oPfDt2WX4+TTSMVqedF+e1e+5nVzZfVETFLU7ime+t7woTXQE2NOkmLNhhIKbymN1e1riY5el+JnZT2Z6EtegEjPOnV+VjNpwNstNOQBptMdrE52BH/PSyh3I+jbayVfQTV625wJDsQRyHR4MnJl7rAph9gHvm+0vzBtPANI8d1nGPjNOszZu7u0XDOBzSaXVM2NbKp41tBuITiLgBQH8lZMOlNSUTD/rlJyw5ylU9EH/3dSu6NAdCWn7pToTcr8D0raUP596YCObBeI57owbbHp6NUdsX5ZYaRhB5+2Afm/HB4qWFuUP+GaTc8qzHN635ucbMj7FdLWcehmGCzyBBJaejE3vpQgt+oxW0SpyZ3LgQxkFnITuMg5Q9C228ME6to6qO26pcyKSv64k2DtLrFIi5Gn/gcXlUSsZBrEudvnnYahykbjIMJA6YVFWFgL0IKAOxF28lLcUQUAaSYh2m1LUXAWUg9uKtpKUYAspAUqzDlLr2IqAMxF68lbQUQ0AZSIp1mFLXXgSUgdiLt5KWYggoA0mxDlPq2ouAMhB78VbSUgwBZSAp1mFKXXsRUAZiL95KWoohoAwkxTpMqWsvAspA7MVbSUsxBJSBpFiHKXXtRUAZiL14K2kphoAykBTrMKWuvQgoA7EXbyUtxRBQBpJiHabUtRcBZSD24q2kpRgCykBSrMOUuvYioAzEXryVtBRDQBlIinWYUtdeBJSB2Iu3kpZiCCgDSbEOU+rai4AyEHvxVtJSDAFlICnWYUpdexFQBmIv3kpaiiGgwWss/s9j+4PD2cY2ucloI/4HadvaZ/YePOfY34v2SYRbiaS0D9Yhv7evmT5JXGptM4/63hIdG1JsT7SMIPxtce3gkwuvuc3Oc3x5iY4xdr5OtIxm/pIdaE7bmNCkk78DeabXIPvkikrTl6BNAjNkG9Mbqk3ivGI4s8SZZcQ6S2l7G+HVtixi/eIkbJT6ZrAI5uMyTs5hqnNWpdXMLdgHp4gLw5BZXgSf2NMsZxqGYf2Wr/bDa4Z9Xp84M3RdPh1GJcuLuMdwganbcsahGe5fnDfko9DF1pY8MHDgt/BStsRaruG5wc01eUGFz2DJpiLaG57colLOnj/1iMNWX37LluUb2MOSM0hb9rFcyFlw2vm+RYhFxGbBNblfwC2ZXTM67TimRKSYhUSSuR8AO5u2Wnx5j8sHbTANZE3xmF3w95QH4bssbE9rVpytqD/iKKQB27owsTnLHsyFWzQ+GFJqEyuJ/et8lntPgmUEZd972JAiTALLgxZalIkZxgNvs9MXDhv6gkUsI2Zzz4Ch24VhXA0dvo24UgyEnLM3dIONyufcCHDXdUPRS+1r5bEZ8Cd8LRxd9gDvgPIYZBGLBsblh7hjebaqeEwJ3m2ZxUPpOtb1SgePlvE0lk/yW04u2SxoI8OFA3+XS/H4Elfey6Fk25U/7vWaO7GNvRXyelkokya1/6KdDy0cNmSlhXyjZvXUhg2dhS5nYsW8Gr1nlVs9A/w+Yjqfc2zF6/8gv/CkWMjBMXLk0rTaTNYxau39KjQaDlFZeuN+v6yTKnm9a3nbjLSO7eJSqvGYscx1FZb95Bp+sDYUrlzdk+uinyF4XK6gnZqjVrD6/5bm5Z10ffno2rWdMjLi83Zb73GIgQPrd2fz5HlfDtZ/Kk8hoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQkAhoBBQCCgEFAIKAYWAQuAkQ4BnFS3Jg3/EsN5uuxzRKmPx6ZFVVHalxrS2OpMfvvHcmET6s+NZkysGMCEGaYyfKgSrg2/SDzMd7V9dPvf6Y/FgnjdlaXfhEecxKTxVxQVVLXnlTFo8hEk9I1gbB09ZeIbT4/xxqLoteZ3oPWtSxY80KfLw3/SfAW9Ljfi/+b9AO1+tnFcQt7+MvMkV5wO3bi114EJIg8m9rMeZH9a4rP3fz0eMXdShId0xkGTGOsZa6kvvuUWLsyXTnYKzr2vmj/4wGI0vD/3bEf07gN4POQ5VvzP35gAvXQ4ujVfgsCPsf49f36kduQiIeqDBsWSxZKK3h8vxqF9KSlgdciZWXCK5mMsMcSnxhmNJ+HcxE6zWfeRwTlHZg1XPFcTsDs1oaOzGNA3+MKSbuVwZeEy/Ed52SDSxYgUMINMj5bPI+5033/vrdDvuRPvvxtsXeHr7l0WTzp1cdpo0+DwpxHU+5yrUTvMfRm92UdkzUjTcW1MysT4avv60sIPfw2fKb/zzKA1fMbBD/Oz4Zn/2xPLp1cVjZrekifW9IU17Ukoxmervby/uQPS3WHn51xOCP8+46MQFOzC8sLTb6wvHhXQYa7g9s1CXxidLMzp1QRTg1NZ/5fgPChcEezIO1HmQf9KF7KKFF2IAVmGgwDjkTvQjDIHfDguZCmVX4emA2XZ2TlH5Y7Eq79EaP/PW5c7sXX1H+PPJmVieDUdDmWYe57+A8fjjicElb6AyDK+3/OtFkx488blThcHWwyCuQz3ynDsHbb0d8f0YuHDWgxZKdhtnac94h3M03IPSfolcv3Egy9AAcvSKwSP/ljux7KagtaLMpNUDU9kkXzW04rFLp8wJO1H7aCOOOevUmOYYFoo+a8LS01FmGkcoGoevQDPYQ5WlBbb61fPJjjmWOjl17AjfNZVOt/GzljNF9oTyApQtxsx4d9aEspKakoJPopW1trhoL2bo9zEIf8KEvBP1VxznwX+LQeN7PTN323m9KxkzDWpYUfmZWFX6UCGX/B8+omhjB8uYiTo/BJPtUjReglUiwL109iS0UchSTAqTcicufqqyGF6S4gtvVRcXTGjJAqsHGeYUIdk9KFvUsjzad6wel3kNmu+B68jOQDG9Y0PH7uBDq61lAW6NHsTE9Z/Alb+JPXf/ypy+wkgLmPHC0J10RTlFFTSb98VTJ4XzppbGQQpXl4wpw3arAkkd8YuUF1uQT5r1OLvcvz461XzHCmE6JTUcRn9fucfwYACYYX9lyZi1vvxo4pwpFT8EPc1wHifXb2hpHMSrev4YctxJLpL3SMmxkiYoCEY7DBpPHeKWAEWxdXuC+OD8ew+M/1FKS108TrF1QQraXWTtPi+g34j/9VPmtMUKPAN957dlbi05ZQ0EO4tbmppTXlOSv6d107w5kjlc3hQ7pymOOoJxrW6qlOGr7HLRxYbsivcjnGkzKB+9fnw51/RBTbQfNsXRR24xhCqhEz9+PcxhEzP+QDxnVJWMXRi9kAhraIKMFUEG7NG9edH9Dhm3sAcNXPA6VllcsEhs2/pnpGkw/zJrQvEPouMWjlozJ0V4HfaNlWbiI+52OXhpj63d582ZQRLNWyw4RSzOmVjW6jCDDe6R6uKx1wapm9QszN7mjQsGz7pwijjTHAfcDeYRKub9bZfDzm/2ZbrdOFQ4ce64As5IN6z+sgwOJDkZzILB53zyypodfY+hk0e4cA7BQ7PSVV69+Evh9AtbxnlfOmJg5OwOpJM8d1xp58A8xozMNg01f8+vbZkfzTvOBe1zf11hbg2pnhvXQZqQOdDjr3htkLp2VzT8gtHqDufPgRWKuLkdralxebJ7lsHPI51z0ulW67Vg9aLOE2IG03gedC/Mmrzo/pp5N3lxRB/xHXwa+Emu8fuwBX8hFG+/FYRfBGMY1PLBIGi1PIViZm++9A54KcMOCO72mNYB3WBLsQVccTdyrpVQbYB5P8Wc8z9QrEm+mAxCMLEFXd513Ze9+2WNKTsFRRfiEWmavoDoYgnoi/ZUD7ID2nj9lJfbCN25r+XD64xnYpHjXwfngRG49vzU9+iG+AhXdcR3L846g2rmjanxp4827T2ISxqc1CHNxgY8/2jmcfYsQI65rwL00XUPGJlnRk3wyb6ynG3nXgbpl+B9pxD1r/ryg8UOXyYO6RdXlo593/d+8seykSAWnLeaSf11bzBYmnnty3HpF0fARIpziOfXEHkNPADr+5iHAMbGQN9GMTr4bUyK/QW2VrwN+xqdTNkfrZyfH/uWRLJDxARSTEPxpun3azyZR46/mytZzCvkcT5IcbYP7TBn9qb8Dtir94KxnobZuArXpmcGO+8F8Ajz0tHdoX+T4X/fztl+4/VTlrdtIl+Ma/mnke6VM2FJryqLDuu4KHlYZ3w0VsbbYZyP0HcOqXNzy4XJYKHZS2H0bTaQMDQna9GnUOwiDH7a9+OGJXgQrPFUHUdBDNiAD0DBqUPnZjQ0flOfZi64fPfZLC3joHlYra0qHUmjFecP9jzAvgVycvHqNRrGNobmeOIS8PvInEql7O5PvXzuzdjOHT8sZxctLsVmodCfJua0ZFU4z4zyr29OCJme7cjr5nE4H0N8m395NGmswLfACCl0hkG02tJTgdTEWETTKR1vWFMyditu4FaDa1ZHd/vxOeOWviqZZzx0qK2aXzA1a2JJejgZfluscGQnX5muad4bD8lu9N8zt9QUdMVNeXGtjq8uvukw+OwgXm0Oex5A5MTM+k/TNPCCc0k1oloM6mGYIbORxqqCbwhxBCfnmEgp8POzxy++1Ju2/5f+igKz7VySjLZdFKsGWH3oUqOgqf5XGKS7Ax7cxDWVPYTDOp3vLAkY5M+ajDi7ReieCZTGhPYGdZCZH+YnZQ1k1fwx76K33qK2Yr9cCUBPD2yn5Lh0+BtoLsWE1SjbOPICy6N/w178IaqF+5b7KdakXkqxN5hgv4N0RxhOf8TfV3bfipkr9kB/noPdOM3YHBfVbwyfXNozODd+avB863Ix85urIji22O5FLsOjO69CS2BrcjtWqbOrnyvoFvDgJg6ldFh3OLR0nBOsCZXnfEqH8G3Y9V6MAT+VuArNcW8k3O3ZYkn+ZPbEsmnBFMKM9O+a4rH3BCs7UZ5kba7irI5Whm6cp2/DB71dMAj6ot6JyfLumB5OhXEIDLKCeG93SJejbvlS2zRwpAGLIIXD3F5R2gycr0fnD6U0CNYH/TjlpYz8VzRMkzx9EMxvsNtwbM0uKt9JAwwMavH8EOPtDAzezsQQMvdSnIiAv6nbK7yMfxArf/SD91DO2fJQPNDOzei3YQYzP0iuC0UXVT7dKhaVzwduj4B3GlaO9TXz88Ne7/r427WCnAKBPYI+klFZTKGm5BcH0zTHRRgZ/8LwyIBx9AEjXOvRHTujWfULTeOXYZZ6PiYBLSp1b0w76svCYPyucsEvAg7g6NwVvnJ8JbHkqtL8+yrZMBzmeCd40+zbE3EOnhvw9MMEA+OQdFbJFeecbt4EId/yYDDHB2CK8cXOzptUfnG0Arx1JN3sGdLJ/xSqPlbnO6gMWF6Hq9mzQ9FFmy9Fxt9Rp4HqaUI8E2l9hyFlPzqlODru3xlppUjppNSGOZkIe7siHPxwpPyC0TXdEk3B4WsaczT2w7VWR6lpAILvGNr9kw+avkkEqxp1Hu3Fc8ZX9Na5oRnCeazlHrZLrb7pYDs3GSnL0Gu/ilpAiApNf4T41xG3LZpfX6thm+DdUkluHBY8bVvNvJFftNQlBKuQ2Q63+2Gp60/VewStTK0CfYzNmlDei/oTK9mRVgQnyKiTxz5vKzP6YOIwVs0duy8UefWCgi1Zk5ae6zDc2DqfFZIuVH3K51LrrzNDc2R+1zymaTLFX1731XGr6f5yK63AZqgpmdAwbEKZ2WdnH/bdGvpKGft/NV7P5Q0myzAAAAAASUVORK5CYII=\"/>\n</svg>\n`;\n\n/**\n * safari/firefox 中指定样式表\n * 1. 样式计算会导致所有css变量被生成到dom元素中，导致svg过大无法正常序列化\n * 2. boxShadow 渲染效果有问题排除掉\n */\nexport const EXPORT_IMAGE_STYLE_PROPERTIES = [\n  'width',\n  'height',\n  'box-sizing',\n  'display',\n  'align-items',\n  'justify-content',\n  'font-size',\n  'gap',\n  'color',\n  'background',\n  'background-color',\n  'font',\n  'font-family',\n  'fill',\n  'stroke',\n  'stroke-width',\n  'margin',\n  'padding',\n  'padding-left',\n  'padding-top',\n  'padding-bottom',\n  'padding-right',\n  'flex-direction',\n  'filter',\n  'position',\n  'top',\n  'left',\n  'bottom',\n  'right',\n  'content',\n  'line-height',\n  'text-decoration',\n  'border-radius',\n  'opacity',\n  'border-right',\n  'border-left',\n  'border-width',\n  'border-style',\n  'border-color',\n  'margin-right',\n  'margin-left',\n  'margin-top',\n  'margin-bottom',\n  'white-space',\n  'overflow',\n  'text-overflow',\n  'font-weight',\n  'min-width',\n  'min-height',\n  'transform',\n  'z-index',\n  'flex',\n  'border-width',\n  'text-wrap',\n  'word-break',\n  'vertical-align',\n  'aspect-ratio',\n  'object-fit',\n  'align-content',\n  'align-self',\n  'background-attachment',\n  'background-clip',\n  'background-image',\n  'background-origin',\n  'background-repeat',\n  'background-size',\n  'block-size',\n  'border-block-end-color',\n  'border-block-end-style',\n  'border-block-end-width',\n  'border-block-start-color',\n  'border-block-start-style',\n  'border-block-start-width',\n  'border-bottom-color',\n  'border-bottom-style',\n  'border-bottom-width',\n  'border-bottom-left-radius',\n  'border-bottom-right-radius',\n  'border-end-end-radius',\n  'border-end-start-radius',\n  'border-inline-end-color',\n  'border-inline-end-style',\n  'border-inline-end-width',\n  'border-inline-start-color',\n  'border-inline-start-style',\n  'border-inline-start-width',\n  'border-right-color',\n  'border-right-style',\n  'border-right-width',\n  'border-start-end-radius',\n  'border-start-start-radius',\n  'border-top-color',\n  'border-top-style',\n  'border-top-width',\n  'border-top-left-radius',\n  'border-top-right-radius',\n  'flex-basis',\n  'flex-grow',\n  'flex-shrink',\n  'flex-wrap',\n  'font-kerning',\n  'font-palette',\n  'font-stretch',\n  'font-style',\n  'inline-size',\n  'inset-block-end',\n  'inset-block-start',\n  'inset-inline-end',\n  'inset-inline-start',\n  'justify-items',\n  'justify-self',\n  'line-break',\n  'margin-trim',\n  'margin-inline-end',\n  'margin-inline-start',\n  'min-block-size',\n  'min-inline-size',\n  'overflow-wrap',\n  'overflow-x',\n  'overflow-y',\n  'perspective-origin',\n  'transform-box',\n  'transform-origin',\n  'transform-style',\n  'grid-template',\n  'grid-template-rows',\n  'grid-template-columns',\n  'grid-template-areas',\n];\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/export-image-service/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowExportImageService as WorkflowExportImageService } from './service';\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/export-image-service/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { FlowDocument } from '@flowgram.ai/document';\n\nimport { getWorkflowRect } from './utils';\nimport { type IFlowExportImageService, type ExportImageOptions } from './type';\nimport {\n  IN_SAFARI,\n  IN_FIREFOX,\n  EXPORT_IMAGE_WATERMARK_SVG,\n  EXPORT_IMAGE_STYLE_PROPERTIES,\n} from './constant';\nimport { FlowDownloadFormat } from '../constant';\n\nconst PADDING_X = 58;\nconst PADDING_Y = 138;\n\n@injectable()\nexport class FlowExportImageService implements IFlowExportImageService {\n  private modernScreenshot: any;\n\n  @inject(FlowDocument)\n  private document: FlowDocument;\n\n  public async export(options: ExportImageOptions): Promise<string | undefined> {\n    try {\n      const imgUrl = await this.doExport(options);\n      return imgUrl;\n    } catch (e) {\n      console.error('Export image failed:', e);\n      return;\n    }\n  }\n\n  private async loadModernScreenshot() {\n    if (this.modernScreenshot) {\n      return this.modernScreenshot;\n    }\n\n    const modernScreenshot = await import('modern-screenshot');\n    this.modernScreenshot = modernScreenshot;\n  }\n\n  private async doExport(exportOptions: ExportImageOptions): Promise<string | undefined> {\n    if (this.document.layout.name.includes('fixed-layout')) {\n      return await this.doFixedExport(exportOptions);\n    }\n    return await this.doFreeExport(exportOptions);\n  }\n\n  private async doFreeExport(exportOptions: ExportImageOptions): Promise<string | undefined> {\n    const { format } = exportOptions;\n    // const el = this.stackingContextManager.node as HTMLElement;\n    const renderLayer = window.document.querySelector('.gedit-flow-render-layer') as HTMLElement;\n\n    if (!renderLayer) {\n      return;\n    }\n\n    const { width, height, x, y } = getWorkflowRect(this.document);\n\n    await this.loadModernScreenshot();\n    const { domToPng, domToForeignObjectSvg, domToJpeg } = this.modernScreenshot;\n    let imgUrl: string;\n    const options = {\n      scale: 2,\n      includeStyleProperties: IN_SAFARI || IN_FIREFOX ? EXPORT_IMAGE_STYLE_PROPERTIES : undefined,\n      width: width + PADDING_X * 2,\n      height: height + PADDING_Y * 2,\n      onCloneEachNode: (cloned: HTMLElement) => {\n        this.handleFreeClone(cloned, { width, height, x, y, options: exportOptions });\n      },\n    };\n    switch (format) {\n      case FlowDownloadFormat.PNG:\n        imgUrl = await domToPng(renderLayer, options);\n        break;\n      case FlowDownloadFormat.SVG: {\n        const svg = await domToForeignObjectSvg(renderLayer, options);\n        imgUrl = await this.svgToDataURL(svg);\n        break;\n      }\n      case FlowDownloadFormat.JPEG:\n        imgUrl = await domToJpeg(renderLayer, options);\n        break;\n      default:\n        imgUrl = await domToPng(renderLayer, options);\n    }\n    return imgUrl;\n  }\n\n  private async doFixedExport(exportOptions: ExportImageOptions): Promise<string | undefined> {\n    const { format } = exportOptions;\n\n    const el = window.document.querySelector('.gedit-flow-nodes-layer') as HTMLElement;\n\n    if (!el) {\n      return;\n    }\n\n    const { width, height, x, y } = getWorkflowRect(this.document);\n    await this.loadModernScreenshot();\n    const { domToPng, domToForeignObjectSvg, domToJpeg } = this.modernScreenshot;\n    let imgUrl: string;\n    const options = {\n      scale: 2,\n      includeStyleProperties: IN_SAFARI || IN_FIREFOX ? EXPORT_IMAGE_STYLE_PROPERTIES : undefined,\n      width: width + PADDING_X * 2,\n      height: height + PADDING_Y * 2,\n      onCloneEachNode: (cloned: HTMLElement) => {\n        this.handleFixedClone(cloned, { width, height, x, y, options: exportOptions });\n      },\n    };\n    switch (format) {\n      case FlowDownloadFormat.PNG:\n        imgUrl = await domToPng(el, options);\n        break;\n      case FlowDownloadFormat.SVG: {\n        const svg = await domToForeignObjectSvg(el, options);\n        imgUrl = await this.svgToDataURL(svg);\n        break;\n      }\n      case FlowDownloadFormat.JPEG:\n        imgUrl = await domToJpeg(el, options);\n        break;\n      default:\n        imgUrl = await domToPng(el, options);\n    }\n    return imgUrl;\n  }\n\n  private async svgToDataURL(svg: SVGElement): Promise<string> {\n    return Promise.resolve()\n      .then(() => new XMLSerializer().serializeToString(svg))\n      .then(encodeURIComponent)\n      .then((html) => `data:image/svg+xml;charset=utf-8,${html}`);\n  }\n\n  // 处理克隆节点\n  private handleFreeClone(\n    cloned: HTMLElement,\n    {\n      width,\n      height,\n      x,\n      y,\n      options,\n    }: { width: number; height: number; x: number; y: number; options: ExportImageOptions }\n  ) {\n    if (\n      cloned?.classList?.contains('gedit-flow-activity-node') ||\n      cloned?.classList?.contains('gedit-flow-activity-line')\n    ) {\n      this.handlePosition(cloned, x, y);\n    }\n\n    if (cloned?.classList?.contains('gedit-flow-render-layer')) {\n      this.handleCanvas(cloned, width, height, options);\n    }\n\n    this.handleTextareaValue(cloned);\n  }\n\n  private handleTextareaValue(cloned: HTMLElement) {\n    if (cloned.tagName !== 'TEXTAREA') {\n      return;\n    }\n    const textarea = cloned as HTMLTextAreaElement;\n    const value = textarea.getAttribute('value') || textarea.value || '';\n    textarea.textContent = value;\n  }\n\n  // 处理克隆节点\n  private handleFixedClone(\n    cloned: HTMLElement,\n    {\n      width,\n      height,\n      x,\n      y,\n      options,\n    }: { width: number; height: number; x: number; y: number; options: ExportImageOptions }\n  ) {\n    if (\n      cloned?.classList?.contains('gedit-flow-activity-node') ||\n      cloned?.classList?.contains('gedit-flow-activity-line')\n    ) {\n      this.handlePosition(cloned, x, y);\n    }\n\n    if (cloned?.classList?.contains('gedit-flow-nodes-layer')) {\n      const linesLayer = window.document\n        .querySelector('.gedit-flow-lines-layer')\n        ?.cloneNode(true) as HTMLElement;\n      this.handleLines(linesLayer, width, height);\n      cloned.appendChild(linesLayer);\n      this.handleCanvas(cloned, width, height, options);\n    }\n\n    this.handleTextareaValue(cloned);\n  }\n\n  // 处理节点位置\n  private handlePosition(cloned: HTMLElement, x: number, y: number) {\n    cloned.style.transform = `translate(${-x + PADDING_X}px, ${-y + PADDING_Y}px)`;\n  }\n\n  // 处理画布\n  private handleLines(cloned: HTMLElement, width: number, height: number) {\n    cloned.style.position = 'absolute';\n    cloned.style.width = `${width}px`;\n    cloned.style.height = `${height}px`;\n    cloned.style.left = `${width / 2 - PADDING_X}px`;\n    cloned.style.top = `${PADDING_Y}px`;\n    cloned.style.transform = 'none';\n    cloned.style.backgroundColor = 'transparent';\n    cloned.querySelector('.flow-lines-container')!.setAttribute('viewBox', `0 0 1000 1000`);\n  }\n\n  // 处理画布\n  private handleCanvas(\n    cloned: HTMLElement,\n    width: number,\n    height: number,\n    options: ExportImageOptions\n  ) {\n    cloned.style.width = `${width + PADDING_X * 2}px`;\n    cloned.style.height = `${height + PADDING_Y * 2}px`;\n    cloned.style.transform = 'none';\n    cloned.style.backgroundColor = '#ECECEE';\n    this.handleWaterMark(cloned, options);\n  }\n\n  // 添加水印节点\n  private handleWaterMark(element: HTMLElement, options: ExportImageOptions) {\n    const watermarkNode = document.createElement('div');\n    // 水印svg\n    watermarkNode.innerHTML = options?.watermarkSVG ?? EXPORT_IMAGE_WATERMARK_SVG;\n    watermarkNode.style.position = 'absolute';\n    watermarkNode.style.bottom = '32px';\n    watermarkNode.style.right = '32px';\n    watermarkNode.style.zIndex = '999999';\n    element.appendChild(watermarkNode);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/export-image-service/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDownloadFormat } from '../constant';\n\n/**\n * 导出图片服务\n */\nexport interface IFlowExportImageService {\n  /**\n   * 导出\n   */\n  export: (options: ExportImageOptions) => Promise<string | undefined>;\n}\n\n/**\n * 导出图片选项\n */\nexport interface ExportImageOptions {\n  /**\n   * 导出的格式\n   */\n  format: FlowDownloadFormat;\n  watermarkSVG?: string;\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/export-image-service/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocument, FlowNodeEntity } from '@flowgram.ai/document';\nimport { TransformData } from '@flowgram.ai/core';\n\nconst getNodesRect = (nodes: FlowNodeEntity[]) => {\n  const rects = nodes\n    .map((node) => node.getData<TransformData>(TransformData)?.bounds)\n    .filter(Boolean);\n  const x1 = Math.min(...rects.map((rect) => rect.x));\n  const x2 = Math.max(...rects.map((rect) => rect.x + rect.width));\n  const y1 = Math.min(...rects.map((rect) => rect.y));\n  const y2 = Math.max(...rects.map((rect) => rect.y + rect.height));\n\n  const width = x2 - x1;\n  const height = y2 - y1;\n\n  return {\n    width,\n    height,\n    x: x1,\n    y: y1,\n  };\n};\n\n/**\n * 获取流程所有节点矩形坐标\n */\nexport const getWorkflowRect = (document: FlowDocument) => getNodesRect(document.getAllNodes());\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createDownloadPlugin } from './create-plugin';\nexport { FlowDownloadService, type DownloadServiceOptions } from './download-service';\nexport { type CreateDownloadPluginOptions } from './type';\nexport { FlowDownloadFormat } from './constant';\n"
  },
  {
    "path": "packages/plugins/export-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DownloadServiceOptions } from './download-service';\n\nexport interface CreateDownloadPluginOptions extends Partial<DownloadServiceOptions> {}\n"
  },
  {
    "path": "packages/plugins/export-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/export-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/export-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/fixed-drag-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/src/create-fixed-drag-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Xor } from '@flowgram.ai/utils';\nimport { FlowDragLayer } from '@flowgram.ai/renderer';\nimport { FlowNodeEntity, FlowNodeJSON } from '@flowgram.ai/document';\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\n// import { SelectorBounds } from './selector-bounds';\n\nexport interface FixDragPluginOptions<CTX extends PluginContext = PluginContext> {\n  enable?: boolean;\n  /**\n   * Callback when drag drop\n   */\n  onDrop?: (ctx: CTX, dropData: { dragNodes: FlowNodeEntity[]; dropNode: FlowNodeEntity }) => void;\n  /**\n   * Check can drop\n   * @param ctx\n   * @param dropData\n   */\n  canDrop?: (\n    ctx: CTX,\n    dropData: {\n      dropNode: FlowNodeEntity;\n      isBranch?: boolean;\n    } & Xor<\n      {\n        dragNodes: FlowNodeEntity[];\n      },\n      {\n        dragJSON: FlowNodeJSON;\n      }\n    >\n  ) => boolean;\n}\n\nexport const createFixedDragPlugin = definePluginCreator<FixDragPluginOptions<any>>({\n  onInit(ctx, opts): void {\n    // 默认可用，所以强制判断 false\n    if (opts.enable !== false) {\n      ctx.playground.registerLayer(FlowDragLayer, {\n        onDrop: opts.onDrop ? opts.onDrop.bind(null, ctx) : undefined,\n        canDrop: opts.canDrop ? opts.canDrop.bind(null, ctx) : undefined,\n      });\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/src/hooks/use-start-drag-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { FlowDragLayer } from '@flowgram.ai/renderer';\nimport { usePlayground } from '@flowgram.ai/core';\n\nexport function useStartDragNode() {\n  const playground = usePlayground();\n\n  const dragLayer = playground.getLayer(FlowDragLayer);\n\n  return useMemo(\n    () => ({\n      startDrag: dragLayer ? dragLayer.startDrag.bind(dragLayer) : (e: any) => {},\n      dragOffset: dragLayer\n        ? dragLayer.dragOffset\n        : {\n            x: 0,\n            y: 0,\n          },\n    }),\n    [dragLayer]\n  );\n}\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-fixed-drag-plugin';\nexport { useStartDragNode } from './hooks/use-start-drag-node';\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/fixed-drag-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/fixed-history-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/create-fixed-history-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport { bindContributions } from '@flowgram.ai/utils';\nimport { HistoryContainerModule, OperationService } from '@flowgram.ai/history';\nimport { OperationContribution } from '@flowgram.ai/history';\nimport { FlowOperationBaseService } from '@flowgram.ai/document';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { FixedHistoryPluginOptions } from './types';\nimport { FixedHistoryService } from './services/fixed-history-service';\nimport { FixedHistoryOperationService } from './services/fixed-history-operation-service';\nimport { FixedHistoryFormDataService } from './services';\nimport { FixedHistoryRegisters } from './fixed-history-registers';\nimport { FixedHistoryConfig } from './fixed-history-config';\n\nexport function registerHistory(bind: interfaces.Bind, rebind: interfaces.Rebind) {\n  bindContributions(bind, FixedHistoryRegisters, [OperationContribution]);\n  bind(FixedHistoryService).toSelf().inSingletonScope();\n  bind(FixedHistoryFormDataService).toSelf().inSingletonScope();\n  bind(FixedHistoryConfig).toSelf().inSingletonScope();\n  rebind(FlowOperationBaseService).to(FixedHistoryOperationService).inSingletonScope();\n}\n\nexport const createFixedHistoryPlugin = definePluginCreator<FixedHistoryPluginOptions<any>>({\n  onBind: ({ bind, rebind }) => {\n    registerHistory(bind, rebind);\n  },\n  onInit(ctx, opts): void {\n    const fixedHistoryService = ctx.get<FixedHistoryService>(FixedHistoryService);\n    fixedHistoryService.setSource(ctx);\n    const document = ctx.get<FlowDocument>(FlowDocument);\n\n    if (opts?.uri) {\n      fixedHistoryService.historyService.context.uri = opts.uri;\n    }\n\n    if (opts?.getDocumentJSON) {\n      fixedHistoryService.historyService.config.getSnapshot = opts.getDocumentJSON(ctx);\n    } else {\n      fixedHistoryService.historyService.config.getSnapshot = () => document.toJSON();\n    }\n\n    const config = fixedHistoryService.config;\n    config.init(ctx, opts);\n\n    if (opts?.operationMetas) {\n      fixedHistoryService.registerOperationMetas(opts.operationMetas);\n    }\n\n    if (opts.onApply) {\n      ctx.get(OperationService).onApply(opts.onApply.bind(null, ctx));\n    }\n  },\n  containerModules: [HistoryContainerModule],\n});\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/fixed-history-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { FlowNodeJSON } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport {\n  FixedHistoryPluginOptions,\n  GetBlockLabel,\n  GetNodeLabel,\n  GetNodeLabelById,\n  GetNodeURI,\n  NodeToJson,\n} from './types';\n\n@injectable()\nexport class FixedHistoryConfig {\n  init(ctx: PluginContext, options: FixedHistoryPluginOptions) {\n    if (options.nodeToJSON) {\n      this.nodeToJSON = options.nodeToJSON(ctx);\n    }\n\n    if (options.getNodeLabelById) {\n      this.getNodeLabelById = options.getNodeLabelById(ctx);\n    }\n\n    if (options.getNodeLabel) {\n      this.getNodeLabel = options.getNodeLabel(ctx);\n    }\n\n    if (options.getBlockLabel) {\n      this.getBlockLabel = options.getBlockLabel(ctx);\n    }\n\n    if (options.getNodeURI) {\n      this.getNodeURI = options.getNodeURI(ctx);\n    }\n  }\n\n  nodeToJSON: NodeToJson = (node: FlowNodeEntity) => node.toJSON();\n\n  getNodeLabelById: GetNodeLabelById = (id: string) => id;\n\n  getNodeLabel: GetNodeLabel = (node: FlowNodeJSON) => node.id;\n\n  getBlockLabel: GetBlockLabel = (node: FlowNodeJSON) => node.id;\n\n  getNodeURI: GetNodeURI = (id: string) => `node:${id}`;\n\n  getParentName(parentId?: string) {\n    return parentId ? this.getNodeLabelById(parentId) : 'root';\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/fixed-history-registers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { OperationContribution, OperationRegistry } from '@flowgram.ai/history';\n\nimport { operationMetas } from './operation-metas';\n\n@injectable()\nexport class FixedHistoryRegisters implements OperationContribution {\n  registerOperationMeta(operationRegistry: OperationRegistry): void {\n    operationMetas.forEach(operationMeta => {\n      operationRegistry.registerOperationMeta(operationMeta);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createFixedHistoryPlugin } from './create-fixed-history-plugin';\nexport * from './types';\nexport * from './services';\nexport * from '@flowgram.ai/history';\nexport * from './fixed-history-config';\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/add-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { AddOrDeleteBlockValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addBlockOperationMeta: OperationMeta<AddOrDeleteBlockValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.addBlock,\n  inverse: op => ({ ...op, type: OperationType.deleteBlock }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Create ${config.getBlockLabel(value.blockData)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const branchName = config.getBlockLabel(value.blockData);\n    const targetName = config.getNodeLabelById(value.targetId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Create branch ${branchName} in ${targetName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/add-child-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, OperationType } from '@flowgram.ai/document';\nimport { AddOrDeleteChildNodeValue } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addChildNodeOperationMeta: OperationMeta<\n  AddOrDeleteChildNodeValue,\n  PluginContext,\n  FlowNodeEntity\n> = {\n  ...baseOperationMeta,\n  type: OperationType.addChildNode,\n  inverse: op => ({ ...op, type: OperationType.deleteChildNode }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Create ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const nodeName = config.getNodeLabel(value.data);\n    const parentName = config.getParentName(value.parentId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Create ${value.data.type} node ${nodeName} in ${parentName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/add-from-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type AddOrDeleteFromNodeOperationValue,\n  type FlowNodeEntity,\n  OperationType,\n} from '@flowgram.ai/document';\nimport { type PluginContext } from '@flowgram.ai/core';\nimport { type OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addFromNodeOperationMeta: OperationMeta<\n  AddOrDeleteFromNodeOperationValue,\n  PluginContext,\n  FlowNodeEntity\n> = {\n  ...baseOperationMeta,\n  type: OperationType.addFromNode,\n  inverse: op => ({ ...op, type: OperationType.deleteFromNode }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const { value } = op;\n    return `Create ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const { value } = op;\n    const nodeName = config.getNodeLabel(value.data);\n    const fromName = config.getNodeLabelById(value.fromId);\n    return `Create ${value.data.type} node ${nodeName} after ${fromName}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/add-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, FlowOperationBaseService, OperationType } from '@flowgram.ai/document';\nimport { AddOrDeleteNodeValue } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\n\nexport const addNodeOperationMeta: OperationMeta<\n  AddOrDeleteNodeValue,\n  PluginContext,\n  FlowNodeEntity\n> = {\n  type: OperationType.addNode,\n  inverse: op => ({ ...op, type: OperationType.deleteNode }),\n  apply: ({ value: { data, parentId, index, hidden } }, ctx) =>\n    ctx.get<FlowOperationBaseService>(FlowOperationBaseService).addNode(data, {\n      parent: parentId,\n      index,\n      hidden,\n    }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Create ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const nodeName = config.getNodeLabel(value.data);\n    const parentName = config.getParentName(value.parentId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Create ${value.data.type} node ${nodeName} in ${parentName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/add-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { AddOrDeleteNodesOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addNodesOperationMeta: OperationMeta<\n  AddOrDeleteNodesOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: OperationType.addNodes,\n  inverse: op => ({\n    ...op,\n    type: OperationType.deleteNodes,\n  }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `${value.nodes.map(node => `Create ${config.getNodeLabel(node)}`).join(';')}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const fromName = config.getNodeLabelById(value.fromId);\n    return `${value.nodes\n      .map(node => `Create ${node.type} node ${config.getNodeLabel(node)} after ${fromName}`)\n      .join(';')}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/base.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowOperation, FlowOperationBaseService } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryOperationService } from '../services';\n\nexport const baseOperationMeta: Pick<OperationMeta, 'apply'> = {\n  apply: (operation, ctx: PluginContext) => {\n    const fixedHistoryOperationService = ctx.get(\n      FlowOperationBaseService,\n    ) as FixedHistoryOperationService;\n\n    return fixedHistoryOperationService.originApply(operation as FlowOperation);\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/create-group.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createOrUngroupValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { baseOperationMeta } from './base';\n\nexport const createGroupOperationMeta: OperationMeta<createOrUngroupValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.createGroup,\n  inverse: op => ({ ...op, type: OperationType.ungroup }),\n  getLabel: (op, ctx) => {\n    const value = op.value;\n    return `Create group ${value.groupId} from ${value.targetId}`;\n  },\n  getDescription: (op, ctx) => {\n    const value = op.value;\n    return `Create group with nodes ${value.nodeIds.join(', ')}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/delete-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { AddOrDeleteBlockValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteBlockOperationMeta: OperationMeta<AddOrDeleteBlockValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.deleteBlock,\n  inverse: op => ({ ...op, type: OperationType.addBlock }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Delete ${config.getBlockLabel(value.blockData)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const branchName = config.getBlockLabel(value.blockData);\n    const targetName = config.getNodeLabelById(value.targetId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Delete branch ${branchName} in ${targetName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/delete-child-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity, OperationType } from '@flowgram.ai/document';\nimport { AddOrDeleteChildNodeValue } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteChildNodeOperationMeta: OperationMeta<\n  AddOrDeleteChildNodeValue,\n  PluginContext,\n  FlowNodeEntity\n> = {\n  ...baseOperationMeta,\n  type: OperationType.deleteChildNode,\n  inverse: op => ({ ...op, type: OperationType.addChildNode }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Delete ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const nodeName = config.getNodeLabel(value.data);\n    const parentName = config.getParentName(value.parentId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Delete ${value.data.type} node ${nodeName} in ${parentName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/delete-from-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { AddOrDeleteFromNodeOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteFromNodeOperationMeta: OperationMeta<\n  AddOrDeleteFromNodeOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: OperationType.deleteFromNode,\n  inverse: op => ({ ...op, type: OperationType.addFromNode }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Delete ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const nodeName = config.getNodeLabel(value.data);\n    const parentName = config.getNodeLabelById(value.fromId);\n    return `Delete ${value.data.type} node ${nodeName} after ${parentName}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/delete-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowOperationBaseService, OperationType } from '@flowgram.ai/document';\nimport { AddOrDeleteNodeValue } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteNodeOperationMeta: OperationMeta<AddOrDeleteNodeValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.deleteNode,\n  inverse: op => ({ ...op, type: OperationType.addNode }),\n  apply: ({ value: { data } }, ctx) =>\n    ctx.get<FlowOperationBaseService>(FlowOperationBaseService).deleteNode(data.id),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Create ${config.getNodeLabel(value.data)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const nodeName = config.getNodeLabel(value.data);\n    const parentName = config.getParentName(value.parentId);\n    const position = typeof value.index !== 'undefined' ? `position ${value.index}` : 'the end';\n    return `Delete ${value.data.type} node ${nodeName} in ${parentName} at ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/delete-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { AddOrDeleteNodesOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteNodesOperationMeta: OperationMeta<\n  AddOrDeleteNodesOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: OperationType.deleteNodes,\n  inverse: op => ({\n    ...op,\n    type: OperationType.addNodes,\n  }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return value.nodes.map(node => `Delete ${config.getNodeLabel(node)}`).join(';');\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const fromName = config.getNodeLabelById(value.fromId);\n    return value.nodes\n      .map(node => `Delete ${node.type} node ${config.getNodeLabel(node)} after ${fromName}`)\n      .join(';');\n  },\n  shouldMerge: (op, prev, stackItem) => {\n    if (!prev) {\n      return false;\n    }\n    if (\n      // 合并500ms内的操作, 如分组内最后一个节点会联动删除分组\n      Date.now() - stackItem.getTimestamp() <\n      500\n    ) {\n      return true;\n    }\n    return false;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './add-from-node';\nimport { ungroupOperationMeta } from './ungroup';\nimport { moveNodesOperationMeta } from './move-nodes';\nimport { moveChildNodesOperationMeta } from './move-child-nodes';\nimport { moveBlockOperationMeta } from './move-block';\nimport { deleteNodesOperationMeta } from './delete-nodes';\nimport { deleteNodeOperationMeta } from './delete-node';\nimport { deleteFromNodeOperationMeta } from './delete-from-node';\nimport { deleteChildNodeOperationMeta } from './delete-child-node';\nimport { deleteBlockOperationMeta } from './delete-block';\nimport { createGroupOperationMeta } from './create-group';\nimport { addNodesOperationMeta } from './add-nodes';\nimport { addNodeOperationMeta } from './add-node';\nimport { addFromNodeOperationMeta } from './add-from-node';\nimport { addChildNodeOperationMeta } from './add-child-node';\nimport { addBlockOperationMeta } from './add-block';\n\nexport const operationMetas = [\n  deleteFromNodeOperationMeta,\n  addFromNodeOperationMeta,\n  addBlockOperationMeta,\n  deleteBlockOperationMeta,\n  createGroupOperationMeta,\n  ungroupOperationMeta,\n  moveNodesOperationMeta,\n  deleteNodesOperationMeta,\n  addNodesOperationMeta,\n  moveBlockOperationMeta,\n  addChildNodeOperationMeta,\n  deleteChildNodeOperationMeta,\n  moveChildNodesOperationMeta,\n  addNodeOperationMeta,\n  deleteNodeOperationMeta,\n];\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/move-block.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MoveBlockOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const moveBlockOperationMeta: OperationMeta<MoveBlockOperationValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.moveBlock,\n  inverse: op => ({\n    ...op,\n    value: {\n      ...op.value,\n      fromIndex: op.value.toIndex,\n      toIndex: op.value.fromIndex,\n      fromParentId: op.value.toParentId,\n      toParentId: op.value.fromParentId,\n    },\n  }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Move ${config.getNodeLabelById(value.nodeId)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const position = typeof value.toIndex !== 'undefined' ? `position ${value.toIndex}` : 'the end';\n    return `Move branch ${config.getNodeLabelById(value.nodeId)} to ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/move-child-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MoveChildNodesOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const moveChildNodesOperationMeta: OperationMeta<\n  MoveChildNodesOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: OperationType.moveChildNodes,\n  inverse: op => ({\n    ...op,\n    value: {\n      ...op.value,\n      fromIndex: op.value.toIndex,\n      toIndex: op.value.fromIndex,\n      fromParentId: op.value.toParentId,\n      toParentId: op.value.fromParentId,\n    },\n  }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `Move ${value.nodeIds.map(id => config.getNodeLabelById(id)).join(',')}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    const position = typeof value.toIndex !== 'undefined' ? `position ${value.toIndex}` : 'the end';\n    return `Move nodes ${value.nodeIds\n      .map(id => config.getNodeLabelById(id))\n      .join(',')} to ${position}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/move-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MoveNodesOperationValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const moveNodesOperationMeta: OperationMeta<MoveNodesOperationValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.moveNodes,\n  inverse: op => ({\n    ...op,\n    value: {\n      ...op.value,\n      fromId: op.value.toId,\n      toId: op.value.fromId,\n    },\n  }),\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `${value.nodeIds.map(id => `Move ${config.getNodeLabelById(id)}`).join(';')}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const value = op.value;\n    return `${value.nodeIds\n      .map(id => `Move ${config.getNodeLabelById(id)} to ${config.getNodeLabelById(value.toId)}`)\n      .join(';')}`;\n  },\n  getURI: (op, ctx) => {\n    const config = ctx.get<FixedHistoryConfig>(FixedHistoryConfig);\n    const nodeIds = op.value.nodeIds;\n    if (nodeIds.length === 0) {\n      return;\n    }\n    return config.getNodeURI(nodeIds[0]);\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/operation-metas/ungroup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createOrUngroupValue, OperationType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { baseOperationMeta } from './base';\n\nexport const ungroupOperationMeta: OperationMeta<createOrUngroupValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: OperationType.ungroup,\n  inverse: op => ({ ...op, type: OperationType.createGroup }),\n  getLabel: (op, ctx) => {\n    const value = op.value;\n    return `Ungroup ${value.groupId}`;\n  },\n  getDescription: (op, ctx) => {\n    const value = op.value;\n    return `Ungroup with nodes ${value.nodeIds.join(', ')}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/services/fixed-history-form-data-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { cloneDeep } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { Disposable, Emitter } from '@flowgram.ai/utils';\nimport { FlowNodeFormData, FormModel } from '@flowgram.ai/form-core';\nimport { FlowDocument } from '@flowgram.ai/document';\n\n@injectable()\nexport class FixedHistoryFormDataService implements Disposable {\n  @inject(FlowDocument) document: FlowDocument;\n\n  private _cache = new Map<FlowNodeFormData, Map<string, any>>();\n\n  private _formValueChangeByHistoryEmitter = new Emitter<{\n    formData: FlowNodeFormData;\n    value: any;\n    path: string;\n  }>();\n\n  onFormValueChangeByHistory = this._formValueChangeByHistoryEmitter.event;\n\n  resetCache(flowNodeFormData: FlowNodeFormData, value: any) {\n    Object.keys(value).forEach((key) => {\n      this.setCache(flowNodeFormData, key, value[key]);\n    });\n  }\n\n  setCache(flowNodeFormData: FlowNodeFormData, prop: string, value: any) {\n    if (!this._cache.has(flowNodeFormData)) {\n      this._cache.set(flowNodeFormData, new Map());\n    }\n\n    const formData = this._cache.get(flowNodeFormData)!;\n    formData.set(prop, cloneDeep(value));\n  }\n\n  getCache(flowNodeFormData: FlowNodeFormData, prop: string): any {\n    if (!this._cache.has(flowNodeFormData)) {\n      return;\n    }\n\n    const formData = this._cache.get(flowNodeFormData)!;\n    return formData.get(prop);\n  }\n\n  /**\n   * 获取表单数据\n   * @param id node id\n   * @returns 表单数据\n   */\n  getFormDataByNodeId(id: string) {\n    const node = this.document.getNode(id);\n    if (!node) {\n      return;\n    }\n    const formData = node.getData<FlowNodeFormData>(FlowNodeFormData);\n    return formData;\n  }\n\n  getFormItemValue(formData: FlowNodeFormData, path: string) {\n    const formItem = this.getFormItem(formData, path);\n\n    if (!formItem) {\n      return;\n    }\n    return formItem.value;\n  }\n\n  setFormItemValue(formData: FlowNodeFormData, path: string, value: any) {\n    const formItem = this.getFormItem(formData, path);\n\n    if (formItem) {\n      formItem.value = value;\n      this._formValueChangeByHistoryEmitter.fire({\n        formData,\n        path,\n        value,\n      });\n    }\n  }\n\n  getFormItem(formData: FlowNodeFormData, path: string) {\n    if (typeof path === 'undefined') {\n      return;\n    }\n    if (path.endsWith('/')) {\n      path = path.slice(0, -1);\n    }\n\n    if (!path.startsWith('/')) {\n      path = '/' + path;\n    }\n\n    const formItem = formData.getFormModel<FormModel>().getFormItemByPath(path);\n\n    return formItem;\n  }\n\n  dispose() {\n    this._formValueChangeByHistoryEmitter.dispose();\n    this._cache.clear();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/services/fixed-history-operation-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { FlowOperation, FlowOperationBaseServiceImpl } from '@flowgram.ai/document';\nimport { HistoryService } from '@flowgram.ai/history';\n\n@injectable()\nexport class FixedHistoryOperationService extends FlowOperationBaseServiceImpl {\n  @inject(HistoryService) historyService: HistoryService;\n\n  apply(operation: FlowOperation): any {\n    return this.historyService.pushOperation(operation);\n  }\n\n  originApply(operation: FlowOperation): any {\n    return super.apply(operation);\n  }\n\n  transact(transaction: () => void): void {\n    this.historyService.transact(transaction);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/services/fixed-history-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\nimport { HistoryService, Operation } from '@flowgram.ai/history';\nimport { OperationRegistry } from '@flowgram.ai/history';\nimport { OperationMeta } from '@flowgram.ai/history';\nimport {\n  AddOrDeleteBlockValue,\n  AddOrDeleteChildNodeValue,\n  AddOrDeleteFromNodeOperationValue,\n  FlowNodeEntity,\n  FlowNodeJSON,\n  OperationType,\n} from '@flowgram.ai/document';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { FlowOperationBaseService } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport { IHistoryDocument } from '../types';\nimport { FixedHistoryConfig } from '../fixed-history-config';\nimport { FixedHistoryOperationService } from './fixed-history-operation-service';\n\n@injectable()\nexport class FixedHistoryService implements IHistoryDocument {\n  @inject(HistoryService) historyService: HistoryService;\n\n  @inject(OperationRegistry) operationRegistry: OperationRegistry;\n\n  @inject(FlowOperationBaseService)\n  fixedHistoryOperationService: FixedHistoryOperationService;\n\n  @inject(FlowDocument) document: FlowDocument;\n\n  @inject(FixedHistoryConfig) config: FixedHistoryConfig;\n\n  setSource(source: PluginContext) {\n    this.historyService.context.source = source;\n  }\n\n  /**\n   * 注册操作\n   * @param operationMetas\n   */\n  public registerOperationMetas(operationMetas: OperationMeta[]) {\n    operationMetas.forEach((operationMeta) => {\n      this.operationRegistry.registerOperationMeta(operationMeta);\n    });\n  }\n\n  /**\n   * 事务\n   * @param transaction\n   */\n  public transact(transaction: () => void) {\n    this.historyService.transact(transaction);\n  }\n\n  /**\n   * 撤销\n   */\n  async undo() {\n    await this.historyService.undo();\n  }\n\n  /**\n   * 重做\n   */\n  async redo() {\n    await this.historyService.redo();\n  }\n\n  /**\n   * 是否可重做\n   */\n  canRedo() {\n    return this.historyService.canRedo();\n  }\n\n  /**\n   * 是否可撤销\n   */\n  canUndo() {\n    return this.historyService.canUndo();\n  }\n\n  /**\n   * 添加一个操作\n   * @param operation\n   */\n  pushHistoryOperation(operation: Operation) {\n    return this.historyService.pushOperation(operation);\n  }\n\n  /**\n   * 获取历史操作\n   */\n  getHistoryOperations() {\n    return this.historyService.getHistoryOperations();\n  }\n\n  /**\n   * 添加节点\n   * @param value 添加节点配置\n   * @returns\n   * @deprecated 请使用 `FlowOperationService.addFromNode` 代替\n   */\n  addFromNode(fromNode: FlowNodeEntity | string, json: FlowNodeJSON): FlowNodeEntity {\n    const value: AddOrDeleteFromNodeOperationValue = {\n      fromId: typeof fromNode === 'string' ? fromNode : fromNode.id,\n      data: json,\n    };\n    return this.historyService.pushOperation({\n      type: OperationType.addFromNode,\n      value,\n      uri: this.config.getNodeURI(json.id),\n    });\n  }\n\n  /**\n   * 删除节点\n   * @param node 节点\n   * @returns\n   * @deprecated 请使用 `FlowOperationService.deleteNode` 代替\n   */\n  deleteNode(node: FlowNodeEntity): void {\n    const { originParent, parent } = node;\n    const uri = this.config.getNodeURI(node.id);\n    let nodeJSON = this.config.nodeToJSON(node);\n\n    // 非数据节点\n    if (!nodeJSON) {\n      nodeJSON = {\n        id: node.id,\n        type: node.flowNodeType,\n      };\n    }\n\n    if (parent) {\n      const index = parent.children.findIndex((child) => child === node);\n\n      if (originParent) {\n        // 分支节点\n        let parentId: string | undefined = originParent.id;\n        if (!parentId) {\n          console.warn('no parent found');\n          return;\n        }\n\n        const value: AddOrDeleteBlockValue = {\n          targetId: parentId,\n          blockData: nodeJSON,\n        };\n\n        if (index >= 0) {\n          value.index = index;\n        }\n\n        return this.historyService.pushOperation({\n          type: OperationType.deleteBlock,\n          value,\n          uri,\n        });\n      } else {\n        // Reactor节点\n        const value: AddOrDeleteChildNodeValue = {\n          data: nodeJSON,\n          parentId: parent.id,\n        };\n\n        if (index >= 0) {\n          value.index = index;\n        }\n\n        return this.historyService.pushOperation({\n          type: OperationType.deleteChildNode,\n          value,\n          uri,\n        });\n      }\n    } else {\n      // 普通节点\n      if (!node.pre) {\n        console.warn('no pre found');\n        return;\n      }\n\n      return this.historyService.pushOperation({\n        type: OperationType.deleteFromNode,\n        value: {\n          fromId: node.pre.id,\n          data: nodeJSON,\n          uri,\n        },\n      });\n    }\n  }\n\n  /**\n   * 添加子节点\n   * @param data\n   * @param parent\n   * @param index\n   * @param originParent\n   * @returns\n   * @deprecated 请使用 `FlowOperationService.addNode` 代替\n   */\n  addChildNode(\n    data: FlowNodeJSON,\n    parent?: FlowNodeEntity,\n    index?: number,\n    originParent?: FlowNodeEntity\n  ) {\n    const value: AddOrDeleteChildNodeValue = {\n      data,\n      parentId: parent?.id,\n      originParentId: originParent?.id,\n      index,\n    };\n\n    return this.historyService.pushOperation({\n      type: OperationType.addChildNode,\n      value: value,\n      uri: this.config.getNodeURI(data.id),\n    });\n  }\n\n  /**\n   * 批量删除\n   * @param nodes\n   * @deprecated 请使用 `FlowOperationService.deleteNodes` 代替\n   */\n  deleteNodes(nodes: FlowNodeEntity[]) {\n    if (nodes.length === 0) {\n      return;\n    }\n\n    this.historyService.transact(() => {\n      nodes.reverse().forEach((node) => {\n        this.deleteNode(node);\n      });\n    });\n  }\n\n  /**\n   * 批量添加\n   * @param from\n   * @param nodes\n   */\n  addFromNodes(from: FlowNodeEntity, nodes: FlowNodeEntity[]) {\n    if (nodes.length === 0) {\n      return;\n    }\n    return this.historyService.pushOperation({\n      type: OperationType.addNodes,\n      value: {\n        fromId: from.id,\n        nodes: nodes.map((node) => this.config.nodeToJSON(node)),\n        uri: this.config.getNodeURI(nodes[0].id),\n      },\n    });\n  }\n\n  /**\n   * 添加块级元素\n   * @param target 目标\n   * @param blockData 块数据\n   * @param parent 父级\n   * @returns\n   * @deprecated 请使用 `FlowOperationService.addBlock` 代替\n   */\n  addBlock(\n    target: string | FlowNodeEntity,\n    blockData: FlowNodeJSON,\n    parent?: FlowNodeEntity | undefined,\n    index?: number\n  ): FlowNodeEntity {\n    const value: AddOrDeleteBlockValue = {\n      targetId: typeof target === 'string' ? target : target.id,\n      blockData,\n      index,\n    };\n\n    if (parent) {\n      value.parentId = parent.id;\n    }\n\n    return this.historyService.pushOperation({\n      type: OperationType.addBlock,\n      value,\n      uri: this.config.getNodeURI(value.blockData.id),\n    });\n  }\n\n  /**\n   * 移动节点\n   * @param node 被移动的节点\n   * @param toNode 被放置的节点\n   * @returns\n   */\n  moveNode(node: FlowNodeEntity, toNode: FlowNodeEntity) {\n    return this.fixedHistoryOperationService.dragNodes({\n      dropNode: toNode,\n      nodes: [node],\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FixedHistoryOperationService } from './fixed-history-operation-service';\nexport { FixedHistoryService } from './fixed-history-service';\nexport { FixedHistoryFormDataService } from './fixed-history-form-data-service';\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { HistoryPluginOptions, OperationMeta } from '@flowgram.ai/history';\nimport { FlowNodeEntity, FlowNodeJSON } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\n\nexport interface IHistoryDocument {\n  addFromNode(fromNode: FlowNodeEntity | string, json: FlowNodeJSON): FlowNodeEntity;\n  addBlock(\n    target: FlowNodeEntity | string,\n    blockData: FlowNodeJSON,\n    parent?: FlowNodeEntity,\n    index?: number\n  ): FlowNodeEntity;\n  deleteNode(fromNode: FlowNodeEntity): void;\n}\n\n/**\n * 将node转成json\n */\nexport type NodeToJson = (node: FlowNodeEntity) => FlowNodeJSON;\n/**\n * 根据节点id获取label\n */\nexport type GetNodeLabelById = (id: string) => string;\n/**\n * 根据节点获取label\n */\nexport type GetNodeLabel = (node: FlowNodeJSON) => string;\n/**\n * 根据分支获取label\n */\nexport type GetBlockLabel = (node: FlowNodeJSON) => string;\n/**\n * 根据节点获取URI\n */\nexport type GetNodeURI = (id: string) => string | any;\n/**\n * 获取文档JSON\n */\nexport type GetDocumentJSON = () => unknown;\n\n/**\n * 插件配置\n */\nexport interface FixedHistoryPluginOptions<CTX extends PluginContext = PluginContext>\n  extends HistoryPluginOptions<CTX> {\n  nodeToJSON?: (ctx: CTX) => NodeToJson;\n  getDocumentJSON?: (ctx: CTX) => GetDocumentJSON;\n  getNodeLabelById?: (ctx: CTX) => GetNodeLabelById;\n  getNodeLabel?: (ctx: CTX) => GetNodeLabel;\n  getBlockLabel?: (ctx: CTX) => GetBlockLabel;\n  getNodeURI?: (ctx: CTX) => GetNodeURI;\n  operationMetas?: OperationMeta[];\n  uri?: string | any;\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/fixed-history-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-auto-layout-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@dagrejs/graphlib\": \"2.2.2\",\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/create-auto-layout-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { AutoLayoutOptions } from './type';\nimport { AutoLayoutService } from './services';\n\n/**\n * Auto layout plugin - 自动布局插件\n * https://flowgram.ai/guide/plugin/free-auto-layout-plugin.html\n */\nexport const createFreeAutoLayoutPlugin = definePluginCreator<AutoLayoutOptions>({\n  onBind: ({ bind }) => {\n    bind(AutoLayoutService).toSelf().inSingletonScope();\n  },\n  onInit: (ctx, opts) => {\n    ctx.get(AutoLayoutService).init(opts);\n  },\n  singleton: true,\n});\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/acyclic.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutGraph } from './graph';\n\n/**\n * DFS去环算法\n * @param graph 布局图实例\n * @returns 反馈弧集（需要反转的边的ID数组）\n */\nconst dfsFAS = (graph: LayoutGraph): string[] => {\n  const visited: { [key: string]: boolean } = {};\n  const stack: { [key: string]: boolean } = {};\n  const fas: string[] = [];\n\n  /**\n   * DFS遍历\n   * @param nodeId 当前节点ID\n   */\n  const dfs = (nodeId: string): void => {\n    visited[nodeId] = true;\n    stack[nodeId] = true;\n\n    const outEdges = graph.edges.filter((edge) => edge.from === nodeId);\n    outEdges.forEach((edge) => {\n      if (!visited[edge.to]) {\n        dfs(edge.to);\n      } else if (stack[edge.to]) {\n        // 发现环，将该边添加到反馈弧集\n        fas.push(edge.id);\n      }\n    });\n\n    stack[nodeId] = false;\n  };\n\n  // 对每个未访问的节点进行DFS\n  graph.nodes.forEach((node) => {\n    if (!visited[node.id]) {\n      dfs(node.id);\n    }\n  });\n\n  return fas;\n};\n\n/**\n * 贪心去环算法\n * @param graph 布局图实例\n * @returns 反馈弧集（需要反转的边的ID数组）\n */\nconst greedyFAS = (graph: LayoutGraph): string[] => {\n  const fas: string[] = [];\n  const nodeOrder: string[] = [];\n\n  // 计算节点的入度和出度\n  const inDegree: { [key: string]: number } = {};\n  const outDegree: { [key: string]: number } = {};\n\n  graph.nodes.forEach((node) => {\n    inDegree[node.id] = 0;\n    outDegree[node.id] = 0;\n  });\n\n  graph.edges.forEach((edge) => {\n    inDegree[edge.to]++;\n    outDegree[edge.from]++;\n  });\n\n  // 贪心选择节点\n  while (nodeOrder.length < graph.nodes.length) {\n    let maxDiff = -Infinity;\n    let bestNode: string | null = null;\n\n    graph.nodes.forEach((node) => {\n      if (!nodeOrder.includes(node.id)) {\n        const diff = outDegree[node.id] - inDegree[node.id];\n        if (diff > maxDiff) {\n          maxDiff = diff;\n          bestNode = node.id;\n        }\n      }\n    });\n\n    if (bestNode) {\n      nodeOrder.push(bestNode);\n      // 更新相邻节点的入度和出度\n      graph.edges.forEach((edge) => {\n        if (edge.from === bestNode) {\n          inDegree[edge.to]--;\n        }\n        if (edge.to === bestNode) {\n          outDegree[edge.from]--;\n        }\n      });\n    }\n  }\n\n  // 根据节点顺序确定需要反转的边\n  graph.edges.forEach((edge) => {\n    if (nodeOrder.indexOf(edge.from) > nodeOrder.indexOf(edge.to)) {\n      fas.push(edge.id);\n    }\n  });\n\n  return fas;\n};\n\n/**\n * 去环\n */\nexport const acyclic = (graph: LayoutGraph, acyclicer: 'dfs' | 'greedy' = 'dfs'): LayoutGraph => {\n  // 使用DFS或贪心算法获取反馈弧集\n  const fas = acyclicer === 'dfs' ? dfsFAS(graph) : greedyFAS(graph);\n\n  // 反转反馈弧集中的边\n  fas.forEach((edgeId) => {\n    const edge = graph.edges.find((e) => e.id === edgeId);\n    if (edge) {\n      const { from, to } = edge;\n      graph.removeEdge(edgeId);\n      graph.addLayoutEdge({ id: edgeId, from: to, to: from });\n    }\n  });\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/graph.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport type { ILayoutGraph, LayoutEdge, LayoutNode } from './type';\n\nexport class LayoutGraph implements ILayoutGraph {\n  public readonly store: {\n    nodes: Map<string, LayoutNode>;\n    edges: Map<string, LayoutEdge>;\n  } = {\n    nodes: new Map(),\n    edges: new Map(),\n  };\n\n  public get nodes(): LayoutNode[] {\n    return Array.from(this.store.nodes.values());\n  }\n\n  public get edges(): LayoutEdge[] {\n    return Array.from(this.store.edges.values());\n  }\n\n  public getNode(id: string): LayoutNode | undefined {\n    return this.store.nodes.get(id);\n  }\n\n  public hasNode(id: string): boolean {\n    return this.store.nodes.has(id);\n  }\n\n  public addNode(nodeEntity: WorkflowNodeEntity): LayoutNode {\n    const transform = nodeEntity.getData(TransformData);\n    const layoutNode: LayoutNode = {\n      id: nodeEntity.id,\n      node: nodeEntity,\n      rank: -1,\n      order: -1,\n      position: { x: transform.position.x, y: transform.position.y },\n      size: { width: transform.bounds.width, height: transform.bounds.height },\n    };\n    this.store.nodes.set(layoutNode.id, layoutNode);\n    return layoutNode;\n  }\n\n  public addLayoutNode(layoutNode: LayoutNode): void {\n    this.store.nodes.set(layoutNode.id, layoutNode);\n  }\n\n  public addEdge(edgeEntity: WorkflowLineEntity): LayoutEdge {\n    const layoutEdge: LayoutEdge = {\n      id: edgeEntity.id,\n      from: edgeEntity.from.id,\n      to: edgeEntity.to!.id,\n    };\n    this.store.edges.set(layoutEdge.id, layoutEdge);\n    return layoutEdge;\n  }\n\n  public addLayoutEdge(layoutEdge: LayoutEdge): void {\n    this.store.edges.set(layoutEdge.id, layoutEdge);\n  }\n\n  public removeEdge(id: string): void {\n    this.store.edges.delete(id);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './acyclic';\nexport * from './rank';\nexport * from './order';\nexport * from './layout';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-core';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { LayoutNode } from './type';\nimport { LayoutGraph } from './graph';\nimport { acyclic, feasibleTree, longestPath, networkSimplex, normalizeRanks, order } from './';\n\n/**\n * 布局算法\n * 参考 dagre.js 的实现 https://github.com/dagrejs/dagre\n */\nexport namespace DagreLayout {\n  const getNextEdges = (node: WorkflowNodeEntity): WorkflowLineEntity[] => {\n    const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);\n    return linesData.outputLines.filter((line) => line.from && line.to);\n  };\n\n  const getPrevEdges = (node: WorkflowNodeEntity): WorkflowLineEntity[] => {\n    const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);\n    return linesData.inputLines.filter((line) => line.from && line.to);\n  };\n\n  /** 添加节点 */\n  const createData = (params: {\n    node: WorkflowNodeEntity;\n    depth: number;\n    graph: LayoutGraph;\n  }): LayoutGraph => {\n    const { node, depth, graph } = params;\n    if (graph.hasNode(node.id)) {\n      return graph;\n    }\n    graph.addNode(node);\n    const prevEdges = getPrevEdges(node);\n    const nextEdges = getNextEdges(node);\n    prevEdges.forEach((prevEdge) => {\n      graph.addEdge(prevEdge);\n      createData({ node: prevEdge.from, depth: depth - 1, graph });\n    });\n    nextEdges.forEach((nextEdge) => {\n      graph.addEdge(nextEdge);\n      createData({ node: nextEdge.to!, depth: depth + 1, graph });\n    });\n    return graph;\n  };\n\n  // 定义一些常量\n  const NODE_SPACING = 100; // 同层级节点之间的垂直间距\n  const RANK_SPACING = 100; // 层级之间的水平间距\n\n  /** 计算图中所有节点的坐标 */\n  const calcCoordinates = (graph: LayoutGraph): LayoutGraph => {\n    // 按rank对节点进行分组\n    const rankGroups = groupNodesByRank(graph.nodes);\n\n    // 计算每个rank的最大高度\n    const rankHeights = calculateRankHeights(rankGroups);\n\n    // 计算每个节点的坐标\n    let currentX = 0;\n    rankGroups.forEach((nodesInRank, rank) => {\n      const rankHeight = rankHeights[rank];\n\n      nodesInRank.forEach((node) => {\n        // 计算X坐标\n        node.position.x = currentX + node.size.width / 2;\n\n        // 计算Y坐标\n        const totalHeightOfRank = nodesInRank.reduce((sum, n) => sum + n.size.height, 0);\n        const totalSpacing = (nodesInRank.length - 1) * NODE_SPACING;\n        const startY = (rankHeight - totalHeightOfRank - totalSpacing) / 2;\n\n        let currentY = startY;\n        for (let i = 0; i < node.order; i++) {\n          currentY += nodesInRank[i].size.height + NODE_SPACING;\n        }\n        node.position.y = currentY + node.size.height / 2;\n      });\n\n      // 更新X坐标为下一个rank的起始位置\n      currentX += rankHeight + RANK_SPACING;\n    });\n    return graph;\n  };\n\n  /** 按rank对节点进行分组 */\n  const groupNodesByRank = (nodes: LayoutNode[]): LayoutNode[][] => {\n    const groups: LayoutNode[][] = [];\n    nodes.forEach((node) => {\n      if (!groups[node.rank]) {\n        groups[node.rank] = [];\n      }\n      groups[node.rank].push(node);\n    });\n    return groups;\n  };\n\n  /** 计算每个rank的最大高度 */\n  const calculateRankHeights = (rankGroups: LayoutNode[][]): number[] =>\n    rankGroups.map((nodesInRank) => Math.max(...nodesInRank.map((node) => node.size.width)));\n\n  const positioning = (graph: LayoutGraph): LayoutGraph => {\n    graph.nodes.forEach((node) => {\n      const transform = node.node.getData(TransformData);\n      transform.update({\n        position: node.position,\n      });\n    });\n    return graph;\n  };\n\n  const rank = (\n    graph: LayoutGraph,\n    ranker: 'longest-path' | 'network-simplex' | 'tight-tree' = 'network-simplex'\n  ): LayoutGraph => {\n    if (ranker === 'longest-path') {\n      longestPath(graph);\n    } else if (ranker === 'network-simplex') {\n      networkSimplex(graph);\n    } else if (ranker === 'tight-tree') {\n      feasibleTree(graph);\n    }\n    return graph;\n  };\n\n  export const applyLayout = (graph: LayoutGraph): void => {\n    acyclic(graph); // 去环\n    rank(graph); // 分层\n    normalizeRanks(graph); // 归一化 rank 值\n    order(graph); // 重心法对同层级节点进行排序\n    calcCoordinates(graph); // 分配坐标\n    positioning(graph); // 应用布局\n  };\n\n  /** 创建布局图 */\n  export const createGraph = (node: WorkflowNodeEntity): LayoutGraph => {\n    const graph = new LayoutGraph();\n    createData({ node, depth: 0, graph });\n    applyLayout(graph);\n    return graph;\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/order.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutGraph } from './graph';\n\n// 辅助函数：获取图中的最大rank\nconst getMaxRank = (graph: LayoutGraph): number =>\n  Math.max(...graph.nodes.map((node) => node.rank));\n\n// 辅助函数：根据rank构建层级图\nconst buildLayerGraph = (\n  graph: LayoutGraph,\n  rank: number,\n  edgeType: 'inEdges' | 'outEdges'\n): LayoutGraph => {\n  const layerGraph = new LayoutGraph();\n\n  graph.nodes\n    .filter((node) => node.rank === rank)\n    .forEach((node) => {\n      layerGraph.addLayoutNode(node);\n    });\n\n  graph.edges.forEach((edge) => {\n    const sourceNode = graph.getNode(edge.from);\n    const targetNode = graph.getNode(edge.to);\n    if (!sourceNode || !targetNode) return;\n\n    if (edgeType === 'inEdges' && targetNode.rank === rank) {\n      layerGraph.addLayoutEdge(edge);\n    } else if (edgeType === 'outEdges' && sourceNode.rank === rank) {\n      layerGraph.addLayoutEdge(edge);\n    }\n  });\n\n  return layerGraph;\n};\n\n// 辅助函数：初始化order\nconst initOrder = (graph: LayoutGraph): { [key: number]: string[] } => {\n  const layering: { [key: number]: string[] } = {};\n  graph.nodes.forEach((node) => {\n    if (!layering[node.rank]) {\n      layering[node.rank] = [];\n    }\n    layering[node.rank].push(node.id);\n  });\n  return layering;\n};\n\n// 辅助函数：分配order\nconst assignOrder = (graph: LayoutGraph, layering: { [key: number]: string[] }): void => {\n  Object.entries(layering).forEach(([rank, layer]) => {\n    layer.forEach((nodeId, index) => {\n      const node = graph.getNode(nodeId);\n      if (node) {\n        node.order = index;\n      }\n    });\n  });\n};\n\n// 辅助函数：计算交叉数\nconst crossCount = (graph: LayoutGraph, layering: { [key: number]: string[] }): number => {\n  let cc = 0;\n  const layers = Object.values(layering);\n\n  for (let i = 1; i < layers.length; i++) {\n    const northLayer = layers[i - 1];\n    const southLayer = layers[i];\n\n    for (let j = 0; j < northLayer.length; j++) {\n      for (let k = j + 1; k < northLayer.length; k++) {\n        const v = graph.getNode(northLayer[j]);\n        const w = graph.getNode(northLayer[k]);\n        if (!v || !w) continue;\n\n        // 获取v和w的南向邻居\n        const vNeighbors = graph.edges\n          .filter((e) => e.from === v.id)\n          .map((e) => graph.getNode(e.to));\n        const wNeighbors = graph.edges\n          .filter((e) => e.from === w.id)\n          .map((e) => graph.getNode(e.to));\n\n        for (const vNeighbor of vNeighbors) {\n          for (const wNeighbor of wNeighbors) {\n            if (!vNeighbor || !wNeighbor) continue;\n            if (southLayer.indexOf(vNeighbor.id) > southLayer.indexOf(wNeighbor.id)) {\n              cc++;\n            }\n          }\n        }\n      }\n    }\n  }\n\n  return cc;\n};\n\n// 辅助函数：构建复合图\nconst buildCompoundGraph = (): LayoutGraph => new LayoutGraph();\n\n// 辅助函数：添加子图约束\nconst addSubgraphConstraints = (layerGraph: LayoutGraph, cg: LayoutGraph, vs: string[]): void => {\n  const prev: { [key: string]: string } = {};\n  let root = layerGraph.nodes[0]?.id;\n  vs.forEach((v) => {\n    let prevV = prev[root];\n    if (prevV) {\n      cg.addLayoutEdge({ id: `${prevV}-${v}`, from: prevV, to: v, weight: 0 });\n    }\n    prev[root] = v;\n  });\n};\n\n// 辅助函数：对子图进行排序\nconst sortSubgraph = (\n  layerGraph: LayoutGraph,\n  root: string,\n  cg: LayoutGraph,\n  biasRight: boolean\n): { vs: string[] } => {\n  const vs: string[] = [];\n  const visited = new Set<string>();\n  const nodeData = new Map<string, { barycenter: number; weight: number }>();\n\n  const dfs = (v: string) => {\n    if (visited.has(v)) return;\n    visited.add(v);\n\n    let barycenter = 0;\n    let weight = 0;\n\n    const node = layerGraph.getNode(v);\n    if (node) {\n      const edges = biasRight\n        ? layerGraph.edges.filter((e) => e.to === v)\n        : layerGraph.edges.filter((e) => e.from === v);\n\n      edges.forEach((edge) => {\n        const w = biasRight ? edge.from : edge.to;\n        const otherNode = layerGraph.getNode(w);\n        if (otherNode) {\n          const edgeWeight = edge.weight || 1;\n          weight += edgeWeight;\n          barycenter += (otherNode.order || 0) * edgeWeight;\n        }\n      });\n\n      if (weight > 0) {\n        barycenter /= weight;\n      }\n    }\n\n    nodeData.set(v, { barycenter, weight });\n    vs.push(v);\n\n    const neighbors = layerGraph.edges\n      .filter((e) => e.from === v || e.to === v)\n      .map((e) => (e.from === v ? e.to : e.from));\n    neighbors.sort((a, b) => {\n      const nodeA = layerGraph.getNode(a);\n      const nodeB = layerGraph.getNode(b);\n      return (nodeA?.order || 0) - (nodeB?.order || 0);\n    });\n    neighbors.forEach(dfs);\n  };\n\n  dfs(root);\n\n  // 根据重心值和权重排序\n  vs.sort((a, b) => {\n    const aData = nodeData.get(a);\n    const bData = nodeData.get(b);\n    if (aData && bData) {\n      if (Math.abs(aData.barycenter - bData.barycenter) < 0.001) {\n        return bData.weight - aData.weight;\n      }\n      return aData.barycenter - bData.barycenter;\n    }\n    return 0;\n  });\n\n  return { vs };\n};\n\n// 新增：局部搜索优化\nconst localSearch = (graph: LayoutGraph, layering: { [key: number]: string[] }): void => {\n  const ranks = Object.keys(layering).map(Number);\n  ranks.forEach((rank) => {\n    const layer = layering[rank];\n    for (let i = 0; i < layer.length - 1; i++) {\n      for (let j = i + 1; j < layer.length; j++) {\n        const currentCC = crossCount(graph, layering);\n        // 交换两个节点的位置\n        [layer[i], layer[j]] = [layer[j], layer[i]];\n        const newCC = crossCount(graph, layering);\n        // 如果交叉数增加，则恢复交换\n        if (newCC > currentCC) {\n          [layer[i], layer[j]] = [layer[j], layer[i]];\n        }\n      }\n    }\n  });\n};\n\n// 优化：sweepLayerGraphs 函数\nconst sweepLayerGraphs = (layerGraphs: LayoutGraph[], biasRight: boolean): void => {\n  const cg = buildCompoundGraph();\n  layerGraphs.forEach((lg) => {\n    const root = lg.nodes[0]?.id;\n    if (root) {\n      const sorted = sortSubgraph(lg, root, cg, biasRight);\n      sorted.vs.forEach((v, i) => {\n        const node = lg.getNode(v);\n        if (node) {\n          node.order = i;\n        }\n      });\n      addSubgraphConstraints(lg, cg, sorted.vs);\n    }\n  });\n};\n\n// 更新主函数 order\nexport const order = (graph: LayoutGraph): LayoutGraph => {\n  const maxRank = getMaxRank(graph);\n  const downLayerGraphs = Array.from({ length: maxRank + 1 }, (_, i) =>\n    buildLayerGraph(graph, i, 'inEdges')\n  );\n  const upLayerGraphs = Array.from({ length: maxRank + 1 }, (_, i) =>\n    buildLayerGraph(graph, maxRank - i, 'outEdges')\n  );\n\n  let layering = initOrder(graph);\n  assignOrder(graph, layering);\n\n  let bestCC = Number.POSITIVE_INFINITY;\n  let bestLayering = layering;\n\n  // 增加迭代次数\n  for (let i = 0, lastBest = 0; lastBest < 8; ++i, ++lastBest) {\n    sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2);\n\n    layering = initOrder(graph);\n    localSearch(graph, layering); // 应用局部搜索\n    const cc = crossCount(graph, layering);\n    if (cc < bestCC) {\n      lastBest = 0;\n      bestLayering = JSON.parse(JSON.stringify(layering));\n      bestCC = cc;\n    }\n  }\n\n  assignOrder(graph, bestLayering);\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/rank/feasible-tree.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutEdge } from '../type';\nimport { LayoutGraph } from '../graph';\n\n/**\n * 计算边的松弛度\n */\nconst calculateSlack = (graph: LayoutGraph, edge: LayoutEdge): number => {\n  const sourceNode = graph.getNode(edge.from);\n  const targetNode = graph.getNode(edge.to);\n  if (!sourceNode || !targetNode) {\n    return Number.POSITIVE_INFINITY;\n  }\n  return targetNode.rank - sourceNode.rank - (edge.minlen || 1);\n};\n\n/**\n * 深度优先搜索构建紧致树\n */\nconst dfs = (graph: LayoutGraph, tightTree: LayoutGraph, nodeId: string): void => {\n  const edges = graph.edges.filter((e) => e.from === nodeId || e.to === nodeId);\n  edges.forEach((edge) => {\n    const neighborId = edge.from === nodeId ? edge.to : edge.from;\n    if (!tightTree.hasNode(neighborId) && calculateSlack(graph, edge) === 0) {\n      const neighborNode = graph.getNode(neighborId);\n      if (neighborNode) {\n        tightTree.addLayoutNode({ ...neighborNode });\n        tightTree.addLayoutEdge({ ...edge });\n        dfs(graph, tightTree, neighborId);\n      }\n    }\n  });\n};\n\n/**\n * 构建最大紧致树\n */\nconst buildTightTree = (graph: LayoutGraph, tightTree: LayoutGraph): number => {\n  tightTree.nodes.forEach((node) => dfs(graph, tightTree, node.id));\n  return tightTree.nodes.length;\n};\n\n/**\n * 找到具有最小松弛度的边\n */\nconst findMinSlackEdge = (graph: LayoutGraph, tightTree: LayoutGraph): LayoutEdge | undefined => {\n  let minSlack = Number.POSITIVE_INFINITY;\n  let minSlackEdge: LayoutEdge | undefined;\n\n  graph.edges.forEach((edge) => {\n    const hasSource = tightTree.hasNode(edge.from);\n    const hasTarget = tightTree.hasNode(edge.to);\n    if (hasSource !== hasTarget) {\n      const slack = calculateSlack(graph, edge);\n      if (slack < minSlack) {\n        minSlack = slack;\n        minSlackEdge = edge;\n      }\n    }\n  });\n\n  return minSlackEdge;\n};\n\n/**\n * 调整rank值\n */\nconst shiftRanks = (graph: LayoutGraph, tightTree: LayoutGraph, delta: number): void => {\n  tightTree.nodes.forEach((node) => {\n    const graphNode = graph.getNode(node.id);\n    if (graphNode) {\n      graphNode.rank += delta;\n    }\n  });\n};\n\n/**\n * 构建紧致生成树\n */\nexport const feasibleTree = (graph: LayoutGraph): LayoutGraph => {\n  const tightTree = new LayoutGraph();\n\n  // 选择任意节点作为起始节点\n  const startNode = graph.nodes[0];\n  tightTree.addLayoutNode({ ...startNode });\n\n  while (buildTightTree(graph, tightTree) < graph.nodes.length) {\n    const minSlackEdge = findMinSlackEdge(graph, tightTree);\n    if (minSlackEdge) {\n      const delta = tightTree.hasNode(minSlackEdge.from)\n        ? calculateSlack(graph, minSlackEdge)\n        : -calculateSlack(graph, minSlackEdge);\n      shiftRanks(graph, tightTree, delta);\n    }\n  }\n\n  return tightTree;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/rank/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { feasibleTree } from './feasible-tree';\nexport { longestPath } from './longest-path';\nexport { networkSimplex } from './network-simplex';\nexport { normalizeRanks } from './normalize-ranks';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/rank/longest-path.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutGraph } from '../graph';\n\n/**\n * 计算图中节点的最长路径\n */\nexport const longestPath = (graph: LayoutGraph): LayoutGraph => {\n  // 用于记录已访问的节点\n  const visited: Record<string, boolean> = {};\n\n  /**\n   * 深度优先搜索计算节点的层级\n   * @param nodeId 当前节点ID\n   * @returns 计算得到的层级\n   */\n  const dfs = (nodeId: string): number => {\n    const node = graph.getNode(nodeId);\n\n    // 如果节点不存在，返回 -1\n    if (!node) {\n      return -1;\n    }\n\n    // 如果节点已访问且已经计算过rank，直接返回其rank\n    if (visited[nodeId] && node.rank !== -1) {\n      return node.rank;\n    }\n\n    // 标记节点为已访问\n    visited[nodeId] = true;\n\n    // 获取所有以当前节点为起点的边\n    const outgoingEdges = graph.edges.filter((edge) => edge.from === nodeId);\n\n    // 如果没有出边，说明是叶子节点，rank为0\n    if (outgoingEdges.length === 0) {\n      node.rank = 0;\n      return 0;\n    }\n\n    // 计算所有子节点的最大rank\n    let maxChildRank = -1;\n    outgoingEdges.forEach((edge) => {\n      const childRank = dfs(edge.to);\n      const minlen = edge.minlen || 1; // 使用默认最小长度1，如果未指定\n      maxChildRank = Math.max(maxChildRank, childRank + minlen);\n    });\n\n    // 当前节点的rank为子节点最大rank + 1\n    node.rank = maxChildRank;\n    return node.rank;\n  };\n\n  // 从每个没有入边的节点（源节点）开始DFS\n  const sourceNodes = graph.nodes.filter(\n    (node) => !graph.edges.some((edge) => edge.to === node.id)\n  );\n\n  sourceNodes.forEach((node) => dfs(node.id));\n\n  // 确保所有节点都被访问到\n  graph.nodes.forEach((node) => {\n    if (node.rank === -1) {\n      dfs(node.id);\n    }\n  });\n\n  // 反转rank值，使得源节点的rank最小\n  const maxRank = Math.max(...graph.nodes.map((node) => node.rank));\n  graph.nodes.forEach((node) => {\n    node.rank = maxRank - node.rank;\n  });\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/rank/network-simplex.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutEdge, LayoutNode } from '../type';\nimport { LayoutGraph } from '../graph';\nimport { longestPath } from './longest-path';\nimport { feasibleTree } from './feasible-tree';\n\n/**\n * 网络单纯形\n * 参考 https://github.com/dagrejs/dagre/blob/master/lib/rank/network-simplex.js\n */\nexport const networkSimplex = (g: LayoutGraph): LayoutGraph => {\n  longestPath(g); // 初始化层级\n  const t = feasibleTree(g); // 构建紧致生成树\n  initLowLimValues(t);\n  initCutValues(t, g);\n\n  let e: LayoutEdge | undefined;\n  while ((e = leaveEdge(t))) {\n    const f = enterEdge(t, g, e);\n    if (f) {\n      exchangeEdges(t, g, e, f);\n    }\n  }\n  return g;\n};\n\nconst initLowLimValues = (t: LayoutGraph): void => {\n  const root = t.nodes[0];\n  if (root) {\n    dfsAssignLowLim(t, new Map<string, boolean>(), { nextLim: 1 }, root.id);\n  }\n};\n\nconst dfsAssignLowLim = (\n  t: LayoutGraph,\n  visited: Map<string, boolean>,\n  state: { nextLim: number },\n  v: string,\n  parent?: string\n): void => {\n  const node = t.getNode(v);\n  if (!node) return;\n\n  const low = state.nextLim;\n  visited.set(v, true);\n\n  t.edges\n    .filter((e) => e.from === v || e.to === v)\n    .forEach((e) => {\n      const w = e.from === v ? e.to : e.from;\n      if (!visited.get(w)) {\n        dfsAssignLowLim(t, visited, state, w, v);\n      }\n    });\n\n  node.low = low;\n  node.lim = state.nextLim++;\n  if (parent) {\n    node.parent = parent;\n  } else {\n    delete node.parent;\n  }\n};\n\nconst initCutValues = (t: LayoutGraph, g: LayoutGraph): void => {\n  const vs = postorder(t);\n  vs.slice(0, -1).forEach((v) => assignCutValue(t, g, v));\n};\n\nconst postorder = (t: LayoutGraph): string[] => {\n  const visited = new Set<string>();\n  const result: string[] = [];\n\n  const dfs = (nodeId: string): void => {\n    const node = t.getNode(nodeId);\n    if (!node || visited.has(nodeId)) return;\n\n    visited.add(nodeId);\n\n    t.edges\n      .filter((e) => e.from === nodeId || e.to === nodeId)\n      .forEach((e) => {\n        const neighborId = e.from === nodeId ? e.to : e.from;\n        dfs(neighborId);\n      });\n\n    result.push(nodeId);\n  };\n\n  t.nodes.forEach((node) => dfs(node.id));\n  return result;\n};\n\nconst assignCutValue = (t: LayoutGraph, g: LayoutGraph, childId: string): void => {\n  const child = t.getNode(childId);\n  if (!child || !child.parent) return;\n\n  const edge = t.edges.find((e) => e.from === childId && e.to === child.parent);\n  if (edge) {\n    edge.cutvalue = calcCutValue(t, g, childId);\n  }\n};\n\nconst calcCutValue = (t: LayoutGraph, g: LayoutGraph, childId: string): number => {\n  const child = t.getNode(childId);\n  if (!child || !child.parent) return 0;\n\n  let cutValue = 0;\n  const graphEdge = g.edges.find(\n    (e) =>\n      (e.from === childId && e.to === child.parent) || (e.from === child.parent && e.to === childId)\n  );\n\n  if (graphEdge) {\n    cutValue = graphEdge.weight || 0;\n  }\n\n  g.edges\n    .filter((e) => e.from === childId || e.to === childId)\n    .forEach((e) => {\n      const otherId = e.from === childId ? e.to : e.from;\n      if (otherId !== child.parent) {\n        const otherWeight = e.weight || 0;\n        cutValue += e.from === childId ? otherWeight : -otherWeight;\n        if (isTreeEdge(t, childId, otherId)) {\n          const treeEdge = t.edges.find(\n            (te) =>\n              (te.from === childId && te.to === otherId) ||\n              (te.from === otherId && te.to === childId)\n          );\n          if (treeEdge && treeEdge.cutvalue !== undefined) {\n            cutValue += e.from === childId ? -treeEdge.cutvalue : treeEdge.cutvalue;\n          }\n        }\n      }\n    });\n\n  return cutValue;\n};\n\nconst isTreeEdge = (t: LayoutGraph, u: string, v: string): boolean =>\n  t.edges.some((e) => (e.from === u && e.to === v) || (e.from === v && e.to === u));\n\nconst leaveEdge = (t: LayoutGraph): LayoutEdge | undefined =>\n  t.edges.find((e) => (e.cutvalue || 0) < 0);\n\nconst enterEdge = (t: LayoutGraph, g: LayoutGraph, edge: LayoutEdge): LayoutEdge | undefined => {\n  const vLabel = t.getNode(edge.from);\n  const wLabel = t.getNode(edge.to);\n  if (!vLabel || !wLabel) return undefined;\n\n  const tailLabel = vLabel.lim! > wLabel.lim! ? wLabel : vLabel;\n  const flip = tailLabel === wLabel;\n\n  const candidates = g.edges.filter((e) => {\n    const vNode = t.getNode(e.from);\n    const wNode = t.getNode(e.to);\n    return (\n      vNode &&\n      wNode &&\n      flip === isDescendant(t, vNode, tailLabel) &&\n      flip !== isDescendant(t, wNode, tailLabel)\n    );\n  });\n\n  return candidates.reduce((acc, e) => {\n    if (slack(g, e) < slack(g, acc)) {\n      return e;\n    }\n    return acc;\n  });\n};\n\nconst isDescendant = (t: LayoutGraph, vLabel: LayoutNode, rootLabel: LayoutNode): boolean =>\n  (rootLabel.low || 0) <= (vLabel.lim || 0) && (vLabel.lim || 0) <= (rootLabel.lim || 0);\n\nconst slack = (g: LayoutGraph, edge: LayoutEdge): number => {\n  const source = g.getNode(edge.from);\n  const target = g.getNode(edge.to);\n  if (!source || !target) return Number.POSITIVE_INFINITY;\n  return Math.abs(target.rank - source.rank) - (edge.minlen || 1);\n};\n\nconst exchangeEdges = (t: LayoutGraph, g: LayoutGraph, e: LayoutEdge, f: LayoutEdge): void => {\n  t.removeEdge(e.id);\n  t.addLayoutEdge(f);\n  initLowLimValues(t);\n  initCutValues(t, g);\n  updateRanks(t, g);\n};\n\nconst updateRanks = (t: LayoutGraph, g: LayoutGraph): void => {\n  const root = t.nodes.find((v) => !v.parent);\n  if (!root) return;\n\n  const vs = preorder(t, root.id);\n  vs.slice(1).forEach((v) => {\n    const node = t.getNode(v);\n    const parent = t.getNode(node?.parent || '');\n    if (!node || !parent) return;\n\n    const edge = g.edges.find(\n      (e) => (e.from === v && e.to === node.parent) || (e.from === node.parent && e.to === v)\n    );\n    if (!edge) return;\n\n    const flipped = edge.from === node.parent;\n    node.rank = parent.rank + (flipped ? edge.minlen || 1 : -(edge.minlen || 1));\n  });\n};\n\nconst preorder = (t: LayoutGraph, root: string): string[] => {\n  const result: string[] = [];\n  const visited = new Set<string>();\n\n  const dfs = (nodeId: string): void => {\n    if (visited.has(nodeId)) return;\n    visited.add(nodeId);\n    result.push(nodeId);\n\n    t.edges\n      .filter((e) => e.from === nodeId || e.to === nodeId)\n      .forEach((e) => {\n        const neighborId = e.from === nodeId ? e.to : e.from;\n        dfs(neighborId);\n      });\n  };\n\n  dfs(root);\n  return result;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/rank/normalize-ranks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutGraph } from '../graph';\n\nexport const normalizeRanks = (graph: LayoutGraph): LayoutGraph => {\n  // 获取所有节点的 rank 值\n  const nodeRanks: number[] = graph.nodes.map((node) => {\n    const rank: number = node.rank;\n    return rank === -1 ? Number.MAX_VALUE : rank;\n  });\n\n  // 找出最小的 rank 值\n  const minRank: number = Math.min(...nodeRanks);\n\n  // 调整所有节点的 rank 值\n  graph.nodes.forEach((node) => {\n    if (node.rank !== -1) {\n      node.rank -= minRank;\n    }\n  });\n\n  return graph;\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-layout/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\n\nexport interface LayoutNode {\n  id: string;\n  node: WorkflowNodeEntity;\n  /** 层级 */\n  rank: number;\n  /** 同层级索引 */\n  order: number;\n  /** 位置 */\n  position: {\n    x: number;\n    y: number;\n  };\n  /** 宽高 */\n  size: {\n    width: number;\n    height: number;\n  };\n  low?: number;\n  lim?: number;\n  parent?: string;\n}\n\nexport interface LayoutEdge {\n  id: string;\n  from: string;\n  to: string;\n\n  cutvalue?: number;\n  minlen?: number;\n  weight?: number;\n}\n\nexport interface ILayoutGraph {\n  nodes: LayoutNode[];\n  edges: LayoutEdge[];\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/acyclic.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport greedyFAS from './greedy-fas';\nimport { uniqueId } from './util';\n\nexport const acyclic = {\n  run,\n  undo,\n};\n\nexport default acyclic;\n\nfunction run(g) {\n  let fas = g.graph().acyclicer === 'greedy' ? greedyFAS(g, weightFn(g)) : dfsFAS(g);\n  fas.forEach((e) => {\n    let label = g.edge(e);\n    g.removeEdge(e);\n    label.forwardName = e.name;\n    label.reversed = true;\n    g.setEdge(e.w, e.v, label, uniqueId('rev'));\n  });\n\n  function weightFn(g) {\n    return (e) => {\n      return g.edge(e).weight;\n    };\n  }\n}\n\nfunction dfsFAS(g) {\n  let fas = [];\n  let stack = {};\n  let visited = {};\n\n  function dfs(v) {\n    if (Object.hasOwn(visited, v)) {\n      return;\n    }\n    visited[v] = true;\n    stack[v] = true;\n    g.outEdges(v).forEach((e) => {\n      if (Object.hasOwn(stack, e.w)) {\n        fas.push(e);\n      } else {\n        dfs(e.w);\n      }\n    });\n    delete stack[v];\n  }\n\n  g.nodes().forEach(dfs);\n  return fas;\n}\n\nfunction undo(g) {\n  g.edges().forEach((e) => {\n    let label = g.edge(e);\n    if (label.reversed) {\n      g.removeEdge(e);\n\n      let forwardName = label.forwardName;\n      delete label.reversed;\n      delete label.forwardName;\n      g.setEdge(e.w, e.v, label, forwardName);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/add-border-segments.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { util } from './util';\n\nexport { addBorderSegments };\nexport default addBorderSegments;\n\nfunction addBorderSegments(g) {\n  function dfs(v) {\n    let children = g.children(v);\n    let node = g.node(v);\n    if (children.length) {\n      children.forEach(dfs);\n    }\n\n    if (Object.hasOwn(node, 'minRank')) {\n      node.borderLeft = [];\n      node.borderRight = [];\n      for (let rank = node.minRank, maxRank = node.maxRank + 1; rank < maxRank; ++rank) {\n        addBorderNode(g, 'borderLeft', '_bl', v, node, rank);\n        addBorderNode(g, 'borderRight', '_br', v, node, rank);\n      }\n    }\n  }\n\n  g.children().forEach(dfs);\n}\n\nfunction addBorderNode(g, prop, prefix, sg, sgNode, rank) {\n  let label = { width: 0, height: 0, rank: rank, borderType: prop };\n  let prev = sgNode[prop][rank - 1];\n  let curr = util.addDummyNode(g, 'border', label, prefix);\n  sgNode[prop][rank] = curr;\n  g.setParent(curr, sg);\n  if (prev) {\n    g.setEdge(prev, curr, { weight: 1 });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/coordinate-system.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nexport const coordinateSystem = {\n  adjust,\n  undo,\n};\n\nexport default coordinateSystem;\n\nfunction adjust(g) {\n  let rankDir = g.graph().rankdir.toLowerCase();\n  if (rankDir === 'lr' || rankDir === 'rl') {\n    swapWidthHeight(g);\n  }\n}\n\nfunction undo(g) {\n  let rankDir = g.graph().rankdir.toLowerCase();\n  if (rankDir === 'bt' || rankDir === 'rl') {\n    reverseY(g);\n  }\n\n  if (rankDir === 'lr' || rankDir === 'rl') {\n    swapXY(g);\n    swapWidthHeight(g);\n  }\n}\n\nfunction swapWidthHeight(g) {\n  g.nodes().forEach((v) => swapWidthHeightOne(g.node(v)));\n  g.edges().forEach((e) => swapWidthHeightOne(g.edge(e)));\n}\n\nfunction swapWidthHeightOne(attrs) {\n  let w = attrs.width;\n  attrs.width = attrs.height;\n  attrs.height = w;\n}\n\nfunction reverseY(g) {\n  g.nodes().forEach((v) => reverseYOne(g.node(v)));\n\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    edge.points.forEach(reverseYOne);\n    if (Object.hasOwn(edge, 'y')) {\n      reverseYOne(edge);\n    }\n  });\n}\n\nfunction reverseYOne(attrs) {\n  attrs.y = -attrs.y;\n}\n\nfunction swapXY(g) {\n  g.nodes().forEach((v) => swapXYOne(g.node(v)));\n\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    edge.points.forEach(swapXYOne);\n    if (Object.hasOwn(edge, 'x')) {\n      swapXYOne(edge);\n    }\n  });\n}\n\nfunction swapXYOne(attrs) {\n  let x = attrs.x;\n  attrs.x = attrs.y;\n  attrs.y = x;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/data/list.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/*\n * Simple doubly linked list implementation derived from Cormen, et al.,\n * \"Introduction to Algorithms\".\n */\n\nclass List {\n  constructor() {\n    let sentinel = {};\n    sentinel._next = sentinel._prev = sentinel;\n    this._sentinel = sentinel;\n  }\n\n  dequeue() {\n    let sentinel = this._sentinel;\n    let entry = sentinel._prev;\n    if (entry !== sentinel) {\n      unlink(entry);\n      return entry;\n    }\n  }\n\n  enqueue(entry) {\n    let sentinel = this._sentinel;\n    if (entry._prev && entry._next) {\n      unlink(entry);\n    }\n    entry._next = sentinel._next;\n    sentinel._next._prev = entry;\n    sentinel._next = entry;\n    entry._prev = sentinel;\n  }\n\n  toString() {\n    let strs = [];\n    let sentinel = this._sentinel;\n    let curr = sentinel._prev;\n    while (curr !== sentinel) {\n      strs.push(JSON.stringify(curr, filterOutLinks));\n      curr = curr._prev;\n    }\n    return '[' + strs.join(', ') + ']';\n  }\n}\n\nfunction unlink(entry) {\n  entry._prev._next = entry._next;\n  entry._next._prev = entry._prev;\n  delete entry._next;\n  delete entry._prev;\n}\n\nfunction filterOutLinks(k, v) {\n  if (k !== '_next' && k !== '_prev') {\n    return v;\n  }\n}\n\nexport default List;\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/debug.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { util } from './util';\nimport { Graph } from '@dagrejs/graphlib';\n\nexport { debugOrdering };\n\n/* istanbul ignore next */\nfunction debugOrdering(g) {\n  let layerMatrix = util.buildLayerMatrix(g);\n\n  let h = new Graph({ compound: true, multigraph: true }).setGraph({});\n\n  g.nodes().forEach((v) => {\n    h.setNode(v, { label: v });\n    h.setParent(v, 'layer' + g.node(v).rank);\n  });\n\n  g.edges().forEach((e) => h.setEdge(e.v, e.w, {}, e.name));\n\n  layerMatrix.forEach((layer, i) => {\n    let layerV = 'layer' + i;\n    h.setNode(layerV, { rank: 'same' });\n    layer.reduce((u, v) => {\n      h.setEdge(u, v, { style: 'invis' });\n      return v;\n    });\n  });\n\n  return h;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/greedy-fas.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Graph } from '@dagrejs/graphlib';\nimport List from './data/list';\n\n/*\n * A greedy heuristic for finding a feedback arc set for a graph. A feedback\n * arc set is a set of edges that can be removed to make a graph acyclic.\n * The algorithm comes from: P. Eades, X. Lin, and W. F. Smyth, \"A fast and\n * effective heuristic for the feedback arc set problem.\" This implementation\n * adjusts that from the paper to allow for weighted edges.\n */\nexport { greedyFAS };\nexport default greedyFAS;\n\nlet DEFAULT_WEIGHT_FN = () => 1;\n\nfunction greedyFAS(g, weightFn) {\n  if (g.nodeCount() <= 1) {\n    return [];\n  }\n  let state = buildState(g, weightFn || DEFAULT_WEIGHT_FN);\n  let results = doGreedyFAS(state.graph, state.buckets, state.zeroIdx);\n\n  // Expand multi-edges\n  return results.flatMap((e) => g.outEdges(e.v, e.w));\n}\n\nfunction doGreedyFAS(g, buckets, zeroIdx) {\n  let results = [];\n  let sources = buckets[buckets.length - 1];\n  let sinks = buckets[0];\n\n  let entry;\n  while (g.nodeCount()) {\n    while ((entry = sinks.dequeue())) {\n      removeNode(g, buckets, zeroIdx, entry);\n    }\n    while ((entry = sources.dequeue())) {\n      removeNode(g, buckets, zeroIdx, entry);\n    }\n    if (g.nodeCount()) {\n      for (let i = buckets.length - 2; i > 0; --i) {\n        entry = buckets[i].dequeue();\n        if (entry) {\n          results = results.concat(removeNode(g, buckets, zeroIdx, entry, true));\n          break;\n        }\n      }\n    }\n  }\n\n  return results;\n}\n\nfunction removeNode(g, buckets, zeroIdx, entry, collectPredecessors) {\n  let results = collectPredecessors ? [] : undefined;\n\n  g.inEdges(entry.v).forEach((edge) => {\n    let weight = g.edge(edge);\n    let uEntry = g.node(edge.v);\n\n    if (collectPredecessors) {\n      results.push({ v: edge.v, w: edge.w });\n    }\n\n    uEntry.out -= weight;\n    assignBucket(buckets, zeroIdx, uEntry);\n  });\n\n  g.outEdges(entry.v).forEach((edge) => {\n    let weight = g.edge(edge);\n    let w = edge.w;\n    let wEntry = g.node(w);\n    wEntry['in'] -= weight;\n    assignBucket(buckets, zeroIdx, wEntry);\n  });\n\n  g.removeNode(entry.v);\n\n  return results;\n}\n\nfunction buildState(g, weightFn) {\n  let fasGraph = new Graph();\n  let maxIn = 0;\n  let maxOut = 0;\n\n  g.nodes().forEach((v) => {\n    fasGraph.setNode(v, { v: v, in: 0, out: 0 });\n  });\n\n  // Aggregate weights on nodes, but also sum the weights across multi-edges\n  // into a single edge for the fasGraph.\n  g.edges().forEach((e) => {\n    let prevWeight = fasGraph.edge(e.v, e.w) || 0;\n    let weight = weightFn(e);\n    let edgeWeight = prevWeight + weight;\n    fasGraph.setEdge(e.v, e.w, edgeWeight);\n    maxOut = Math.max(maxOut, (fasGraph.node(e.v).out += weight));\n    maxIn = Math.max(maxIn, (fasGraph.node(e.w)['in'] += weight));\n  });\n\n  let buckets = range(maxOut + maxIn + 3).map(() => new List());\n  let zeroIdx = maxIn + 1;\n\n  fasGraph.nodes().forEach((v) => {\n    assignBucket(buckets, zeroIdx, fasGraph.node(v));\n  });\n\n  return { graph: fasGraph, buckets: buckets, zeroIdx: zeroIdx };\n}\n\nfunction assignBucket(buckets, zeroIdx, entry) {\n  if (!entry.out) {\n    buckets[0].enqueue(entry);\n  } else if (!entry['in']) {\n    buckets[buckets.length - 1].enqueue(entry);\n  } else {\n    buckets[entry.out - entry['in'] + zeroIdx].enqueue(entry);\n  }\n}\n\nfunction range(limit) {\n  const range = [];\n  for (let i = 0; i < limit; i++) {\n    range.push(i);\n  }\n\n  return range;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\n/**\n * Dagre DAG 布局库\n * 开源协议 - MIT\n * 源码 - https://github.com/dagrejs/dagre\n * 论文 - https://graphviz.org/documentation/TSE93.pdf\n */\n\nimport acyclic from './acyclic';\nimport normalize from './normalize';\nimport rank from './rank';\nimport { normalizeRanks, removeEmptyRanks } from './util';\nimport parentDummyChains from './parent-dummy-chains';\nimport nestingGraph from './nesting-graph';\nimport addBorderSegments from './add-border-segments';\nimport coordinateSystem from './coordinate-system';\nimport order from './order';\nimport position from './position';\nimport { util } from './util';\n\nimport {\n  layout,\n  buildLayoutGraph,\n  updateInputGraph,\n  makeSpaceForEdgeLabels,\n  removeSelfEdges,\n  injectEdgeLabelProxies,\n  assignRankMinMax,\n  removeEdgeLabelProxies,\n  insertSelfEdges,\n  positionSelfEdges,\n  removeBorderNodes,\n  fixupEdgeLabelCoords,\n  translateGraph,\n  assignNodeIntersects,\n  reversePointsForReversedEdges,\n} from './layout';\n\nconst dagreLib = {\n  layout,\n  buildLayoutGraph,\n  updateInputGraph,\n  makeSpaceForEdgeLabels,\n  removeSelfEdges,\n  acyclic,\n  nestingGraph,\n  rank,\n  util,\n  injectEdgeLabelProxies,\n  removeEmptyRanks,\n  normalizeRanks,\n  assignRankMinMax,\n  removeEdgeLabelProxies,\n  normalize,\n  parentDummyChains,\n  addBorderSegments,\n  order,\n  insertSelfEdges,\n  coordinateSystem,\n  position,\n  positionSelfEdges,\n  removeBorderNodes,\n  fixupEdgeLabelCoords,\n  translateGraph,\n  assignNodeIntersects,\n  reversePointsForReversedEdges,\n};\n\nexport { dagreLib };\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/layout.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport acyclic from './acyclic';\nimport normalize from './normalize';\nimport rank from './rank';\nimport { normalizeRanks, removeEmptyRanks, util } from './util';\nimport parentDummyChains from './parent-dummy-chains';\nimport nestingGraph from './nesting-graph';\nimport addBorderSegments from './add-border-segments';\nimport coordinateSystem from './coordinate-system';\nimport order from './order';\nimport position from './position';\nimport { Graph } from '@dagrejs/graphlib';\n\nexport {\n  layout,\n  buildLayoutGraph,\n  updateInputGraph,\n  makeSpaceForEdgeLabels,\n  removeSelfEdges,\n  injectEdgeLabelProxies,\n  assignRankMinMax,\n  removeEdgeLabelProxies,\n  insertSelfEdges,\n  positionSelfEdges,\n  removeBorderNodes,\n  fixupEdgeLabelCoords,\n  translateGraph,\n  assignNodeIntersects,\n  reversePointsForReversedEdges,\n};\n\nfunction layout(g, opts) {\n  let time = opts && opts.debugTiming ? util.time : util.notime;\n  time('layout', () => {\n    let layoutGraph = time('  buildLayoutGraph', () => buildLayoutGraph(g));\n    time('  runLayout', () => runLayout(layoutGraph, time, opts));\n    time('  updateInputGraph', () => updateInputGraph(g, layoutGraph));\n  });\n}\n\nfunction runLayout(g, time, opts) {\n  time('    makeSpaceForEdgeLabels', () => makeSpaceForEdgeLabels(g));\n  time('    removeSelfEdges', () => removeSelfEdges(g));\n  time('    acyclic', () => acyclic.run(g));\n  time('    nestingGraph.run', () => nestingGraph.run(g));\n  time('    rank', () => rank(util.asNonCompoundGraph(g)));\n  time('    injectEdgeLabelProxies', () => injectEdgeLabelProxies(g));\n  time('    removeEmptyRanks', () => removeEmptyRanks(g));\n  time('    nestingGraph.cleanup', () => nestingGraph.cleanup(g));\n  time('    normalizeRanks', () => normalizeRanks(g));\n  time('    assignRankMinMax', () => assignRankMinMax(g));\n  time('    removeEdgeLabelProxies', () => removeEdgeLabelProxies(g));\n  time('    normalize.run', () => normalize.run(g));\n  time('    parentDummyChains', () => parentDummyChains(g));\n  time('    addBorderSegments', () => addBorderSegments(g));\n  time('    order', () => order(g, opts));\n  time('    insertSelfEdges', () => insertSelfEdges(g));\n  time('    adjustCoordinateSystem', () => coordinateSystem.adjust(g));\n  time('    position', () => position(g));\n  time('    positionSelfEdges', () => positionSelfEdges(g));\n  time('    removeBorderNodes', () => removeBorderNodes(g));\n  time('    normalize.undo', () => normalize.undo(g));\n  time('    fixupEdgeLabelCoords', () => fixupEdgeLabelCoords(g));\n  time('    undoCoordinateSystem', () => coordinateSystem.undo(g));\n  time('    translateGraph', () => translateGraph(g));\n  time('    assignNodeIntersects', () => assignNodeIntersects(g));\n  time('    reversePoints', () => reversePointsForReversedEdges(g));\n  time('    acyclic.undo', () => acyclic.undo(g));\n}\n\n/*\n * Copies final layout information from the layout graph back to the input\n * graph. This process only copies whitelisted attributes from the layout graph\n * to the input graph, so it serves as a good place to determine what\n * attributes can influence layout.\n */\nfunction updateInputGraph(inputGraph, layoutGraph) {\n  inputGraph.nodes().forEach((v) => {\n    let inputLabel = inputGraph.node(v);\n    let layoutLabel = layoutGraph.node(v);\n\n    if (inputLabel) {\n      inputLabel.x = layoutLabel.x;\n      inputLabel.y = layoutLabel.y;\n      inputLabel.rank = layoutLabel.rank;\n\n      if (layoutGraph.children(v).length) {\n        inputLabel.width = layoutLabel.width;\n        inputLabel.height = layoutLabel.height;\n      }\n    }\n  });\n\n  inputGraph.edges().forEach((e) => {\n    let inputLabel = inputGraph.edge(e);\n    let layoutLabel = layoutGraph.edge(e);\n\n    inputLabel.points = layoutLabel.points;\n    if (Object.hasOwn(layoutLabel, 'x')) {\n      inputLabel.x = layoutLabel.x;\n      inputLabel.y = layoutLabel.y;\n    }\n  });\n\n  inputGraph.graph().width = layoutGraph.graph().width;\n  inputGraph.graph().height = layoutGraph.graph().height;\n}\n\nlet graphNumAttrs = ['nodesep', 'edgesep', 'ranksep', 'marginx', 'marginy'];\nlet graphDefaults = { ranksep: 50, edgesep: 20, nodesep: 50, rankdir: 'tb' };\nlet graphAttrs = ['acyclicer', 'ranker', 'rankdir', 'align'];\nlet nodeNumAttrs = ['width', 'height'];\nlet nodeDefaults = { width: 0, height: 0 };\nlet edgeNumAttrs = ['minlen', 'weight', 'width', 'height', 'labeloffset'];\nlet edgeDefaults = {\n  minlen: 1,\n  weight: 1,\n  width: 0,\n  height: 0,\n  labeloffset: 10,\n  labelpos: 'r',\n};\nlet edgeAttrs = ['labelpos'];\n\n/*\n * Constructs a new graph from the input graph, which can be used for layout.\n * This process copies only whitelisted attributes from the input graph to the\n * layout graph. Thus this function serves as a good place to determine what\n * attributes can influence layout.\n */\nfunction buildLayoutGraph(inputGraph) {\n  let g = new Graph({ multigraph: true, compound: true });\n  let graph = canonicalize(inputGraph.graph());\n\n  g.setGraph(\n    Object.assign(\n      {},\n      graphDefaults,\n      selectNumberAttrs(graph, graphNumAttrs),\n      util.pick(graph, graphAttrs)\n    )\n  );\n\n  inputGraph.nodes().forEach((v) => {\n    let node = canonicalize(inputGraph.node(v));\n    const newNode = selectNumberAttrs(node, nodeNumAttrs);\n    Object.keys(nodeDefaults).forEach((k) => {\n      if (newNode[k] === undefined) {\n        newNode[k] = nodeDefaults[k];\n      }\n    });\n\n    g.setNode(v, newNode);\n    g.setParent(v, inputGraph.parent(v));\n  });\n\n  inputGraph.edges().forEach((e) => {\n    let edge = canonicalize(inputGraph.edge(e));\n    g.setEdge(\n      e,\n      Object.assign(\n        {},\n        edgeDefaults,\n        selectNumberAttrs(edge, edgeNumAttrs),\n        util.pick(edge, edgeAttrs)\n      )\n    );\n  });\n\n  return g;\n}\n\n/*\n * This idea comes from the Gansner paper: to account for edge labels in our\n * layout we split each rank in half by doubling minlen and halving ranksep.\n * Then we can place labels at these mid-points between nodes.\n *\n * We also add some minimal padding to the width to push the label for the edge\n * away from the edge itself a bit.\n */\nfunction makeSpaceForEdgeLabels(g) {\n  let graph = g.graph();\n  graph.ranksep /= 2;\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    edge.minlen *= 2;\n    if (edge.labelpos.toLowerCase() !== 'c') {\n      if (graph.rankdir === 'TB' || graph.rankdir === 'BT') {\n        edge.width += edge.labeloffset;\n      } else {\n        edge.height += edge.labeloffset;\n      }\n    }\n  });\n}\n\n/*\n * Creates temporary dummy nodes that capture the rank in which each edge's\n * label is going to, if it has one of non-zero width and height. We do this\n * so that we can safely remove empty ranks while preserving balance for the\n * label's position.\n */\nfunction injectEdgeLabelProxies(g) {\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    if (edge.width && edge.height) {\n      let v = g.node(e.v);\n      let w = g.node(e.w);\n      let label = { rank: (w.rank - v.rank) / 2 + v.rank, e: e };\n      util.addDummyNode(g, 'edge-proxy', label, '_ep');\n    }\n  });\n}\n\nfunction assignRankMinMax(g) {\n  let maxRank = 0;\n  g.nodes().forEach((v) => {\n    let node = g.node(v);\n    if (node.borderTop) {\n      node.minRank = g.node(node.borderTop).rank;\n      node.maxRank = g.node(node.borderBottom).rank;\n      maxRank = Math.max(maxRank, node.maxRank);\n    }\n  });\n  g.graph().maxRank = maxRank;\n}\n\nfunction removeEdgeLabelProxies(g) {\n  g.nodes().forEach((v) => {\n    let node = g.node(v);\n    if (node.dummy === 'edge-proxy') {\n      g.edge(node.e).labelRank = node.rank;\n      g.removeNode(v);\n    }\n  });\n}\n\nfunction translateGraph(g) {\n  let minX = Number.POSITIVE_INFINITY;\n  let maxX = 0;\n  let minY = Number.POSITIVE_INFINITY;\n  let maxY = 0;\n  let graphLabel = g.graph();\n  let marginX = graphLabel.marginx || 0;\n  let marginY = graphLabel.marginy || 0;\n\n  function getExtremes(attrs) {\n    let x = attrs.x;\n    let y = attrs.y;\n    let w = attrs.width;\n    let h = attrs.height;\n    minX = Math.min(minX, x - w / 2);\n    maxX = Math.max(maxX, x + w / 2);\n    minY = Math.min(minY, y - h / 2);\n    maxY = Math.max(maxY, y + h / 2);\n  }\n\n  g.nodes().forEach((v) => getExtremes(g.node(v)));\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    if (Object.hasOwn(edge, 'x')) {\n      getExtremes(edge);\n    }\n  });\n\n  minX -= marginX;\n  minY -= marginY;\n\n  g.nodes().forEach((v) => {\n    let node = g.node(v);\n    node.x -= minX;\n    node.y -= minY;\n  });\n\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    edge.points.forEach((p) => {\n      p.x -= minX;\n      p.y -= minY;\n    });\n    if (Object.hasOwn(edge, 'x')) {\n      edge.x -= minX;\n    }\n    if (Object.hasOwn(edge, 'y')) {\n      edge.y -= minY;\n    }\n  });\n\n  graphLabel.width = maxX - minX + marginX;\n  graphLabel.height = maxY - minY + marginY;\n}\n\nfunction assignNodeIntersects(g) {\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    let nodeV = g.node(e.v);\n    let nodeW = g.node(e.w);\n    let p1, p2;\n    if (!edge.points) {\n      edge.points = [];\n      p1 = nodeW;\n      p2 = nodeV;\n    } else {\n      p1 = edge.points[0];\n      p2 = edge.points[edge.points.length - 1];\n    }\n    edge.points.unshift(util.intersectRect(nodeV, p1));\n    edge.points.push(util.intersectRect(nodeW, p2));\n  });\n}\n\nfunction fixupEdgeLabelCoords(g) {\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    if (Object.hasOwn(edge, 'x')) {\n      if (edge.labelpos === 'l' || edge.labelpos === 'r') {\n        edge.width -= edge.labeloffset;\n      }\n      switch (edge.labelpos) {\n        case 'l':\n          edge.x -= edge.width / 2 + edge.labeloffset;\n          break;\n        case 'r':\n          edge.x += edge.width / 2 + edge.labeloffset;\n          break;\n      }\n    }\n  });\n}\n\nfunction reversePointsForReversedEdges(g) {\n  g.edges().forEach((e) => {\n    let edge = g.edge(e);\n    if (edge.reversed) {\n      edge.points.reverse();\n    }\n  });\n}\n\nfunction removeBorderNodes(g) {\n  g.nodes().forEach((v) => {\n    if (g.children(v).length) {\n      let node = g.node(v);\n      let t = g.node(node.borderTop);\n      let b = g.node(node.borderBottom);\n      let l = g.node(node.borderLeft[node.borderLeft.length - 1]);\n      let r = g.node(node.borderRight[node.borderRight.length - 1]);\n\n      node.width = Math.abs(r.x - l.x);\n      node.height = Math.abs(b.y - t.y);\n      node.x = l.x + node.width / 2;\n      node.y = t.y + node.height / 2;\n    }\n  });\n\n  g.nodes().forEach((v) => {\n    if (g.node(v).dummy === 'border') {\n      g.removeNode(v);\n    }\n  });\n}\n\nfunction removeSelfEdges(g) {\n  g.edges().forEach((e) => {\n    if (e.v === e.w) {\n      var node = g.node(e.v);\n      if (!node.selfEdges) {\n        node.selfEdges = [];\n      }\n      node.selfEdges.push({ e: e, label: g.edge(e) });\n      g.removeEdge(e);\n    }\n  });\n}\n\nfunction insertSelfEdges(g) {\n  var layers = util.buildLayerMatrix(g);\n  layers.forEach((layer) => {\n    var orderShift = 0;\n    layer.forEach((v, i) => {\n      var node = g.node(v);\n      node.order = i + orderShift;\n      (node.selfEdges || []).forEach((selfEdge) => {\n        util.addDummyNode(\n          g,\n          'selfedge',\n          {\n            width: selfEdge.label.width,\n            height: selfEdge.label.height,\n            rank: node.rank,\n            order: i + ++orderShift,\n            e: selfEdge.e,\n            label: selfEdge.label,\n          },\n          '_se'\n        );\n      });\n      delete node.selfEdges;\n    });\n  });\n}\n\nfunction positionSelfEdges(g) {\n  g.nodes().forEach((v) => {\n    var node = g.node(v);\n    if (node.dummy === 'selfedge') {\n      var selfNode = g.node(node.e.v);\n      var x = selfNode.x + selfNode.width / 2;\n      var y = selfNode.y;\n      var dx = node.x - x;\n      var dy = selfNode.height / 2;\n      g.setEdge(node.e, node.label);\n      g.removeNode(v);\n      node.label.points = [\n        { x: x + (2 * dx) / 3, y: y - dy },\n        { x: x + (5 * dx) / 6, y: y - dy },\n        { x: x + dx, y: y },\n        { x: x + (5 * dx) / 6, y: y + dy },\n        { x: x + (2 * dx) / 3, y: y + dy },\n      ];\n      node.label.x = node.x;\n      node.label.y = node.y;\n    }\n  });\n}\n\nfunction selectNumberAttrs(obj, attrs) {\n  return util.mapValues(util.pick(obj, attrs), Number);\n}\n\nfunction canonicalize(attrs) {\n  var newAttrs = {};\n  if (attrs) {\n    Object.entries(attrs).forEach(([k, v]) => {\n      if (typeof k === 'string') {\n        k = k.toLowerCase();\n      }\n\n      newAttrs[k] = v;\n    });\n  }\n  return newAttrs;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/nesting-graph.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { util } from './util';\n\nexport const nestingGraph = {\n  run,\n  cleanup,\n};\n\nexport default nestingGraph;\n\n/*\n * A nesting graph creates dummy nodes for the tops and bottoms of subgraphs,\n * adds appropriate edges to ensure that all cluster nodes are placed between\n * these boundaries, and ensures that the graph is connected.\n *\n * In addition we ensure, through the use of the minlen property, that nodes\n * and subgraph border nodes to not end up on the same rank.\n *\n * Preconditions:\n *\n *    1. Input graph is a DAG\n *    2. Nodes in the input graph has a minlen attribute\n *\n * Postconditions:\n *\n *    1. Input graph is connected.\n *    2. Dummy nodes are added for the tops and bottoms of subgraphs.\n *    3. The minlen attribute for nodes is adjusted to ensure nodes do not\n *       get placed on the same rank as subgraph border nodes.\n *\n * The nesting graph idea comes from Sander, \"Layout of Compound Directed\n * Graphs.\"\n */\nfunction run(g) {\n  let root = util.addDummyNode(g, 'root', {}, '_root');\n  let depths = treeDepths(g);\n  let depthsArr = Object.values(depths);\n  let height = util.applyWithChunking(Math.max, depthsArr) - 1; // Note: depths is an Object not an array\n  let nodeSep = 2 * height + 1;\n\n  g.graph().nestingRoot = root;\n\n  // Multiply minlen by nodeSep to align nodes on non-border ranks.\n  g.edges().forEach((e) => (g.edge(e).minlen *= nodeSep));\n\n  // Calculate a weight that is sufficient to keep subgraphs vertically compact\n  let weight = sumWeights(g) + 1;\n\n  // Create border nodes and link them up\n  g.children().forEach((child) => dfs(g, root, nodeSep, weight, height, depths, child));\n\n  // Save the multiplier for node layers for later removal of empty border\n  // layers.\n  g.graph().nodeRankFactor = nodeSep;\n}\n\nfunction dfs(g, root, nodeSep, weight, height, depths, v) {\n  let children = g.children(v);\n  if (!children.length) {\n    if (v !== root) {\n      g.setEdge(root, v, { weight: 0, minlen: nodeSep });\n    }\n    return;\n  }\n\n  let top = util.addBorderNode(g, '_bt');\n  let bottom = util.addBorderNode(g, '_bb');\n  let label = g.node(v);\n\n  g.setParent(top, v);\n  label.borderTop = top;\n  g.setParent(bottom, v);\n  label.borderBottom = bottom;\n\n  children.forEach((child) => {\n    dfs(g, root, nodeSep, weight, height, depths, child);\n\n    let childNode = g.node(child);\n    let childTop = childNode.borderTop ? childNode.borderTop : child;\n    let childBottom = childNode.borderBottom ? childNode.borderBottom : child;\n    let thisWeight = childNode.borderTop ? weight : 2 * weight;\n    let minlen = childTop !== childBottom ? 1 : height - depths[v] + 1;\n\n    g.setEdge(top, childTop, {\n      weight: thisWeight,\n      minlen: minlen,\n      nestingEdge: true,\n    });\n\n    g.setEdge(childBottom, bottom, {\n      weight: thisWeight,\n      minlen: minlen,\n      nestingEdge: true,\n    });\n  });\n\n  if (!g.parent(v)) {\n    g.setEdge(root, top, { weight: 0, minlen: height + depths[v] });\n  }\n}\n\nfunction treeDepths(g) {\n  var depths = {};\n  function dfs(v, depth) {\n    var children = g.children(v);\n    if (children && children.length) {\n      children.forEach((child) => dfs(child, depth + 1));\n    }\n    depths[v] = depth;\n  }\n  g.children().forEach((v) => dfs(v, 1));\n  return depths;\n}\n\nfunction sumWeights(g) {\n  return g.edges().reduce((acc, e) => acc + g.edge(e).weight, 0);\n}\n\nfunction cleanup(g) {\n  var graphLabel = g.graph();\n  g.removeNode(graphLabel.nestingRoot);\n  delete graphLabel.nestingRoot;\n  g.edges().forEach((e) => {\n    var edge = g.edge(e);\n    if (edge.nestingEdge) {\n      g.removeEdge(e);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/normalize.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { util } from './util';\n\nexport const normalize = {\n  run,\n  undo,\n};\n\nexport default normalize;\n\n/*\n * Breaks any long edges in the graph into short segments that span 1 layer\n * each. This operation is undoable with the denormalize function.\n *\n * Pre-conditions:\n *\n *    1. The input graph is a DAG.\n *    2. Each node in the graph has a \"rank\" property.\n *\n * Post-condition:\n *\n *    1. All edges in the graph have a length of 1.\n *    2. Dummy nodes are added where edges have been split into segments.\n *    3. The graph is augmented with a \"dummyChains\" attribute which contains\n *       the first dummy in each chain of dummy nodes produced.\n */\nfunction run(g) {\n  g.graph().dummyChains = [];\n  g.edges().forEach((edge) => normalizeEdge(g, edge));\n}\n\nfunction normalizeEdge(g, e) {\n  let v = e.v;\n  let vRank = g.node(v).rank;\n  let w = e.w;\n  let wRank = g.node(w).rank;\n  let name = e.name;\n  let edgeLabel = g.edge(e);\n  let labelRank = edgeLabel.labelRank;\n\n  if (wRank === vRank + 1) return;\n\n  g.removeEdge(e);\n\n  let dummy, attrs, i;\n  for (i = 0, ++vRank; vRank < wRank; ++i, ++vRank) {\n    edgeLabel.points = [];\n    attrs = {\n      width: 0,\n      height: 0,\n      edgeLabel: edgeLabel,\n      edgeObj: e,\n      rank: vRank,\n    };\n    dummy = util.addDummyNode(g, 'edge', attrs, '_d');\n    if (vRank === labelRank) {\n      attrs.width = edgeLabel.width;\n      attrs.height = edgeLabel.height;\n      attrs.dummy = 'edge-label';\n      attrs.labelpos = edgeLabel.labelpos;\n    }\n    g.setEdge(v, dummy, { weight: edgeLabel.weight }, name);\n    if (i === 0) {\n      g.graph().dummyChains.push(dummy);\n    }\n    v = dummy;\n  }\n\n  g.setEdge(v, w, { weight: edgeLabel.weight }, name);\n}\n\nfunction undo(g) {\n  g.graph().dummyChains.forEach((v) => {\n    let node = g.node(v);\n    let origLabel = node.edgeLabel;\n    let w;\n    g.setEdge(node.edgeObj, origLabel);\n    while (node.dummy) {\n      w = g.successors(v)[0];\n      g.removeNode(v);\n      origLabel.points.push({ x: node.x, y: node.y });\n      if (node.dummy === 'edge-label') {\n        origLabel.x = node.x;\n        origLabel.y = node.y;\n        origLabel.width = node.width;\n        origLabel.height = node.height;\n      }\n      v = w;\n      node = g.node(v);\n    }\n  });\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/add-subgraph-constraints.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { addSubgraphConstraints };\nexport default addSubgraphConstraints;\n\nfunction addSubgraphConstraints(g, cg, vs) {\n  let prev = {},\n    rootPrev;\n\n  vs.forEach((v) => {\n    let child = g.parent(v),\n      parent,\n      prevChild;\n    while (child) {\n      parent = g.parent(child);\n      if (parent) {\n        prevChild = prev[parent];\n        prev[parent] = child;\n      } else {\n        prevChild = rootPrev;\n        rootPrev = child;\n      }\n      if (prevChild && prevChild !== child) {\n        cg.setEdge(prevChild, child);\n        return;\n      }\n      child = parent;\n    }\n  });\n\n  /*\n  function dfs(v) {\n    var children = v ? g.children(v) : g.children();\n    if (children.length) {\n      var min = Number.POSITIVE_INFINITY,\n          subgraphs = [];\n      children.forEach(function(child) {\n        var childMin = dfs(child);\n        if (g.children(child).length) {\n          subgraphs.push({ v: child, order: childMin });\n        }\n        min = Math.min(min, childMin);\n      });\n      _.sortBy(subgraphs, \"order\").reduce(function(prev, curr) {\n        cg.setEdge(prev.v, curr.v);\n        return curr;\n      });\n      return min;\n    }\n    return g.node(v).order;\n  }\n  dfs(undefined);\n  */\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/barycenter.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { barycenter };\nexport default barycenter;\n\nfunction barycenter(g, movable = []) {\n  return movable.map((v) => {\n    let inV = g.inEdges(v);\n    if (!inV.length) {\n      return { v: v };\n    } else {\n      let result = inV.reduce(\n        (acc, e) => {\n          let edge = g.edge(e),\n            nodeU = g.node(e.v);\n          return {\n            sum: acc.sum + edge.weight * nodeU.order,\n            weight: acc.weight + edge.weight,\n          };\n        },\n        { sum: 0, weight: 0 }\n      );\n\n      return {\n        v: v,\n        barycenter: result.sum / result.weight,\n        weight: result.weight,\n      };\n    }\n  });\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/build-layer-graph.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Graph } from '@dagrejs/graphlib';\nimport { util } from '../util';\n\nexport { buildLayerGraph };\nexport default buildLayerGraph;\n\n/*\n * Constructs a graph that can be used to sort a layer of nodes. The graph will\n * contain all base and subgraph nodes from the request layer in their original\n * hierarchy and any edges that are incident on these nodes and are of the type\n * requested by the \"relationship\" parameter.\n *\n * Nodes from the requested rank that do not have parents are assigned a root\n * node in the output graph, which is set in the root graph attribute. This\n * makes it easy to walk the hierarchy of movable nodes during ordering.\n *\n * Pre-conditions:\n *\n *    1. Input graph is a DAG\n *    2. Base nodes in the input graph have a rank attribute\n *    3. Subgraph nodes in the input graph has minRank and maxRank attributes\n *    4. Edges have an assigned weight\n *\n * Post-conditions:\n *\n *    1. Output graph has all nodes in the movable rank with preserved\n *       hierarchy.\n *    2. Root nodes in the movable layer are made children of the node\n *       indicated by the root attribute of the graph.\n *    3. Non-movable nodes incident on movable nodes, selected by the\n *       relationship parameter, are included in the graph (without hierarchy).\n *    4. Edges incident on movable nodes, selected by the relationship\n *       parameter, are added to the output graph.\n *    5. The weights for copied edges are aggregated as need, since the output\n *       graph is not a multi-graph.\n */\nfunction buildLayerGraph(g, rank, relationship) {\n  let root = createRootNode(g),\n    result = new Graph({ compound: true })\n      .setGraph({ root: root })\n      .setDefaultNodeLabel((v) => g.node(v));\n\n  g.nodes().forEach((v) => {\n    let node = g.node(v),\n      parent = g.parent(v);\n\n    if (node.rank === rank || (node.minRank <= rank && rank <= node.maxRank)) {\n      result.setNode(v);\n      result.setParent(v, parent || root);\n\n      // This assumes we have only short edges!\n      g[relationship](v).forEach((e) => {\n        let u = e.v === v ? e.w : e.v,\n          edge = result.edge(u, v),\n          weight = edge !== undefined ? edge.weight : 0;\n        result.setEdge(u, v, { weight: g.edge(e).weight + weight });\n      });\n\n      if (Object.hasOwn(node, 'minRank')) {\n        result.setNode(v, {\n          borderLeft: node.borderLeft[rank],\n          borderRight: node.borderRight[rank],\n        });\n      }\n    }\n  });\n\n  return result;\n}\n\nfunction createRootNode(g) {\n  var v;\n  while (g.hasNode((v = util.uniqueId('_root'))));\n  return v;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/cross-count.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { zipObject } from '../util';\n\nexport { crossCount };\nexport default crossCount;\n\n/*\n * A function that takes a layering (an array of layers, each with an array of\n * ordererd nodes) and a graph and returns a weighted crossing count.\n *\n * Pre-conditions:\n *\n *    1. Input graph must be simple (not a multigraph), directed, and include\n *       only simple edges.\n *    2. Edges in the input graph must have assigned weights.\n *\n * Post-conditions:\n *\n *    1. The graph and layering matrix are left unchanged.\n *\n * This algorithm is derived from Barth, et al., \"Bilayer Cross Counting.\"\n */\nfunction crossCount(g, layering) {\n  let cc = 0;\n  for (let i = 1; i < layering.length; ++i) {\n    cc += twoLayerCrossCount(g, layering[i - 1], layering[i]);\n  }\n  return cc;\n}\n\nfunction twoLayerCrossCount(g, northLayer, southLayer) {\n  // Sort all of the edges between the north and south layers by their position\n  // in the north layer and then the south. Map these edges to the position of\n  // their head in the south layer.\n  let southPos = zipObject(\n    southLayer,\n    southLayer.map((v, i) => i)\n  );\n  let southEntries = northLayer.flatMap((v) => {\n    return g\n      .outEdges(v)\n      .map((e) => {\n        return { pos: southPos[e.w], weight: g.edge(e).weight };\n      })\n      .sort((a, b) => a.pos - b.pos);\n  });\n\n  // Build the accumulator tree\n  let firstIndex = 1;\n  while (firstIndex < southLayer.length) firstIndex <<= 1;\n  let treeSize = 2 * firstIndex - 1;\n  firstIndex -= 1;\n  let tree = new Array(treeSize).fill(0);\n\n  // Calculate the weighted crossings\n  let cc = 0;\n  southEntries.forEach((entry) => {\n    let index = entry.pos + firstIndex;\n    tree[index] += entry.weight;\n    let weightSum = 0;\n    while (index > 0) {\n      if (index % 2) {\n        weightSum += tree[index + 1];\n      }\n      index = (index - 1) >> 1;\n      tree[index] += entry.weight;\n    }\n    cc += entry.weight * weightSum;\n  });\n\n  return cc;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport initOrder from './init-order';\nimport crossCount from './cross-count';\nimport sortSubgraph from './sort-subgraph';\nimport buildLayerGraph from './build-layer-graph';\nimport addSubgraphConstraints from './add-subgraph-constraints';\nimport { Graph } from '@dagrejs/graphlib';\nimport { util } from '../util';\n\nexport default order;\n\n/*\n * Applies heuristics to minimize edge crossings in the graph and sets the best\n * order solution as an order attribute on each node.\n *\n * Pre-conditions:\n *\n *    1. Graph must be DAG\n *    2. Graph nodes must be objects with a \"rank\" attribute\n *    3. Graph edges must have the \"weight\" attribute\n *\n * Post-conditions:\n *\n *    1. Graph nodes will have an \"order\" attribute based on the results of the\n *       algorithm.\n */\nfunction order(g, opts) {\n  if (opts && typeof opts.customOrder === 'function') {\n    opts.customOrder(g, order);\n    return;\n  }\n\n  let maxRank = util.maxRank(g),\n    downLayerGraphs = buildLayerGraphs(g, util.range(1, maxRank + 1), 'inEdges'),\n    upLayerGraphs = buildLayerGraphs(g, util.range(maxRank - 1, -1, -1), 'outEdges');\n\n  let layering = initOrder(g);\n  assignOrder(g, layering);\n\n  if (opts && opts.disableOptimalOrderHeuristic) {\n    return;\n  }\n\n  let bestCC = Number.POSITIVE_INFINITY,\n    best;\n\n  for (let i = 0, lastBest = 0; lastBest < 4; ++i, ++lastBest) {\n    sweepLayerGraphs(i % 2 ? downLayerGraphs : upLayerGraphs, i % 4 >= 2);\n\n    layering = util.buildLayerMatrix(g);\n    let cc = crossCount(g, layering);\n    if (cc < bestCC) {\n      lastBest = 0;\n      best = Object.assign({}, layering);\n      bestCC = cc;\n    }\n  }\n\n  assignOrder(g, best);\n}\n\nfunction buildLayerGraphs(g, ranks, relationship) {\n  return ranks.map(function (rank) {\n    return buildLayerGraph(g, rank, relationship);\n  });\n}\n\nfunction sweepLayerGraphs(layerGraphs, biasRight) {\n  let cg = new Graph();\n  layerGraphs.forEach(function (lg) {\n    let root = lg.graph().root;\n    let sorted = sortSubgraph(lg, root, cg, biasRight);\n    sorted.vs.forEach((v, i) => (lg.node(v).order = i));\n    addSubgraphConstraints(lg, cg, sorted.vs);\n  });\n}\n\nfunction assignOrder(g, layering) {\n  Object.values(layering).forEach((layer) => layer.forEach((v, i) => (g.node(v).order = i)));\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/init-order.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { util } from '../util';\n\nexport { initOrder };\nexport default initOrder;\n\n/*\n * Assigns an initial order value for each node by performing a DFS search\n * starting from nodes in the first rank. Nodes are assigned an order in their\n * rank as they are first visited.\n *\n * This approach comes from Gansner, et al., \"A Technique for Drawing Directed\n * Graphs.\"\n *\n * Returns a layering matrix with an array per layer and each layer sorted by\n * the order of its nodes.\n */\nfunction initOrder(g) {\n  let visited = {};\n  let simpleNodes = g.nodes().filter((v) => !g.children(v).length);\n  let simpleNodesRanks = simpleNodes.map((v) => g.node(v).rank);\n  let maxRank = util.applyWithChunking(Math.max, simpleNodesRanks);\n  let layers = util.range(maxRank + 1).map(() => []);\n\n  function dfs(v) {\n    if (visited[v]) return;\n    visited[v] = true;\n    let node = g.node(v);\n    layers[node.rank].push(v);\n    g.successors(v).forEach(dfs);\n  }\n\n  let orderedVs = simpleNodes.sort((a, b) => g.node(a).rank - g.node(b).rank);\n  orderedVs.forEach(dfs);\n\n  return layers;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/resolve-conflicts.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport util from '../util';\n\nexport { resolveConflicts };\nexport default resolveConflicts;\n\n/*\n * Given a list of entries of the form {v, barycenter, weight} and a\n * constraint graph this function will resolve any conflicts between the\n * constraint graph and the barycenters for the entries. If the barycenters for\n * an entry would violate a constraint in the constraint graph then we coalesce\n * the nodes in the conflict into a new node that respects the contraint and\n * aggregates barycenter and weight information.\n *\n * This implementation is based on the description in Forster, \"A Fast and\n * Simple Hueristic for Constrained Two-Level Crossing Reduction,\" thought it\n * differs in some specific details.\n *\n * Pre-conditions:\n *\n *    1. Each entry has the form {v, barycenter, weight}, or if the node has\n *       no barycenter, then {v}.\n *\n * Returns:\n *\n *    A new list of entries of the form {vs, i, barycenter, weight}. The list\n *    `vs` may either be a singleton or it may be an aggregation of nodes\n *    ordered such that they do not violate constraints from the constraint\n *    graph. The property `i` is the lowest original index of any of the\n *    elements in `vs`.\n */\nfunction resolveConflicts(entries, cg) {\n  let mappedEntries = {};\n  entries.forEach((entry, i) => {\n    let tmp = (mappedEntries[entry.v] = {\n      indegree: 0,\n      in: [],\n      out: [],\n      vs: [entry.v],\n      i: i,\n    });\n    if (entry.barycenter !== undefined) {\n      tmp.barycenter = entry.barycenter;\n      tmp.weight = entry.weight;\n    }\n  });\n\n  cg.edges().forEach((e) => {\n    let entryV = mappedEntries[e.v];\n    let entryW = mappedEntries[e.w];\n    if (entryV !== undefined && entryW !== undefined) {\n      entryW.indegree++;\n      entryV.out.push(mappedEntries[e.w]);\n    }\n  });\n\n  let sourceSet = Object.values(mappedEntries).filter((entry) => !entry.indegree);\n\n  return doResolveConflicts(sourceSet);\n}\n\nfunction doResolveConflicts(sourceSet) {\n  let entries = [];\n\n  function handleIn(vEntry) {\n    return (uEntry) => {\n      if (uEntry.merged) {\n        return;\n      }\n      if (\n        uEntry.barycenter === undefined ||\n        vEntry.barycenter === undefined ||\n        uEntry.barycenter >= vEntry.barycenter\n      ) {\n        mergeEntries(vEntry, uEntry);\n      }\n    };\n  }\n\n  function handleOut(vEntry) {\n    return (wEntry) => {\n      wEntry['in'].push(vEntry);\n      if (--wEntry.indegree === 0) {\n        sourceSet.push(wEntry);\n      }\n    };\n  }\n\n  while (sourceSet.length) {\n    let entry = sourceSet.pop();\n    entries.push(entry);\n    entry['in'].reverse().forEach(handleIn(entry));\n    entry.out.forEach(handleOut(entry));\n  }\n\n  return entries\n    .filter((entry) => !entry.merged)\n    .map((entry) => {\n      return util.pick(entry, ['vs', 'i', 'barycenter', 'weight']);\n    });\n}\n\nfunction mergeEntries(target, source) {\n  let sum = 0;\n  let weight = 0;\n\n  if (target.weight) {\n    sum += target.barycenter * target.weight;\n    weight += target.weight;\n  }\n\n  if (source.weight) {\n    sum += source.barycenter * source.weight;\n    weight += source.weight;\n  }\n\n  target.vs = source.vs.concat(target.vs);\n  target.barycenter = sum / weight;\n  target.weight = weight;\n  target.i = Math.min(source.i, target.i);\n  source.merged = true;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/sort-subgraph.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport barycenter from './barycenter';\nimport resolveConflicts from './resolve-conflicts';\nimport sort from './sort';\n\nexport { sortSubgraph };\nexport default sortSubgraph;\n\nfunction sortSubgraph(g, v, cg, biasRight) {\n  let movable = g.children(v);\n  let node = g.node(v);\n  let bl = node ? node.borderLeft : undefined;\n  let br = node ? node.borderRight : undefined;\n  let subgraphs = {};\n\n  if (bl) {\n    movable = movable.filter((w) => w !== bl && w !== br);\n  }\n\n  let barycenters = barycenter(g, movable);\n  barycenters.forEach((entry) => {\n    if (g.children(entry.v).length) {\n      let subgraphResult = sortSubgraph(g, entry.v, cg, biasRight);\n      subgraphs[entry.v] = subgraphResult;\n      if (Object.hasOwn(subgraphResult, 'barycenter')) {\n        mergeBarycenters(entry, subgraphResult);\n      }\n    }\n  });\n\n  let entries = resolveConflicts(barycenters, cg);\n  expandSubgraphs(entries, subgraphs);\n\n  let result = sort(entries, biasRight);\n\n  if (bl) {\n    result.vs = [bl, result.vs, br].flat(true);\n    if (g.predecessors(bl).length) {\n      let blPred = g.node(g.predecessors(bl)[0]),\n        brPred = g.node(g.predecessors(br)[0]);\n      if (!Object.hasOwn(result, 'barycenter')) {\n        result.barycenter = 0;\n        result.weight = 0;\n      }\n      result.barycenter =\n        (result.barycenter * result.weight + blPred.order + brPred.order) / (result.weight + 2);\n      result.weight += 2;\n    }\n  }\n\n  return result;\n}\n\nfunction expandSubgraphs(entries, subgraphs) {\n  entries.forEach((entry) => {\n    entry.vs = entry.vs.flatMap((v) => {\n      if (subgraphs[v]) {\n        return subgraphs[v].vs;\n      }\n      return v;\n    });\n  });\n}\n\nfunction mergeBarycenters(target, other) {\n  if (target.barycenter !== undefined) {\n    target.barycenter =\n      (target.barycenter * target.weight + other.barycenter * other.weight) /\n      (target.weight + other.weight);\n    target.weight += other.weight;\n  } else {\n    target.barycenter = other.barycenter;\n    target.weight = other.weight;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/order/sort.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport util from '../util';\n\nexport { sort };\nexport default sort;\n\nfunction sort(entries, biasRight) {\n  let parts = util.partition(entries, (entry) => {\n    return Object.hasOwn(entry, 'barycenter');\n  });\n  let sortable = parts.lhs,\n    unsortable = parts.rhs.sort((a, b) => b.i - a.i),\n    vs = [],\n    sum = 0,\n    weight = 0,\n    vsIndex = 0;\n\n  sortable.sort(compareWithBias(!!biasRight));\n\n  vsIndex = consumeUnsortable(vs, unsortable, vsIndex);\n\n  sortable.forEach((entry) => {\n    vsIndex += entry.vs.length;\n    vs.push(entry.vs);\n    sum += entry.barycenter * entry.weight;\n    weight += entry.weight;\n    vsIndex = consumeUnsortable(vs, unsortable, vsIndex);\n  });\n\n  let result = { vs: vs.flat(true) };\n  if (weight) {\n    result.barycenter = sum / weight;\n    result.weight = weight;\n  }\n  return result;\n}\n\nfunction consumeUnsortable(vs, unsortable, index) {\n  let last;\n  while (unsortable.length && (last = unsortable[unsortable.length - 1]).i <= index) {\n    unsortable.pop();\n    vs.push(last.vs);\n    index++;\n  }\n  return index;\n}\n\nfunction compareWithBias(bias) {\n  return (entryV, entryW) => {\n    if (entryV.barycenter < entryW.barycenter) {\n      return -1;\n    } else if (entryV.barycenter > entryW.barycenter) {\n      return 1;\n    }\n\n    return !bias ? entryV.i - entryW.i : entryW.i - entryV.i;\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/parent-dummy-chains.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { parentDummyChains };\nexport default parentDummyChains;\n\nfunction parentDummyChains(g) {\n  let postorderNums = postorder(g);\n\n  g.graph().dummyChains.forEach((v) => {\n    let node = g.node(v);\n    let edgeObj = node.edgeObj;\n    let pathData = findPath(g, postorderNums, edgeObj.v, edgeObj.w);\n    let path = pathData.path;\n    let lca = pathData.lca;\n    let pathIdx = 0;\n    let pathV = path[pathIdx];\n    let ascending = true;\n\n    while (v !== edgeObj.w) {\n      node = g.node(v);\n\n      if (ascending) {\n        while ((pathV = path[pathIdx]) !== lca && g.node(pathV).maxRank < node.rank) {\n          pathIdx++;\n        }\n\n        if (pathV === lca) {\n          ascending = false;\n        }\n      }\n\n      if (!ascending) {\n        while (\n          pathIdx < path.length - 1 &&\n          g.node((pathV = path[pathIdx + 1])).minRank <= node.rank\n        ) {\n          pathIdx++;\n        }\n        pathV = path[pathIdx];\n      }\n\n      g.setParent(v, pathV);\n      v = g.successors(v)[0];\n    }\n  });\n}\n\n// Find a path from v to w through the lowest common ancestor (LCA). Return the\n// full path and the LCA.\nfunction findPath(g, postorderNums, v, w) {\n  let vPath = [];\n  let wPath = [];\n  let low = Math.min(postorderNums[v].low, postorderNums[w].low);\n  let lim = Math.max(postorderNums[v].lim, postorderNums[w].lim);\n  let parent;\n  let lca;\n\n  // Traverse up from v to find the LCA\n  parent = v;\n  do {\n    parent = g.parent(parent);\n    vPath.push(parent);\n  } while (parent && (postorderNums[parent].low > low || lim > postorderNums[parent].lim));\n  lca = parent;\n\n  // Traverse from w to LCA\n  parent = w;\n  while ((parent = g.parent(parent)) !== lca) {\n    wPath.push(parent);\n  }\n\n  return { path: vPath.concat(wPath.reverse()), lca: lca };\n}\n\nfunction postorder(g) {\n  let result = {};\n  let lim = 0;\n\n  function dfs(v) {\n    let low = lim;\n    g.children(v).forEach(dfs);\n    result[v] = { low: low, lim: lim++ };\n  }\n  g.children().forEach(dfs);\n\n  return result;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/position/bk.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { Graph } from '@dagrejs/graphlib';\nimport { util } from '../util';\n\n/*\n * This module provides coordinate assignment based on Brandes and Köpf, \"Fast\n * and Simple Horizontal Coordinate Assignment.\"\n */\n\nexport {\n  positionX,\n  findType1Conflicts,\n  findType2Conflicts,\n  addConflict,\n  hasConflict,\n  verticalAlignment,\n  horizontalCompaction,\n  alignCoordinates,\n  findSmallestWidthAlignment,\n  balance,\n};\n\n/*\n * Marks all edges in the graph with a type-1 conflict with the \"type1Conflict\"\n * property. A type-1 conflict is one where a non-inner segment crosses an\n * inner segment. An inner segment is an edge with both incident nodes marked\n * with the \"dummy\" property.\n *\n * This algorithm scans layer by layer, starting with the second, for type-1\n * conflicts between the current layer and the previous layer. For each layer\n * it scans the nodes from left to right until it reaches one that is incident\n * on an inner segment. It then scans predecessors to determine if they have\n * edges that cross that inner segment. At the end a final scan is done for all\n * nodes on the current rank to see if they cross the last visited inner\n * segment.\n *\n * This algorithm (safely) assumes that a dummy node will only be incident on a\n * single node in the layers being scanned.\n */\nfunction findType1Conflicts(g, layering) {\n  let conflicts = {};\n\n  function visitLayer(prevLayer, layer) {\n    let // last visited node in the previous layer that is incident on an inner\n      // segment.\n      k0 = 0,\n      // Tracks the last node in this layer scanned for crossings with a type-1\n      // segment.\n      scanPos = 0,\n      prevLayerLength = prevLayer.length,\n      lastNode = layer[layer.length - 1];\n\n    layer.forEach((v, i) => {\n      let w = findOtherInnerSegmentNode(g, v),\n        k1 = w ? g.node(w).order : prevLayerLength;\n\n      if (w || v === lastNode) {\n        layer.slice(scanPos, i + 1).forEach((scanNode) => {\n          g.predecessors(scanNode).forEach((u) => {\n            let uLabel = g.node(u),\n              uPos = uLabel.order;\n            if ((uPos < k0 || k1 < uPos) && !(uLabel.dummy && g.node(scanNode).dummy)) {\n              addConflict(conflicts, u, scanNode);\n            }\n          });\n        });\n        scanPos = i + 1;\n        k0 = k1;\n      }\n    });\n\n    return layer;\n  }\n\n  layering.length && layering.reduce(visitLayer);\n\n  return conflicts;\n}\n\nfunction findType2Conflicts(g, layering) {\n  let conflicts = {};\n\n  function scan(south, southPos, southEnd, prevNorthBorder, nextNorthBorder) {\n    let v;\n    util.range(southPos, southEnd).forEach((i) => {\n      v = south[i];\n      if (g.node(v).dummy) {\n        g.predecessors(v).forEach((u) => {\n          let uNode = g.node(u);\n          if (uNode.dummy && (uNode.order < prevNorthBorder || uNode.order > nextNorthBorder)) {\n            addConflict(conflicts, u, v);\n          }\n        });\n      }\n    });\n  }\n\n  function visitLayer(north, south) {\n    let prevNorthPos = -1,\n      nextNorthPos,\n      southPos = 0;\n\n    south.forEach((v, southLookahead) => {\n      if (g.node(v).dummy === 'border') {\n        let predecessors = g.predecessors(v);\n        if (predecessors.length) {\n          nextNorthPos = g.node(predecessors[0]).order;\n          scan(south, southPos, southLookahead, prevNorthPos, nextNorthPos);\n          southPos = southLookahead;\n          prevNorthPos = nextNorthPos;\n        }\n      }\n      scan(south, southPos, south.length, nextNorthPos, north.length);\n    });\n\n    return south;\n  }\n\n  layering.length && layering.reduce(visitLayer);\n\n  return conflicts;\n}\n\nfunction findOtherInnerSegmentNode(g, v) {\n  if (g.node(v).dummy) {\n    return g.predecessors(v).find((u) => g.node(u).dummy);\n  }\n}\n\nfunction addConflict(conflicts, v, w) {\n  if (v > w) {\n    let tmp = v;\n    v = w;\n    w = tmp;\n  }\n\n  let conflictsV = conflicts[v];\n  if (!conflictsV) {\n    conflicts[v] = conflictsV = {};\n  }\n  conflictsV[w] = true;\n}\n\nfunction hasConflict(conflicts, v, w) {\n  if (v > w) {\n    let tmp = v;\n    v = w;\n    w = tmp;\n  }\n  return !!conflicts[v] && Object.hasOwn(conflicts[v], w);\n}\n\n/*\n * Try to align nodes into vertical \"blocks\" where possible. This algorithm\n * attempts to align a node with one of its median neighbors. If the edge\n * connecting a neighbor is a type-1 conflict then we ignore that possibility.\n * If a previous node has already formed a block with a node after the node\n * we're trying to form a block with, we also ignore that possibility - our\n * blocks would be split in that scenario.\n */\nfunction verticalAlignment(g, layering, conflicts, neighborFn) {\n  let root = {},\n    align = {},\n    pos = {};\n\n  // We cache the position here based on the layering because the graph and\n  // layering may be out of sync. The layering matrix is manipulated to\n  // generate different extreme alignments.\n  layering.forEach((layer) => {\n    layer.forEach((v, order) => {\n      root[v] = v;\n      align[v] = v;\n      pos[v] = order;\n    });\n  });\n\n  layering.forEach((layer) => {\n    let prevIdx = -1;\n    layer.forEach((v) => {\n      let ws = neighborFn(v);\n      if (ws.length) {\n        ws = ws.sort((a, b) => pos[a] - pos[b]);\n        let mp = (ws.length - 1) / 2;\n        for (let i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) {\n          let w = ws[i];\n          if (align[v] === v && prevIdx < pos[w] && !hasConflict(conflicts, v, w)) {\n            align[w] = v;\n            align[v] = root[v] = root[w];\n            prevIdx = pos[w];\n          }\n        }\n      }\n    });\n  });\n\n  return { root: root, align: align };\n}\n\nfunction horizontalCompaction(g, layering, root, align, reverseSep) {\n  // This portion of the algorithm differs from BK due to a number of problems.\n  // Instead of their algorithm we construct a new block graph and do two\n  // sweeps. The first sweep places blocks with the smallest possible\n  // coordinates. The second sweep removes unused space by moving blocks to the\n  // greatest coordinates without violating separation.\n  let xs = {},\n    blockG = buildBlockGraph(g, layering, root, reverseSep),\n    borderType = reverseSep ? 'borderLeft' : 'borderRight';\n\n  function iterate(setXsFunc, nextNodesFunc) {\n    let stack = blockG.nodes();\n    let elem = stack.pop();\n    let visited = {};\n    while (elem) {\n      if (visited[elem]) {\n        setXsFunc(elem);\n      } else {\n        visited[elem] = true;\n        stack.push(elem);\n        stack = stack.concat(nextNodesFunc(elem));\n      }\n\n      elem = stack.pop();\n    }\n  }\n\n  // First pass, assign smallest coordinates\n  function pass1(elem) {\n    xs[elem] = blockG.inEdges(elem).reduce((acc, e) => {\n      return Math.max(acc, xs[e.v] + blockG.edge(e));\n    }, 0);\n  }\n\n  // Second pass, assign greatest coordinates\n  function pass2(elem) {\n    let min = blockG.outEdges(elem).reduce((acc, e) => {\n      return Math.min(acc, xs[e.w] - blockG.edge(e));\n    }, Number.POSITIVE_INFINITY);\n\n    let node = g.node(elem);\n    if (min !== Number.POSITIVE_INFINITY && node.borderType !== borderType) {\n      xs[elem] = Math.max(xs[elem], min);\n    }\n  }\n\n  iterate(pass1, blockG.predecessors.bind(blockG));\n  iterate(pass2, blockG.successors.bind(blockG));\n\n  // Assign x coordinates to all nodes\n  Object.keys(align).forEach((v) => (xs[v] = xs[root[v]]));\n\n  return xs;\n}\n\nfunction buildBlockGraph(g, layering, root, reverseSep) {\n  let blockGraph = new Graph(),\n    graphLabel = g.graph(),\n    sepFn = sep(graphLabel.nodesep, graphLabel.edgesep, reverseSep);\n\n  layering.forEach((layer) => {\n    let u;\n    layer.forEach((v) => {\n      let vRoot = root[v];\n      blockGraph.setNode(vRoot);\n      if (u) {\n        var uRoot = root[u],\n          prevMax = blockGraph.edge(uRoot, vRoot);\n        blockGraph.setEdge(uRoot, vRoot, Math.max(sepFn(g, v, u), prevMax || 0));\n      }\n      u = v;\n    });\n  });\n\n  return blockGraph;\n}\n\n/*\n * Returns the alignment that has the smallest width of the given alignments.\n */\nfunction findSmallestWidthAlignment(g, xss) {\n  return Object.values(xss).reduce(\n    (currentMinAndXs, xs) => {\n      let max = Number.NEGATIVE_INFINITY;\n      let min = Number.POSITIVE_INFINITY;\n\n      Object.entries(xs).forEach(([v, x]) => {\n        let halfWidth = width(g, v) / 2;\n\n        max = Math.max(x + halfWidth, max);\n        min = Math.min(x - halfWidth, min);\n      });\n\n      const newMin = max - min;\n      if (newMin < currentMinAndXs[0]) {\n        currentMinAndXs = [newMin, xs];\n      }\n      return currentMinAndXs;\n    },\n    [Number.POSITIVE_INFINITY, null]\n  )[1];\n}\n\n/*\n * Align the coordinates of each of the layout alignments such that\n * left-biased alignments have their minimum coordinate at the same point as\n * the minimum coordinate of the smallest width alignment and right-biased\n * alignments have their maximum coordinate at the same point as the maximum\n * coordinate of the smallest width alignment.\n */\nfunction alignCoordinates(xss, alignTo) {\n  let alignToVals = Object.values(alignTo),\n    alignToMin = util.applyWithChunking(Math.min, alignToVals),\n    alignToMax = util.applyWithChunking(Math.max, alignToVals);\n\n  ['u', 'd'].forEach((vert) => {\n    ['l', 'r'].forEach((horiz) => {\n      let alignment = vert + horiz,\n        xs = xss[alignment];\n\n      if (xs === alignTo) return;\n\n      let xsVals = Object.values(xs);\n      let delta = alignToMin - util.applyWithChunking(Math.min, xsVals);\n      if (horiz !== 'l') {\n        delta = alignToMax - util.applyWithChunking(Math.max, xsVals);\n      }\n\n      if (delta) {\n        xss[alignment] = util.mapValues(xs, (x) => x + delta);\n      }\n    });\n  });\n}\n\nfunction balance(xss, align) {\n  return util.mapValues(xss.ul, (num, v) => {\n    if (align) {\n      return xss[align.toLowerCase()][v];\n    } else {\n      let xs = Object.values(xss)\n        .map((xs) => xs[v])\n        .sort((a, b) => a - b);\n      return (xs[1] + xs[2]) / 2;\n    }\n  });\n}\n\nfunction positionX(g) {\n  let layering = util.buildLayerMatrix(g);\n  let conflicts = Object.assign(findType1Conflicts(g, layering), findType2Conflicts(g, layering));\n\n  let xss = {};\n  let adjustedLayering;\n  ['u', 'd'].forEach((vert) => {\n    adjustedLayering = vert === 'u' ? layering : Object.values(layering).reverse();\n    ['l', 'r'].forEach((horiz) => {\n      if (horiz === 'r') {\n        adjustedLayering = adjustedLayering.map((inner) => {\n          return Object.values(inner).reverse();\n        });\n      }\n\n      let neighborFn = (vert === 'u' ? g.predecessors : g.successors).bind(g);\n      let align = verticalAlignment(g, adjustedLayering, conflicts, neighborFn);\n      let xs = horizontalCompaction(g, adjustedLayering, align.root, align.align, horiz === 'r');\n      if (horiz === 'r') {\n        xs = util.mapValues(xs, (x) => -x);\n      }\n      xss[vert + horiz] = xs;\n    });\n  });\n\n  let smallestWidth = findSmallestWidthAlignment(g, xss);\n  alignCoordinates(xss, smallestWidth);\n  return balance(xss, g.graph().align);\n}\n\nfunction sep(nodeSep, edgeSep, reverseSep) {\n  return (g, v, w) => {\n    let vLabel = g.node(v);\n    let wLabel = g.node(w);\n    let sum = 0;\n    let delta;\n\n    sum += vLabel.width / 2;\n    if (Object.hasOwn(vLabel, 'labelpos')) {\n      switch (vLabel.labelpos.toLowerCase()) {\n        case 'l':\n          delta = -vLabel.width / 2;\n          break;\n        case 'r':\n          delta = vLabel.width / 2;\n          break;\n      }\n    }\n    if (delta) {\n      sum += reverseSep ? delta : -delta;\n    }\n    delta = 0;\n\n    sum += (vLabel.dummy ? edgeSep : nodeSep) / 2;\n    sum += (wLabel.dummy ? edgeSep : nodeSep) / 2;\n\n    sum += wLabel.width / 2;\n    if (Object.hasOwn(wLabel, 'labelpos')) {\n      switch (wLabel.labelpos.toLowerCase()) {\n        case 'l':\n          delta = wLabel.width / 2;\n          break;\n        case 'r':\n          delta = -wLabel.width / 2;\n          break;\n      }\n    }\n    if (delta) {\n      sum += reverseSep ? delta : -delta;\n    }\n    delta = 0;\n\n    return sum;\n  };\n}\n\nfunction width(g, v) {\n  return g.node(v).width;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/position/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport util from '../util';\nimport { positionX } from './bk';\n\nexport { position };\nexport default position;\n\nfunction position(g) {\n  g = util.asNonCompoundGraph(g);\n\n  positionY(g);\n  Object.entries(positionX(g)).forEach(([v, x]) => (g.node(v).x = x));\n}\n\nfunction positionY(g) {\n  let layering = util.buildLayerMatrix(g);\n  let rankSep = g.graph().ranksep;\n  let prevY = 0;\n  layering.forEach((layer) => {\n    const maxHeight = layer.reduce((acc, v) => {\n      const height = g.node(v).height;\n      if (acc > height) {\n        return acc;\n      } else {\n        return height;\n      }\n    }, 0);\n    layer.forEach((v) => (g.node(v).y = prevY + maxHeight / 2));\n    prevY += maxHeight + rankSep;\n  });\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/rank/feasible-tree.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { Graph } from '@dagrejs/graphlib';\nimport { slack } from './util';\n\nexport { feasibleTree };\nexport default feasibleTree;\n\n/*\n * Constructs a spanning tree with tight edges and adjusted the input node's\n * ranks to achieve this. A tight edge is one that is has a length that matches\n * its \"minlen\" attribute.\n *\n * The basic structure for this function is derived from Gansner, et al., \"A\n * Technique for Drawing Directed Graphs.\"\n *\n * Pre-conditions:\n *\n *    1. Graph must be a DAG.\n *    2. Graph must be connected.\n *    3. Graph must have at least one node.\n *    5. Graph nodes must have been previously assigned a \"rank\" property that\n *       respects the \"minlen\" property of incident edges.\n *    6. Graph edges must have a \"minlen\" property.\n *\n * Post-conditions:\n *\n *    - Graph nodes will have their rank adjusted to ensure that all edges are\n *      tight.\n *\n * Returns a tree (undirected graph) that is constructed using only \"tight\"\n * edges.\n */\nfunction feasibleTree(g) {\n  var t = new Graph({ directed: false });\n\n  // Choose arbitrary node from which to start our tree\n  var start = g.nodes()[0];\n  var size = g.nodeCount();\n  t.setNode(start, {});\n\n  var edge, delta;\n  while (tightTree(t, g) < size) {\n    edge = findMinSlackEdge(t, g);\n    delta = t.hasNode(edge.v) ? slack(g, edge) : -slack(g, edge);\n    shiftRanks(t, g, delta);\n  }\n\n  return t;\n}\n\n/*\n * Finds a maximal tree of tight edges and returns the number of nodes in the\n * tree.\n */\nfunction tightTree(t, g) {\n  function dfs(v) {\n    g.nodeEdges(v).forEach((e) => {\n      var edgeV = e.v,\n        w = v === edgeV ? e.w : edgeV;\n      if (!t.hasNode(w) && !slack(g, e)) {\n        t.setNode(w, {});\n        t.setEdge(v, w, {});\n        dfs(w);\n      }\n    });\n  }\n\n  t.nodes().forEach(dfs);\n  return t.nodeCount();\n}\n\n/*\n * Finds the edge with the smallest slack that is incident on tree and returns\n * it.\n */\nfunction findMinSlackEdge(t, g) {\n  const edges = g.edges();\n\n  return edges.reduce(\n    (acc, edge) => {\n      let edgeSlack = Number.POSITIVE_INFINITY;\n      if (t.hasNode(edge.v) !== t.hasNode(edge.w)) {\n        edgeSlack = slack(g, edge);\n      }\n\n      if (edgeSlack < acc[0]) {\n        return [edgeSlack, edge];\n      }\n\n      return acc;\n    },\n    [Number.POSITIVE_INFINITY, null]\n  )[1];\n}\n\nfunction shiftRanks(t, g, delta) {\n  t.nodes().forEach((v) => (g.node(v).rank += delta));\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/rank/index.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport rankUtil from './util';\nimport { longestPath } from './util';\nimport feasibleTree from './feasible-tree';\nimport networkSimplex from './network-simplex';\n\nexport { rank };\nexport default rank;\n\n/*\n * Assigns a rank to each node in the input graph that respects the \"minlen\"\n * constraint specified on edges between nodes.\n *\n * This basic structure is derived from Gansner, et al., \"A Technique for\n * Drawing Directed Graphs.\"\n *\n * Pre-conditions:\n *\n *    1. Graph must be a connected DAG\n *    2. Graph nodes must be objects\n *    3. Graph edges must have \"weight\" and \"minlen\" attributes\n *\n * Post-conditions:\n *\n *    1. Graph nodes will have a \"rank\" attribute based on the results of the\n *       algorithm. Ranks can start at any index (including negative), we'll\n *       fix them up later.\n */\nfunction rank(g) {\n  switch (g.graph().ranker) {\n    case 'network-simplex':\n      networkSimplexRanker(g);\n      break;\n    case 'tight-tree':\n      tightTreeRanker(g);\n      break;\n    case 'longest-path':\n      longestPathRanker(g);\n      break;\n    default:\n      networkSimplexRanker(g);\n  }\n}\n\n// A fast and simple ranker, but results are far from optimal.\nvar longestPathRanker = longestPath;\n\nfunction tightTreeRanker(g) {\n  longestPath(g);\n  feasibleTree(g);\n}\n\nfunction networkSimplexRanker(g) {\n  networkSimplex(g);\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/rank/network-simplex.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { alg } from '@dagrejs/graphlib';\nimport { feasibleTree } from './feasible-tree';\nimport { slack, longestPath as initRank } from './util';\nimport { simplify } from '../util';\n\nconst { preorder, postorder } = alg;\n\nexport { networkSimplex };\nexport default networkSimplex;\n\n// Expose some internals for testing purposes\nnetworkSimplex.initLowLimValues = initLowLimValues;\nnetworkSimplex.initCutValues = initCutValues;\nnetworkSimplex.calcCutValue = calcCutValue;\nnetworkSimplex.leaveEdge = leaveEdge;\nnetworkSimplex.enterEdge = enterEdge;\nnetworkSimplex.exchangeEdges = exchangeEdges;\n\n/*\n * The network simplex algorithm assigns ranks to each node in the input graph\n * and iteratively improves the ranking to reduce the length of edges.\n *\n * Preconditions:\n *\n *    1. The input graph must be a DAG.\n *    2. All nodes in the graph must have an object value.\n *    3. All edges in the graph must have \"minlen\" and \"weight\" attributes.\n *\n * Postconditions:\n *\n *    1. All nodes in the graph will have an assigned \"rank\" attribute that has\n *       been optimized by the network simplex algorithm. Ranks start at 0.\n *\n *\n * A rough sketch of the algorithm is as follows:\n *\n *    1. Assign initial ranks to each node. We use the longest path algorithm,\n *       which assigns ranks to the lowest position possible. In general this\n *       leads to very wide bottom ranks and unnecessarily long edges.\n *    2. Construct a feasible tight tree. A tight tree is one such that all\n *       edges in the tree have no slack (difference between length of edge\n *       and minlen for the edge). This by itself greatly improves the assigned\n *       rankings by shorting edges.\n *    3. Iteratively find edges that have negative cut values. Generally a\n *       negative cut value indicates that the edge could be removed and a new\n *       tree edge could be added to produce a more compact graph.\n *\n * Much of the algorithms here are derived from Gansner, et al., \"A Technique\n * for Drawing Directed Graphs.\" The structure of the file roughly follows the\n * structure of the overall algorithm.\n */\nfunction networkSimplex(g) {\n  g = simplify(g);\n  initRank(g);\n  var t = feasibleTree(g);\n  initLowLimValues(t);\n  initCutValues(t, g);\n\n  var e, f;\n  while ((e = leaveEdge(t))) {\n    f = enterEdge(t, g, e);\n    exchangeEdges(t, g, e, f);\n  }\n}\n\n/*\n * Initializes cut values for all edges in the tree.\n */\nfunction initCutValues(t, g) {\n  var vs = postorder(t, t.nodes());\n  vs = vs.slice(0, vs.length - 1);\n  vs.forEach((v) => assignCutValue(t, g, v));\n}\n\nfunction assignCutValue(t, g, child) {\n  var childLab = t.node(child);\n  var parent = childLab.parent;\n  t.edge(child, parent).cutvalue = calcCutValue(t, g, child);\n}\n\n/*\n * Given the tight tree, its graph, and a child in the graph calculate and\n * return the cut value for the edge between the child and its parent.\n */\nfunction calcCutValue(t, g, child) {\n  var childLab = t.node(child);\n  var parent = childLab.parent;\n  // True if the child is on the tail end of the edge in the directed graph\n  var childIsTail = true;\n  // The graph's view of the tree edge we're inspecting\n  var graphEdge = g.edge(child, parent);\n  // The accumulated cut value for the edge between this node and its parent\n  var cutValue = 0;\n\n  if (!graphEdge) {\n    childIsTail = false;\n    graphEdge = g.edge(parent, child);\n  }\n\n  cutValue = graphEdge.weight;\n\n  g.nodeEdges(child).forEach((e) => {\n    var isOutEdge = e.v === child,\n      other = isOutEdge ? e.w : e.v;\n\n    if (other !== parent) {\n      var pointsToHead = isOutEdge === childIsTail,\n        otherWeight = g.edge(e).weight;\n\n      cutValue += pointsToHead ? otherWeight : -otherWeight;\n      if (isTreeEdge(t, child, other)) {\n        var otherCutValue = t.edge(child, other).cutvalue;\n        cutValue += pointsToHead ? -otherCutValue : otherCutValue;\n      }\n    }\n  });\n\n  return cutValue;\n}\n\nfunction initLowLimValues(tree, root) {\n  if (arguments.length < 2) {\n    root = tree.nodes()[0];\n  }\n  dfsAssignLowLim(tree, {}, 1, root);\n}\n\nfunction dfsAssignLowLim(tree, visited, nextLim, v, parent) {\n  var low = nextLim;\n  var label = tree.node(v);\n\n  visited[v] = true;\n  tree.neighbors(v).forEach((w) => {\n    if (!Object.hasOwn(visited, w)) {\n      nextLim = dfsAssignLowLim(tree, visited, nextLim, w, v);\n    }\n  });\n\n  label.low = low;\n  label.lim = nextLim++;\n  if (parent) {\n    label.parent = parent;\n  } else {\n    // TODO should be able to remove this when we incrementally update low lim\n    delete label.parent;\n  }\n\n  return nextLim;\n}\n\nfunction leaveEdge(tree) {\n  return tree.edges().find((e) => tree.edge(e).cutvalue < 0);\n}\n\nfunction enterEdge(t, g, edge) {\n  var v = edge.v;\n  var w = edge.w;\n\n  // For the rest of this function we assume that v is the tail and w is the\n  // head, so if we don't have this edge in the graph we should flip it to\n  // match the correct orientation.\n  if (!g.hasEdge(v, w)) {\n    v = edge.w;\n    w = edge.v;\n  }\n\n  var vLabel = t.node(v);\n  var wLabel = t.node(w);\n  var tailLabel = vLabel;\n  var flip = false;\n\n  // If the root is in the tail of the edge then we need to flip the logic that\n  // checks for the head and tail nodes in the candidates function below.\n  if (vLabel.lim > wLabel.lim) {\n    tailLabel = wLabel;\n    flip = true;\n  }\n\n  var candidates = g.edges().filter((edge) => {\n    return (\n      flip === isDescendant(t, t.node(edge.v), tailLabel) &&\n      flip !== isDescendant(t, t.node(edge.w), tailLabel)\n    );\n  });\n\n  return candidates.reduce((acc, edge) => {\n    if (slack(g, edge) < slack(g, acc)) {\n      return edge;\n    }\n\n    return acc;\n  });\n}\n\nfunction exchangeEdges(t, g, e, f) {\n  var v = e.v;\n  var w = e.w;\n  t.removeEdge(v, w);\n  t.setEdge(f.v, f.w, {});\n  initLowLimValues(t);\n  initCutValues(t, g);\n  updateRanks(t, g);\n}\n\nfunction updateRanks(t, g) {\n  var root = t.nodes().find((v) => !g.node(v).parent);\n  var vs = preorder(t, root);\n  vs = vs.slice(1);\n  vs.forEach((v) => {\n    var parent = t.node(v).parent,\n      edge = g.edge(v, parent),\n      flipped = false;\n\n    if (!edge) {\n      edge = g.edge(parent, v);\n      flipped = true;\n    }\n\n    g.node(v).rank = g.node(parent).rank + (flipped ? edge.minlen : -edge.minlen);\n  });\n}\n\n/*\n * Returns true if the edge is in the tree.\n */\nfunction isTreeEdge(tree, u, v) {\n  return tree.hasEdge(u, v);\n}\n\n/*\n * Returns true if the specified node is descendant of the root node per the\n * assigned low and lim attributes in the tree.\n */\nfunction isDescendant(tree, vLabel, rootLabel) {\n  return rootLabel.low <= vLabel.lim && vLabel.lim <= rootLabel.lim;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/rank/util.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n'use strict';\n\nimport { applyWithChunking } from '../util';\n\nexport { longestPath, slack };\nexport default { longestPath, slack };\n\n/*\n * Initializes ranks for the input graph using the longest path algorithm. This\n * algorithm scales well and is fast in practice, it yields rather poor\n * solutions. Nodes are pushed to the lowest layer possible, leaving the bottom\n * ranks wide and leaving edges longer than necessary. However, due to its\n * speed, this algorithm is good for getting an initial ranking that can be fed\n * into other algorithms.\n *\n * This algorithm does not normalize layers because it will be used by other\n * algorithms in most cases. If using this algorithm directly, be sure to\n * run normalize at the end.\n *\n * Pre-conditions:\n *\n *    1. Input graph is a DAG.\n *    2. Input graph node labels can be assigned properties.\n *\n * Post-conditions:\n *\n *    1. Each node will be assign an (unnormalized) \"rank\" property.\n */\nfunction longestPath(g) {\n  var visited = {};\n\n  function dfs(v) {\n    var label = g.node(v);\n    if (Object.hasOwn(visited, v)) {\n      return label.rank;\n    }\n    visited[v] = true;\n\n    let outEdgesMinLens = g.outEdges(v).map((e) => {\n      if (e == null) {\n        return Number.POSITIVE_INFINITY;\n      }\n\n      return dfs(e.w) - g.edge(e).minlen;\n    });\n\n    var rank = applyWithChunking(Math.min, outEdgesMinLens);\n\n    if (rank === Number.POSITIVE_INFINITY) {\n      rank = 0;\n    }\n\n    return (label.rank = rank);\n  }\n\n  g.sources().forEach(dfs);\n}\n\n/*\n * Returns the amount of slack for the given edge. The slack is defined as the\n * difference between the length of the edge and its minimum length.\n */\nfunction slack(g, e) {\n  return g.node(e.w).rank - g.node(e.v).rank - g.edge(e).minlen;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/util.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint \"no-console\": off */\n\n'use strict';\n\nimport { Graph } from '@dagrejs/graphlib';\n\nconst util = {\n  addBorderNode,\n  addDummyNode,\n  applyWithChunking,\n  asNonCompoundGraph,\n  buildLayerMatrix,\n  intersectRect,\n  mapValues,\n  maxRank,\n  normalizeRanks,\n  notime,\n  partition,\n  pick,\n  predecessorWeights,\n  range,\n  removeEmptyRanks,\n  simplify,\n  successorWeights,\n  time,\n  uniqueId,\n  zipObject,\n};\n\nexport {\n  util,\n  addBorderNode,\n  addDummyNode,\n  applyWithChunking,\n  asNonCompoundGraph,\n  buildLayerMatrix,\n  intersectRect,\n  mapValues,\n  maxRank,\n  normalizeRanks,\n  notime,\n  partition,\n  pick,\n  predecessorWeights,\n  range,\n  removeEmptyRanks,\n  simplify,\n  successorWeights,\n  time,\n  uniqueId,\n  zipObject,\n};\n\nexport default util;\n\n/*\n * Adds a dummy node to the graph and return v.\n */\nfunction addDummyNode(g, type, attrs, name) {\n  let v;\n  do {\n    v = uniqueId(name);\n  } while (g.hasNode(v));\n\n  attrs.dummy = type;\n  g.setNode(v, attrs);\n  return v;\n}\n\n/*\n * Returns a new graph with only simple edges. Handles aggregation of data\n * associated with multi-edges.\n */\nfunction simplify(g) {\n  let simplified = new Graph().setGraph(g.graph());\n  g.nodes().forEach((v) => simplified.setNode(v, g.node(v)));\n  g.edges().forEach((e) => {\n    let simpleLabel = simplified.edge(e.v, e.w) || { weight: 0, minlen: 1 };\n    let label = g.edge(e);\n    simplified.setEdge(e.v, e.w, {\n      weight: simpleLabel.weight + label.weight,\n      minlen: Math.max(simpleLabel.minlen, label.minlen),\n    });\n  });\n  return simplified;\n}\n\nfunction asNonCompoundGraph(g) {\n  let simplified = new Graph({ multigraph: g.isMultigraph() }).setGraph(g.graph());\n  g.nodes().forEach((v) => {\n    if (!g.children(v).length) {\n      simplified.setNode(v, g.node(v));\n    }\n  });\n  g.edges().forEach((e) => {\n    simplified.setEdge(e, g.edge(e));\n  });\n  return simplified;\n}\n\nfunction successorWeights(g) {\n  let weightMap = g.nodes().map((v) => {\n    let sucs = {};\n    g.outEdges(v).forEach((e) => {\n      sucs[e.w] = (sucs[e.w] || 0) + g.edge(e).weight;\n    });\n    return sucs;\n  });\n  return zipObject(g.nodes(), weightMap);\n}\n\nfunction predecessorWeights(g) {\n  let weightMap = g.nodes().map((v) => {\n    let preds = {};\n    g.inEdges(v).forEach((e) => {\n      preds[e.v] = (preds[e.v] || 0) + g.edge(e).weight;\n    });\n    return preds;\n  });\n  return zipObject(g.nodes(), weightMap);\n}\n\n/*\n * Finds where a line starting at point ({x, y}) would intersect a rectangle\n * ({x, y, width, height}) if it were pointing at the rectangle's center.\n */\nfunction intersectRect(rect, point) {\n  let x = rect.x;\n  let y = rect.y;\n\n  // Rectangle intersection algorithm from:\n  // http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes\n  let dx = point.x - x;\n  let dy = point.y - y;\n  let w = rect.width / 2;\n  let h = rect.height / 2;\n\n  if (!dx && !dy) {\n    throw new Error('Not possible to find intersection inside of the rectangle');\n  }\n\n  let sx, sy;\n  if (Math.abs(dy) * w > Math.abs(dx) * h) {\n    // Intersection is top or bottom of rect.\n    if (dy < 0) {\n      h = -h;\n    }\n    sx = (h * dx) / dy;\n    sy = h;\n  } else {\n    // Intersection is left or right of rect.\n    if (dx < 0) {\n      w = -w;\n    }\n    sx = w;\n    sy = (w * dy) / dx;\n  }\n\n  return { x: x + sx, y: y + sy };\n}\n\n/*\n * Given a DAG with each node assigned \"rank\" and \"order\" properties, this\n * function will produce a matrix with the ids of each node.\n */\nfunction buildLayerMatrix(g) {\n  let layering = range(maxRank(g) + 1).map(() => []);\n  g.nodes().forEach((v) => {\n    let node = g.node(v);\n    let rank = node.rank;\n    if (rank !== undefined) {\n      layering[rank][node.order] = v;\n    }\n  });\n  return layering;\n}\n\n/*\n * Adjusts the ranks for all nodes in the graph such that all nodes v have\n * rank(v) >= 0 and at least one node w has rank(w) = 0.\n */\nfunction normalizeRanks(g) {\n  let nodeRanks = g.nodes().map((v) => {\n    let rank = g.node(v).rank;\n    if (rank === undefined) {\n      return Number.MAX_VALUE;\n    }\n\n    return rank;\n  });\n  let min = applyWithChunking(Math.min, nodeRanks);\n  g.nodes().forEach((v) => {\n    let node = g.node(v);\n    if (Object.hasOwn(node, 'rank')) {\n      node.rank -= min;\n    }\n  });\n}\n\nfunction removeEmptyRanks(g) {\n  // Ranks may not start at 0, so we need to offset them\n  let nodeRanks = g.nodes().map((v) => g.node(v).rank);\n  let offset = applyWithChunking(Math.min, nodeRanks);\n\n  let layers = [];\n  g.nodes().forEach((v) => {\n    let rank = g.node(v).rank - offset;\n    if (!layers[rank]) {\n      layers[rank] = [];\n    }\n    layers[rank].push(v);\n  });\n\n  let delta = 0;\n  let nodeRankFactor = g.graph().nodeRankFactor;\n  Array.from(layers).forEach((vs, i) => {\n    if (vs === undefined && i % nodeRankFactor !== 0) {\n      --delta;\n    } else if (vs !== undefined && delta) {\n      vs.forEach((v) => (g.node(v).rank += delta));\n    }\n  });\n}\n\nfunction addBorderNode(g, prefix, rank, order) {\n  let node = {\n    width: 0,\n    height: 0,\n  };\n  if (arguments.length >= 4) {\n    node.rank = rank;\n    node.order = order;\n  }\n  return addDummyNode(g, 'border', node, prefix);\n}\n\nfunction splitToChunks(array, chunkSize = CHUNKING_THRESHOLD) {\n  const chunks = [];\n  for (let i = 0; i < array.length; i += chunkSize) {\n    const chunk = array.slice(i, i + chunkSize);\n    chunks.push(chunk);\n  }\n  return chunks;\n}\n\nconst CHUNKING_THRESHOLD = 65535;\n\nfunction applyWithChunking(fn, argsArray) {\n  if (argsArray.length > CHUNKING_THRESHOLD) {\n    const chunks = splitToChunks(argsArray);\n    return fn.apply(\n      null,\n      chunks.map((chunk) => fn.apply(null, chunk))\n    );\n  } else {\n    return fn.apply(null, argsArray);\n  }\n}\n\nfunction maxRank(g) {\n  const nodes = g.nodes();\n  const nodeRanks = nodes.map((v) => {\n    let rank = g.node(v).rank;\n    if (rank === undefined) {\n      return Number.MIN_VALUE;\n    }\n    return rank;\n  });\n\n  return applyWithChunking(Math.max, nodeRanks);\n}\n\n/*\n * Partition a collection into two groups: `lhs` and `rhs`. If the supplied\n * function returns true for an entry it goes into `lhs`. Otherwise it goes\n * into `rhs.\n */\nfunction partition(collection, fn) {\n  let result = { lhs: [], rhs: [] };\n  collection.forEach((value) => {\n    if (fn(value)) {\n      result.lhs.push(value);\n    } else {\n      result.rhs.push(value);\n    }\n  });\n  return result;\n}\n\n/*\n * Returns a new function that wraps `fn` with a timer. The wrapper logs the\n * time it takes to execute the function.\n */\nfunction time(name, fn) {\n  let start = Date.now();\n  try {\n    return fn();\n  } finally {\n    console.log(name + ' time: ' + (Date.now() - start) + 'ms');\n  }\n}\n\nfunction notime(name, fn) {\n  return fn();\n}\n\nlet idCounter = 0;\nfunction uniqueId(prefix) {\n  var id = ++idCounter;\n  return toString(prefix) + id;\n}\n\nfunction range(start, limit, step = 1) {\n  if (limit == null) {\n    limit = start;\n    start = 0;\n  }\n\n  let endCon = (i) => i < limit;\n  if (step < 0) {\n    endCon = (i) => limit < i;\n  }\n\n  const range = [];\n  for (let i = start; endCon(i); i += step) {\n    range.push(i);\n  }\n\n  return range;\n}\n\nfunction pick(source, keys) {\n  const dest = {};\n  for (const key of keys) {\n    if (source[key] !== undefined) {\n      dest[key] = source[key];\n    }\n  }\n\n  return dest;\n}\n\nfunction mapValues(obj, funcOrProp) {\n  let func = funcOrProp;\n  if (typeof funcOrProp === 'string') {\n    func = (val) => val[funcOrProp];\n  }\n\n  return Object.entries(obj).reduce((acc, [k, v]) => {\n    acc[k] = func(v, k);\n    return acc;\n  }, {});\n}\n\nfunction zipObject(props, values) {\n  return props.reduce((acc, key, i) => {\n    acc[key] = values[i];\n    return acc;\n  }, {});\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/dagre-lib/version.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport default '1.1.5-pre';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { dagreLib } from './dagre-lib';\nexport { createFreeAutoLayoutPlugin } from './create-auto-layout-plugin';\nexport { AutoLayoutService } from './services';\nexport { Graph as DagreGraph } from '@dagrejs/graphlib';\nexport * from './layout';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutConfig, LayoutOptions } from './type';\n\nexport const DefaultLayoutConfig: LayoutConfig = {\n  rankdir: 'LR',\n  align: undefined,\n  nodesep: 100,\n  edgesep: 10,\n  ranksep: 100,\n  marginx: 0,\n  marginy: 0,\n  acyclicer: undefined,\n  ranker: 'network-simplex',\n};\n\nexport const DefaultLayoutOptions: LayoutOptions = {\n  filterNode: undefined,\n  getFollowNode: undefined,\n  disableFitView: false,\n  enableAnimation: false,\n  animationDuration: 300,\n};\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/dagre.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Graph as DagreGraph } from '@dagrejs/graphlib';\n\nimport { dagreLib } from '../dagre-lib/index';\nimport { DagreNode, LayoutNode } from './type';\nimport { LayoutStore } from './store';\n\nexport class DagreLayout {\n  private readonly graph: DagreGraph;\n\n  constructor(private readonly store: LayoutStore) {\n    this.graph = this.createGraph();\n  }\n\n  public layout(): void {\n    this.graphSetData();\n    this.dagreLayout();\n    this.layoutSetPosition();\n  }\n\n  private dagreLayout(): void {\n    let layoutGraph = dagreLib.buildLayoutGraph(this.graph);\n    this.runLayout(layoutGraph);\n    dagreLib.updateInputGraph(this.graph, layoutGraph);\n  }\n\n  private runLayout(graph: DagreGraph): void {\n    dagreLib.makeSpaceForEdgeLabels(graph);\n    dagreLib.removeSelfEdges(graph);\n    dagreLib.acyclic.run(graph);\n    dagreLib.nestingGraph.run(graph);\n    dagreLib.rank(dagreLib.util.asNonCompoundGraph(graph));\n    dagreLib.injectEdgeLabelProxies(graph);\n    dagreLib.removeEmptyRanks(graph);\n    dagreLib.nestingGraph.cleanup(graph);\n    dagreLib.normalizeRanks(graph);\n    dagreLib.assignRankMinMax(graph);\n    dagreLib.removeEdgeLabelProxies(graph);\n    dagreLib.normalize.run(graph);\n    dagreLib.parentDummyChains(graph);\n    dagreLib.addBorderSegments(graph);\n    dagreLib.order(graph);\n    this.setOrderAndRank(graph);\n    dagreLib.insertSelfEdges(graph);\n    dagreLib.coordinateSystem.adjust(graph);\n    dagreLib.position(graph);\n    dagreLib.positionSelfEdges(graph);\n    dagreLib.removeBorderNodes(graph);\n    dagreLib.normalize.undo(graph);\n    dagreLib.fixupEdgeLabelCoords(graph);\n    dagreLib.coordinateSystem.undo(graph);\n    dagreLib.translateGraph(graph);\n    dagreLib.assignNodeIntersects(graph);\n    dagreLib.reversePointsForReversedEdges(graph);\n    dagreLib.acyclic.undo(graph);\n  }\n\n  private createGraph(): DagreGraph {\n    const graph = new DagreGraph({ multigraph: true });\n    graph.setDefaultEdgeLabel(() => ({}));\n    graph.setGraph(this.store.config);\n    return graph;\n  }\n\n  private graphSetData(): void {\n    const { nodes, edges } = this.store;\n    nodes.forEach((layoutNode) => {\n      this.graph.setNode(layoutNode.index, {\n        originID: layoutNode.id,\n        width: layoutNode.size.width,\n        height: layoutNode.size.height,\n      });\n    });\n    edges\n      .sort((next, prev) => {\n        if (next.fromIndex === prev.fromIndex) {\n          return next.toIndex! < prev.toIndex! ? -1 : 1;\n        }\n        return next.fromIndex < prev.fromIndex ? -1 : 1;\n      })\n      .forEach((layoutEdge) => {\n        this.graph.setEdge({\n          v: layoutEdge.fromIndex,\n          w: layoutEdge.toIndex,\n          name: layoutEdge.name,\n        });\n      });\n  }\n\n  private layoutSetPosition(): void {\n    this.store.nodes.forEach((layoutNode) => {\n      const offsetX = this.getOffsetX(layoutNode);\n      const graphNode = this.graph.node(layoutNode.index);\n      if (!graphNode) {\n        // 异常兜底，一般不会出现\n        layoutNode.rank = -1;\n        layoutNode.position = {\n          x: layoutNode.position.x + offsetX,\n          y: layoutNode.position.y,\n        };\n        return;\n      }\n      layoutNode.rank = graphNode.rank ?? -1;\n      layoutNode.position = {\n        x: this.normalizeNumber(graphNode.x) + offsetX,\n        y: this.normalizeNumber(graphNode.y),\n      };\n    });\n  }\n\n  private normalizeNumber(number: number): number {\n    // NaN 转为 0，异常兜底，一般不会出现\n    return Number.isNaN(number) ? 0 : number;\n  }\n\n  private getOffsetX(layoutNode: LayoutNode): number {\n    if (layoutNode.layoutNodes.length === 0) {\n      return 0;\n    }\n    // 存在子节点才需计算padding带来的偏移\n    const { padding } = layoutNode;\n    const leftOffset = -layoutNode.size.width / 2 + padding.left;\n    return leftOffset;\n  }\n\n  private setOrderAndRank(g: DagreGraph): DagreGraph {\n    // 跟随调整\n    this.followAdjust(g);\n    // 重新排序\n    this.normalizeOrder(g);\n    return g;\n  }\n\n  /** 跟随调整 */\n  private followAdjust(g: DagreGraph): void {\n    const rankGroup = this.rankGroup(g);\n    g.nodes().forEach((i) => {\n      const graphNode: DagreNode = g.node(i);\n      const layoutNode = this.store.getNodeByIndex(i);\n\n      // 没有跟随节点，则不调整\n      if (!graphNode || !layoutNode?.followedBy) return;\n      const { followedBy } = layoutNode;\n      const { rank: targetRank, order: targetOrder } = graphNode;\n\n      // 跟随节点索引\n      const followIndexes = followedBy\n        .map((id) => this.store.getNode(id)?.index)\n        .filter(Boolean) as string[];\n      const followSet = new Set(followIndexes);\n\n      // 目标节点之后的节点\n      const rankIndexes = rankGroup.get(targetRank);\n      if (!rankIndexes) return;\n      const afterIndexes = Array.from(rankIndexes).filter((index) => {\n        if (followSet.has(index)) return false;\n        const graphNode = g.node(index);\n        return graphNode.order > targetOrder;\n      });\n\n      // 目标节点之后的节点 order 增加跟随节点数量\n      afterIndexes.forEach((index) => {\n        const graphNode = g.node(index);\n        graphNode.order = graphNode.order + followedBy.length;\n      });\n\n      // 跟随节点 order 增加\n      followIndexes.forEach((followIndex, index) => {\n        const graphNode = g.node(followIndex);\n        graphNode.order = targetOrder + index + 1;\n        // 更新 rank 分组缓存\n        const originRank = graphNode.rank;\n        graphNode.rank = targetRank;\n        rankGroup.get(originRank)?.delete(followIndex);\n        rankGroup.get(targetRank)?.add(followIndex);\n      });\n    });\n  }\n\n  /** rank 内 order 可能不连续，需要重新排序 */\n  private normalizeOrder(g: DagreGraph): void {\n    const rankGroup = this.rankGroup(g);\n    rankGroup.forEach((indexSet, rank) => {\n      const graphNodes: DagreNode[] = Array.from(indexSet).map((id) => g.node(id));\n      graphNodes.sort((a, b) => a.order - b.order);\n      graphNodes.forEach((node, index) => {\n        node.order = index;\n      });\n    });\n  }\n\n  /** 获取 rank 分组 */\n  private rankGroup(g: DagreGraph): Map<number, Set<string>> {\n    const rankGroup = new Map<number, Set<string>>();\n    g.nodes().forEach((i) => {\n      const graphNode = g.node(i);\n      const rank = graphNode.rank;\n      if (!rankGroup.has(rank)) {\n        rankGroup.set(rank, new Set());\n      }\n      rankGroup.get(rank)?.add(i);\n    });\n    return rankGroup;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { Layout } from './layout';\nexport type { LayoutNode, LayoutEdge, GetFollowNode, LayoutOptions } from './type';\nexport type { LayoutStore } from './store';\nexport { DefaultLayoutConfig } from './constant';\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ILayout, LayoutConfig, LayoutOptions, LayoutParams } from './type';\nimport { LayoutStore } from './store';\nimport { LayoutPosition } from './position';\nimport { DagreLayout } from './dagre';\n\nexport class Layout implements ILayout {\n  private readonly _store: LayoutStore;\n\n  private readonly _layout: DagreLayout;\n\n  private readonly _position: LayoutPosition;\n\n  constructor(config: LayoutConfig) {\n    this._store = new LayoutStore(config);\n    this._layout = new DagreLayout(this._store);\n    this._position = new LayoutPosition(this._store);\n  }\n\n  public init(params: LayoutParams, options: LayoutOptions): void {\n    this._store.create(params, options);\n  }\n\n  public layout(): void {\n    if (!this._store.initialized) {\n      return;\n    }\n    this._layout.layout();\n  }\n\n  public async position(): Promise<void> {\n    if (!this._store.initialized) {\n      return;\n    }\n    return await this._position.position();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { PositionSchema, startTween } from '@flowgram.ai/core';\n\nimport { LayoutNode } from './type';\nimport { LayoutStore } from './store';\n\nexport class LayoutPosition {\n  constructor(private readonly store: LayoutStore) {}\n\n  public async position(): Promise<void> {\n    if (this.store.options.enableAnimation) {\n      return this.positionWithAnimation();\n    }\n    return this.positionDirectly();\n  }\n\n  private positionDirectly(): void {\n    this.store.nodes.forEach((layoutNode) => {\n      this.updateNodePosition({ layoutNode, step: 100 });\n    });\n  }\n\n  private async positionWithAnimation(): Promise<void> {\n    return new Promise((resolve) => {\n      startTween({\n        from: { d: 0 },\n        to: { d: 100 },\n        duration: this.store.options.animationDuration ?? 0,\n        onUpdate: (v) => {\n          this.store.nodes.forEach((layoutNode) => {\n            this.updateNodePosition({ layoutNode, step: v.d });\n          });\n        },\n        onComplete: () => {\n          resolve();\n        },\n      });\n    });\n  }\n\n  private updateNodePosition(params: { layoutNode: LayoutNode; step: number }): void {\n    const { layoutNode, step } = params;\n    const { transform } = layoutNode.entity.transform;\n\n    // layoutNode.position.y is the center point, but the canvas node origin is at the top edge.\n    // Subtract half the inner height to convert center-y to top-edge-y.\n    // When alignTopEdge is true, skip the offset so all nodes' top edges share the same horizontal line.\n    const centerToTopEdgeOffset = this.store.options.alignTopEdge\n      ? 0\n      : (layoutNode.size.height - layoutNode.padding.top - layoutNode.padding.bottom) / 2;\n\n    const layoutPosition: PositionSchema = {\n      x: layoutNode.position.x + layoutNode.offset.x,\n      y: layoutNode.position.y + layoutNode.offset.y - centerToTopEdgeOffset,\n    };\n\n    const deltaX = ((layoutPosition.x - transform.position.x) * step) / 100;\n    const deltaY = ((layoutPosition.y - transform.position.y) * step) / 100;\n\n    const position = {\n      x: transform.position.x + deltaX,\n      y: transform.position.y + deltaY,\n    };\n\n    transform.update({\n      position,\n    });\n    const document = layoutNode.entity.document as WorkflowDocument;\n    document.layout.updateAffectedTransform(layoutNode.entity);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\nimport type {\n  GetFollowNode,\n  ILayoutStore,\n  LayoutConfig,\n  LayoutEdge,\n  LayoutNode,\n  LayoutOptions,\n  LayoutParams,\n  LayoutStoreData,\n} from './type';\n\nexport class LayoutStore implements ILayoutStore {\n  private indexMap: Map<string, string>;\n\n  private init: boolean = false;\n\n  private store: LayoutStoreData;\n\n  public options: LayoutOptions;\n\n  public container: LayoutNode;\n\n  constructor(public readonly config: LayoutConfig) {}\n\n  public get initialized(): boolean {\n    return this.init;\n  }\n\n  public getNode(id?: string): LayoutNode | undefined {\n    if (!id) {\n      return undefined;\n    }\n    return this.store.nodes.get(id);\n  }\n\n  public getNodeByIndex(index: string): LayoutNode | undefined {\n    const id = this.indexMap.get(index);\n    return id ? this.getNode(id) : undefined;\n  }\n\n  public getEdge(id: string): LayoutEdge | undefined {\n    return this.store.edges.get(id);\n  }\n\n  public get nodes(): LayoutNode[] {\n    return Array.from(this.store.nodes.values());\n  }\n\n  public get edges(): LayoutEdge[] {\n    return Array.from(this.store.edges.values());\n  }\n\n  public create(params: LayoutParams, options: LayoutOptions): void {\n    this.container = params.container;\n    this.store = this.createStore(params);\n    this.indexMap = this.createIndexMap();\n    this.setOptions(options);\n    this.init = true;\n  }\n\n  /** 创建布局数据 */\n  private createStore(params: LayoutParams): LayoutStoreData {\n    const { layoutNodes, layoutEdges } = params;\n    const virtualEdges = this.createVirtualEdges(params);\n    const store = {\n      nodes: new Map(),\n      edges: new Map(),\n    };\n    layoutNodes.forEach((node) => store.nodes.set(node.id, node));\n    layoutEdges.concat(virtualEdges).forEach((edge) => store.edges.set(edge.id, edge));\n    return store;\n  }\n\n  /** 创建虚拟线条数据 */\n  private createVirtualEdges(params: LayoutParams): LayoutEdge[] {\n    const { layoutNodes, layoutEdges } = params;\n    const nodes = layoutNodes.map((layoutNode) => layoutNode.entity);\n    const edges = layoutEdges.map((layoutEdge) => layoutEdge.entity);\n    const groupNodes = nodes.filter((n) => n.flowNodeType === FlowNodeBaseType.GROUP);\n    const virtualEdges = groupNodes\n      .map((group) => {\n        const { id: groupId, blocks = [] } = group;\n        const blockIdSet = new Set(blocks.map((b) => b.id));\n        const groupFromEdges = edges\n          .filter((edge) => blockIdSet.has(edge.to?.id ?? ''))\n          .map((edge) => {\n            const { from, to } = edge.info;\n            if (!from || !to) {\n              return;\n            }\n            const id = `virtual_${groupId}_from_${from}_to_${to}`;\n            const layoutEdge: LayoutEdge = {\n              id: id,\n              entity: edge,\n              from,\n              to: groupId,\n              fromIndex: '', // 初始化时，index 未计算\n              toIndex: '', // 初始化时，index 未计算\n              name: id,\n            };\n            return layoutEdge;\n          })\n          .filter(Boolean) as LayoutEdge[];\n        const groupToEdges = edges\n          .filter((edge) => blockIdSet.has(edge.from?.id ?? ''))\n          .map((edge) => {\n            const { from, to } = edge.info;\n            if (!from || !to) {\n              return;\n            }\n            const id = `virtual_${groupId}_from_${from}_to_${to}`;\n            const layoutEdge: LayoutEdge = {\n              id: id,\n              entity: edge,\n              from: groupId,\n              to,\n              fromIndex: '', // 初始化时，index 未计算\n              toIndex: '', // 初始化时，index 未计算\n              name: id,\n            };\n            return layoutEdge;\n          })\n          .filter(Boolean) as LayoutEdge[];\n        return [...groupFromEdges, ...groupToEdges];\n      })\n      .flat();\n    return virtualEdges;\n  }\n\n  /** 创建节点索引映射 */\n  private createIndexMap(): Map<string, string> {\n    const nodeIndexes = this.sortNodes();\n    const nodeToIndex = new Map<string, string>();\n\n    // 创建节点索引映射\n    nodeIndexes.forEach((nodeId, nodeIndex) => {\n      const node = this.getNode(nodeId);\n      if (!node) {\n        return;\n      }\n      const graphIndex = String(100000 + nodeIndex);\n      nodeToIndex.set(node.id, graphIndex);\n      node.index = graphIndex;\n    });\n\n    // 创建连线索引映射\n    this.edges.forEach((edge) => {\n      const fromIndex = nodeToIndex.get(edge.from);\n      const toIndex = nodeToIndex.get(edge.to);\n      if (!fromIndex || !toIndex) {\n        this.store.edges.delete(edge.id);\n        return;\n      }\n      edge.fromIndex = fromIndex;\n      edge.toIndex = toIndex;\n    });\n\n    // 创建索引到节点的映射\n    const indexToNode = new Map();\n    nodeToIndex.forEach((index, id) => {\n      indexToNode.set(index, id);\n    });\n\n    return indexToNode;\n  }\n\n  /** 节点排序 */\n  private sortNodes(): Array<string> {\n    // 节点 id 列表，id 可能重复\n    const nodeIdList: string[] = [];\n\n    // 第1级排序：按照 node 添加顺序排序\n    this.nodes.forEach((node) => {\n      nodeIdList.push(node.id);\n    });\n\n    // 第2级排序：被连线节点排序靠后\n    this.edges.forEach((edge) => {\n      nodeIdList.push(edge.to);\n    });\n\n    // 第3级排序：按照从开始节点进行遍历排序\n    const visited = new Set<string>();\n    const visit = (node?: WorkflowNodeEntity) => {\n      if (!node || visited.has(node.id)) {\n        return;\n      }\n      visited.add(node.id);\n      nodeIdList.push(node.id);\n      // 访问子节点\n      node.blocks.forEach((child) => {\n        visit(child);\n      });\n      // 访问后续节点\n      const { outputLines } = node.lines;\n      const sortedLines = outputLines.sort((a, b) => {\n        const aNode = this.getNode(a.to?.id);\n        const bNode = this.getNode(b.to?.id);\n        const aPort = a.fromPort;\n        const bPort = b.fromPort;\n        // 同端口，对比to节点y轴坐标\n        if (aPort === bPort && aNode && bNode) {\n          return aNode.position.y - bNode.position.y;\n        }\n        // 同from节点的不同端口，对比端口y轴坐标\n        if (aPort && bPort) {\n          return aPort.point.y - bPort.point.y;\n        }\n        return 0;\n      });\n      sortedLines.forEach((line) => {\n        const { to } = line;\n        if (!to) {\n          return;\n        }\n        visit(to);\n      });\n    };\n    visit(this.container.entity);\n\n    // 使用 reduceRight 去重并保留最后一个出现的节点 id\n    const uniqueNodeIds: string[] = nodeIdList.reduceRight((acc: string[], nodeId: string) => {\n      if (!acc.includes(nodeId)) {\n        acc.unshift(nodeId);\n      }\n      return acc;\n    }, []);\n\n    return uniqueNodeIds;\n  }\n\n  /** 记录运行选项 */\n  private setOptions(options: LayoutOptions): void {\n    this.options = options;\n    this.setFollowNode(options.getFollowNode);\n  }\n\n  /** 设置跟随节点配置 */\n  private setFollowNode(getFollowNode?: GetFollowNode): void {\n    if (!getFollowNode) return;\n    const context = { store: this };\n    this.nodes.forEach((node) => {\n      const followTo = getFollowNode(node, context)?.followTo;\n      if (!followTo) return;\n      const followToNode = this.getNode(followTo);\n      if (!followToNode) return;\n      if (!followToNode.followedBy) {\n        followToNode.followedBy = [];\n      }\n      followToNode.followedBy.push(node.id);\n      node.followTo = followTo;\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/layout/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowLineEntity, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\n\nexport interface LayoutStoreData {\n  nodes: Map<string, LayoutNode>;\n  edges: Map<string, LayoutEdge>;\n}\n\nexport interface ILayoutStore {\n  container: LayoutNode;\n  options: LayoutOptions;\n  get initialized(): boolean;\n  getNode(id?: string): LayoutNode | undefined;\n  getNodeByIndex(index: string): LayoutNode | undefined;\n  getEdge(id: string): LayoutEdge | undefined;\n  nodes: LayoutNode[];\n  edges: LayoutEdge[];\n  create(params: LayoutParams, options: LayoutOptions): void;\n}\n\nexport interface ILayout {\n  init(params: LayoutParams, options: LayoutOptions): void;\n  layout(): void;\n  position(): Promise<void>;\n}\n\nexport interface LayoutSize {\n  width: number;\n  height: number;\n}\n\nexport interface LayoutNode {\n  id: string;\n  /** 节点索引 */\n  index: string;\n  /** 节点实体 */\n  entity: WorkflowNodeEntity;\n  /** 层级 */\n  rank: number;\n  /** 顺序 */\n  order: number;\n  /** 位置 */\n  position: {\n    x: number;\n    y: number;\n  };\n  /** 偏移量 */\n  offset: {\n    x: number;\n    y: number;\n  };\n  /** 边距 */\n  padding: {\n    top: number;\n    bottom: number;\n    left: number;\n    right: number;\n  };\n  /** 宽高 */\n  size: LayoutSize;\n  /** 子节点 */\n  layoutNodes: LayoutNode[];\n  /** 子线条 */\n  layoutEdges: LayoutEdge[];\n  /** 被跟随节点 */\n  followedBy?: string[];\n  /** 跟随节点 */\n  followTo?: string;\n}\n\nexport interface LayoutEdge {\n  id: string;\n  /** 线条实体 */\n  entity: WorkflowLineEntity;\n  /** 起点 */\n  from: string;\n  /** 终点 */\n  to: string;\n  /** 起点索引 */\n  fromIndex: string;\n  /** 终点索引 */\n  toIndex: string;\n  /** 线条名称 */\n  name: string;\n}\n\nexport interface DagreNode {\n  width: number;\n  height: number;\n  order: number;\n  rank: number;\n}\n\nexport interface LayoutParams {\n  container: LayoutNode;\n  layoutNodes: LayoutNode[];\n  layoutEdges: LayoutEdge[];\n}\n\nexport interface LayoutOptions {\n  /** Custom layout configuration to override default dagre settings. */\n  layoutConfig?: Partial<LayoutConfig>;\n  /** The container node entity used as the root for the layout. */\n  containerNode?: WorkflowNodeEntity;\n  /** Custom function to determine follow-node relationships between layout nodes. */\n  getFollowNode?: GetFollowNode;\n  /** Whether to animate node movements during layout positioning. */\n  enableAnimation?: boolean;\n  /** Duration of the position animation in milliseconds. Only effective when `enableAnimation` is true. */\n  animationDuration?: number;\n  /** When true, skips the fit-view step after layout is applied. */\n  disableFitView?: boolean;\n  /**\n   * When true, aligns nodes by their top edge instead of their center point.\n   * Defaults to false (center-aligned). Set to true to place all nodes' top edges on the same horizontal line.\n   */\n  alignTopEdge?: boolean;\n  /** Filter function to exclude specific nodes from the layout. Return false to skip a node. */\n  filterNode?: (params: { node: WorkflowNodeEntity; parent?: WorkflowNodeEntity }) => boolean;\n  /** Filter function to exclude specific edges from the layout. Return false to skip an edge. */\n  filterLine?: (params: { line: WorkflowLineEntity }) => boolean;\n}\n\nexport interface LayoutConfig {\n  /** Direction for rank nodes. Can be TB, BT, LR, or RL, where T = top, B = bottom, L = left, and R = right. */\n  rankdir: 'TB' | 'BT' | 'LR' | 'RL';\n  /** Alignment for rank nodes. Can be UL, UR, DL, or DR, where U = up, D = down, L = left, and R = right. */\n  align: 'UL' | 'UR' | 'DL' | 'DR' | undefined;\n  /** Number of pixels that separate nodes horizontally in the layout. */\n  nodesep: number;\n  /** Number of pixels that separate edges horizontally in the layout. */\n  edgesep: number;\n  /** Number of pixels that separate edges horizontally in the layout. */\n  ranksep: number;\n  /** Number of pixels to use as a margin around the left and right of the graph. */\n  marginx: number;\n  /** Number of pixels to use as a margin around the top and bottom of the graph. */\n  marginy: number;\n  /** If set to greedy, uses a greedy heuristic for finding a feedback arc set for a graph. A feedback arc set is a set of edges that can be removed to make a graph acyclic. */\n  acyclicer: 'greedy' | undefined;\n  /** Type of algorithm to assigns a rank to each node in the input graph. Possible values: network-simplex, tight-tree or longest-path */\n  ranker: 'network-simplex' | 'tight-tree' | 'longest-path';\n}\n\nexport type GetFollowNode = (\n  node: LayoutNode,\n  context: {\n    store: ILayoutStore;\n    /** 业务自定义参数 */\n    [key: string]: any;\n  }\n) =>\n  | {\n      followTo?: string;\n    }\n  | undefined;\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/services.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Rectangle } from '@flowgram.ai/utils';\nimport {\n  WorkflowDocument,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-core';\nimport { Playground } from '@flowgram.ai/core';\n\nimport { AutoLayoutOptions } from './type';\nimport { LayoutConfig, LayoutEdge, LayoutNode } from './layout/type';\nimport { DefaultLayoutOptions } from './layout/constant';\nimport { DefaultLayoutConfig, Layout, type LayoutOptions } from './layout';\n\n@injectable()\nexport class AutoLayoutService {\n  @inject(Playground)\n  private playground: Playground;\n\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  private layoutConfig: LayoutConfig = DefaultLayoutConfig;\n\n  public init(options: AutoLayoutOptions) {\n    this.layoutConfig = {\n      ...this.layoutConfig,\n      ...options.layoutConfig,\n    };\n  }\n\n  public async layout(options: Partial<LayoutOptions> = {}): Promise<void> {\n    const layoutOptions: LayoutOptions = {\n      ...DefaultLayoutOptions,\n      ...options,\n    };\n    const containerNode = layoutOptions.containerNode ?? this.document.root;\n    const container = this.createLayoutNode(containerNode, options);\n    const layouts = await this.layoutNode(container, layoutOptions);\n    const rect = this.getLayoutNodeRect(container);\n    const positionPromise = layouts.map((layout) => layout.position());\n    const fitViewPromise = this.fitView(layoutOptions, rect);\n    await Promise.all([...positionPromise, fitViewPromise]);\n  }\n\n  private async fitView(options: LayoutOptions, rect: Rectangle): Promise<void> {\n    if (options.disableFitView === true) {\n      return;\n    }\n    // 留出 30 像素的边界\n    return this.playground.config.fitView(rect, options.enableAnimation, 30);\n  }\n\n  private async layoutNode(container: LayoutNode, options: LayoutOptions): Promise<Layout[]> {\n    const { layoutNodes, layoutEdges } = container;\n    if (layoutNodes.length === 0) {\n      return [];\n    }\n    // 触发子节点布局\n    const childrenLayouts = (\n      await Promise.all(layoutNodes.map((n) => this.layoutNode(n, options)))\n    ).flat();\n    const layoutConfig: LayoutConfig = {\n      ...this.layoutConfig,\n      ...options.layoutConfig,\n    };\n    const layout = new Layout(layoutConfig);\n    layout.init({ container, layoutNodes, layoutEdges }, options);\n    layout.layout();\n    const rect = this.getLayoutNodeRect(container);\n    container.size = {\n      width: rect.width,\n      height: rect.height,\n    };\n    return [...childrenLayouts, layout];\n  }\n\n  private createLayoutNodes(nodes: WorkflowNodeEntity[], options: LayoutOptions): LayoutNode[] {\n    return nodes.map((node) => this.createLayoutNode(node, options));\n  }\n\n  /** 创建节点布局数据 */\n  private createLayoutNode(node: WorkflowNodeEntity, options: LayoutOptions): LayoutNode {\n    const blocks = node.blocks.filter((blockNode) =>\n      options.filterNode ? options.filterNode?.({ node: blockNode, parent: node.parent }) : true\n    );\n    const edges = this.getNodesAllLines(blocks).filter((edge) =>\n      options.filterLine ? options.filterLine?.({ line: edge }) : true\n    );\n\n    // 创建子布局节点\n    const layoutNodes = this.createLayoutNodes(blocks, options);\n    const layoutEdges = this.createLayoutEdges(edges);\n\n    const { bounds, padding } = node.transform;\n    const { width, height, center } = bounds;\n    const { x, y } = center;\n    const layoutNode: LayoutNode = {\n      id: node.id,\n      entity: node,\n      index: '', // 初始化时，index 未计算\n      rank: -1, // 初始化时，节点还未布局，层级为-1\n      order: -1, // 初始化时，节点还未布局，顺序为-1\n      position: { x, y },\n      offset: { x: 0, y: 0 },\n      padding,\n      size: { width, height },\n      layoutNodes,\n      layoutEdges,\n    };\n    return layoutNode;\n  }\n\n  private createLayoutEdges(edges: WorkflowLineEntity[]): LayoutEdge[] {\n    const layoutEdges = edges\n      .map((edge) => this.createLayoutEdge(edge))\n      .filter(Boolean) as LayoutEdge[];\n    return layoutEdges;\n  }\n\n  /** 创建线条布局数据 */\n  private createLayoutEdge(edge: WorkflowLineEntity): LayoutEdge | undefined {\n    const { from, to } = edge.info;\n    if (!from || !to) {\n      return;\n    }\n    const layoutEdge: LayoutEdge = {\n      id: edge.id,\n      entity: edge,\n      from,\n      to,\n      fromIndex: '', // 初始化时，index 未计算\n      toIndex: '', // 初始化时，index 未计算\n      name: edge.id,\n    };\n    return layoutEdge;\n  }\n\n  private getNodesAllLines(nodes: WorkflowNodeEntity[]): WorkflowLineEntity[] {\n    const lines = nodes\n      .map((node) => {\n        const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);\n        const outputLines = linesData.outputLines.filter(Boolean);\n        const inputLines = linesData.inputLines.filter(Boolean);\n        return [...outputLines, ...inputLines];\n      })\n      .flat();\n\n    return lines;\n  }\n\n  private getLayoutNodeRect(layoutNode: LayoutNode): Rectangle {\n    const rects = layoutNode.layoutNodes.map((node) => this.layoutNodeRect(node));\n    const rect = Rectangle.enlarge(rects);\n    const { padding } = layoutNode;\n    const width = rect.width + padding.left + padding.right;\n    const height = rect.height + padding.top + padding.bottom;\n    const x = rect.x - padding.left;\n    const y = rect.y - padding.top;\n    return new Rectangle(x, y, width, height);\n  }\n\n  private layoutNodeRect(layoutNode: LayoutNode): Rectangle {\n    const { width, height } = layoutNode.size;\n    const x = layoutNode.position.x - width / 2;\n    const y = layoutNode.position.y - height / 2;\n    return new Rectangle(x, y, width, height);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { LayoutConfig } from './layout/type';\n\nexport interface AutoLayoutOptions {\n  layoutConfig?: Partial<LayoutConfig>;\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n}\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-auto-layout-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/package.json",
    "content": "{\n    \"name\": \"@flowgram.ai/free-container-plugin\",\n    \"version\": \"0.1.8\",\n    \"homepage\": \"https://flowgram.ai/\",\n    \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n    \"license\": \"MIT\",\n    \"exports\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"import\": \"./dist/esm/index.js\",\n        \"require\": \"./dist/index.js\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"module\": \"./dist/esm/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"npm run build:fast -- --dts-resolve\",\n        \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n        \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n        \"clean\": \"rimraf dist\",\n        \"test\": \"exit 0\",\n        \"test:cov\": \"exit 0\",\n        \"ts-check\": \"tsc --noEmit\",\n        \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n    },\n    \"dependencies\": {\n        \"@flowgram.ai/free-history-plugin\": \"workspace:*\",\n        \"@flowgram.ai/core\": \"workspace:*\",\n        \"@flowgram.ai/document\": \"workspace:*\",\n        \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n        \"@flowgram.ai/utils\": \"workspace:*\",\n        \"@flowgram.ai/i18n\": \"workspace:*\",\n        \"@flowgram.ai/background-plugin\": \"workspace:*\",\n        \"inversify\": \"^6.0.1\",\n        \"reflect-metadata\": \"~0.2.2\",\n        \"lodash-es\": \"^4.17.21\"\n    },\n    \"devDependencies\": {\n        \"@flowgram.ai/eslint-config\": \"workspace:*\",\n        \"@flowgram.ai/ts-config\": \"workspace:*\",\n        \"@types/bezier-js\": \"4.1.3\",\n        \"@types/lodash-es\": \"^4.17.12\",\n        \"@types/react\": \"^18\",\n        \"@types/react-dom\": \"^18\",\n        \"@types/styled-components\": \"^5\",\n        \"@vitest/coverage-v8\": \"^3.2.4\",\n        \"eslint\": \"^9.0.0\",\n        \"react\": \"^18\",\n        \"react-dom\": \"^18\",\n        \"styled-components\": \"^5\",\n        \"tsup\": \"^8.0.1\",\n        \"typescript\": \"^5.8.3\",\n        \"vitest\": \"^3.2.4\"\n    },\n    \"peerDependencies\": {\n        \"react\": \">=16.8\",\n        \"react-dom\": \">=16.8\",\n        \"styled-components\": \">=5\"\n    },\n    \"publishConfig\": {\n        \"access\": \"public\",\n        \"registry\": \"https://registry.npmjs.org/\"\n    }\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './node-into-container';\nexport * from './sub-canvas';\nexport { ContainerUtils } from './utils';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/node-into-container/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum NodeIntoContainerType {\n  In = 'in',\n  Out = 'out',\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/node-into-container/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeIntoContainerType } from './constant';\nexport { NodeIntoContainerService } from './service';\nexport {\n  NodeIntoContainerState,\n  NodeIntoContainerEvent,\n  WorkflowContainerPluginOptions,\n} from './type';\nexport { createContainerNodePlugin } from './plugin';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/node-into-container/plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport type { WorkflowContainerPluginOptions } from './type';\nimport { NodeIntoContainerService } from '.';\n\nexport const createContainerNodePlugin = definePluginCreator<WorkflowContainerPluginOptions>({\n  onBind: ({ bind }) => {\n    bind(NodeIntoContainerService).toSelf().inSingletonScope();\n  },\n  onInit(ctx, options) {\n    ctx.get(NodeIntoContainerService).init();\n  },\n  onReady(ctx, options) {\n    if (options.disableNodeIntoContainer !== true) {\n      ctx.get(NodeIntoContainerService).ready();\n    }\n  },\n  onDispose(ctx) {\n    ctx.get(NodeIntoContainerService).dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/node-into-container/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/no-non-null-assertion -- no need */\nimport { throttle } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { type Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport {\n  type NodesDragEvent,\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  type WorkflowNodeEntity,\n  WorkflowNodeMeta,\n  WorkflowOperationBaseService,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\nimport { FlowNodeRenderData, FlowNodeBaseType } from '@flowgram.ai/document';\nimport { PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';\n\nimport type { NodeIntoContainerEvent, NodeIntoContainerState } from './type';\nimport { NodeIntoContainerType } from './constant';\nimport { ContainerUtils } from '../utils';\n\n@injectable()\nexport class NodeIntoContainerService {\n  public state: NodeIntoContainerState;\n\n  @inject(WorkflowDragService)\n  private dragService: WorkflowDragService;\n\n  @inject(WorkflowDocument)\n  private document: WorkflowDocument;\n\n  @inject(PlaygroundConfigEntity)\n  private playgroundConfig: PlaygroundConfigEntity;\n\n  @inject(WorkflowOperationBaseService)\n  private operationService: WorkflowOperationBaseService;\n\n  @inject(WorkflowLinesManager)\n  private linesManager: WorkflowLinesManager;\n\n  @inject(HistoryService) private historyService: HistoryService;\n\n  @inject(WorkflowSelectService) private selectService: WorkflowSelectService;\n\n  private emitter = new Emitter<NodeIntoContainerEvent>();\n\n  private toDispose = new DisposableCollection();\n\n  public readonly on = this.emitter.event;\n\n  public init(): void {\n    this.initState();\n    this.toDispose.push(this.emitter);\n  }\n\n  public ready(): void {\n    this.toDispose.push(this.listenDragToContainer());\n  }\n\n  public dispose(): void {\n    this.initState();\n    this.toDispose.dispose();\n  }\n\n  /** 将节点移出容器 */\n  public async moveOutContainer(params: { node: WorkflowNodeEntity }): Promise<void> {\n    const { node } = params;\n    const parentNode = node.parent;\n    const containerNode = parentNode?.parent;\n    const nodeJSON = this.document.toNodeJSON(node);\n    if (\n      !parentNode ||\n      !containerNode ||\n      !ContainerUtils.isContainer(parentNode) ||\n      !nodeJSON.meta?.position\n    ) {\n      return;\n    }\n    const parentTransform = parentNode.getData<TransformData>(TransformData);\n    this.operationService.moveNode(node, {\n      parent: containerNode,\n    });\n    await ContainerUtils.nextFrame();\n    parentTransform.fireChange();\n    this.operationService.updateNodePosition(node, {\n      x: parentTransform.position.x + nodeJSON.meta!.position!.x,\n      y: parentTransform.position.y + nodeJSON.meta!.position!.y,\n    });\n    this.emitter.fire({\n      type: NodeIntoContainerType.Out,\n      node,\n      sourceContainer: parentNode,\n      targetContainer: containerNode,\n    });\n  }\n\n  /** 能否将节点移出容器 */\n  public canMoveOutContainer(node: WorkflowNodeEntity): boolean {\n    const parentNode = node.parent;\n    const containerNode = parentNode?.parent;\n    if (!parentNode || !containerNode || !ContainerUtils.isContainer(parentNode)) {\n      return false;\n    }\n    const canDrop = this.dragService.canDropToNode({\n      dragNodeType: node.flowNodeType,\n      dragNode: node,\n      dropNodeType: containerNode?.flowNodeType,\n      dropNode: containerNode,\n    });\n    if (!canDrop.allowDrop) {\n      return false;\n    }\n    return true;\n  }\n\n  /** 移除节点所有非法连线 */\n  public async clearInvalidLines(params: {\n    dragNode?: WorkflowNodeEntity;\n    sourceParent?: WorkflowNodeEntity;\n  }): Promise<void> {\n    const { dragNode, sourceParent } = params;\n    if (!dragNode) {\n      return;\n    }\n    if (dragNode.parent === sourceParent) {\n      // 容器节点未改变\n      return;\n    }\n    if (\n      dragNode.parent?.flowNodeType === FlowNodeBaseType.GROUP ||\n      sourceParent?.flowNodeType === FlowNodeBaseType.GROUP\n    ) {\n      // 移入移出 group 节点无需删除节点\n      return;\n    }\n    await this.removeNodeLines(dragNode);\n  }\n\n  /** 移除节点连线 */\n  public async removeNodeLines(node: WorkflowNodeEntity): Promise<void> {\n    const lines = this.linesManager.getAllLines();\n    lines.forEach((line) => {\n      if (line.from?.id !== node.id && line.to?.id !== node.id) {\n        return;\n      }\n      line.dispose();\n    });\n    await ContainerUtils.nextFrame();\n  }\n\n  /** 初始化状态 */\n  private initState(): void {\n    this.state = {\n      isDraggingNode: false,\n      isSkipEvent: false,\n      transforms: undefined,\n      dragNode: undefined,\n      dropNode: undefined,\n      sourceParent: undefined,\n    };\n  }\n\n  /** 监听节点拖拽 */\n  private listenDragToContainer(): Disposable {\n    const draggingNode = (e: NodesDragEvent) => this.draggingNode(e);\n    const throttledDraggingNode = throttle(draggingNode, 200); // 200ms触发一次计算\n    return this.dragService.onNodesDrag(async (event) => {\n      if (this.selectService.selectedNodes.length !== 1) {\n        return;\n      }\n      if (event.type === 'onDragStart') {\n        if (this.state.isSkipEvent) {\n          // 拖出容器后重新进入\n          this.state.isSkipEvent = false;\n          return;\n        }\n        this.historyService.startTransaction(); // 开始合并历史记录\n        this.state.isDraggingNode = true;\n        this.state.transforms = ContainerUtils.getContainerTransforms(this.document.getAllNodes());\n        this.state.dragNode = this.selectService.selectedNodes[0];\n        this.state.dropNode = undefined;\n        this.state.sourceParent = this.state.dragNode?.parent;\n        await this.dragOutContainer(event); // 检查是否需拖出容器\n      }\n      if (event.type === 'onDragging') {\n        throttledDraggingNode(event);\n      }\n      if (event.type === 'onDragEnd') {\n        if (this.state.isSkipEvent) {\n          // 拖出容器情况下需跳过本次事件\n          return;\n        }\n        throttledDraggingNode.cancel();\n        draggingNode(event); // 直接触发一次计算，防止延迟\n        await this.dropNodeToContainer(); // 放置节点\n        await this.clearInvalidLines({\n          dragNode: this.state.dragNode,\n          sourceParent: this.state.sourceParent,\n        }); // 清除非法线条\n        this.setDropNode(undefined);\n        this.initState(); // 重置状态\n        this.historyService.endTransaction(); // 结束合并历史记录\n      }\n    });\n  }\n\n  /** 监听节点拖拽出容器 */\n  private async dragOutContainer(event: NodesDragEvent): Promise<void> {\n    const { dragNode } = this.state;\n    const activated = event.triggerEvent.metaKey || event.triggerEvent.ctrlKey;\n    if (\n      !activated || // 必须按住指定按键\n      !dragNode || // 必须有一个节点\n      !this.canMoveOutContainer(dragNode) // 需要能被移出容器\n    ) {\n      return;\n    }\n    this.moveOutContainer({ node: dragNode });\n    this.state.isSkipEvent = true;\n    event.dragger.stop(event.dragEvent.clientX, event.dragEvent.clientY);\n    await ContainerUtils.nextFrame();\n    this.dragService.startDragSelectedNodes(event.triggerEvent);\n  }\n\n  /** 设置放置节点高亮 */\n  private setDropNode(dropNode?: WorkflowNodeEntity) {\n    if (this.state.dropNode === dropNode) {\n      return;\n    }\n    if (this.state.dropNode) {\n      // 清除上一个节点高亮\n      const renderData = this.state.dropNode.getData(FlowNodeRenderData);\n      const renderDom = renderData.node?.children?.[0] as HTMLElement;\n      if (renderDom) {\n        renderDom.classList.remove('selected');\n      }\n    }\n    this.state.dropNode = dropNode;\n    if (!dropNode) {\n      return;\n    }\n    // 设置当前节点高亮\n    const renderData = dropNode.getData(FlowNodeRenderData);\n    const renderDom = renderData.node?.children?.[0] as HTMLElement;\n    if (renderDom) {\n      renderDom.classList.add('selected');\n    }\n  }\n\n  /** 放置节点到容器 */\n  private async dropNodeToContainer(): Promise<void> {\n    const { dropNode, dragNode, isDraggingNode } = this.state;\n    if (!isDraggingNode || !dragNode || !dropNode) {\n      return;\n    }\n    return await this.moveIntoContainer({\n      node: dragNode,\n      containerNode: dropNode,\n    });\n  }\n\n  /** 拖拽节点 */\n  private draggingNode(nodeDragEvent: NodesDragEvent): void {\n    const { dragNode, isDraggingNode, transforms = [] } = this.state;\n    if (!isDraggingNode || !dragNode || !transforms?.length) {\n      return this.setDropNode(undefined);\n    }\n    const mousePos = this.playgroundConfig.getPosFromMouseEvent(nodeDragEvent.dragEvent);\n    const availableTransforms = transforms.filter(\n      (transform) => transform.entity.id !== dragNode.id\n    );\n    const collisionTransform = ContainerUtils.getCollisionTransform({\n      targetPoint: mousePos,\n      transforms: availableTransforms,\n      document: this.document,\n    });\n    const dropNode = collisionTransform?.entity;\n    const canDrop = this.canDropToContainer({\n      dragNode,\n      dropNode,\n    });\n    if (!canDrop) {\n      return this.setDropNode(undefined);\n    }\n    return this.setDropNode(dropNode);\n  }\n\n  /** 判断能否将节点拖入容器 */\n  protected canDropToContainer(params: {\n    dragNode: WorkflowNodeEntity;\n    dropNode?: WorkflowNodeEntity;\n  }): boolean {\n    const { dragNode, dropNode } = params;\n    const isDropContainer = dropNode?.getNodeMeta<WorkflowNodeMeta>().isContainer;\n    if (\n      !dropNode ||\n      !isDropContainer ||\n      this.isParent(dragNode, dropNode) ||\n      this.isParent(dropNode, dragNode)\n    ) {\n      return false;\n    }\n    if (\n      dragNode.flowNodeType === FlowNodeBaseType.GROUP &&\n      dropNode.flowNodeType !== FlowNodeBaseType.GROUP\n    ) {\n      // 禁止将 group 节点拖入非 group 节点（由于目前不支持多节点拖入容器，无法计算有效线条，因此进行屏蔽）\n      return false;\n    }\n    const canDrop = this.dragService.canDropToNode({\n      dragNodeType: dragNode.flowNodeType,\n      dropNodeType: dropNode?.flowNodeType,\n      dragNode,\n      dropNode,\n    });\n    if (!canDrop.allowDrop) {\n      return false;\n    }\n    return true;\n  }\n\n  /** 判断一个节点是否为另一个节点的父节点(向上查找直到根节点) */\n  private isParent(node: WorkflowNodeEntity, parent: WorkflowNodeEntity): boolean {\n    let current = node.parent;\n    while (current) {\n      if (current.id === parent.id) {\n        return true;\n      }\n      current = current.parent;\n    }\n    return false;\n  }\n\n  /** 将节点移入容器 */\n  private async moveIntoContainer(params: {\n    node: WorkflowNodeEntity;\n    containerNode: WorkflowNodeEntity;\n  }): Promise<void> {\n    const { node, containerNode } = params;\n    const parentNode = node.parent;\n\n    this.operationService.moveNode(node, {\n      parent: containerNode,\n    });\n\n    const containerPadding = this.document.layout.getPadding(containerNode);\n    const position = ContainerUtils.adjustSubNodePosition({\n      targetNode: node,\n      containerNode,\n      containerPadding,\n    });\n\n    this.operationService.updateNodePosition(node, position);\n\n    await ContainerUtils.nextFrame();\n\n    this.emitter.fire({\n      type: NodeIntoContainerType.In,\n      node,\n      sourceContainer: parentNode,\n      targetContainer: containerNode,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/node-into-container/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport type { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport type { NodeIntoContainerType } from './constant';\n\nexport interface NodeIntoContainerState {\n  isDraggingNode: boolean;\n  isSkipEvent: boolean;\n  transforms?: FlowNodeTransformData[];\n  dragNode?: WorkflowNodeEntity;\n  dropNode?: WorkflowNodeEntity;\n  sourceParent?: WorkflowNodeEntity;\n}\n\nexport interface NodeIntoContainerEvent {\n  type: NodeIntoContainerType;\n  node: WorkflowNodeEntity;\n  sourceContainer?: WorkflowNodeEntity;\n  targetContainer: WorkflowNodeEntity;\n}\n\nexport interface WorkflowContainerPluginOptions {\n  disableNodeIntoContainer?: boolean;\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/background/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { type FC } from 'react';\n\nimport { useCurrentEntity } from '@flowgram.ai/free-layout-core';\nimport { useService } from '@flowgram.ai/core';\nimport { BackgroundConfig, BackgroundLayerOptions } from '@flowgram.ai/background-plugin';\n\nimport { SubCanvasBackgroundStyle } from './style';\n\nexport const SubCanvasBackground: FC = () => {\n  const node = useCurrentEntity();\n\n  // 通过 inversify 获取背景配置，如果没有配置则使用默认值\n  let backgroundConfig: BackgroundLayerOptions = {};\n  try {\n    backgroundConfig = useService<BackgroundLayerOptions>(BackgroundConfig);\n  } catch (error) {\n    // 如果 BackgroundConfig 没有注册，使用默认配置\n    // 静默处理，使用默认配置\n  }\n\n  // 获取配置值，如果没有则使用默认值\n  const gridSize = backgroundConfig.gridSize ?? 20;\n  const dotSize = backgroundConfig.dotSize ?? 1;\n  const dotColor = backgroundConfig.dotColor ?? '#eceeef';\n  const dotOpacity = backgroundConfig.dotOpacity ?? 0.5;\n  const backgroundColor = backgroundConfig.backgroundColor ?? '#f2f3f5';\n  // 只有当 dotFillColor 被明确设置且与 dotColor 不同时才添加 fill 属性\n  const dotFillColor =\n    backgroundConfig.dotFillColor === dotColor ? '' : backgroundConfig.dotFillColor;\n\n  // 生成唯一的 pattern ID\n  const patternId = `sub-canvas-dot-pattern-${node.id}`;\n\n  return (\n    <SubCanvasBackgroundStyle\n      className=\"sub-canvas-background\"\n      data-flow-editor-selectable=\"true\"\n      style={{ backgroundColor: backgroundColor }}\n    >\n      <svg width=\"100%\" height=\"100%\">\n        <pattern id={patternId} width={gridSize} height={gridSize} patternUnits=\"userSpaceOnUse\">\n          <circle\n            cx={dotSize}\n            cy={dotSize}\n            r={dotSize}\n            stroke={dotColor}\n            fill={dotFillColor}\n            fillOpacity={dotOpacity}\n          />\n        </pattern>\n        <rect\n          width=\"100%\"\n          height=\"100%\"\n          fill={`url(#${patternId})`}\n          data-node-panel-container={node.id}\n        />\n      </svg>\n    </SubCanvasBackgroundStyle>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/background/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const SubCanvasBackgroundStyle = styled.div`\n  width: 100%;\n  height: 100%;\n  inset: 56px 18px 18px;\n  /* 背景色现在通过 style 属性动态设置 */\n`;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/border/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { CSSProperties, ReactNode, type FC } from 'react';\n\nimport { SubCanvasBorderStyle } from './style';\n\ninterface ISubCanvasBorder {\n  style?: CSSProperties;\n  children?: ReactNode | ReactNode[];\n}\n\nexport const SubCanvasBorder: FC<ISubCanvasBorder> = ({ style, children }) => (\n  <SubCanvasBorderStyle\n    className=\"sub-canvas-border\"\n    style={{\n      ...style,\n    }}\n  >\n    {children}\n  </SubCanvasBorderStyle>\n);\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/border/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const SubCanvasBorderStyle = styled.div`\n  pointer-events: none;\n\n  position: relative;\n\n  display: flex;\n  align-items: center;\n\n  width: 100%;\n  height: 100%;\n\n  background-color: transparent;\n  border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));\n  border-color: var(--coz-bg-plus, rgb(249, 249, 249));\n  border-style: solid;\n  border-width: 8px;\n  border-radius: 8px;\n\n  &::before {\n    content: '';\n\n    position: absolute;\n    z-index: 0;\n    inset: -4px;\n\n    background-color: transparent;\n    border-color: var(--coz-bg-plus, rgb(249, 249, 249));\n    border-style: solid;\n    border-width: 4px;\n    border-radius: 8px;\n  }\n`;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { SubCanvasBackground } from './background';\nexport { SubCanvasBorder } from './border';\nexport { SubCanvasRender } from './render';\nexport { SubCanvasTips } from './tips';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { CSSProperties, type FC } from 'react';\n\nimport { SubCanvasRenderStyle } from './style';\nimport { SubCanvasTips } from '../tips';\nimport { SubCanvasBorder } from '../border';\nimport { SubCanvasBackground } from '../background';\nimport { useNodeSize, useSyncNodeRenderSize } from '../../hooks';\n\ninterface ISubCanvasRender {\n  offsetY?: number;\n  className?: string;\n  style?: CSSProperties;\n  tipText?: string | React.ReactNode;\n}\n\nexport const SubCanvasRender: FC<ISubCanvasRender> = ({\n  className,\n  style,\n  offsetY = 0,\n  tipText,\n}) => {\n  const nodeSize = useNodeSize();\n  const nodeHeight = nodeSize?.height ?? 0;\n\n  useSyncNodeRenderSize(nodeSize);\n\n  return (\n    <SubCanvasRenderStyle\n      className={`sub-canvas-render ${className ?? ''}`}\n      style={{\n        height: nodeHeight + offsetY,\n        ...style,\n      }}\n      data-flow-editor-selectable=\"true\"\n      onDragStart={(e) => {\n        e.stopPropagation();\n      }}\n    >\n      <SubCanvasBorder>\n        <SubCanvasBackground />\n        <SubCanvasTips tipText={tipText} />\n      </SubCanvasBorder>\n    </SubCanvasRenderStyle>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/render/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const SubCanvasRenderStyle = styled.div`\n  width: 100%;\n  height: 100%;\n`;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/global-store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention -- no need */\n\nconst STORAGE_KEY = 'workflow-move-into-sub-canvas-tip-visible';\nconst STORAGE_VALUE = 'false';\n\nexport class TipsGlobalStore {\n  private static _instance?: TipsGlobalStore;\n\n  public static get instance(): TipsGlobalStore {\n    if (!this._instance) {\n      this._instance = new TipsGlobalStore();\n    }\n    return this._instance;\n  }\n\n  private closed = false;\n\n  public isClosed(): boolean {\n    return this.isCloseForever() || this.closed;\n  }\n\n  public close(): void {\n    this.closed = true;\n  }\n\n  public isCloseForever(): boolean {\n    return localStorage.getItem(STORAGE_KEY) === STORAGE_VALUE;\n  }\n\n  public closeForever(): void {\n    localStorage.setItem(STORAGE_KEY, STORAGE_VALUE);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/icon-close.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nexport const IconClose = () => (\n  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"none\" viewBox=\"0 0 16 16\">\n    <path\n      fill=\"#060709\"\n      fillOpacity=\"0.5\"\n      d=\"M12.13 12.128a.5.5 0 0 0 .001-.706L8.71 8l3.422-3.423a.5.5 0 0 0-.001-.705.5.5 0 0 0-.706-.002L8.002 7.293 4.579 3.87a.5.5 0 0 0-.705.002.5.5 0 0 0-.002.705L7.295 8l-3.423 3.422a.5.5 0 0 0 .002.706c.195.195.51.197.705.001l3.423-3.422 3.422 3.422c.196.196.51.194.706-.001\"\n    ></path>\n  </svg>\n);\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { I18n } from '@flowgram.ai/i18n';\n\nimport { useControlTips } from './use-control';\nimport { SubCanvasTipsStyle } from './style';\nimport { isMacOS } from './is-mac-os';\nimport { IconClose } from './icon-close';\n\ninterface SubCanvasTipsProps {\n  tipText?: string | React.ReactNode;\n  neverRemindText?: string | React.ReactNode;\n}\n\nexport const SubCanvasTips = ({ tipText, neverRemindText }: SubCanvasTipsProps) => {\n  const { visible, close, closeForever } = useControlTips();\n\n  const displayContent =\n    tipText || I18n.t('Hold {{key}} to drag node out', { key: isMacOS ? 'Cmd ⌘' : 'Ctrl' });\n\n  if (!visible) {\n    return null;\n  }\n  return (\n    <SubCanvasTipsStyle className={'sub-canvas-tips'}>\n      <div className=\"container\">\n        <div className=\"content\">\n          {typeof displayContent === 'string' ? (\n            <p className=\"text\">{displayContent}</p>\n          ) : (\n            <div className=\"custom-content\">{displayContent}</div>\n          )}\n          <div\n            className=\"space\"\n            style={{\n              width: 0,\n            }}\n          />\n        </div>\n        <div className=\"actions\">\n          <p className=\"close-forever\" onClick={closeForever}>\n            {neverRemindText || I18n.t('Never Remind')}\n          </p>\n          <div className=\"close\" onClick={close}>\n            <IconClose />\n          </div>\n        </div>\n      </div>\n    </SubCanvasTipsStyle>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/is-mac-os.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const isMacOS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const SubCanvasTipsStyle = styled.div`\n  pointer-events: auto;\n  position: absolute;\n  top: 0;\n\n  width: 100%;\n  height: 28px;\n\n  .container {\n    height: 100%;\n    background-color: #e4e6f5;\n    border-radius: 4px 4px 0 0;\n\n    .content {\n      overflow: hidden;\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n\n      width: 100%;\n      height: 100%;\n\n      .text {\n        font-size: 14px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 20px;\n        color: rgba(15, 21, 40, 82%);\n        text-overflow: ellipsis;\n      }\n\n      .custom-content {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        width: 100%;\n        height: 100%;\n\n        /* 为自定义内容提供默认样式，但允许覆盖 */\n        font-size: 14px;\n        font-weight: 400;\n        line-height: 20px;\n        color: rgba(15, 21, 40, 82%);\n\n        /* 确保自定义内容不会超出容器 */\n        overflow: hidden;\n      }\n\n      .space {\n        width: 128px;\n      }\n    }\n\n    .actions {\n      position: absolute;\n      top: 0;\n      right: 0;\n\n      display: flex;\n      gap: 8px;\n      align-items: center;\n\n      height: 28px;\n      padding: 0 16px;\n\n      .close-forever {\n        cursor: pointer;\n\n        padding: 0 3px;\n\n        font-size: 12px;\n        font-weight: 400;\n        font-style: normal;\n        line-height: 12px;\n        color: rgba(32, 41, 69, 62%);\n      }\n\n      .close {\n        display: flex;\n        cursor: pointer;\n        height: 100%;\n        align-items: center;\n      }\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/components/tips/use-control.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { useCurrentEntity } from '@flowgram.ai/free-layout-core';\nimport { useService } from '@flowgram.ai/core';\n\nimport { NodeIntoContainerService, NodeIntoContainerType } from '../../../node-into-container';\nimport { TipsGlobalStore } from './global-store';\n\nexport const useControlTips = () => {\n  const node = useCurrentEntity();\n  const [visible, setVisible] = useState(false);\n  const globalStore = TipsGlobalStore.instance;\n\n  const nodeIntoContainerService = useService<NodeIntoContainerService>(NodeIntoContainerService);\n\n  const show = useCallback(() => {\n    if (globalStore.isClosed()) {\n      return;\n    }\n\n    setVisible(true);\n  }, [globalStore]);\n\n  const close = useCallback(() => {\n    globalStore.close();\n    setVisible(false);\n  }, [globalStore]);\n\n  const closeForever = useCallback(() => {\n    globalStore.closeForever();\n    close();\n  }, [close, globalStore]);\n\n  useEffect(() => {\n    // 监听移入\n    const inDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.In) {\n        return;\n      }\n      if (e.targetContainer === node) {\n        show();\n      }\n    });\n    // 监听移出事件\n    const outDisposer = nodeIntoContainerService.on((e) => {\n      if (e.type !== NodeIntoContainerType.Out) {\n        return;\n      }\n      if (e.sourceContainer === node && !node.blocks.length) {\n        setVisible(false);\n      }\n    });\n    return () => {\n      inDisposer.dispose();\n      outDisposer.dispose();\n    };\n  }, [nodeIntoContainerService, node, show, close, visible]);\n\n  return {\n    visible,\n    close,\n    closeForever,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { NodeSize, useNodeSize } from './use-node-size';\nexport { useSyncNodeRenderSize } from './use-sync-node-render-size';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/hooks/use-node-size.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState, useEffect } from 'react';\n\nimport {\n  useCurrentEntity,\n  WorkflowNodeMeta,\n  WorkflowNodePortsData,\n} from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nexport interface NodeSize {\n  width: number;\n  height: number;\n}\n\nexport const useNodeSize = (): NodeSize | undefined => {\n  const node = useCurrentEntity();\n  const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n  const { size = { width: 300, height: 200 }, isContainer } = nodeMeta;\n\n  const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n  const [width, setWidth] = useState(size.width);\n  const [height, setHeight] = useState(size.height);\n\n  const updatePorts = () => {\n    const portsData = node.getData<WorkflowNodePortsData>(WorkflowNodePortsData);\n    portsData.updateDynamicPorts();\n  };\n\n  const updateSize = () => {\n    // 无子节点时\n    if (node.blocks.length === 0) {\n      setWidth(size.width);\n      setHeight(size.height);\n      return;\n    }\n    // 存在子节点时，只监听宽高变化\n    setWidth(transform.bounds.width);\n    setHeight(transform.bounds.height);\n  };\n\n  useEffect(() => {\n    const dispose = transform.onDataChange(() => {\n      updateSize();\n      updatePorts();\n    });\n    return () => dispose.dispose();\n  }, [transform, width, height]);\n\n  useEffect(() => {\n    // 初始化触发一次\n    updateSize();\n  }, []);\n\n  if (!isContainer) {\n    return;\n  }\n\n  return {\n    width,\n    height,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/hooks/use-sync-node-render-size.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useLayoutEffect } from 'react';\n\nimport { useCurrentEntity } from '@flowgram.ai/free-layout-core';\n\nimport { NodeSize } from './use-node-size';\n\nexport const useSyncNodeRenderSize = (nodeSize?: NodeSize) => {\n  const node = useCurrentEntity();\n\n  useLayoutEffect(() => {\n    if (!nodeSize) {\n      return;\n    }\n    node.renderData.node.style.width = nodeSize.width + 'px';\n    node.renderData.node.style.height = nodeSize.height + 'px';\n  }, [nodeSize?.width, nodeSize?.height]);\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/sub-canvas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './hooks';\nexport * from './components';\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/adjust-sub-node-position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, PaddingSchema } from '@flowgram.ai/utils';\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\n/**\n * 如果存在容器节点，且传入鼠标坐标，需要用容器的坐标减去传入的鼠标坐标\n */\nexport const adjustSubNodePosition = (params: {\n  targetNode: WorkflowNodeEntity;\n  containerNode: WorkflowNodeEntity;\n  containerPadding: PaddingSchema;\n}): IPoint => {\n  const { targetNode, containerNode, containerPadding } = params;\n  if (containerNode.flowNodeType === FlowNodeBaseType.ROOT) {\n    return targetNode.transform.position;\n  }\n  const nodeWorldTransform = targetNode.transform.transform.worldTransform;\n  const containerWorldTransform = containerNode.transform.transform.worldTransform;\n  const nodePosition = {\n    x: nodeWorldTransform.tx,\n    y: nodeWorldTransform.ty,\n  };\n  const isParentEmpty = !containerNode.children || containerNode.children.length === 0;\n  if (isParentEmpty) {\n    // 确保空容器节点不偏移\n    return {\n      x: 0,\n      y: containerPadding.top,\n    };\n  } else {\n    return {\n      x: nodePosition.x - containerWorldTransform.tx,\n      y: nodePosition.y - containerWorldTransform.ty,\n    };\n  }\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/get-collision-transform.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchema, Rectangle } from '@flowgram.ai/utils';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { isRectIntersects } from './is-rect-intersects';\nimport { isPointInRect } from './is-point-in-rect';\n\n/** 获取重叠位置 */\nexport const getCollisionTransform = (params: {\n  transforms: FlowNodeTransformData[];\n  targetRect?: Rectangle;\n  targetPoint?: PositionSchema;\n  withPadding?: boolean;\n  document: WorkflowDocument;\n}): FlowNodeTransformData | undefined => {\n  const { targetRect, targetPoint, transforms, withPadding = false, document } = params;\n  const collisionTransform = transforms.find((transform) => {\n    const { bounds, entity } = transform;\n    const padding = withPadding ? document.layout.getPadding(entity) : { left: 0, right: 0 };\n    const transformRect = new Rectangle(\n      bounds.x + padding.left + padding.right,\n      bounds.y,\n      bounds.width,\n      bounds.height\n    );\n    // 检测两个正方形是否相互碰撞\n    if (targetRect) {\n      return isRectIntersects(targetRect, transformRect);\n    }\n    if (targetPoint) {\n      return isPointInRect(targetPoint, transformRect);\n    }\n    return false;\n  });\n  return collisionTransform;\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/get-container-transforms.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { isContainer } from './is-container';\n\n/** 获取容器节点transforms */\nexport const getContainerTransforms = (allNodes: WorkflowNodeEntity[]): FlowNodeTransformData[] =>\n  allNodes\n    .filter((node) => {\n      if (node.originParent) {\n        return node.getNodeMeta().selectable && node.originParent.getNodeMeta().selectable;\n      }\n      return node.getNodeMeta().selectable;\n    })\n    .filter((node) => isContainer(node))\n    .sort((a, b) => {\n      const aIndex = a.renderData.stackIndex;\n      const bIndex = b.renderData.stackIndex;\n      //  确保层级高的节点在前面\n      return bIndex - aIndex;\n    })\n    .map((node) => node.transform);\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { nextFrame } from './next-frame';\nimport { isContainer } from './is-container';\nimport { getContainerTransforms } from './get-container-transforms';\nimport { getCollisionTransform } from './get-collision-transform';\nimport { adjustSubNodePosition } from './adjust-sub-node-position';\n\nexport const ContainerUtils = {\n  nextFrame,\n  isContainer,\n  adjustSubNodePosition,\n  getContainerTransforms,\n  getCollisionTransform,\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/is-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';\n\nexport const isContainer = (node?: WorkflowNodeEntity): boolean =>\n  node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/is-point-in-rect.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchema, Rectangle } from '@flowgram.ai/utils';\n\nexport const isPointInRect = (point: PositionSchema, rect: Rectangle): boolean =>\n  point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom;\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/is-rect-intersects.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\n\nexport const isRectIntersects = (rectA: Rectangle, rectB: Rectangle): boolean => {\n  // 检查水平方向是否有重叠\n  const hasHorizontalOverlap = rectA.right > rectB.left && rectA.left < rectB.right;\n  // 检查垂直方向是否有重叠\n  const hasVerticalOverlap = rectA.bottom > rectB.top && rectA.top < rectB.bottom;\n  // 只有当水平和垂直方向都有重叠时,两个矩形才相交\n  return hasHorizontalOverlap && hasVerticalOverlap;\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/src/utils/next-frame.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const nextFrame = async (): Promise<void> => {\n  await new Promise((resolve) => requestAnimationFrame(resolve));\n};\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-container-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/package.json",
    "content": "{\n    \"name\": \"@flowgram.ai/free-group-plugin\",\n    \"version\": \"0.1.8\",\n    \"homepage\": \"https://flowgram.ai/\",\n    \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n    \"license\": \"MIT\",\n    \"exports\": {\n        \"types\": \"./dist/index.d.ts\",\n        \"import\": \"./dist/esm/index.js\",\n        \"require\": \"./dist/index.js\"\n    },\n    \"main\": \"./dist/index.js\",\n    \"module\": \"./dist/esm/index.js\",\n    \"types\": \"./dist/index.d.ts\",\n    \"files\": [\n        \"dist\"\n    ],\n    \"scripts\": {\n        \"build\": \"npm run build:fast -- --dts-resolve\",\n        \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n        \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n        \"clean\": \"rimraf dist\",\n        \"test\": \"exit 0\",\n        \"test:cov\": \"exit 0\",\n        \"ts-check\": \"tsc --noEmit\",\n        \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n    },\n    \"dependencies\": {\n        \"@flowgram.ai/free-history-plugin\": \"workspace:*\",\n        \"@flowgram.ai/free-container-plugin\": \"workspace:*\",\n        \"@flowgram.ai/core\": \"workspace:*\",\n        \"@flowgram.ai/document\": \"workspace:*\",\n        \"@flowgram.ai/shortcuts-plugin\": \"workspace:*\",\n        \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n        \"@flowgram.ai/renderer\": \"workspace:*\",\n        \"@flowgram.ai/utils\": \"workspace:*\",\n        \"@flowgram.ai/free-layout-editor\": \"workspace:*\",\n        \"inversify\": \"^6.0.1\",\n        \"reflect-metadata\": \"~0.2.2\"\n    },\n    \"devDependencies\": {\n        \"@flowgram.ai/eslint-config\": \"workspace:*\",\n        \"@flowgram.ai/ts-config\": \"workspace:*\",\n        \"@types/bezier-js\": \"4.1.3\",\n        \"@types/react\": \"^18\",\n        \"@types/react-dom\": \"^18\",\n        \"@vitest/coverage-v8\": \"^3.2.4\",\n        \"eslint\": \"^9.0.0\",\n        \"react\": \"^18\",\n        \"react-dom\": \"^18\",\n        \"tsup\": \"^8.0.1\",\n        \"typescript\": \"^5.8.3\",\n        \"vitest\": \"^3.2.4\"\n    },\n    \"peerDependencies\": {\n        \"react\": \">=16.8\",\n        \"react-dom\": \">=16.8\"\n    },\n    \"publishConfig\": {\n        \"access\": \"public\",\n        \"registry\": \"https://registry.npmjs.org/\"\n    }\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowGroupCommand {\n  Group = 'group',\n  Ungroup = 'ungroup',\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/create-free-group-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ShortcutsRegistry } from '@flowgram.ai/shortcuts-plugin';\nimport { FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { FlowGroupService, FlowNodeBaseType } from '@flowgram.ai/document';\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { WorkflowGroupService } from './workflow-group-service';\nimport { WorkflowGroupPluginOptions } from './type';\nimport { GroupShortcut, UngroupShortcut } from './shortcuts';\nimport { GroupNodeRegistry } from './group-node';\n\nexport const createFreeGroupPlugin = definePluginCreator<WorkflowGroupPluginOptions, PluginContext>(\n  {\n    onBind({ bind, rebind }, opts) {\n      bind(WorkflowGroupService).toSelf().inSingletonScope();\n      bind(WorkflowGroupPluginOptions).toConstantValue(opts);\n      rebind(FlowGroupService).toService(WorkflowGroupService);\n    },\n    onInit(ctx, { groupNodeRender, disableGroupShortcuts = false }) {\n      // register node render\n      if (groupNodeRender) {\n        const renderRegistry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);\n        renderRegistry.registerReactComponent(FlowNodeBaseType.GROUP, groupNodeRender);\n      }\n      // register shortcuts\n      if (!disableGroupShortcuts) {\n        const shortcutsRegistry = ctx.get(ShortcutsRegistry);\n        shortcutsRegistry.addHandlers(new GroupShortcut(ctx), new UngroupShortcut(ctx));\n      }\n      const document = ctx.get(WorkflowDocument);\n      if (!document.getNodeRegistry(FlowNodeBaseType.GROUP)) {\n        document.registerFlowNodes(GroupNodeRegistry);\n      }\n    },\n    onReady(ctx) {\n      const groupService = ctx.get(WorkflowGroupService);\n      groupService.ready();\n    },\n    onDispose(ctx) {\n      const groupService = ctx.get(WorkflowGroupService);\n      groupService.dispose();\n    },\n  }\n);\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/group-node.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchema } from '@flowgram.ai/utils';\nimport { nanoid, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeRegistry, FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';\n\nexport const GroupNodeRegistry: FlowNodeRegistry = {\n  type: FlowNodeBaseType.GROUP,\n  meta: {\n    renderKey: FlowNodeBaseType.GROUP,\n    defaultPorts: [],\n    isContainer: true,\n    disableSideBar: true,\n    size: {\n      width: 560,\n      height: 400,\n    },\n    padding: () => ({\n      top: 80,\n      bottom: 40,\n      left: 65,\n      right: 65,\n    }),\n    selectable(node: WorkflowNodeEntity, mousePos?: PositionSchema): boolean {\n      if (!mousePos) {\n        return true;\n      }\n      const transform = node.getData<FlowNodeTransformData>(FlowNodeTransformData);\n      return !transform.bounds.contains(mousePos.x, mousePos.y);\n    },\n    expandable: false,\n  },\n  formMeta: {\n    render: () => <></>,\n  },\n  onAdd() {\n    return {\n      type: FlowNodeBaseType.GROUP,\n      id: `group_${nanoid(5)}`,\n      meta: {\n        position: {\n          x: 0,\n          y: 0,\n        },\n      },\n      data: {},\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createFreeGroupPlugin } from './create-free-group-plugin';\nexport { WorkflowGroupService } from './workflow-group-service';\nexport { WorkflowGroupCommand } from './constant';\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/shortcuts/group.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ShortcutsHandler } from '@flowgram.ai/shortcuts-plugin';\nimport { WorkflowSelectService } from '@flowgram.ai/free-layout-core';\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport { WorkflowGroupService } from '../workflow-group-service';\nimport { WorkflowGroupCommand } from '../constant';\n\nexport class GroupShortcut implements ShortcutsHandler {\n  public commandId = WorkflowGroupCommand.Group;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Group',\n  };\n\n  public shortcuts = ['meta g', 'ctrl g'];\n\n  private selectService: WorkflowSelectService;\n\n  private groupService: WorkflowGroupService;\n\n  constructor(context: PluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.groupService = context.get(WorkflowGroupService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(): Promise<void> {\n    this.groupService.createGroup(this.selectService.selectedNodes);\n    this.selectService.clear();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/shortcuts/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { GroupShortcut } from './group';\nexport { UngroupShortcut } from './ungroup';\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/shortcuts/ungroup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ShortcutsHandler } from '@flowgram.ai/shortcuts-plugin';\nimport { WorkflowSelectService, WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport { WorkflowGroupService } from '../workflow-group-service';\nimport { WorkflowGroupCommand } from '../constant';\n\nexport class UngroupShortcut implements ShortcutsHandler {\n  public commandId = WorkflowGroupCommand.Ungroup;\n\n  public commandDetail: ShortcutsHandler['commandDetail'] = {\n    label: 'Ungroup',\n  };\n\n  public shortcuts = ['meta shift g', 'ctrl shift g'];\n\n  private selectService: WorkflowSelectService;\n\n  private groupService: WorkflowGroupService;\n\n  constructor(context: PluginContext) {\n    this.selectService = context.get(WorkflowSelectService);\n    this.groupService = context.get(WorkflowGroupService);\n    this.execute = this.execute.bind(this);\n  }\n\n  public async execute(_groupNode?: WorkflowNodeEntity): Promise<void> {\n    const groupNode = _groupNode || this.selectService.activatedNode;\n    if (!groupNode || groupNode.flowNodeType !== FlowNodeBaseType.GROUP) {\n      return;\n    }\n    this.groupService.ungroup(groupNode);\n    this.selectService.clear();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC } from 'react';\n\nimport { WorkflowNodeEntity, WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';\n\nexport interface WorkflowGroupPluginOptions {\n  groupNodeRender: FC;\n  disableGroupShortcuts?: boolean;\n  /** @deprecated */\n  disableGroupNodeRegister?: boolean;\n  /** @deprecated use groupNodeRegistry.onAdd instead */\n  initGroupJSON?: (json: WorkflowNodeJSON, nodes: WorkflowNodeEntity[]) => WorkflowNodeJSON;\n}\n\nexport const WorkflowGroupPluginOptions = Symbol('WorkflowGroupPluginOptions');\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\nexport namespace WorkflowGroupUtils {\n  /** 找到节点所有上级 */\n  // const findNodeParents = (node: WorkflowNodeEntity): WorkflowNodeEntity[] => {\n  //   const parents = [];\n  //   let parent = node.parent;\n  //   while (parent) {\n  //     parents.push(parent);\n  //     parent = parent.parent;\n  //   }\n  //   return parents;\n  // };\n\n  /** 节点是否处于分组中 */\n  const isNodeInGroup = (node: WorkflowNodeEntity): boolean => {\n    // 处于分组中\n    if (node?.parent?.flowNodeType === FlowNodeBaseType.GROUP) {\n      return true;\n    }\n    return false;\n  };\n\n  /** 是否分组节点 */\n  const isGroupNode = (group: WorkflowNodeEntity): boolean =>\n    group.flowNodeType === FlowNodeBaseType.GROUP;\n\n  /** 判断节点能否组成分组 */\n  export const validate = (nodes: WorkflowNodeEntity[]): boolean => {\n    if (!nodes || !Array.isArray(nodes) || nodes.length === 0) {\n      // 参数不合法\n      return false;\n    }\n\n    // 判断是否有分组节点\n    const isGroupRelatedNode = nodes.some((node) => isGroupNode(node));\n    if (isGroupRelatedNode) return false;\n\n    // 判断是否有节点已经处于分组中\n    const hasGroup = nodes.some((node) => node && isNodeInGroup(node));\n    if (hasGroup) return false;\n\n    // 判断是否来自同一个父亲\n    const parent = nodes[0].parent;\n    const isSameParent = nodes.every((node) => node.parent === parent);\n    if (!isSameParent) return false;\n\n    // 判断节点父亲是否已经在分组中\n    // const parents = findNodeParents(nodes[0]);\n    // const parentsInGroup = parents.some((parent) => isNodeInGroup(parent));\n    // if (parentsInGroup) return false;\n\n    // 参数正确\n    return true;\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/src/workflow-group-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject, optional } from 'inversify';\nimport { DisposableCollection, Disposable } from '@flowgram.ai/utils';\nimport { FreeLayoutPluginContext } from '@flowgram.ai/free-layout-editor';\nimport {\n  WorkflowDocument,\n  WorkflowOperationBaseService,\n  WorkflowNodeEntity,\n  WorkflowNodeJSON,\n  WorkflowNodeRegistry,\n} from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\nimport {\n  NodeIntoContainerService,\n  NodeIntoContainerType,\n} from '@flowgram.ai/free-container-plugin';\nimport { FlowGroupService, FlowNodeBaseType } from '@flowgram.ai/document';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { WorkflowGroupUtils } from './utils';\nimport { WorkflowGroupPluginOptions } from './type';\n\n@injectable()\n/** 分组服务 */\nexport class WorkflowGroupService extends FlowGroupService {\n  @inject(WorkflowDocument) private document: WorkflowDocument;\n\n  @inject(WorkflowOperationBaseService) freeOperationService: WorkflowOperationBaseService;\n\n  @inject(HistoryService) private historyService: HistoryService;\n\n  @inject(NodeIntoContainerService)\n  @optional()\n  private nodeIntoContainerService?: NodeIntoContainerService;\n\n  @inject(WorkflowGroupPluginOptions) private opts: WorkflowGroupPluginOptions;\n\n  @inject(FreeLayoutPluginContext) private context: FreeLayoutPluginContext;\n\n  private toDispose = new DisposableCollection();\n\n  public ready(): void {\n    const listenContainerDisposer = this.listenContainer();\n    if (listenContainerDisposer) {\n      this.toDispose.push(listenContainerDisposer);\n    }\n  }\n\n  public dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  /** 创建分组节点 */\n  public createGroup(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity | undefined {\n    if (!WorkflowGroupUtils.validate(nodes)) {\n      return;\n    }\n    const parent = nodes[0].parent ?? this.document.root;\n    const nodeRegistry = this.document.getNodeRegistry<WorkflowNodeRegistry>(\n      FlowNodeBaseType.GROUP\n    );\n    let groupJSON: WorkflowNodeJSON = nodeRegistry?.onAdd?.(this.context);\n    if (this.opts.initGroupJSON) {\n      groupJSON = this.opts.initGroupJSON(groupJSON, nodes);\n    }\n    this.historyService.startTransaction();\n    this.document.createWorkflowNodeByType(\n      FlowNodeBaseType.GROUP,\n      {\n        x: 0,\n        y: 0,\n      },\n      groupJSON,\n      parent.id\n    );\n    nodes.forEach((node) => {\n      this.freeOperationService.moveNode(node, {\n        parent: groupJSON.id,\n      });\n    });\n    this.historyService.endTransaction();\n  }\n\n  /** 取消分组 */\n  public ungroup(groupNode: WorkflowNodeEntity): void {\n    const groupBlocks = groupNode.blocks.slice();\n    if (!groupNode.parent) {\n      return;\n    }\n    const groupPosition = groupNode.transform.position;\n\n    this.historyService.startTransaction();\n    groupBlocks.forEach((node) => {\n      this.freeOperationService.moveNode(node, {\n        parent: groupNode.parent?.id,\n      });\n    });\n    groupNode.dispose();\n    groupBlocks.forEach((node) => {\n      const transform = node.getData(TransformData);\n      const position = {\n        x: transform.position.x + groupPosition.x,\n        y: transform.position.y + groupPosition.y,\n      };\n      this.freeOperationService.updateNodePosition(node, position);\n    });\n    this.historyService.endTransaction();\n  }\n\n  private listenContainer(): Disposable | undefined {\n    return this.nodeIntoContainerService?.on((e) => {\n      if (\n        e.type !== NodeIntoContainerType.Out ||\n        e.sourceContainer?.flowNodeType !== FlowNodeBaseType.GROUP\n      ) {\n        return;\n      }\n      if (e.sourceContainer?.blocks.length === 0) {\n        e.sourceContainer.dispose();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-group-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-history-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/add-line-change.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type WorkflowLineEntity } from '@flowgram.ai/free-layout-core';\nimport { WorkflowContentChangeType } from '@flowgram.ai/free-layout-core';\n\nimport {\n  type AddLineOperation,\n  type AddOrDeleteLineOperationValue,\n  type ContentChangeTypeToOperation,\n  FreeOperationType,\n} from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\n\nexport const addLineChange: ContentChangeTypeToOperation<AddLineOperation> = {\n  type: WorkflowContentChangeType.ADD_LINE,\n  toOperation: (event, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const line = event.entity as WorkflowLineEntity;\n    const value: AddOrDeleteLineOperationValue = {\n      from: line.info.from,\n      to: line.info.to || '',\n      fromPort: line.info.fromPort || '',\n      toPort: line.info.toPort || '',\n      data: line.info.data,\n      id: line.id,\n    };\n    return {\n      type: FreeOperationType.addLine,\n      value,\n      uri: config.getLineURI(line.id),\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/add-node-change.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowDocument, type WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';\nimport { WorkflowContentChangeType } from '@flowgram.ai/free-layout-core';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\n\nimport {\n  type AddWorkflowNodeOperation,\n  type ContentChangeTypeToOperation,\n  FreeOperationType,\n} from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\n\nexport const addNodeChange: ContentChangeTypeToOperation<AddWorkflowNodeOperation> = {\n  type: WorkflowContentChangeType.ADD_NODE,\n  toOperation: (event, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    const node = event.entity as FlowNodeEntity;\n    const parentID = node.parent?.id;\n    const json: WorkflowNodeJSON = document.toNodeJSON(node);\n\n    return {\n      type: FreeOperationType.addNode,\n      value: {\n        node: json,\n        parentID,\n      },\n      uri: config.getNodeURI(node.id),\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/change-line-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type WorkflowLineEntity } from '@flowgram.ai/free-layout-core';\nimport { WorkflowContentChangeType } from '@flowgram.ai/free-layout-core';\n\nimport {\n  type ChangeLineDataOperation,\n  type ChangeLineDataValue,\n  type ContentChangeTypeToOperation,\n  FreeOperationType,\n} from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\n\nexport const changeLineData: ContentChangeTypeToOperation<ChangeLineDataOperation> = {\n  type: WorkflowContentChangeType.LINE_DATA_CHANGE,\n  toOperation: (event, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const line = event.entity as WorkflowLineEntity;\n    const value: ChangeLineDataValue = {\n      id: line.id,\n      oldValue: event.oldValue,\n      newValue: line.lineData,\n    };\n    return {\n      type: FreeOperationType.changeLineData,\n      value,\n      uri: config.getLineURI(line.id),\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/delete-line-change.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type WorkflowLineEntity } from '@flowgram.ai/free-layout-core';\nimport { WorkflowContentChangeType } from '@flowgram.ai/free-layout-core';\n\nimport {\n  type AddOrDeleteLineOperationValue,\n  type ContentChangeTypeToOperation,\n  type DeleteLineOperation,\n  FreeOperationType,\n} from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\n\nexport const deleteLineChange: ContentChangeTypeToOperation<DeleteLineOperation> = {\n  type: WorkflowContentChangeType.DELETE_LINE,\n  toOperation: (event, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const line = event.entity as WorkflowLineEntity;\n    const value: AddOrDeleteLineOperationValue = {\n      from: line.info.from,\n      to: line.info.to || '',\n      fromPort: line.info.fromPort || '',\n      toPort: line.info.toPort || '',\n      data: line.info.data,\n      id: line.id,\n    };\n    return {\n      type: FreeOperationType.deleteLine,\n      value,\n      uri: config.getNodeURI(line.id),\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/delete-node-change.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowContentChangeType } from '@flowgram.ai/free-layout-core';\nimport { type FlowNodeEntity } from '@flowgram.ai/document';\n\nimport {\n  type ContentChangeTypeToOperation,\n  FreeOperationType,\n  type DeleteWorkflowNodeOperation,\n} from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\n\nexport const deleteNodeChange: ContentChangeTypeToOperation<DeleteWorkflowNodeOperation> = {\n  type: WorkflowContentChangeType.DELETE_NODE,\n  toOperation: (event, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const node = event.entity as FlowNodeEntity;\n    const json = event.toJSON();\n    const parentID = node.parent?.id;\n\n    return {\n      type: FreeOperationType.deleteNode,\n      value: {\n        node: json,\n        parentID,\n      },\n      uri: config.getNodeURI(node.id),\n    };\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/changes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { deleteNodeChange } from './delete-node-change';\nimport { deleteLineChange } from './delete-line-change';\nimport { changeLineData } from './change-line-data';\nimport { addNodeChange } from './add-node-change';\nimport { addLineChange } from './add-line-change';\n\nexport default [addLineChange, deleteLineChange, addNodeChange, deleteNodeChange, changeLineData];\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/create-free-history-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { HistoryContainerModule, OperationContribution } from '@flowgram.ai/history';\nimport { bindContributions, definePluginCreator } from '@flowgram.ai/core';\n\nimport { type FreeHistoryPluginOptions } from './types';\nimport { HistoryEntityManager } from './history-entity-manager';\nimport { DragNodesHandler } from './handlers/drag-nodes-handler';\nimport { ChangeContentHandler } from './handlers/change-content-handler';\nimport { FreeHistoryRegisters } from './free-history-registers';\nimport { FreeHistoryManager } from './free-history-manager';\nimport { FreeHistoryConfig } from './free-history-config';\n\nexport const createFreeHistoryPlugin = definePluginCreator<FreeHistoryPluginOptions>({\n  onBind: ({ bind }) => {\n    bindContributions(bind, FreeHistoryRegisters, [OperationContribution]);\n    bind(FreeHistoryConfig).toSelf().inSingletonScope();\n    bind(FreeHistoryManager).toSelf().inSingletonScope();\n    bind(HistoryEntityManager).toSelf().inSingletonScope();\n    bind(DragNodesHandler).toSelf().inSingletonScope();\n    bind(ChangeContentHandler).toSelf().inSingletonScope();\n  },\n  onInit(ctx, opts): void {\n    ctx.get<FreeHistoryConfig>(FreeHistoryConfig).init(ctx, opts);\n\n    if (!opts.enable) {\n      return;\n    }\n    ctx.get<FreeHistoryManager>(FreeHistoryManager).onInit(ctx, opts);\n  },\n  onDispose(ctx) {\n    ctx.get<HistoryEntityManager>(HistoryEntityManager).dispose();\n  },\n  containerModules: [HistoryContainerModule],\n});\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/free-history-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { type FlowNodeEntity, type FlowNodeJSON } from '@flowgram.ai/document';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport {\n  type FreeHistoryPluginOptions,\n  type GetBlockLabel,\n  type GetLineURI,\n  type GetNodeLabel,\n  type GetNodeLabelById,\n  type GetNodeURI,\n  type NodeToJson,\n} from './types';\n\n@injectable()\nexport class FreeHistoryConfig {\n  init(ctx: PluginContext, options: FreeHistoryPluginOptions) {\n    this.enable = !!options?.enable;\n\n    if (options.nodeToJSON) {\n      this.nodeToJSON = options.nodeToJSON(ctx);\n    }\n\n    if (options.getNodeLabelById) {\n      this.getNodeLabelById = options.getNodeLabelById(ctx);\n    }\n\n    if (options.getNodeLabel) {\n      this.getNodeLabel = options.getNodeLabel(ctx);\n    }\n\n    if (options.getBlockLabel) {\n      this.getBlockLabel = options.getBlockLabel(ctx);\n    }\n\n    if (options.getNodeURI) {\n      this.getNodeURI = options.getNodeURI(ctx);\n    }\n\n    if (options.getLineURI) {\n      this.getLineURI = options.getLineURI(ctx);\n    }\n    if (options.enableChangeNode !== undefined) {\n      this.enableChangeNode = options.enableChangeNode;\n    }\n    if (options.enableChangeLineData !== undefined) {\n      this.enableChangeLineData = options.enableChangeLineData;\n    }\n  }\n\n  enableChangeNode = true;\n\n  enableChangeLineData = true;\n\n  enable = false;\n\n  nodeToJSON: NodeToJson = (node: FlowNodeEntity) => node.toJSON();\n\n  getNodeLabelById: GetNodeLabelById = (id: string) => id;\n\n  getNodeLabel: GetNodeLabel = (node: FlowNodeJSON) => node.id;\n\n  getBlockLabel: GetBlockLabel = (node: FlowNodeJSON) => node.id;\n\n  getNodeURI: GetNodeURI = (id: string) => `node:${id}`;\n\n  getLineURI: GetLineURI = (id: string) => `line:${id}`;\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/free-history-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention */\nimport { injectable, inject } from 'inversify';\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport { HistoryService } from '@flowgram.ai/history';\nimport {\n  WorkflowDocument,\n  WorkflowResetLayoutService,\n  WorkflowDragService,\n  WorkflowOperationBaseService,\n} from '@flowgram.ai/free-layout-core';\nimport { OperationType } from '@flowgram.ai/document';\nimport { type PluginContext, PositionData } from '@flowgram.ai/core';\n\nimport { DragNodeOperationValue, type FreeHistoryPluginOptions, FreeOperationType } from './types';\nimport { HistoryEntityManager } from './history-entity-manager';\nimport { DragNodesHandler } from './handlers/drag-nodes-handler';\nimport { ChangeContentHandler } from './handlers/change-content-handler';\n\n/**\n * 历史管理\n */\n@injectable()\nexport class FreeHistoryManager {\n  @inject(DragNodesHandler)\n  private _dragNodesHandler: DragNodesHandler;\n\n  @inject(ChangeContentHandler)\n  private _changeContentHandler: ChangeContentHandler;\n\n  @inject(HistoryEntityManager)\n  private _entityManager: HistoryEntityManager;\n\n  private _toDispose: DisposableCollection = new DisposableCollection();\n\n  @inject(WorkflowOperationBaseService)\n  private _operationService: WorkflowOperationBaseService;\n\n  onInit(ctx: PluginContext, opts: FreeHistoryPluginOptions) {\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    const historyService = ctx.get<HistoryService>(HistoryService);\n    const dragService = ctx.get<WorkflowDragService>(WorkflowDragService);\n\n    const resetLayoutService = ctx.get<WorkflowResetLayoutService>(WorkflowResetLayoutService);\n\n    if (opts?.limit) {\n      historyService.limit(opts.limit);\n    }\n    historyService.context.source = ctx;\n\n    this._toDispose.pushAll([\n      dragService.onNodesDrag(async (event) => {\n        if (event.type !== 'onDragEnd') {\n          return;\n        }\n        this._dragNodesHandler.handle(event);\n      }),\n      document.onNodeCreate(({ node, data }) => {\n        const positionData = node.getData(PositionData);\n        if (positionData) {\n          this._entityManager.addEntityData(positionData);\n        }\n      }),\n      document.onContentChange((event) => {\n        this._changeContentHandler.handle(event, ctx);\n      }),\n      document.onReload((_event) => {\n        historyService.clear();\n      }),\n      resetLayoutService.onResetLayout((event) => {\n        historyService.pushOperation(\n          {\n            type: FreeOperationType.resetLayout,\n            value: {\n              ids: event.nodeIds,\n              value: event.positionMap,\n              oldValue: event.oldPositionMap,\n            },\n          },\n          { noApply: true }\n        );\n      }),\n      this._operationService.onNodeMove(({ node, fromParent, fromIndex, toParent, toIndex }) => {\n        historyService.pushOperation(\n          {\n            type: OperationType.moveChildNodes,\n            value: {\n              fromParentId: fromParent.id,\n              fromIndex,\n              nodeIds: [node.id],\n              toParentId: toParent.id,\n              toIndex,\n            },\n          },\n          {\n            noApply: true,\n          }\n        );\n      }),\n      this._operationService.onNodePostionUpdate((event) => {\n        const value: DragNodeOperationValue = {\n          ids: [event.node.id],\n          value: [event.newPosition],\n          oldValue: [event.oldPosition],\n        };\n        historyService.pushOperation(\n          {\n            type: FreeOperationType.dragNodes,\n            value,\n          },\n          {\n            noApply: true,\n          }\n        );\n      }),\n    ]);\n  }\n\n  dispose() {\n    this._entityManager.dispose();\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/free-history-registers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { type OperationContribution, type OperationRegistry } from '@flowgram.ai/history';\n\nimport { operationMetas } from './operation-metas';\n\n@injectable()\nexport class FreeHistoryRegisters implements OperationContribution {\n  registerOperationMeta(operationRegistry: OperationRegistry): void {\n    operationMetas.forEach(operationMeta => {\n      operationRegistry.registerOperationMeta(operationMeta);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/handlers/change-content-handler.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention */\nimport { injectable, inject } from 'inversify';\nimport { HistoryService, Operation } from '@flowgram.ai/history';\nimport {\n  type WorkflowContentChangeEvent,\n  WorkflowContentChangeType,\n} from '@flowgram.ai/free-layout-core';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport { type IHandler } from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\nimport changes from '../changes';\n\n@injectable()\nexport class ChangeContentHandler implements IHandler<WorkflowContentChangeEvent> {\n  @inject(HistoryService)\n  private _historyService: HistoryService;\n\n  @inject(FreeHistoryConfig) private _historyConfig: FreeHistoryConfig;\n\n  handle(event: WorkflowContentChangeEvent, ctx: PluginContext) {\n    if (!this._historyService.undoRedoService.canPush()) {\n      return;\n    }\n    if (\n      !this._historyConfig.enableChangeLineData &&\n      event.type === WorkflowContentChangeType.LINE_DATA_CHANGE\n    ) {\n      return;\n    }\n\n    const change = changes.find((c) => c.type === event.type);\n    if (!change) {\n      return;\n    }\n    const operation: Operation | undefined = change.toOperation(event, ctx);\n    if (!operation) {\n      return;\n    }\n\n    this._historyService.pushOperation(operation, { noApply: true });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/handlers/drag-nodes-handler.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention */\nimport { injectable, inject } from 'inversify';\nimport { HistoryService } from '@flowgram.ai/history';\nimport { type NodesDragEndEvent } from '@flowgram.ai/free-layout-core';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { FreeOperationType, type IHandler } from '../types';\n\n@injectable()\nexport class DragNodesHandler implements IHandler<NodesDragEndEvent> {\n  @inject(HistoryService)\n  private _historyService: HistoryService;\n\n  handle(event: NodesDragEndEvent) {\n    if (event.type === 'onDragEnd') {\n      this._dragNode(event);\n    }\n  }\n\n  private _dragNode(event: NodesDragEndEvent) {\n    this._historyService.pushOperation(\n      {\n        type: FreeOperationType.dragNodes,\n        value: {\n          ids: event.nodes.map((node) => node.id),\n          value: event.nodes.map((node) => {\n            const { x, y } = node.getData(TransformData).position;\n            return {\n              x,\n              y,\n            };\n          }),\n          oldValue: event.startPositions,\n        },\n      },\n      { noApply: true }\n    );\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/history-entity-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention */\nimport { cloneDeep, isEqual } from 'lodash-es';\nimport { injectable } from 'inversify';\nimport { type Disposable, DisposableCollection } from '@flowgram.ai/utils';\nimport { type EntityData } from '@flowgram.ai/core';\n\n@injectable()\nexport class HistoryEntityManager implements Disposable {\n  private _entityDataValues: Map<EntityData, unknown> = new Map();\n\n  private _toDispose: DisposableCollection = new DisposableCollection();\n\n  addEntityData(entityData: EntityData) {\n    this._entityDataValues.set(entityData, cloneDeep(entityData.toJSON()));\n    this._toDispose.push(\n      entityData.onWillChange((event) => {\n        const value = event.toJSON();\n        const oldValue = this._entityDataValues.get(entityData);\n        if (isEqual(value, oldValue)) {\n          return;\n        }\n        this._entityDataValues.set(entityData, cloneDeep(value));\n      })\n    );\n  }\n\n  getValue(entityData: EntityData) {\n    return this._entityDataValues.get(entityData);\n  }\n\n  setValue(entityData: EntityData, value: unknown) {\n    return this._entityDataValues.set(entityData, value);\n  }\n\n  dispose() {\n    this._entityDataValues.clear();\n    this._toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './use-undo-redo';\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/hooks/use-undo-redo.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useState } from 'react';\n\nimport { useService } from '@flowgram.ai/core';\nimport { HistoryService } from '@flowgram.ai/history';\n\ninterface UndoRedo {\n  canUndo: boolean;\n  canRedo: boolean;\n  undo: () => Promise<void>;\n  redo: () => Promise<void>;\n}\n\nexport function useUndoRedo(): UndoRedo {\n  const historyService = useService<HistoryService>(HistoryService);\n  const [canUndo, setCanUndo] = useState(false);\n  const [canRedo, setCanRedo] = useState(false);\n\n  useEffect(() => {\n    const toDispose = historyService.undoRedoService.onChange(() => {\n      setCanUndo(historyService.canUndo());\n      setCanRedo(historyService.canRedo());\n    });\n    return () => {\n      toDispose.dispose();\n    };\n  }, []);\n\n  return {\n    canUndo,\n    canRedo,\n    undo: () => historyService.undo(),\n    redo: () => historyService.redo(),\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-free-history-plugin';\nexport * from './types';\nexport * from '@flowgram.ai/history';\nexport * from './free-history-config';\nexport * from './hooks';\nexport * from './free-history-registers';\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/add-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type PluginContext } from '@flowgram.ai/core';\nimport { WorkflowLinesManager } from '@flowgram.ai/free-layout-core';\nimport { type OperationMeta } from '@flowgram.ai/history';\n\nimport { type AddOrDeleteLineOperationValue, FreeOperationType } from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addLineOperationMeta: OperationMeta<\n  AddOrDeleteLineOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.addLine,\n  inverse: op => ({\n    ...op,\n    type: FreeOperationType.deleteLine,\n  }),\n  apply: (operation, ctx: PluginContext) => {\n    const linesManager = ctx.get<WorkflowLinesManager>(WorkflowLinesManager);\n    linesManager.createLine({\n      ...operation.value,\n      key: operation.value.id,\n    });\n  },\n  getLabel: (op, ctx) => 'Create Line',\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const { value } = op;\n    if (!value.from || !value.to) {\n      return 'Create Line';\n    }\n\n    const fromName = config.getNodeLabelById(value.from);\n    const toName = config.getNodeLabelById(value.to);\n    return `Create Line from ${fromName} to ${toName}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/add-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { cloneDeep } from 'lodash-es';\nimport { type OperationMeta } from '@flowgram.ai/history';\nimport { WorkflowDocument, type WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport { type AddOrDeleteWorkflowNodeOperationValue, FreeOperationType } from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const addNodeOperationMeta: OperationMeta<\n  AddOrDeleteWorkflowNodeOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.addNode,\n  inverse: (op) => ({\n    ...op,\n    type: FreeOperationType.deleteNode,\n  }),\n  apply: async (operation, ctx: PluginContext) => {\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    await document.createWorkflowNode(\n      cloneDeep(operation.value.node) as WorkflowNodeJSON,\n      false,\n      operation.value.parentID\n    );\n  },\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    return `Create Node ${config.getNodeLabel(op.value.node)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    let desc = `Create Node ${config.getNodeLabel(op.value.node)}`;\n    if (op.value.node.meta?.position) {\n      desc += ` at ${op.value.node.meta.position.x},${op.value.node.meta.position.y}`;\n    }\n    return desc;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/base.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type OperationMeta } from '@flowgram.ai/history';\n\nexport const baseOperationMeta: Partial<OperationMeta> = {\n  shouldMerge: (_op, prev, element) => {\n    if (!prev) {\n      return false;\n    }\n\n    if (\n      // 合并500ms内的操作, 如删除节点会联动删除线条\n      Date.now() - element.getTimestamp() <\n      500\n    ) {\n      return true;\n    }\n    return false;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/change-line-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type OperationMeta } from '@flowgram.ai/history';\nimport { WorkflowLinesManager } from '@flowgram.ai/free-layout-core';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport { FreeOperationType, type ChangeLineDataValue } from '../types';\nimport { baseOperationMeta } from './base';\n\nexport const changeLineDataOperationMeta: OperationMeta<ChangeLineDataValue, PluginContext, void> =\n  {\n    ...baseOperationMeta,\n    type: FreeOperationType.changeLineData,\n    inverse: (op) => ({\n      ...op,\n      value: {\n        ...op.value,\n        newValue: op.value.oldValue,\n        oldValue: op.value.newValue,\n      },\n    }),\n    apply: (op, ctx: PluginContext) => {\n      const linesManager = ctx.get<WorkflowLinesManager>(WorkflowLinesManager);\n      const line = linesManager.getLineById(op.value.id);\n\n      if (!line) {\n        return;\n      }\n\n      line.lineData = op.value.newValue;\n    },\n    shouldMerge: (op, prev, element) => {\n      if (!prev) {\n        return false;\n      }\n\n      if (Date.now() - element.getTimestamp() < 500) {\n        if (\n          op.type === prev.type && // 相同类型\n          op.value.id === prev.value.id // 相同节点\n        ) {\n          return {\n            type: op.type,\n            value: {\n              ...op.value,\n              newValue: op.value.newValue,\n              oldValue: prev.value.oldValue,\n            },\n          };\n        }\n        return true;\n      }\n      return false;\n    },\n  };\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/delete-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type OperationMeta } from '@flowgram.ai/history';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport { type AddOrDeleteLineOperationValue, FreeOperationType } from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteLineOperationMeta: OperationMeta<\n  AddOrDeleteLineOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.deleteLine,\n  inverse: (op) => ({\n    ...op,\n    type: FreeOperationType.addLine,\n  }),\n  apply: (operation, ctx: PluginContext) => {\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    document.removeNode(operation.value.id);\n  },\n  getLabel: (op, ctx) => 'Delete Line',\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    const { value } = op;\n    if (!value.from || !value.to) {\n      return 'Delete Line';\n    }\n\n    const fromName = config.getNodeLabelById(value.from);\n    const toName = config.getNodeLabelById(value.to);\n    return `Delete Line from ${fromName} to ${toName}`;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/delete-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type PluginContext } from '@flowgram.ai/core';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { type OperationMeta } from '@flowgram.ai/history';\n\nimport { type AddOrDeleteWorkflowNodeOperationValue, FreeOperationType } from '../types';\nimport { FreeHistoryConfig } from '../free-history-config';\nimport { baseOperationMeta } from './base';\n\nexport const deleteNodeOperationMeta: OperationMeta<\n  AddOrDeleteWorkflowNodeOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.deleteNode,\n  inverse: op => ({\n    ...op,\n    type: FreeOperationType.addNode,\n  }),\n  apply: (operation, ctx: PluginContext) => {\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    const node = document.getNode(operation.value.node.id);\n    if (node) {\n      node.dispose();\n    }\n  },\n  getLabel: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    return `Delete Node ${config.getNodeLabel(op.value.node)}`;\n  },\n  getDescription: (op, ctx) => {\n    const config = ctx.get<FreeHistoryConfig>(FreeHistoryConfig);\n    let desc = `Delete Node ${config.getNodeLabel(op.value.node)}`;\n    if (op.value.node.meta?.position) {\n      desc += ` at ${op.value.node.meta.position.x},${op.value.node.meta.position.y}`;\n    }\n    return desc;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/drag-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type OperationMeta } from '@flowgram.ai/history';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { type PluginContext, TransformData } from '@flowgram.ai/core';\n\nimport { type DragNodeOperationValue, FreeOperationType } from '../types';\nimport { baseOperationMeta } from './base';\n\nexport const dragNodesOperationMeta: OperationMeta<DragNodeOperationValue, PluginContext, void> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.dragNodes,\n  inverse: (op) => ({\n    ...op,\n    value: {\n      ...op.value,\n      value: op.value.oldValue,\n      oldValue: op.value.value,\n    },\n  }),\n  apply: (operation, ctx) => {\n    operation.value.ids.forEach((id, index) => {\n      const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n      const node = document.getNode(id);\n      if (!node) {\n        return;\n      }\n\n      const transform = node.getData(TransformData);\n      const point = operation.value.value[index];\n      transform.update({\n        position: {\n          x: point.x,\n          y: point.y,\n        },\n      });\n      document.layout.updateAffectedTransform(node);\n    });\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { resetLayoutOperationMeta } from './reset-layout';\nimport { moveChildNodesOperationMeta } from './move-child-nodes';\nimport { dragNodesOperationMeta } from './drag-nodes';\nimport { deleteNodeOperationMeta } from './delete-node';\nimport { deleteLineOperationMeta } from './delete-line';\nimport { changeLineDataOperationMeta } from './change-line-data';\nimport { addNodeOperationMeta } from './add-node';\nimport { addLineOperationMeta } from './add-line';\n\nexport const operationMetas = [\n  addLineOperationMeta,\n  deleteLineOperationMeta,\n  addNodeOperationMeta,\n  deleteNodeOperationMeta,\n  resetLayoutOperationMeta,\n  dragNodesOperationMeta,\n  moveChildNodesOperationMeta,\n  changeLineDataOperationMeta,\n];\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/move-child-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OperationMeta } from '@flowgram.ai/history';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { MoveChildNodesOperationValue, OperationType } from '@flowgram.ai/document';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { PluginContext, TransformData } from '@flowgram.ai/core';\n\nimport { baseOperationMeta } from './base';\n\nexport const moveChildNodesOperationMeta: OperationMeta<\n  MoveChildNodesOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: OperationType.moveChildNodes,\n  inverse: (op) => ({\n    ...op,\n    value: {\n      ...op.value,\n      fromIndex: op.value.toIndex,\n      toIndex: op.value.fromIndex,\n      fromParentId: op.value.toParentId,\n      toParentId: op.value.fromParentId,\n    },\n  }),\n  apply: (operation, ctx: PluginContext) => {\n    const document = ctx.get<WorkflowDocument>(WorkflowDocument);\n    document.moveChildNodes(operation.value);\n    const fromContainer = document.getNode(operation.value.fromParentId);\n    requestAnimationFrame(() => {\n      if (fromContainer && fromContainer.flowNodeType !== FlowNodeBaseType.ROOT) {\n        const fromContainerTransformData = fromContainer.getData(TransformData);\n        fromContainerTransformData.fireChange();\n      }\n    });\n  },\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/operation-metas/reset-layout.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type PluginContext } from '@flowgram.ai/core';\nimport { WorkflowResetLayoutService } from '@flowgram.ai/free-layout-core';\nimport { type OperationMeta } from '@flowgram.ai/history';\n\nimport { FreeOperationType, type ResetLayoutOperationValue } from '../types';\nimport { baseOperationMeta } from './base';\n\nexport const resetLayoutOperationMeta: OperationMeta<\n  ResetLayoutOperationValue,\n  PluginContext,\n  void\n> = {\n  ...baseOperationMeta,\n  type: FreeOperationType.resetLayout,\n  inverse: op => ({\n    ...op,\n    value: {\n      ...op.value,\n      value: op.value.oldValue,\n      oldValue: op.value.value,\n    },\n  }),\n  apply: async (operation, ctx: PluginContext) => {\n    const reset = ctx.get<WorkflowResetLayoutService>(WorkflowResetLayoutService);\n    await reset.layoutToPositions(operation.value.ids, operation.value.value);\n  },\n  shouldMerge: () => false,\n};\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable @typescript-eslint/naming-convention */\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { type Operation, type OperationMeta } from '@flowgram.ai/history';\nimport {\n  type WorkflowContentChangeType,\n  type WorkflowContentChangeEvent,\n  type WorkflowLineEntity,\n  type WorkflowLinePortInfo,\n  type WorkflowNodeJSON,\n  type PositionMap,\n} from '@flowgram.ai/free-layout-core';\nimport { type FlowNodeEntity, type FlowNodeJSON } from '@flowgram.ai/document';\nimport { type EntityData, type PluginContext } from '@flowgram.ai/core';\n\nexport enum FreeOperationType {\n  addLine = 'addLine',\n  deleteLine = 'deleteLine',\n  changeLineData = 'changeLineData',\n  moveNode = 'moveNode',\n  addNode = 'addNode',\n  deleteNode = 'deleteNode',\n  changeNodeData = 'changeNodeData',\n  resetLayout = 'resetLayout',\n  dragNodes = 'dragNodes',\n  moveChildNodes = 'moveChildNodes',\n}\n\nexport interface AddOrDeleteLineOperationValue extends WorkflowLinePortInfo {\n  id: string;\n}\n\nexport interface ChangeLineDataValue {\n  id: string;\n  oldValue: unknown;\n  newValue: unknown;\n}\nexport interface AddOrDeleteWorkflowNodeOperationValue {\n  node: WorkflowNodeJSON;\n  parentID?: string;\n}\n\nexport interface AddLineOperation extends Operation {\n  type: FreeOperationType.addLine;\n  value: AddOrDeleteLineOperationValue;\n}\n\nexport interface ChangeLineDataOperation extends Operation {\n  type: FreeOperationType.changeLineData;\n  value: ChangeLineDataValue;\n}\n\nexport interface DeleteLineOperation extends Operation {\n  type: FreeOperationType.deleteLine;\n  value: AddOrDeleteLineOperationValue;\n}\n\nexport interface MoveNodeOperation extends Operation {\n  type: FreeOperationType.moveNode;\n  value: MoveNodeOperationValue;\n}\n\nexport interface AddWorkflowNodeOperation extends Operation {\n  type: FreeOperationType.addNode;\n  value: AddOrDeleteWorkflowNodeOperationValue;\n}\n\nexport interface DeleteWorkflowNodeOperation extends Operation {\n  type: FreeOperationType.deleteNode;\n  value: AddOrDeleteWorkflowNodeOperationValue;\n}\n\nexport interface MoveNodeOperationValue {\n  id: string;\n  value: {\n    x: number;\n    y: number;\n  };\n  oldValue: {\n    x: number;\n    y: number;\n  };\n}\n\nexport interface DragNodeOperationValue {\n  ids: string[];\n  value: IPoint[];\n  oldValue: IPoint[];\n}\n\nexport interface ResetLayoutOperationValue {\n  ids: string[];\n  value: PositionMap;\n  oldValue: PositionMap;\n}\n\nexport interface ContentChangeTypeToOperation<T extends Operation> {\n  type: WorkflowContentChangeType;\n  toOperation: (event: WorkflowContentChangeEvent, ctx: PluginContext) => T | undefined;\n}\n\nexport interface EntityDataType {\n  type: FreeOperationType;\n  toEntityData: (node: FlowNodeEntity, ctx: PluginContext) => EntityData;\n}\n\nexport interface ChangeNodeDataValue {\n  id: string;\n  value: unknown;\n  oldValue: unknown;\n  path: string;\n}\n\nexport interface ChangeLineDataValue {\n  id: string;\n  newValue: unknown;\n  oldValue: unknown;\n}\n\n/**\n * 将node转成json\n */\nexport type NodeToJson = (node: FlowNodeEntity) => FlowNodeJSON;\n/**\n * 将line转成json\n */\nexport type LineToJson = (node: WorkflowLineEntity) => FlowNodeJSON;\n/**\n * 根据节点id获取label\n */\nexport type GetNodeLabelById = (id: string) => string;\n/**\n * 根据节点获取label\n */\nexport type GetNodeLabel = (node: FlowNodeJSON) => string;\n/**\n * 根据分支获取label\n */\nexport type GetBlockLabel = (node: FlowNodeJSON) => string;\n/**\n * 根据节点获取URI\n */\nexport type GetNodeURI = (id: string) => string | any;\n/**\n * 根据连线获取URI\n */\nexport type GetLineURI = (id: string) => string | any;\n\n/**\n * 插件配置\n */\nexport interface FreeHistoryPluginOptions<CTX extends PluginContext = PluginContext> {\n  enable?: boolean;\n  limit?: number;\n  nodeToJSON?: (ctx: CTX) => NodeToJson;\n  getNodeLabelById?: (ctx: CTX) => GetNodeLabelById;\n  getNodeLabel?: (ctx: CTX) => GetNodeLabel;\n  getBlockLabel?: (ctx: CTX) => GetBlockLabel;\n  getNodeURI?: (ctx: CTX) => GetNodeURI;\n  getLineURI?: (ctx: CTX) => GetLineURI;\n  operationMetas?: OperationMeta[];\n  enableChangeNode?: boolean; // default true\n  enableChangeLineData?: boolean; // default true\n  uri?: string | any;\n}\n\nexport interface IHandler<E> {\n  handle: (event: E, ctx: PluginContext) => void | Promise<void>;\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-history-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-hover-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/src/create-free-hover-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { HoverLayer } from './hover-layer';\n\nexport const createFreeHoverPlugin = definePluginCreator({\n  onInit(ctx): void {\n    ctx.playground.registerLayer(HoverLayer);\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/src/hover-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable complexity */\nimport { inject, injectable } from 'inversify';\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { SelectorBoxConfigEntity } from '@flowgram.ai/renderer';\nimport {\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowHoverService,\n  WorkflowLineEntity,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType, FlowNodeTransformData } from '@flowgram.ai/document';\nimport {\n  EditorState,\n  EditorStateConfigEntity,\n  Layer,\n  PlaygroundConfigEntity,\n  observeEntities,\n  observeEntity,\n  observeEntityDatas,\n  type LayerOptions,\n} from '@flowgram.ai/core';\n\nimport { getSelectionBounds } from './selection-utils';\nconst PORT_BG_CLASS_NAME = 'workflow-port-bg';\n\nexport interface HoverLayerOptions extends LayerOptions {\n  canHovered?: (e: MouseEvent, service: WorkflowHoverService) => boolean;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace HoverLayerOptions {\n  export const DEFAULT: HoverLayerOptions = {\n    canHovered: () => true,\n  };\n}\n\nconst LINE_CLASS_NAME = '.gedit-flow-activity-line';\nconst NODE_CLASS_NAME = '.gedit-flow-activity-node';\n\n@injectable()\nexport class HoverLayer extends Layer<HoverLayerOptions> {\n  static type = 'HoverLayer';\n\n  @inject(WorkflowDocument) document: WorkflowDocument;\n\n  @inject(WorkflowSelectService) selectionService: WorkflowSelectService;\n\n  @inject(WorkflowDragService) dragService: WorkflowDragService;\n\n  @inject(WorkflowHoverService) hoverService: WorkflowHoverService;\n\n  @inject(WorkflowLinesManager)\n  linesManager: WorkflowLinesManager;\n\n  @observeEntity(EditorStateConfigEntity)\n  protected editorStateConfig: EditorStateConfigEntity;\n\n  @observeEntity(SelectorBoxConfigEntity)\n  protected selectorBoxConfigEntity: SelectorBoxConfigEntity;\n\n  @inject(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;\n\n  /**\n   * 监听节点 transform\n   */\n  @observeEntityDatas(WorkflowNodeEntity, FlowNodeTransformData)\n  protected readonly nodeTransforms: FlowNodeTransformData[];\n\n  /**\n   * 按选中排序\n   * @private\n   */\n  protected nodeTransformsWithSort: FlowNodeTransformData[] = [];\n\n  autorun(): void {\n    const { activatedNode } = this.selectionService;\n    this.nodeTransformsWithSort = this.nodeTransforms\n      .filter((n) => n.entity.id !== 'root' && n.entity.flowNodeType !== FlowNodeBaseType.GROUP)\n      .reverse() // 后创建的排在前面\n      .sort((n1) => (n1.entity === activatedNode ? -1 : 0));\n  }\n\n  /**\n   * 监听线条\n   */\n  @observeEntities(WorkflowLineEntity)\n  protected readonly lines: WorkflowLineEntity[];\n\n  /**\n   * 是否正在调整线条\n   * @protected\n   */\n  get isDrawing(): boolean {\n    return this.linesManager.isDrawing;\n  }\n\n  onReady(): void {\n    this.options = {\n      ...HoverLayerOptions.DEFAULT,\n      ...this.options,\n    };\n    this.toDispose.pushAll([\n      // 监听主动触发的 hover 事件\n      this.hoverService.onUpdateHoverPosition((hoverPosition) => {\n        const { position, target } = hoverPosition;\n        const canvasPosition = this.config.getPosFromMouseEvent({\n          clientX: position.x,\n          clientY: position.y,\n        });\n        this.updateHoveredState(canvasPosition, target);\n      }),\n      // 监听画布鼠标移动事件\n      this.listenPlaygroundEvent('mousemove', (e: MouseEvent) => {\n        this.hoverService.hoveredPos = this.config.getPosFromMouseEvent(e);\n        if (!this.isEnabled()) {\n          return;\n        }\n        if (!this.options.canHovered!(e, this.hoverService)) {\n          return;\n        }\n        const mousePos = this.config.getPosFromMouseEvent(e);\n        // 更新 hover 状态\n        this.updateHoveredState(mousePos, e?.target as HTMLElement);\n      }),\n      this.selectionService.onSelectionChanged(() => this.autorun()),\n      // 控制触控\n      this.listenPlaygroundEvent('touchstart', (e: MouseEvent): boolean | undefined => {\n        if (!this.isEnabled() || this.isDrawing) {\n          return undefined;\n        }\n        return this.handleDragLine(e);\n      }),\n      // 控制选中逻辑\n      this.listenPlaygroundEvent('mousedown', (e: MouseEvent): boolean | undefined => {\n        if (!this.isEnabled() || this.isDrawing) {\n          return undefined;\n        }\n        const { hoveredNode } = this.hoverService;\n        const lineDrag = this.handleDragLine(e);\n        if (lineDrag) {\n          return true;\n        }\n        const mousePos = this.config.getPosFromMouseEvent(e);\n        const selectionBounds = getSelectionBounds(\n          this.selectionService.selection,\n          // 这里只考虑多选模式，单选模式已经下沉到 use-node-render 中\n          true\n        );\n        if (selectionBounds.width > 0 && selectionBounds.contains(mousePos.x, mousePos.y)) {\n          /**\n           * 拖拽选择框\n           */\n          this.dragService.startDragSelectedNodes(e)?.then((dragSuccess) => {\n            if (!dragSuccess) {\n              // 拖拽没有成功触发了点击\n              if (hoveredNode && hoveredNode instanceof WorkflowNodeEntity) {\n                // 追加选择\n                if (e.shiftKey) {\n                  this.selectionService.toggleSelect(hoveredNode);\n                } else {\n                  this.selectionService.selectNode(hoveredNode);\n                }\n              } else {\n                this.selectionService.clear();\n              }\n            }\n          });\n          // 这里会组织触发 selector box\n          return true;\n        } else {\n          if (!hoveredNode) {\n            this.selectionService.clear();\n          }\n        }\n        return undefined;\n      }),\n    ]);\n  }\n\n  /**\n   * 更新 hoverd\n   * @param mousePos\n   */\n  updateHoveredState(mousePos: IPoint, target?: HTMLElement): void {\n    const { hoverService } = this;\n    const nodeTransforms = this.nodeTransformsWithSort;\n    const outputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'output');\n    const inputPortHovered = this.linesManager.getPortFromMousePos(mousePos, 'input');\n    // 在两个端口叠加情况，优先使用 outputPort\n    const portHovered = outputPortHovered || inputPortHovered;\n\n    const lineDomNodes = this.playgroundNode.querySelectorAll(LINE_CLASS_NAME);\n    const checkTargetFromLine = [...lineDomNodes].some((lineDom) =>\n      lineDom.contains(target as HTMLElement)\n    );\n    if (portHovered) {\n      if (this.document.options.twoWayConnection) {\n        hoverService.updateHoveredKey(portHovered.id);\n      } else {\n        // 默认 只有 output 点位可以 hover\n        if (portHovered.portType === 'output') {\n          hoverService.updateHoveredKey(portHovered.id);\n        } else if (checkTargetFromLine || target?.className?.includes?.(PORT_BG_CLASS_NAME)) {\n          // 输入点采用获取最接近的线条\n          const lineHovered = this.linesManager.getCloseInLineFromMousePos(mousePos);\n          if (lineHovered) {\n            this.updateHoveredKey(lineHovered.id);\n          }\n        }\n      }\n      return;\n    }\n\n    // Drawing 情况，不能选中节点和线条\n    if (this.isDrawing) {\n      return;\n    }\n\n    const nodeHovered = nodeTransforms.find((trans: FlowNodeTransformData) =>\n      trans.bounds.contains(mousePos.x, mousePos.y)\n    )?.entity as WorkflowNodeEntity;\n\n    // 判断当前鼠标位置所在元素是否在节点内部\n    const nodeDomNodes = this.playgroundNode.querySelectorAll(NODE_CLASS_NAME);\n    const checkTargetFromNode = [...nodeDomNodes].some((nodeDom) =>\n      nodeDom.contains(target as HTMLElement)\n    );\n\n    if (nodeHovered || checkTargetFromNode) {\n      if (nodeHovered?.id) {\n        this.updateHoveredKey(nodeHovered.id);\n      }\n    }\n\n    // 获取最接近的线条\n    // 线条会相交需要获取最接近点位的线条，不能删除的线条不能被选中\n    const lineHovered = checkTargetFromLine\n      ? this.linesManager.getCloseInLineFromMousePos(mousePos)\n      : undefined;\n\n    if (nodeHovered && lineHovered) {\n      const nodeStackIndex = nodeHovered.renderData.stackIndex;\n      const lineStackIndex = lineHovered.stackIndex;\n      if (nodeStackIndex > lineStackIndex) {\n        return this.updateHoveredKey(nodeHovered.id);\n      } else {\n        return this.updateHoveredKey(lineHovered.id);\n      }\n    }\n\n    // 判断节点是否 hover\n    if (nodeHovered) {\n      return this.updateHoveredKey(nodeHovered.id);\n    }\n    // 判断线条是否 hover\n    if (lineHovered) {\n      return this.updateHoveredKey(lineHovered.id);\n    }\n\n    // 上述逻辑都未命中 则清空 hovered\n    hoverService.clearHovered();\n\n    const currentState = this.editorStateConfig.getCurrentState();\n    const isMouseFriendly = currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT;\n\n    // 鼠标优先，并且不是按住 shift 键，更新为小手\n    if (isMouseFriendly && !this.editorStateConfig.isPressingShift) {\n      this.configEntity.updateCursor('grab');\n    }\n  }\n\n  updateHoveredKey(key: string): void {\n    // 鼠标优先交互模式，如果是 hover，需要将鼠标的小手去掉，还原鼠标原有样式\n    this.configEntity.updateCursor('default');\n    this.hoverService.updateHoveredKey(key);\n  }\n\n  /**\n   * 判断是否能够 hover\n   * @returns 是否能 hover\n   */\n  isEnabled(): boolean {\n    const currentState = this.editorStateConfig.getCurrentState();\n    // 选择框情况禁止 hover\n    return (\n      // 鼠标友好模式下，也需要支持 hover 效果，不然线条选择不到\n      // Coze 中没有使用该插件，需要在 workflow/render 包相应位置改动\n      (currentState === EditorState.STATE_SELECT ||\n        currentState === EditorState.STATE_MOUSE_FRIENDLY_SELECT) &&\n      !this.selectorBoxConfigEntity.isStart &&\n      !this.dragService.isDragging\n    );\n  }\n\n  private handleDragLine(e: MouseEvent): boolean | undefined {\n    const { someHovered } = this.hoverService;\n    // 重置线条\n    if (someHovered && someHovered instanceof WorkflowLineEntity) {\n      this.dragService.resetLine(someHovered, e);\n      return true;\n    }\n    if (\n      someHovered &&\n      someHovered instanceof WorkflowPortEntity &&\n      !someHovered.disabled &&\n      e.button !== 1\n    ) {\n      e.stopPropagation();\n      e.preventDefault();\n      this.dragService.startDrawingLine(someHovered, e);\n      return true;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-free-hover-plugin';\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/src/selection-utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport { type Entity } from '@flowgram.ai/core';\n\nconst BOUNDS_PADDING = 2;\n\nexport function getSelectionBounds(\n  selection: Entity[],\n  ignoreOneSelect: boolean = true // 忽略单选\n): Rectangle {\n  const selectedNodes = selection.filter((node) => node instanceof WorkflowNodeEntity);\n\n  // 选中单个的时候不显示\n  return selectedNodes.length > (ignoreOneSelect ? 1 : 0)\n    ? Rectangle.enlarge(selectedNodes.map((n) => n.getData(FlowNodeTransformData)!.bounds)).pad(\n        BOUNDS_PADDING\n      )\n    : Rectangle.EMPTY;\n}\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-hover-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-lines-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/free-stack-plugin\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"bezier-js\": \"^6.1.4\",\n    \"clsx\": \"^1.1.1\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/__tests__/__snapshots__/bezier-controls.spec.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`bezier-controls > getBezierControlPoints 1`] = `\n{\n  \"center\": {\n    \"x\": 275,\n    \"y\": 69.25,\n  },\n  \"controls\": [\n    {\n      \"x\": 325,\n      \"y\": 69.25,\n    },\n    {\n      \"x\": 225,\n      \"y\": 69.25,\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/__tests__/bezier-controls.spec.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, it, expect } from 'vitest';\nimport { LinePoint } from '@flowgram.ai/free-layout-core';\n\nimport { getBezierControlPoints } from '../contributions/bezier/bezier-controls';\n\ndescribe('bezier-controls', () => {\n  it('getBezierControlPoints', () => {\n    const fromPos: LinePoint = {\n      x: 325,\n      y: 41.5,\n      location: 'bottom',\n    };\n    const toPos: LinePoint = {\n      x: 225,\n      y: 97,\n      location: 'top',\n    };\n    expect(getBezierControlPoints(fromPos, toPos)).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowPortRenderProps, WorkflowPortRender } from './workflow-port-render';\nexport { WorkflowLineRender } from './workflow-line-render';\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-line-render/arrow.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IPoint } from '@flowgram.ai/utils';\nimport { LinePointLocation } from '@flowgram.ai/free-layout-core';\n\nimport { type ArrowRendererProps } from '../../types/arrow-renderer';\nimport { LINE_OFFSET } from '../../constants/lines';\n\nfunction getArrowPath(pos: IPoint, location: LinePointLocation): string {\n  switch (location) {\n    case 'left':\n      return `M ${pos.x - LINE_OFFSET},${pos.y - LINE_OFFSET} L ${pos.x},${pos.y} L ${\n        pos.x - LINE_OFFSET\n      },${pos.y + LINE_OFFSET}`;\n    case 'right':\n      return `M ${pos.x + LINE_OFFSET},${pos.y + LINE_OFFSET} L ${pos.x},${pos.y} L ${\n        pos.x + LINE_OFFSET\n      },${pos.y - LINE_OFFSET}`;\n    case 'bottom':\n      return `M ${pos.x - LINE_OFFSET},${pos.y + LINE_OFFSET} L ${pos.x},${pos.y} L ${\n        pos.x + LINE_OFFSET\n      },${pos.y + LINE_OFFSET}`;\n    case 'top':\n      return `M ${pos.x - LINE_OFFSET},${pos.y - LINE_OFFSET} L ${pos.x},${pos.y} L ${\n        pos.x + LINE_OFFSET\n      },${pos.y - LINE_OFFSET}`;\n  }\n}\nexport function ArrowRenderer({ id, pos, strokeWidth, location, hide }: ArrowRendererProps) {\n  if (hide) {\n    return null;\n  }\n  const arrowPath = getArrowPath(pos, location);\n\n  return (\n    <path\n      d={arrowPath}\n      strokeLinecap=\"round\"\n      stroke={`url(#${id})`}\n      fill=\"none\"\n      strokeWidth={strokeWidth}\n    />\n  );\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-line-render/index.style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\n// 添加一个固定类名，用于选中该节点\n\nexport const LineStyle = styled.div`\n  position: absolute;\n\n  @keyframes flowingDash {\n    to {\n      stroke-dashoffset: -13;\n    }\n  }\n\n  .dashed-line {\n    stroke-dasharray: 8, 5;\n  }\n\n  .flowing-line {\n    animation: flowingDash 0.5s linear infinite;\n  }\n`;\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-line-render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { memo } from 'react';\n\nimport { LineSVG } from './line-svg';\n\nexport const WorkflowLineRender = memo(\n  LineSVG,\n  (prevProps, nextProps) => prevProps.version === nextProps.version\n);\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-line-render/line-svg.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport clsx from 'clsx';\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { WorkflowLineRenderData } from '@flowgram.ai/free-layout-core';\n\nimport { type ArrowRendererComponent } from '../../types/arrow-renderer';\nimport { LineRenderProps } from '../../type';\nimport { posWithShrink } from '../../contributions/utils';\nimport { STROKE_WIDTH_SLECTED, STROKE_WIDTH } from '../../constants/points';\nimport { LineStyle } from './index.style';\nimport { ArrowRenderer } from './arrow';\n\nconst PADDING = 12;\n\nexport const LineSVG = (props: LineRenderProps) => {\n  const { line, color, selected, children, strokePrefix, rendererRegistry } = props;\n  const { position, reverse, hideArrow, vertical } = line;\n\n  const renderData = line.getData(WorkflowLineRenderData);\n  const { bounds, path: bezierPath } = renderData;\n\n  // 相对位置转换函数\n  const toRelative = (p: IPoint): IPoint => ({\n    x: p.x - bounds.x + PADDING,\n    y: p.y - bounds.y + PADDING,\n  });\n\n  const fromPos = toRelative(position.from);\n  const toPos = toRelative(position.to);\n\n  // 箭头位置计算\n  const arrowToPos: IPoint = posWithShrink(toPos, position.to.location, line.uiState.shrink);\n  const arrowFromPos: IPoint = posWithShrink(fromPos, position.from.location, line.uiState.shrink);\n\n  const strokeWidth = selected\n    ? line.uiState.strokeWidthSelected ?? STROKE_WIDTH_SLECTED\n    : line.uiState.strokeWidth ?? STROKE_WIDTH;\n\n  const strokeID = strokePrefix ? `${strokePrefix}-${line.id}` : line.id;\n\n  // 获取自定义箭头渲染器，如果没有则使用默认的\n  const CustomArrowRenderer = rendererRegistry?.tryToGetRendererComponent('arrow-renderer')\n    ?.renderer as ArrowRendererComponent;\n  const ArrowComponent = CustomArrowRenderer || ArrowRenderer;\n\n  const path = (\n    <path\n      d={bezierPath}\n      fill=\"none\"\n      stroke={`url(#${strokeID})`}\n      strokeWidth={strokeWidth}\n      className={line.processing || line.flowing ? 'dashed-line flowing-line' : ''}\n    />\n  );\n\n  return (\n    <LineStyle\n      className={clsx('gedit-flow-activity-edge', line.className)}\n      style={{\n        ...line.uiState.style,\n        left: bounds.x - PADDING,\n        top: bounds.y - PADDING,\n        position: 'absolute',\n      }}\n    >\n      {children}\n      <svg width={bounds.width + PADDING * 2} height={bounds.height + PADDING * 2}>\n        <defs>\n          <linearGradient\n            x1={vertical ? '100%' : '0%'}\n            y1={vertical ? '0%' : '100%'}\n            x2=\"100%\"\n            y2=\"100%\"\n            id={strokeID}\n            gradientUnits=\"userSpaceOnUse\"\n          >\n            <stop stopColor={color} offset=\"0%\" />\n            <stop stopColor={color} offset=\"100%\" />\n          </linearGradient>\n        </defs>\n        <g>\n          {path}\n          <ArrowComponent\n            id={strokeID}\n            pos={reverse ? arrowFromPos : arrowToPos}\n            strokeWidth={strokeWidth}\n            location={reverse ? position.from.location : position.to.location}\n            hide={hideArrow}\n            line={line}\n          />\n        </g>\n      </svg>\n    </LineStyle>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-port-render/cross-hair.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\n// demo 环境自绘 cross-hair，正式环境使用 IconAdd\nexport default function CrossHair(): JSX.Element {\n  return (\n    <div className=\"symbol\">\n      <div className=\"cross-hair\" />\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-port-render/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React, { useEffect, useState } from 'react';\n\nimport classNames from 'clsx';\nimport {\n  WorkflowHoverService,\n  type WorkflowPortEntity,\n  usePlaygroundReadonlyState,\n  WorkflowLinesManager,\n} from '@flowgram.ai/free-layout-core';\nimport { MouseTouchEvent, useService } from '@flowgram.ai/core';\n\nimport { PORT_BG_CLASS_NAME } from '../../constants/points';\nimport { WorkflowPointStyle } from './style';\nimport CrossHair from './cross-hair';\n\nexport interface WorkflowPortRenderProps {\n  entity: WorkflowPortEntity;\n  className?: string;\n  style?: React.CSSProperties;\n  onClick?: (e: React.MouseEvent<HTMLDivElement>, port: WorkflowPortEntity) => void;\n  /** 激活状态颜色 (linked/hovered) */\n  primaryColor?: string;\n  /** 默认状态颜色 */\n  secondaryColor?: string;\n  /** 错误状态颜色 */\n  errorColor?: string;\n  /** 背景颜色 */\n  backgroundColor?: string;\n}\n\nexport const WorkflowPortRender: React.FC<WorkflowPortRenderProps> =\n  // eslint-disable-next-line react/display-name\n  React.memo<WorkflowPortRenderProps>((props: WorkflowPortRenderProps) => {\n    const hoverService = useService<WorkflowHoverService>(WorkflowHoverService);\n    const linesManager = useService<WorkflowLinesManager>(WorkflowLinesManager);\n    const { entity, onClick } = props;\n    const { relativePosition, disabled } = entity;\n    const [targetElement, setTargetElement] = useState(entity.targetElement);\n    const [posX, updatePosX] = useState(relativePosition.x);\n    const [posY, updatePosY] = useState(relativePosition.y);\n    const [hovered, setHovered] = useState(false);\n    const [linked, setLinked] = useState(Boolean(entity?.lines?.length));\n    const [hasError, setHasError] = useState(props.entity.hasError);\n    const readonly = usePlaygroundReadonlyState();\n\n    useEffect(() => {\n      // useEffect 时序问题可能导致 port.hasError 非最新，需重新触发一次 validate\n      entity.validate();\n      setHasError(entity.hasError);\n      const dispose = entity.onEntityChange(() => {\n        // 如果有挂载的节点，不需要更新位置信息\n        if (entity.targetElement) {\n          if (entity.targetElement !== targetElement) {\n            setTargetElement(entity.targetElement);\n          }\n          return;\n        }\n        const newPos = entity.relativePosition;\n        // 加上 round 避免点位抖动\n        updatePosX(Math.round(newPos.x));\n        updatePosY(Math.round(newPos.y));\n      });\n      const dispose2 = hoverService.onHoveredChange((id) => {\n        setHovered(hoverService.isHovered(entity.id));\n      });\n      const dispose3 = entity.onErrorChanged(() => {\n        setHasError(entity.hasError);\n      });\n      const dispose4 = linesManager.onAvailableLinesChange(() => {\n        setTimeout(() => {\n          if (linesManager.disposed || entity.disposed) return;\n          setLinked(Boolean(entity.lines.length));\n        }, 0);\n      });\n      return () => {\n        dispose.dispose();\n        dispose2.dispose();\n        dispose3.dispose();\n        dispose4.dispose();\n      };\n    }, [hoverService, entity, targetElement]);\n\n    // 监听变化\n    const className = classNames('workflow-port-render', props.className || '', {\n      hovered: !readonly && hovered && !disabled,\n      // 有线条链接的时候深蓝色小圆点\n      linked,\n    });\n\n    // 构建 CSS 自定义属性用于颜色覆盖\n    const colorStyles: Record<string, string> = {};\n    if (props.primaryColor) {\n      colorStyles['--g-workflow-port-color-primary'] = props.primaryColor;\n    }\n    if (props.secondaryColor) {\n      colorStyles['--g-workflow-port-color-secondary'] = props.secondaryColor;\n    }\n    if (props.errorColor) {\n      colorStyles['--g-workflow-port-color-error'] = props.errorColor;\n    }\n    if (props.backgroundColor) {\n      colorStyles['--g-workflow-port-color-background'] = props.backgroundColor;\n    }\n\n    const combinedStyle = targetElement\n      ? { ...props.style, ...colorStyles }\n      : { ...props.style, ...colorStyles, left: posX, top: posY };\n\n    const content = (\n      <WorkflowPointStyle\n        className={className}\n        style={combinedStyle}\n        onClick={(e) => onClick?.(e, entity)}\n        onTouchStart={(e) => {\n          if (!onClick) {\n            return;\n          }\n          MouseTouchEvent.onTouched(e, (mouseEvent) => {\n            onClick(mouseEvent as unknown as React.MouseEvent<HTMLDivElement>, entity);\n          });\n        }}\n        data-port-entity-id={entity.id}\n        data-port-entity-type={entity.portType}\n        data-testid=\"sdk.workflow.canvas.node.port\"\n      >\n        <div className={classNames('bg-circle', 'workflow-bg-circle')}></div>\n        <div\n          className={classNames({\n            bg: true,\n            [PORT_BG_CLASS_NAME]: true,\n            'workflow-point-bg': true,\n            hasError,\n          })}\n        >\n          <CrossHair />\n        </div>\n        <div className=\"focus-circle\" />\n      </WorkflowPointStyle>\n    );\n    if (targetElement) {\n      return ReactDOM.createPortal(content, targetElement);\n    }\n    return content;\n  });\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/components/workflow-port-render/style.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport styled from 'styled-components';\n\nexport const WorkflowPointStyle = styled.div`\n  width: 20px;\n  height: 20px;\n  border-radius: 50%;\n  margin-top: -10px;\n  margin-left: -10px;\n  left: 50%;\n  top: 50%;\n  position: absolute;\n  // 非 hover 状态下的样式\n  border: none;\n\n  & > .symbol {\n    opacity: 0;\n  }\n\n  .bg-circle {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: absolute;\n    border-radius: 50%;\n    width: 20px;\n    height: 20px;\n    background-color: var(--g-workflow-port-color-background, #fff);\n    transform: scale(0.5);\n    transition: all 0.2s linear 0s;\n  }\n\n  .bg {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: relative;\n    width: 100%;\n    height: 100%;\n    border-radius: 50%;\n    background: var(--g-workflow-port-color-secondary, #9197f1);\n    transform: scale(0.4, 0.4);\n    transition: all 0.2s linear 0s;\n\n    &.hasError {\n      background: var(--g-workflow-port-color-error, red);\n    }\n\n    .symbol {\n      position: absolute;\n      width: 14px;\n      height: 14px;\n      opacity: 0;\n      pointer-events: none;\n      color: var(--g-workflow-port-color-background, #fff);\n      transition: opacity 0.2s linear 0s;\n\n      & > svg {\n        width: 14px;\n        height: 14px;\n      }\n    }\n\n    .focus-circle {\n      position: absolute;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      width: 8px;\n      height: 8px;\n      opacity: 0;\n      background: var(--g-workflow-port-color-secondary, #9197f1);\n      border-radius: 50%;\n      transition: opacity 0.2s linear 0s;\n    }\n  }\n\n  &.linked .bg:not(.hasError) {\n    background: var(--g-workflow-port-color-primary, #4d53e8);\n  }\n\n  &.hovered .bg:not(.hasError) {\n    border: none;\n    cursor: crosshair;\n    transform: scale(1, 1);\n    background: var(--g-workflow-port-color-primary, #4d53e8);\n\n    & > .symbol {\n      opacity: 1;\n    }\n  }\n\n  .cross-hair {\n    position: relative;\n    left: 2px;\n    top: 2px;\n\n    &::after,\n    &::before {\n      content: '';\n      background: var(--g-workflow-port-color-background, #fff);\n      border-radius: 2px;\n      position: absolute;\n    }\n\n    &::after {\n      left: 4px;\n      width: 2px;\n      height: 6px;\n      box-shadow: 0 4px var(--g-workflow-port-color-background, #fff);\n    }\n\n    &::before {\n      top: 4px;\n      width: 6px;\n      height: 2px;\n      box-shadow: 4px 0 var(--g-workflow-port-color-background, #fff);\n    }\n  }\n`;\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/constants/lines.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// 箭头宽度\nexport const LINE_OFFSET = 6;\n\nexport const LINE_PADDING = 12;\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/constants/points.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// 连接点半径\n\nexport const STROKE_WIDTH_SLECTED = 3;\n\nexport const STROKE_WIDTH = 2;\n\nexport const PORT_BG_CLASS_NAME = 'workflow-port-bg';\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/bezier/bezier-controls.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { LinePoint, LinePointLocation } from '@flowgram.ai/free-layout-core';\n\n/**\n * Fork from: https://github.com/xyflow/xyflow/blob/main/packages/system/src/utils/edges/bezier-edge.ts\n * MIT License\n * Copyright (c) 2019-2024 webkid GmbH\n */\nexport function getBezierEdgeCenter(\n  fromPos: IPoint,\n  toPos: IPoint,\n  fromControl: IPoint,\n  toControl: IPoint\n): IPoint {\n  /*\n   * cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate\n   * https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve\n   */\n  const x = fromPos.x * 0.125 + fromControl.x * 0.375 + toControl.x * 0.375 + toPos.x * 0.125;\n  const y = fromPos.y * 0.125 + fromControl.y * 0.375 + toControl.y * 0.375 + toPos.y * 0.125;\n  return {\n    x,\n    y,\n  };\n}\n\nfunction getControlOffset(distance: number, curvature: number): number {\n  if (distance >= 0) {\n    return 0.5 * distance;\n  }\n\n  return curvature * 25 * Math.sqrt(-distance);\n}\n\nfunction getControlWithCurvature({\n  location,\n  x1,\n  y1,\n  x2,\n  y2,\n  curvature,\n}: {\n  location: LinePointLocation;\n  curvature: number;\n  x1: number;\n  x2: number;\n  y1: number;\n  y2: number;\n}): IPoint {\n  switch (location) {\n    case 'left':\n      return {\n        x: x1 - getControlOffset(x1 - x2, curvature),\n        y: y1,\n      };\n    case 'right':\n      return {\n        x: x1 + getControlOffset(x2 - x1, curvature),\n        y: y1,\n      };\n    case 'top':\n      return {\n        x: x1,\n        y: y1 - getControlOffset(y1 - y2, curvature),\n      };\n    case 'bottom':\n      return {\n        x: x1,\n        y: y1 + getControlOffset(y2 - y1, curvature),\n      };\n  }\n}\n\nexport function getBezierControlPoints(\n  fromPos: LinePoint,\n  toPos: LinePoint,\n  curvature = 0.25\n): { controls: [IPoint, IPoint]; center: IPoint } {\n  const fromControl = getControlWithCurvature({\n    location: fromPos.location,\n    x1: fromPos.x,\n    y1: fromPos.y,\n    x2: toPos.x,\n    y2: toPos.y,\n    curvature,\n  });\n  const toControl = getControlWithCurvature({\n    location: toPos.location,\n    x1: toPos.x,\n    y1: toPos.y,\n    x2: fromPos.x,\n    y2: fromPos.y,\n    curvature,\n  });\n  const center = getBezierEdgeCenter(fromPos, toPos, fromControl, toControl);\n  return {\n    controls: [fromControl, toControl],\n    center,\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/bezier/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Bezier } from 'bezier-js';\nimport { IPoint, Point, Rectangle } from '@flowgram.ai/utils';\nimport {\n  WorkflowLineEntity,\n  WorkflowLineRenderContribution,\n  LinePoint,\n  LineCenterPoint,\n} from '@flowgram.ai/free-layout-core';\nimport { LineType } from '@flowgram.ai/free-layout-core';\n\nimport { posWithShrink, toRelative } from '../utils';\nimport { getBezierControlPoints } from './bezier-controls';\n\nexport interface BezierData {\n  fromPos: IPoint;\n  toPos: IPoint;\n  bbox: Rectangle; // 外围矩形\n  controls: IPoint[]; // 控制点\n  bezier: Bezier;\n  path: string;\n  center: LineCenterPoint;\n}\n\nexport class WorkflowBezierLineContribution implements WorkflowLineRenderContribution {\n  public static type = LineType.BEZIER;\n\n  public entity: WorkflowLineEntity;\n\n  constructor(entity: WorkflowLineEntity) {\n    this.entity = entity;\n  }\n\n  private data?: BezierData;\n\n  public get path(): string {\n    return this.data?.path ?? '';\n  }\n\n  public calcDistance(pos: IPoint): number {\n    if (!this.data) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    return Point.getDistance(pos, this.data.bezier.project(pos));\n  }\n\n  public get bounds(): Rectangle {\n    if (!this.data) {\n      return Rectangle.EMPTY;\n    }\n    return this.data.bbox;\n  }\n\n  get center() {\n    return this.data?.center;\n  }\n\n  public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {\n    this.data = this.calcBezier(params.fromPos, params.toPos);\n  }\n\n  private calcBezier(fromPos: LinePoint, toPos: LinePoint): BezierData {\n    const { controls, center } = getBezierControlPoints(\n      fromPos,\n      toPos,\n      this.entity.uiState.curvature\n    );\n    const bezier = new Bezier([fromPos, ...controls, toPos]);\n    const bbox = bezier.bbox();\n    const bboxBounds = new Rectangle(\n      bbox.x.min,\n      bbox.y.min,\n      bbox.x.max - bbox.x.min,\n      bbox.y.max - bbox.y.min\n    );\n    const centerPoint = toRelative(center, bboxBounds);\n\n    const path = this.getPath({ bbox: bboxBounds, fromPos, toPos, controls });\n\n    this.data = {\n      fromPos,\n      toPos,\n      bezier,\n      bbox: bboxBounds,\n      controls,\n      path,\n      center: {\n        ...center,\n        labelX: centerPoint.x,\n        labelY: centerPoint.y,\n      },\n    };\n    return this.data;\n  }\n\n  private getPath(params: {\n    bbox: Rectangle;\n    fromPos: LinePoint;\n    toPos: LinePoint;\n    controls: [IPoint, IPoint];\n  }): string {\n    const { bbox } = params;\n    // 相对位置转换函数\n    const fromPos = toRelative(params.fromPos, bbox);\n    const toPos = toRelative(params.toPos, bbox);\n\n    const controls = params.controls.map((c) => toRelative(c, bbox));\n    const shrink = this.entity.uiState.shrink;\n\n    const renderFromPos: IPoint = posWithShrink(fromPos, params.fromPos.location, shrink);\n\n    const renderToPos: IPoint = posWithShrink(toPos, params.toPos.location, shrink);\n\n    const controlPoints = controls.map((s) => `${s.x} ${s.y}`).join(',');\n    return `M${renderFromPos.x} ${renderFromPos.y} C ${controlPoints}, ${renderToPos.x} ${renderToPos.y}`;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/fold/fold-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type IPoint, Point, Rectangle } from '@flowgram.ai/utils';\nimport { LinePoint } from '@flowgram.ai/free-layout-core';\n\n/**\n * 计算点到线段的距离\n * @param point 待测试点\n * @param segStart 线段起点\n * @param segEnd 线段终点\n */\nconst getPointToSegmentDistance = (point: IPoint, segStart: IPoint, segEnd: IPoint): number => {\n  const { x: px, y: py } = point;\n  const { x: x1, y: y1 } = segStart;\n  const { x: x2, y: y2 } = segEnd;\n\n  const A = px - x1;\n  const B = py - y1;\n  const C = x2 - x1;\n  const D = y2 - y1;\n\n  const dot = A * C + B * D;\n  const lenSq = C * C + D * D;\n\n  // 参数方程中的t参数\n  const param = lenSq === 0 ? -1 : dot / lenSq;\n\n  let xx: number;\n  let yy: number;\n\n  if (param < 0) {\n    xx = x1;\n    yy = y1;\n  } else if (param > 1) {\n    xx = x2;\n    yy = y2;\n  } else {\n    xx = x1 + param * C;\n    yy = y1 + param * D;\n  }\n\n  const dx = px - xx;\n  const dy = py - yy;\n\n  return Math.sqrt(dx * dx + dy * dy);\n};\n\n/**\n * Fork from: https://github.com/xyflow/xyflow/blob/main/packages/system/src/utils/edges/smoothstep-edge.ts\n * MIT License\n * Copyright (c) 2019-2024 webkid GmbH\n */\nexport namespace FoldLine {\n  const EDGE_RADIUS = 5;\n  const OFFSET = 20;\n\n  function getEdgeCenter({ source, target }: { source: IPoint; target: IPoint }): [number, number] {\n    const xOffset = Math.abs(target.x - source.x) / 2;\n    const centerX = target.x < source.x ? target.x + xOffset : target.x - xOffset;\n\n    const yOffset = Math.abs(target.y - source.y) / 2;\n    const centerY = target.y < source.y ? target.y + yOffset : target.y - yOffset;\n\n    return [centerX, centerY];\n  }\n\n  const getDirection = ({ source, target }: { source: LinePoint; target: LinePoint }): IPoint => {\n    if (source.location === 'left' || source.location === 'right') {\n      return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 };\n    }\n    return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 };\n  };\n\n  const handleDirections = {\n    left: { x: -1, y: 0 },\n    right: { x: 1, y: 0 },\n    top: { x: 0, y: -1 },\n    bottom: { x: 0, y: 1 },\n  };\n  // eslint-disable-next-line complexity\n  export function getPoints({ source, target }: { source: LinePoint; target: LinePoint }): {\n    points: IPoint[];\n    center: IPoint;\n  } {\n    const sourceDir = handleDirections[source.location];\n    const targetDir = handleDirections[target.location];\n    const sourceGapped: LinePoint = {\n      x: source.x + sourceDir.x * OFFSET,\n      y: source.y + sourceDir.y * OFFSET,\n      location: source.location,\n    };\n    const targetGapped: LinePoint = {\n      x: target.x + targetDir.x * OFFSET,\n      y: target.y + targetDir.y * OFFSET,\n      location: target.location,\n    };\n    const dir = getDirection({\n      source: sourceGapped,\n      target: targetGapped,\n    });\n    const dirAccessor = dir.x !== 0 ? 'x' : 'y';\n    const currDir = dir[dirAccessor];\n\n    let points: IPoint[] = [];\n    let centerX, centerY;\n\n    const [defaultCenterX, defaultCenterY] = getEdgeCenter({\n      source,\n      target,\n    });\n\n    // 计算向量乘积\n    if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {\n      centerX = defaultCenterX;\n      centerY = defaultCenterY;\n\n      const verticalSplit: IPoint[] = [\n        { x: centerX, y: sourceGapped.y },\n        { x: centerX, y: targetGapped.y },\n      ];\n\n      const horizontalSplit: IPoint[] = [\n        { x: sourceGapped.x, y: centerY },\n        { x: targetGapped.x, y: centerY },\n      ];\n\n      if (sourceDir[dirAccessor] === currDir) {\n        points = dirAccessor === 'x' ? verticalSplit : horizontalSplit;\n      } else {\n        points = dirAccessor === 'x' ? horizontalSplit : verticalSplit;\n      }\n    } else {\n      // sourceTarget means we take x from source and y from target, targetSource is the opposite\n      const sourceTarget: IPoint[] = [{ x: sourceGapped.x, y: targetGapped.y }];\n      const targetSource: IPoint[] = [{ x: targetGapped.x, y: sourceGapped.y }];\n      // this handles edges with same handle positions\n      if (dirAccessor === 'x') {\n        points = sourceDir.x === currDir ? targetSource : sourceTarget;\n      } else {\n        points = sourceDir.y === currDir ? sourceTarget : targetSource;\n      }\n\n      // these are conditions for handling mixed handle positions like Right -> Bottom for example\n      const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x';\n      const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite];\n      const sourceGtTargetOppo =\n        sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite];\n      const sourceLtTargetOppo =\n        sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite];\n      const flipSourceTarget =\n        (sourceDir[dirAccessor] === 1 &&\n          ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) ||\n        (sourceDir[dirAccessor] !== 1 &&\n          ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo)));\n\n      if (flipSourceTarget) {\n        points = dirAccessor === 'x' ? sourceTarget : targetSource;\n      }\n\n      const sourceGapPoint = { x: sourceGapped.x, y: sourceGapped.y };\n      const targetGapPoint = { x: targetGapped.x, y: targetGapped.y };\n      const maxXDistance = Math.max(\n        Math.abs(sourceGapPoint.x - points[0].x),\n        Math.abs(targetGapPoint.x - points[0].x)\n      );\n      const maxYDistance = Math.max(\n        Math.abs(sourceGapPoint.y - points[0].y),\n        Math.abs(targetGapPoint.y - points[0].y)\n      );\n\n      if (maxXDistance >= maxYDistance) {\n        centerX = (sourceGapPoint.x + targetGapPoint.x) / 2;\n        centerY = points[0].y;\n      } else {\n        centerX = points[0].x;\n        centerY = (sourceGapPoint.y + targetGapPoint.y) / 2;\n      }\n    }\n\n    const pathPoints = [\n      source,\n      { x: sourceGapped.x, y: sourceGapped.y },\n      ...points,\n      { x: targetGapped.x, y: targetGapped.y },\n      target,\n    ];\n\n    return {\n      points: pathPoints,\n      center: {\n        x: centerX,\n        y: centerY,\n      },\n    };\n  }\n\n  function getBend(a: IPoint, b: IPoint, c: IPoint): string {\n    const bendSize = Math.min(\n      Point.getDistance(a, b) / 2,\n      Point.getDistance(b, c) / 2,\n      EDGE_RADIUS\n    );\n    const { x, y } = b;\n\n    // no bend\n    if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) {\n      return `L${x} ${y}`;\n    }\n\n    // first segment is horizontal\n    if (a.y === y) {\n      const xDir = a.x < c.x ? -1 : 1;\n      const yDir = a.y < c.y ? 1 : -1;\n      return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}`;\n    }\n\n    const xDir = a.x < c.x ? 1 : -1;\n    const yDir = a.y < c.y ? -1 : 1;\n    return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}`;\n  }\n\n  /**\n   * 实现 reactFlow 原本的折叠线交互\n   */\n  export function getSmoothStepPath(points: IPoint[]): string {\n    const path = points.reduce<string>((res, p, i) => {\n      let segment = '';\n\n      if (i > 0 && i < points.length - 1) {\n        segment = getBend(points[i - 1], p, points[i + 1]);\n      } else {\n        segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;\n      }\n\n      res += segment;\n\n      return res;\n    }, '');\n\n    return path;\n  }\n  export function getBounds(points: IPoint[]): Rectangle {\n    const xList = points.map((p) => p.x);\n    const yList = points.map((p) => p.y);\n    const left = Math.min(...xList);\n    const right = Math.max(...xList);\n    const top = Math.min(...yList);\n    const bottom = Math.max(...yList);\n    return Rectangle.createRectangleWithTwoPoints(\n      {\n        x: left,\n        y: top,\n      },\n      {\n        x: right,\n        y: bottom,\n      }\n    );\n  }\n  /**\n   * 计算点到折线的最短距离\n   * @param points 折线的所有端点\n   * @param pos 待测试点\n   * @returns 最短距离\n   */\n  export const getFoldLineToPointDistance = (points: IPoint[], pos: IPoint): number => {\n    // 特殊情况处理\n    if (points.length === 0) {\n      return Infinity;\n    }\n\n    if (points.length === 1) {\n      return Point.getDistance(points[0]!, pos);\n    }\n\n    // 构建线段数组\n    const lines: [IPoint, IPoint][] = [];\n    for (let i = 0; i < points.length - 1; i++) {\n      lines.push([points[i]!, points[i + 1]!]);\n    }\n\n    // 计算点到每个线段的最短距离\n    const distances = lines.map((line) => {\n      const [p1, p2] = line;\n      return getPointToSegmentDistance(pos, p1, p2);\n    });\n\n    return Math.min(...distances);\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/fold/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\nimport {\n  WorkflowLineEntity,\n  WorkflowLineRenderContribution,\n  LinePoint,\n  LineCenterPoint,\n} from '@flowgram.ai/free-layout-core';\nimport { LineType } from '@flowgram.ai/free-layout-core';\n\nimport { posWithShrink, toRelative } from '../utils';\nimport { FoldLine } from './fold-line';\n\nexport interface FoldData {\n  points: IPoint[];\n  path: string;\n  bbox: Rectangle;\n  center: LineCenterPoint;\n}\n\nexport class WorkflowFoldLineContribution implements WorkflowLineRenderContribution {\n  public static type = LineType.LINE_CHART;\n\n  public entity: WorkflowLineEntity;\n\n  constructor(entity: WorkflowLineEntity) {\n    this.entity = entity;\n  }\n\n  private data?: FoldData;\n\n  public get path(): string {\n    return this.data?.path ?? '';\n  }\n\n  public calcDistance(pos: IPoint): number {\n    if (!this.data) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    return FoldLine.getFoldLineToPointDistance(this.data.points, pos);\n  }\n\n  public get bounds(): Rectangle {\n    if (!this.data) {\n      return new Rectangle();\n    }\n    return this.data.bbox;\n  }\n\n  get center() {\n    return this.data?.center;\n  }\n\n  public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {\n    const { fromPos, toPos } = params;\n    const shrink = this.entity.uiState.shrink;\n\n    // 根据方向预先计算源点和目标点的偏移\n    const source = posWithShrink(fromPos, fromPos.location, shrink);\n    const target = posWithShrink(toPos, toPos.location, shrink);\n\n    const { points, center } = FoldLine.getPoints({\n      source: {\n        ...source,\n        location: fromPos.location,\n      },\n      target: {\n        ...target,\n        location: toPos.location,\n      },\n    });\n\n    const bbox = FoldLine.getBounds(points);\n\n    // 调整所有点到 SVG 视口坐标系\n    const adjustedPoints = points.map((p) => toRelative(p, bbox));\n\n    const path = FoldLine.getSmoothStepPath(adjustedPoints);\n\n    const relativeCenter = toRelative(center, bbox);\n    this.data = {\n      points,\n      path,\n      bbox,\n      center: {\n        x: center.x,\n        y: center.y,\n        labelX: relativeCenter.x,\n        labelY: relativeCenter.y,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './bezier';\nexport * from './fold';\nexport * from './straight';\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/straight/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Point, Rectangle } from '@flowgram.ai/utils';\nimport {\n  WorkflowLineEntity,\n  WorkflowLineRenderContribution,\n  LinePoint,\n  LineCenterPoint,\n  getLineCenter,\n  LineType,\n} from '@flowgram.ai/free-layout-core';\n\nimport { LINE_PADDING } from '../../constants/lines';\nimport { projectPointOnLine } from './point-on-line';\nimport { posWithShrink } from '../utils';\n\nexport interface StraightData {\n  points: IPoint[];\n  path: string;\n  bbox: Rectangle;\n  center: LineCenterPoint;\n}\n\nexport class WorkflowStraightLineContribution implements WorkflowLineRenderContribution {\n  public static type = LineType.STRAIGHT;\n\n  public entity: WorkflowLineEntity;\n\n  constructor(entity: WorkflowLineEntity) {\n    this.entity = entity;\n  }\n\n  private data?: StraightData;\n\n  public get path(): string {\n    return this.data?.path ?? '';\n  }\n\n  public calcDistance(pos: IPoint): number {\n    if (!this.data) {\n      return Number.MAX_SAFE_INTEGER;\n    }\n    const [start, end] = this.data.points;\n    return Point.getDistance(pos, projectPointOnLine(pos, start, end));\n  }\n\n  public get bounds(): Rectangle {\n    if (!this.data) {\n      return new Rectangle();\n    }\n    return this.data.bbox;\n  }\n\n  get center() {\n    return this.data?.center;\n  }\n\n  public update(params: { fromPos: LinePoint; toPos: LinePoint }): void {\n    const { fromPos, toPos } = params;\n    const shrink = this.entity.uiState.shrink;\n\n    // 根据方向预先计算源点和目标点的偏移\n    const source = posWithShrink(fromPos, fromPos.location, shrink);\n    const target = posWithShrink(toPos, toPos.location, shrink);\n\n    const points = [source, target];\n\n    const bbox = Rectangle.createRectangleWithTwoPoints(points[0], points[1]);\n\n    // 调整所有点到 SVG 视口坐标系\n    const adjustedPoints = points.map((p) => ({\n      x: p.x - bbox.x + LINE_PADDING,\n      y: p.y - bbox.y + LINE_PADDING,\n    }));\n\n    // 生成直线路径\n    const path = `M ${adjustedPoints[0].x} ${adjustedPoints[0].y} L ${adjustedPoints[1].x} ${adjustedPoints[1].y}`;\n\n    this.data = {\n      points,\n      path,\n      bbox,\n      center: getLineCenter(fromPos, toPos, bbox, LINE_PADDING),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/straight/point-on-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\n\nexport interface StraightData {\n  points: IPoint[];\n  path: string;\n  bbox: Rectangle;\n}\n\n/**\n * 计算点到直线的投影点\n */\nexport function projectPointOnLine(point: IPoint, lineStart: IPoint, lineEnd: IPoint): IPoint {\n  const dx = lineEnd.x - lineStart.x;\n  const dy = lineEnd.y - lineStart.y;\n\n  // 如果是垂直线\n  if (dx === 0) {\n    return { x: lineStart.x, y: point.y };\n  }\n  // 如果是水平线\n  if (dy === 0) {\n    return { x: point.x, y: lineStart.y };\n  }\n\n  const t = ((point.x - lineStart.x) * dx + (point.y - lineStart.y) * dy) / (dx * dx + dy * dy);\n  const clampedT = Math.max(0, Math.min(1, t));\n\n  return {\n    x: lineStart.x + clampedT * dx,\n    y: lineStart.y + clampedT * dy,\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/contributions/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\nimport { LinePointLocation } from '@flowgram.ai/free-layout-core';\n\nimport { LINE_PADDING } from '../constants/lines';\n\nexport function toRelative(p: IPoint, bbox: Rectangle): IPoint {\n  return {\n    x: p.x - bbox.x + LINE_PADDING,\n    y: p.y - bbox.y + LINE_PADDING,\n  };\n}\n\nexport function getShrinkOffset(location: LinePointLocation, shrink: number): IPoint {\n  switch (location) {\n    case 'left':\n      return { x: -shrink, y: 0 };\n    case 'right':\n      return { x: shrink, y: 0 };\n    case 'bottom':\n      return { x: 0, y: shrink };\n    case 'top':\n      return { x: 0, y: -shrink };\n  }\n}\n\nexport function posWithShrink(pos: IPoint, location: LinePointLocation, shrink: number): IPoint {\n  const offset = getShrinkOffset(location, shrink);\n  return {\n    x: pos.x + offset.x,\n    y: pos.y + offset.y,\n  };\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/create-free-lines-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowLinesManager } from '@flowgram.ai/free-layout-core';\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { FreeLinesPluginOptions } from './type';\nimport { WorkflowLinesLayer } from './layer';\nimport {\n  WorkflowBezierLineContribution,\n  WorkflowFoldLineContribution,\n  WorkflowStraightLineContribution,\n} from './contributions';\n\nexport const createFreeLinesPlugin = definePluginCreator({\n  singleton: true,\n  onInit: (ctx: PluginContext, opts: FreeLinesPluginOptions) => {\n    ctx.playground.registerLayer(WorkflowLinesLayer, {\n      ...opts,\n    });\n  },\n  onReady: (ctx: PluginContext, opts: FreeLinesPluginOptions) => {\n    const linesManager = ctx.container.get(WorkflowLinesManager);\n    linesManager\n      .registerContribution(WorkflowBezierLineContribution)\n      .registerContribution(WorkflowFoldLineContribution)\n      .registerContribution(WorkflowStraightLineContribution);\n\n    if (opts.contributions) {\n      opts.contributions.forEach((contribution) => {\n        linesManager.registerContribution(contribution);\n      });\n    }\n\n    if (opts.defaultLineType) {\n      linesManager.switchLineType(opts.defaultLineType);\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './components/workflow-port-render';\nexport * from './constants/lines';\nexport * from './create-free-lines-plugin';\nexport * from './layer';\nexport * from './type';\nexport * from './types/arrow-renderer';\nexport * from './contributions';\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/layer/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowLinesLayer } from './workflow-lines-layer';\n\nexport { WorkflowLinesLayer as LinesLayer, WorkflowLinesLayer };\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/layer/workflow-lines-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport React, { ReactNode, useLayoutEffect, useState } from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport { StackingContextManager } from '@flowgram.ai/free-stack-plugin';\nimport {\n  nanoid,\n  WorkflowDocument,\n  WorkflowHoverService,\n  WorkflowLineEntity,\n  WorkflowLineRenderData,\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { Layer, observeEntities, observeEntityDatas, TransformData } from '@flowgram.ai/core';\n\nimport { LineRenderProps, LinesLayerOptions } from '../type';\nimport { WorkflowLineRender } from '../components';\n\n@injectable()\nexport class WorkflowLinesLayer extends Layer<LinesLayerOptions> {\n  static type = 'WorkflowLinesLayer';\n\n  @inject(WorkflowHoverService) hoverService: WorkflowHoverService;\n\n  @inject(WorkflowSelectService) selectService: WorkflowSelectService;\n\n  @inject(StackingContextManager) stackContext: StackingContextManager;\n\n  @inject(FlowRendererRegistry) rendererRegistry: FlowRendererRegistry;\n\n  @observeEntities(WorkflowLineEntity) readonly lines: WorkflowLineEntity[];\n\n  @observeEntities(WorkflowPortEntity) readonly ports: WorkflowPortEntity[];\n\n  @observeEntityDatas(WorkflowNodeEntity, TransformData)\n  readonly trans: TransformData[];\n\n  @inject(WorkflowDocument) protected workflowDocument: WorkflowDocument;\n\n  private layerID = nanoid();\n\n  private mountedLines: Map<\n    string,\n    {\n      line: WorkflowLineEntity;\n      portal: ReactNode;\n      version: string;\n    }\n  > = new Map();\n\n  private _version = 0;\n\n  /**\n   * 节点线条\n   */\n  public node = domUtils.createDivWithClass('gedit-playground-layer gedit-flow-lines-layer');\n\n  public onZoom(scale: number): void {\n    this.node.style.transform = `scale(${scale})`;\n  }\n\n  public onReady() {\n    this.pipelineNode.appendChild(this.node);\n    this.toDispose.pushAll([\n      this.selectService.onSelectionChanged(() => this.render()),\n      this.hoverService.onHoveredChange(() => this.render()),\n      this.workflowDocument.linesManager.onForceUpdate(() => {\n        this.mountedLines.clear();\n        this.bumpVersion();\n        this.render();\n      }),\n    ]);\n  }\n\n  public dispose() {\n    this.mountedLines.clear();\n  }\n\n  public render(): JSX.Element {\n    const [, forceUpdate] = useState({});\n\n    useLayoutEffect(() => {\n      const updateLines = (): void => {\n        let needsUpdate = false;\n\n        // 批量处理所有线条的更新\n        this.lines.forEach((line) => {\n          const renderData = line.getData(WorkflowLineRenderData);\n          const oldVersion = renderData.renderVersion;\n          renderData.update();\n          // 如果有任何一条线发生变化，标记需要更新\n          if (renderData.renderVersion !== oldVersion) {\n            needsUpdate = true;\n          }\n        });\n\n        // 只在确实需要更新时触发重渲染\n        if (needsUpdate) {\n          forceUpdate({});\n        }\n      };\n\n      const rafId = requestAnimationFrame(updateLines);\n      return () => cancelAnimationFrame(rafId);\n    }, [this.lines]); // 依赖项包含 lines\n\n    const lines = this.lines.map((line) => this.renderLine(line));\n    return <>{lines}</>;\n  }\n\n  // 用来绕过 memo\n  private bumpVersion() {\n    this._version = this._version + 1;\n    if (this._version === Number.MAX_SAFE_INTEGER) {\n      this._version = 0;\n    }\n  }\n\n  private lineProps(line: WorkflowLineEntity): LineRenderProps {\n    const { lineType } = this.workflowDocument.linesManager;\n    const selected = this.selectService.isSelected(line.id);\n    const hovered = this.hoverService.isHovered(line.id);\n    const version = this.lineVersion(line);\n\n    return {\n      key: line.id,\n      color: line.color,\n      selected,\n      hovered,\n      line,\n      lineType,\n      version,\n      strokePrefix: this.layerID,\n      rendererRegistry: this.rendererRegistry,\n    };\n  }\n\n  private lineVersion(line: WorkflowLineEntity): string {\n    const renderData = line.getData(WorkflowLineRenderData);\n    const { renderVersion } = renderData;\n    const selected = this.selectService.isSelected(line.id);\n    const hovered = this.hoverService.isHovered(line.id);\n    const { version: lineVersion, color } = line;\n\n    const version = `v:${this._version},lv:${lineVersion},rv:${renderVersion},c:${color},s:${\n      selected ? 'T' : 'F'\n    },h:${hovered ? 'T' : 'F'}`;\n\n    return version;\n  }\n\n  private lineComponent(props: LineRenderProps): ReactNode {\n    const RenderInsideLine = this.options.renderInsideLine ?? (() => <></>);\n    return (\n      <WorkflowLineRender {...props}>\n        <RenderInsideLine {...props} />\n      </WorkflowLineRender>\n    );\n  }\n\n  private renderLine(line: WorkflowLineEntity): ReactNode {\n    const lineProps = this.lineProps(line);\n    const cache = this.mountedLines.get(line.id);\n    const isCached = cache !== undefined;\n    const { portal: cachedPortal, version: cachedVersion } = cache ?? {};\n    if (isCached && cachedVersion === lineProps.version) {\n      // 如果已有缓存且版本相同，则直接返回缓存的 portal\n      return cachedPortal;\n    }\n    if (!isCached) {\n      // 如果缓存不存在，则将 line 挂载到 renderElement 上\n      this.renderElement.appendChild(line.node);\n      line.onDispose(() => {\n        this.mountedLines.delete(line.id);\n        line.node.remove();\n      });\n    }\n    // 刷新缓存\n    const portal = ReactDOM.createPortal(this.lineComponent(lineProps), line.node);\n    this.mountedLines.set(line.id, { line, portal, version: lineProps.version });\n    return portal;\n  }\n\n  private get renderElement(): HTMLElement {\n    return this.stackContext.node;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FC, ReactNode } from 'react';\n\nimport { type FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport type {\n  WorkflowLineEntity,\n  WorkflowLineRenderContributionFactory,\n} from '@flowgram.ai/free-layout-core';\nimport { LineRenderType } from '@flowgram.ai/free-layout-core';\n\nexport interface LineRenderProps {\n  key: string;\n  color?: string; // 高亮颜色，优先级最高\n  selected?: boolean;\n  hovered?: boolean;\n  line: WorkflowLineEntity;\n  lineType: LineRenderType;\n  version: string; // 用于控制 memo 刷新\n  strokePrefix?: string;\n  children?: ReactNode;\n  rendererRegistry?: FlowRendererRegistry; // 渲染器注册表，用于获取自定义箭头组件\n}\n\nexport interface LinesLayerOptions {\n  renderInsideLine?: FC<LineRenderProps>;\n}\n\nexport interface FreeLinesPluginOptions extends LinesLayerOptions {\n  contributions?: WorkflowLineRenderContributionFactory[];\n  defaultLineType?: LineRenderType;\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/src/types/arrow-renderer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { type IPoint } from '@flowgram.ai/utils';\nimport { LinePointLocation } from '@flowgram.ai/free-layout-core';\nimport { type WorkflowLineEntity } from '@flowgram.ai/free-layout-core';\n\n/**\n * 箭头渲染器属性接口\n */\nexport interface ArrowRendererProps {\n  /** 用于渐变的唯一ID */\n  id: string;\n  /** 箭头位置 */\n  pos: IPoint;\n  location: LinePointLocation;\n  /** 描边宽度 */\n  strokeWidth: number;\n  /** 是否隐藏箭头 */\n  hide?: boolean;\n  /** 线条实体，提供更多上下文信息 */\n  line: WorkflowLineEntity;\n}\n\n/**\n * 箭头渲染器组件类型\n */\nexport type ArrowRendererComponent = React.ComponentType<ArrowRendererProps>;\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-lines-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-node-panel-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-history-plugin\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/component.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { FC, ReactNode, useEffect, useRef } from 'react';\n\nimport { PositionSchema } from '@flowgram.ai/utils';\n\ninterface NodePanelContainerProps {\n  onSelect: (nodeType: string | undefined) => void;\n  position: PositionSchema;\n  children: ReactNode;\n}\n\nexport const NodePanelContainer: FC<NodePanelContainerProps> = (props) => {\n  const { onSelect, position, children } = props;\n  const panelRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      // 确保点击事件的目标不是组件本身或其子元素\n      if (panelRef.current && !panelRef.current.contains(event.target as Node)) {\n        onSelect(undefined);\n      }\n    };\n    // 添加事件监听器到document\n    document.addEventListener('mousedown', handleClickOutside);\n    // 清理函数\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n    };\n  }, [onSelect]); // 依赖项为onSelect，确保回调函数变化时重新绑定\n\n  return (\n    <div\n      ref={panelRef}\n      className=\"node-panel-container\"\n      data-flow-editor-selectable=\"false\"\n      style={{\n        position: 'absolute',\n        zIndex: 9999,\n        top: position.y,\n        left: position.x,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/create-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, type PluginBindConfig, type PluginContext } from '@flowgram.ai/core';\n\nimport { NodePanelPluginOptions } from './type';\nimport { WorkflowNodePanelService } from './service';\nimport { WorkflowNodePanelLayer } from './layer';\n\nexport const createFreeNodePanelPlugin = definePluginCreator({\n  onBind({ bind }: PluginBindConfig) {\n    bind(WorkflowNodePanelService).toSelf().inSingletonScope();\n  },\n  onInit: (ctx: PluginContext, opts: NodePanelPluginOptions) => {\n    ctx.playground.registerLayer(WorkflowNodePanelLayer, {\n      renderer: opts.renderer,\n    });\n  },\n  onDispose: (ctx: PluginContext) => {\n    const nodePanelService = ctx.get(WorkflowNodePanelService);\n    nodePanelService.dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createFreeNodePanelPlugin } from './create-plugin';\nexport { WorkflowNodePanelService } from './service';\nexport type {\n  NodePanelResult,\n  NodePanelRenderProps,\n  NodePanelRender,\n  NodePanelLayerOptions as NodePanelServiceOptions,\n  NodePanelPluginOptions,\n  CallNodePanelParams,\n} from './type';\nexport { type IWorkflowNodePanelUtils, WorkflowNodePanelUtils } from './utils';\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/no-deprecated */\nimport React from 'react';\n\nimport { inject } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { nanoid } from '@flowgram.ai/free-layout-core';\nimport { Layer } from '@flowgram.ai/core';\n\nimport type {\n  CallNodePanelParams,\n  NodePanelLayerOptions,\n  NodePanelRenderProps,\n  NodePanelResult,\n} from './type';\nimport { WorkflowNodePanelService } from './service';\n\nexport class WorkflowNodePanelLayer extends Layer<NodePanelLayerOptions> {\n  public static type = 'WorkflowNodePanelLayer';\n\n  @inject(WorkflowNodePanelService) private service: WorkflowNodePanelService;\n\n  public node: HTMLDivElement;\n\n  private renderList: Map<string, NodePanelRenderProps>;\n\n  constructor() {\n    super();\n    this.node = domUtils.createDivWithClass('gedit-playground-layer gedit-node-panel-layer');\n    this.node.style.zIndex = '9999';\n    this.renderList = new Map();\n  }\n\n  public onReady(): void {\n    this.service.setCallNodePanel(this.call.bind(this));\n  }\n\n  public onZoom(zoom: number): void {\n    this.node.style.transform = `scale(${zoom})`;\n  }\n\n  public render(): JSX.Element {\n    const NodePanelRender = this.options.renderer;\n    return (\n      <>\n        {Array.from(this.renderList.keys()).map((taskId) => {\n          const renderProps = this.renderList.get(taskId)!;\n          return <NodePanelRender key={taskId} {...renderProps} />;\n        })}\n      </>\n    );\n  }\n\n  private async call(params: CallNodePanelParams): Promise<void> {\n    const taskId = nanoid();\n    const { onSelect, onClose, enableMultiAdd = false, panelProps = {} } = params;\n    return new Promise((resolve) => {\n      const unmount = () => {\n        // 清理挂载的组件\n        this.renderList.delete(taskId);\n        this.render();\n        resolve();\n      };\n      const handleClose = () => {\n        unmount();\n        onClose();\n      };\n      const handleSelect = (params?: NodePanelResult) => {\n        onSelect(params);\n        if (!enableMultiAdd) {\n          unmount();\n        }\n      };\n      const renderProps: NodePanelRenderProps = {\n        ...params,\n        panelProps,\n        onSelect: handleSelect,\n        onClose: handleClose,\n      };\n      this.renderList.set(taskId, renderProps);\n      this.render();\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport type { PositionSchema } from '@flowgram.ai/utils';\nimport {\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n} from '@flowgram.ai/free-layout-core';\nimport { WorkflowSelectService } from '@flowgram.ai/free-layout-core';\nimport { WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\nimport { PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport { WorkflowNodePanelUtils } from './utils';\nimport type {\n  CallNodePanel,\n  CallNodePanelParams,\n  NodePanelCallParams,\n  NodePanelResult,\n} from './type';\n\n/**\n * 添加节点面板服务\n */\n@injectable()\nexport class WorkflowNodePanelService {\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  @inject(WorkflowDragService)\n  private readonly dragService: WorkflowDragService;\n\n  @inject(WorkflowSelectService)\n  private readonly selectService: WorkflowSelectService;\n\n  @inject(WorkflowLinesManager)\n  private readonly linesManager: WorkflowLinesManager;\n\n  @inject(PlaygroundConfigEntity)\n  private readonly playgroundConfig: PlaygroundConfigEntity;\n\n  @inject(HistoryService) private readonly historyService: HistoryService;\n\n  private readonly toDispose = new DisposableCollection();\n\n  public callNodePanel: CallNodePanel = async () => undefined;\n\n  /** 销毁 */\n  public dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  public setCallNodePanel(callNodePanel: CallNodePanel) {\n    this.callNodePanel = callNodePanel;\n  }\n\n  /** 唤起节点面板 */\n  public async call(\n    callParams: NodePanelCallParams\n  ): Promise<WorkflowNodeEntity | WorkflowNodeEntity[] | undefined> {\n    const {\n      panelPosition,\n      fromPort,\n      enableMultiAdd = false,\n      panelProps = {},\n      containerNode,\n      afterAddNode,\n    } = callParams;\n\n    if (!panelPosition || this.playgroundConfig.readonly) {\n      return;\n    }\n\n    const nodes: WorkflowNodeEntity[] = [];\n\n    return new Promise((resolve) => {\n      this.callNodePanel({\n        position: panelPosition,\n        enableMultiAdd,\n        panelProps,\n        containerNode: WorkflowNodePanelUtils.getContainerNode({\n          fromPort,\n          containerNode,\n        }),\n        onSelect: async (panelParams?: NodePanelResult) => {\n          const node = await this.addNode(callParams, panelParams);\n          afterAddNode?.(node);\n          if (!enableMultiAdd) {\n            resolve(node);\n          } else if (node) {\n            nodes.push(node);\n          }\n        },\n        onClose: () => {\n          resolve(enableMultiAdd ? nodes : undefined);\n        },\n      });\n    });\n  }\n\n  /**\n   * 唤起单选面板\n   */\n  public async singleSelectNodePanel(\n    params: Omit<CallNodePanelParams, 'onSelect' | 'onClose' | 'enableMultiAdd'>\n  ): Promise<NodePanelResult | undefined> {\n    return new Promise((resolve) => {\n      this.callNodePanel({\n        ...params,\n        enableMultiAdd: false,\n        onSelect: async (panelParams?: NodePanelResult) => {\n          resolve(panelParams);\n        },\n        onClose: () => {\n          resolve(undefined);\n        },\n      });\n    });\n  }\n\n  /** 添加节点 */\n  private async addNode(\n    callParams: NodePanelCallParams,\n    panelParams: NodePanelResult\n  ): Promise<WorkflowNodeEntity | undefined> {\n    const {\n      panelPosition,\n      fromPort,\n      toPort,\n      canAddNode,\n      autoOffsetPadding = {\n        x: 100,\n        y: 100,\n      },\n      enableBuildLine = false,\n      enableSelectPosition = false,\n      enableAutoOffset = false,\n      enableDragNode = false,\n    } = callParams;\n\n    if (!panelPosition || !panelParams) {\n      return;\n    }\n\n    const { nodeType, selectEvent, nodeJSON } = panelParams;\n\n    const containerNode = WorkflowNodePanelUtils.getContainerNode({\n      fromPort,\n      containerNode: callParams.containerNode,\n    });\n\n    // 判断是否可以添加节点\n    if (canAddNode) {\n      const canAdd = canAddNode({ nodeType, containerNode });\n      if (!canAdd) {\n        return;\n      }\n    }\n\n    // 鼠标选择坐标\n    const selectPosition = this.playgroundConfig.getPosFromMouseEvent(selectEvent);\n\n    // 自定义坐标\n    const nodePosition: PositionSchema = callParams.customPosition\n      ? callParams.customPosition({ nodeType, selectPosition })\n      : WorkflowNodePanelUtils.adjustNodePosition({\n          nodeType,\n          position: enableSelectPosition ? selectPosition : panelPosition,\n          fromPort,\n          toPort,\n          containerNode,\n          document: this.document,\n          dragService: this.dragService,\n        });\n\n    // 创建节点\n    const node: WorkflowNodeEntity = this.document.createWorkflowNodeByType(\n      nodeType,\n      nodePosition,\n      nodeJSON ?? ({} as WorkflowNodeJSON),\n      containerNode?.id\n    );\n\n    if (!node) {\n      return;\n    }\n\n    // 后续节点偏移\n    if (enableAutoOffset && fromPort && toPort) {\n      WorkflowNodePanelUtils.subNodesAutoOffset({\n        node,\n        fromPort,\n        toPort,\n        padding: autoOffsetPadding,\n        containerNode,\n        historyService: this.historyService,\n        dragService: this.dragService,\n        linesManager: this.linesManager,\n      });\n    }\n\n    if (!enableBuildLine && !enableDragNode) {\n      return node;\n    }\n\n    // 等待节点渲染\n    await WorkflowNodePanelUtils.waitNodeRender();\n\n    // 重建连线（需先让端口完成渲染）\n    if (enableBuildLine) {\n      WorkflowNodePanelUtils.buildLine({\n        fromPort,\n        node,\n        toPort,\n        linesManager: this.linesManager,\n      });\n    }\n\n    // 开始拖拽节点\n    if (enableDragNode) {\n      this.selectService.selectNode(node);\n      this.dragService.startDragSelectedNodes(selectEvent);\n    }\n\n    return node;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type React from 'react';\n\nimport type { PositionSchema } from '@flowgram.ai/utils';\nimport type { WorkflowNodeEntity, WorkflowPortEntity } from '@flowgram.ai/free-layout-core';\nimport type { WorkflowNodeJSON } from '@flowgram.ai/free-layout-core';\n\nexport interface NodePanelCallParams {\n  /** 唤起节点面板的位置 */\n  panelPosition: PositionSchema;\n  /** 自定义传给面板组件的props */\n  panelProps?: Record<string, any>;\n  /** 指定父节点 */\n  containerNode?: WorkflowNodeEntity;\n  /** 创建节点后需连接的前序端口 */\n  fromPort?: WorkflowPortEntity;\n  /** 创建节点后需连接的后序端口 */\n  toPort?: WorkflowPortEntity;\n  /** 偏移后续节点默认间距 */\n  autoOffsetPadding?: PositionSchema;\n  /** 自定义节点位置 */\n  customPosition?: (params: { nodeType: string; selectPosition: PositionSchema }) => PositionSchema;\n  /** 是否可以添加节点 */\n  canAddNode?: (params: { nodeType: string; containerNode?: WorkflowNodeEntity }) => boolean;\n  /** 创建节点后 */\n  afterAddNode?: (node?: WorkflowNodeEntity) => void;\n  /** 是否创建线条 */\n  enableBuildLine?: boolean;\n  /** 是否使用选中位置作为节点位置 */\n  enableSelectPosition?: boolean;\n  /** 是否偏移后续节点 */\n  enableAutoOffset?: boolean;\n  /** 是否触发节点拖拽 */\n  enableDragNode?: boolean;\n  /** 是否可以添加多个节点 */\n  enableMultiAdd?: boolean;\n}\n\nexport type NodePanelResult =\n  | {\n      nodeType: string;\n      nodeJSON?: WorkflowNodeJSON;\n      selectEvent: React.MouseEvent;\n    }\n  | undefined;\n\nexport interface CallNodePanelParams {\n  onSelect: (params?: NodePanelResult) => void;\n  position: PositionSchema;\n  onClose: () => void;\n  panelProps?: Record<string, any>;\n  enableMultiAdd?: boolean;\n  containerNode?: WorkflowNodeEntity;\n}\n\nexport type CallNodePanel = (params: CallNodePanelParams) => Promise<void>;\n\nexport interface NodePanelRenderProps extends CallNodePanelParams {}\n\nexport type NodePanelRender = React.FC<NodePanelRenderProps>;\n\nexport interface NodePanelLayerOptions {\n  renderer: NodePanelRender;\n}\n\nexport interface NodePanelPluginOptions extends NodePanelLayerOptions {}\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/adjust-node-position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchema } from '@flowgram.ai/utils';\nimport {\n  WorkflowDocument,\n  WorkflowDragService,\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-core';\n\nexport type IAdjustNodePosition = (params: {\n  nodeType: string;\n  position: PositionSchema;\n  document: WorkflowDocument;\n  dragService: WorkflowDragService;\n  fromPort?: WorkflowPortEntity;\n  toPort?: WorkflowPortEntity;\n  containerNode?: WorkflowNodeEntity;\n}) => PositionSchema;\n\n/** 调整节点坐标 */\nexport const adjustNodePosition: IAdjustNodePosition = (params) => {\n  const { nodeType, position, fromPort, toPort, containerNode, document, dragService } = params;\n  const register = document.getNodeRegistry(nodeType);\n  const size = register?.meta?.size;\n  let adjustedPosition = position;\n  if (!size) {\n    adjustedPosition = position;\n  }\n  // 计算坐标偏移\n  else if (fromPort && toPort) {\n    // 输入输出\n    adjustedPosition = {\n      x: position.x,\n      y: position.y - size.height / 2,\n    };\n  } else if (fromPort && !toPort) {\n    // 仅输入\n    if (fromPort.location === 'bottom') {\n      adjustedPosition = {\n        x: position.x,\n        y: position.y,\n      };\n    } else {\n      adjustedPosition = {\n        x: position.x + size.width / 2,\n        y: position.y - size.height / 2,\n      };\n    }\n  } else if (!fromPort && toPort) {\n    // 仅输出\n    adjustedPosition = {\n      x: position.x - size.width / 2,\n      y: position.y - size.height / 2,\n    };\n  } else {\n    adjustedPosition = position;\n  }\n  return dragService.adjustSubNodePosition(nodeType, containerNode, adjustedPosition);\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/build-line.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowNodePortsData,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-core';\n\nexport type IBuildLine = (params: {\n  node: WorkflowNodeEntity;\n  linesManager: WorkflowLinesManager;\n  fromPort?: WorkflowPortEntity;\n  toPort?: WorkflowPortEntity;\n}) => void;\n\n/** 建立连线 */\nexport const buildLine: IBuildLine = (params) => {\n  const { fromPort, node, toPort, linesManager } = params;\n  const portsData = node.getData(WorkflowNodePortsData);\n  if (!portsData) {\n    return;\n  }\n\n  const shouldBuildFromLine = portsData.inputPorts?.length > 0;\n  if (fromPort && shouldBuildFromLine) {\n    const isVertical = fromPort.location === 'bottom';\n    const toTargetPort =\n      portsData.inputPorts.find((port) => (isVertical ? port.location === 'top' : true)) ||\n      portsData.inputPorts[0];\n    linesManager.createLine({\n      from: fromPort.node.id,\n      fromPort: fromPort.portID,\n      to: node.id,\n      toPort: toTargetPort.portID,\n    });\n  }\n  const shouldBuildToLine = portsData.outputPorts?.length > 0;\n  if (toPort && shouldBuildToLine) {\n    const fromTargetPort = portsData.outputPorts[0];\n    linesManager.createLine({\n      from: node.id,\n      fromPort: fromTargetPort.portID,\n      to: toPort.node.id,\n      toPort: toPort.portID,\n    });\n  }\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/get-container-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, WorkflowPortEntity } from '@flowgram.ai/free-layout-core';\n\nimport { isContainer } from './is-container';\n\nexport type IGetContainerNode = (params: {\n  containerNode?: WorkflowNodeEntity;\n  fromPort?: WorkflowPortEntity;\n  toPort?: WorkflowPortEntity;\n}) => WorkflowNodeEntity | undefined;\n\nexport const getContainerNode: IGetContainerNode = (params) => {\n  const { fromPort, containerNode } = params;\n  if (containerNode) {\n    return containerNode;\n  }\n  const fromNode = fromPort?.node;\n  const fromContainer = fromNode?.parent;\n  if (isContainer(fromNode)) {\n    // 子画布内部输入连线\n    return fromNode;\n  }\n  return fromContainer;\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/get-port-box.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\nimport { WorkflowPortEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { isContainer } from './is-container';\n\nexport type IGetPortBox = (port: WorkflowPortEntity, offset?: IPoint) => Rectangle;\n\n/** 获取端口矩形 */\nexport const getPortBox: IGetPortBox = (\n  port: WorkflowPortEntity,\n  offset: IPoint = { x: 0, y: 0 }\n): Rectangle => {\n  const node = port.node;\n  if (isContainer(node)) {\n    // 子画布内部端口需要虚拟节点\n    const { point } = port;\n    if (port.portType === 'input') {\n      return new Rectangle(point.x + offset.x, point.y - 50 + offset.y, 300, 100);\n    }\n    return new Rectangle(point.x - 300, point.y - 50, 300, 100);\n  }\n  const box = node.getData(FlowNodeTransformData).bounds;\n  return box;\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/get-sub-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, WorkflowLinesManager } from '@flowgram.ai/free-layout-core';\n\nimport { isContainer } from './is-container';\n\nexport type IGetSubsequentNodes = (params: {\n  node: WorkflowNodeEntity;\n  linesManager: WorkflowLinesManager;\n}) => WorkflowNodeEntity[];\n\n/** 获取后续节点 */\nexport const getSubsequentNodes: IGetSubsequentNodes = (params) => {\n  const { node, linesManager } = params;\n  if (isContainer(node)) {\n    return [];\n  }\n  const brothers = node.parent?.blocks ?? [];\n  const linkedBrothers = new Set();\n  const linesMap = new Map<string, string[]>();\n  linesManager.getAllAvailableLines().forEach((line) => {\n    if (!linesMap.has(line.from!.id)) {\n      linesMap.set(line.from!.id, []);\n    }\n    if (\n      !line.to?.id ||\n      isContainer(line.to) // 子画布内部成环\n    ) {\n      return;\n    }\n    linesMap.get(line.from!.id)?.push(line.to.id);\n  });\n\n  const bfs = (nodeId: string) => {\n    if (linkedBrothers.has(nodeId)) {\n      return;\n    }\n    linkedBrothers.add(nodeId);\n    const nextNodes = linesMap.get(nodeId) ?? [];\n    nextNodes.forEach(bfs);\n  };\n\n  bfs(node.id);\n\n  const subsequentNodes = brothers.filter((node) => linkedBrothers.has(node.id));\n  return subsequentNodes;\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/greater-or-less.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * 检查 a 是否大于 b，考虑浮点数精度问题\n * @param a 第一个数\n * @param b 第二个数\n * @returns 如果 a 大于 b 则返回 true，否则返回 false\n */\nexport const isGreaterThan = (a: number | undefined, b: number | undefined): boolean => {\n  // 如果任一参数为 undefined，返回 false\n  if (a === undefined || b === undefined) {\n    return false;\n  }\n\n  // 定义一个很小的误差值\n  const EPSILON: number = 0.00001;\n\n  // 检查 a 是否显著大于 b\n  return a - b > EPSILON;\n};\n\n/**\n * 检查 a 是否小于 b，考虑浮点数精度问题\n * @param a 第一个数\n * @param b 第二个数\n * @returns 如果 a 小于 b 则返回 true，否则返回 false\n */\nexport const isLessThan = (a: number | undefined, b: number | undefined): boolean => {\n  // 如果任一参数为 undefined，返回 false\n  if (a === undefined || b === undefined) {\n    return false;\n  }\n\n  // 定义一个很小的误差值\n  const EPSILON: number = 0.00001;\n\n  // 检查 a 是否显著小于 b\n  return b - a > EPSILON;\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IWaitNodeRender, waitNodeRender } from './wait-node-render';\nimport {\n  updateSubSequentNodesPosition,\n  IUpdateSubSequentNodesPosition,\n} from './update-sub-nodes-position';\nimport { subPositionOffset, ISubPositionOffset } from './sub-position-offset';\nimport { subNodesAutoOffset, ISubNodesAutoOffset } from './sub-nodes-auto-offset';\nimport { rectDistance, IRectDistance } from './rect-distance';\nimport { getSubsequentNodes, IGetSubsequentNodes } from './get-sub-nodes';\nimport { getPortBox, IGetPortBox } from './get-port-box';\nimport { getContainerNode, IGetContainerNode } from './get-container-node';\nimport { buildLine, IBuildLine } from './build-line';\nimport { adjustNodePosition, IAdjustNodePosition } from './adjust-node-position';\n\nexport interface IWorkflowNodePanelUtils {\n  adjustNodePosition: IAdjustNodePosition;\n  buildLine: IBuildLine;\n  getPortBox: IGetPortBox;\n  getSubsequentNodes: IGetSubsequentNodes;\n  getContainerNode: IGetContainerNode;\n  rectDistance: IRectDistance;\n  subNodesAutoOffset: ISubNodesAutoOffset;\n  subPositionOffset: ISubPositionOffset;\n  updateSubSequentNodesPosition: IUpdateSubSequentNodesPosition;\n  waitNodeRender: IWaitNodeRender;\n}\n\nexport const WorkflowNodePanelUtils: IWorkflowNodePanelUtils = {\n  adjustNodePosition,\n  buildLine,\n  getPortBox,\n  getSubsequentNodes,\n  getContainerNode,\n  rectDistance,\n  subNodesAutoOffset,\n  subPositionOffset,\n  updateSubSequentNodesPosition,\n  waitNodeRender,\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/is-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeEntity, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';\n\n/** 是否容器节点 */\nexport const isContainer = (node?: WorkflowNodeEntity): boolean =>\n  node?.getNodeMeta<WorkflowNodeMeta>().isContainer ?? false;\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/rect-distance.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Rectangle, IPoint } from '@flowgram.ai/utils';\n\nexport type IRectDistance = (rectA: Rectangle, rectB: Rectangle) => IPoint;\n\n/** 矩形间距 */\nexport const rectDistance: IRectDistance = (rectA, rectB) => {\n  // 计算 x 轴距离\n  const distanceX = Math.abs(Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left));\n  // 计算 y 轴距离\n  const distanceY = Math.abs(Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top));\n  if (Rectangle.intersects(rectA, rectB)) {\n    // 相交距离为负\n    return {\n      x: -distanceX,\n      y: -distanceY,\n    };\n  }\n  return {\n    x: distanceX,\n    y: distanceY,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/sub-nodes-auto-offset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowDragService,\n  WorkflowLinesManager,\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n} from '@flowgram.ai/free-layout-core';\nimport { HistoryService } from '@flowgram.ai/free-history-plugin';\n\nimport { updateSubSequentNodesPosition } from './update-sub-nodes-position';\nimport { subPositionOffset, XYSchema } from './sub-position-offset';\nimport { getSubsequentNodes } from './get-sub-nodes';\n\nexport type ISubNodesAutoOffset = (params: {\n  node: WorkflowNodeEntity;\n  fromPort: WorkflowPortEntity;\n  toPort: WorkflowPortEntity;\n  padding?: XYSchema;\n  linesManager: WorkflowLinesManager;\n  historyService: HistoryService;\n  dragService: WorkflowDragService;\n  containerNode?: WorkflowNodeEntity;\n}) => void;\n\nexport const subNodesAutoOffset: ISubNodesAutoOffset = (params) => {\n  const {\n    node,\n    fromPort,\n    toPort,\n    linesManager,\n    historyService,\n    dragService,\n    containerNode,\n    padding = {\n      x: 100,\n      y: 100,\n    },\n  } = params;\n  const subOffset = subPositionOffset({\n    node,\n    fromPort,\n    toPort,\n    padding,\n  });\n  const subsequentNodes = getSubsequentNodes({\n    node: toPort.node,\n    linesManager,\n  });\n  updateSubSequentNodesPosition({\n    node,\n    subsequentNodes,\n    fromPort,\n    toPort,\n    containerNode,\n    offset: subOffset,\n    historyService,\n    dragService,\n  });\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/sub-position-offset.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, Rectangle } from '@flowgram.ai/utils';\nimport { WorkflowNodeEntity, WorkflowPortEntity } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\n\nimport { rectDistance } from './rect-distance';\nimport { isGreaterThan, isLessThan } from './greater-or-less';\nimport { getPortBox } from './get-port-box';\n\nexport interface XYSchema {\n  x: number;\n  y: number;\n}\n\nexport type ISubPositionOffset = (params: {\n  node: WorkflowNodeEntity;\n  fromPort: WorkflowPortEntity;\n  toPort: WorkflowPortEntity;\n  padding: XYSchema;\n}) => XYSchema | undefined;\n\n/** 后续节点位置偏移 */\nexport const subPositionOffset: ISubPositionOffset = (params) => {\n  const { node, fromPort, toPort, padding } = params;\n\n  const fromBox = getPortBox(fromPort);\n  const toBox = getPortBox(toPort);\n\n  const nodeTrans = node.getData(FlowNodeTransformData);\n  const nodeSize = node.getNodeMeta()?.size ?? {\n    width: nodeTrans.bounds.width,\n    height: nodeTrans.bounds.height,\n  };\n\n  // 最小距离\n  const minDistance: IPoint = {\n    x: nodeSize.width + padding.x,\n    y: nodeSize.height + padding.y,\n  };\n  // from 与 to 的距离\n  const boxDistance = rectDistance(fromBox, toBox);\n\n  // 需要的偏移量\n  const neededOffset: IPoint = {\n    x: isGreaterThan(boxDistance.x, minDistance.x) ? 0 : minDistance.x - boxDistance.x,\n    y: isGreaterThan(boxDistance.y, minDistance.y) ? 0 : minDistance.y - boxDistance.y,\n  };\n\n  // 至少有一个方向满足要求，无需偏移\n  if (neededOffset.x === 0 || neededOffset.y === 0) {\n    return;\n  }\n\n  // 是否存在相交\n  const intersection = {\n    // 这里没有写反，Rectangle内置的算法是反的\n    vertical: Rectangle.intersects(fromBox, toBox, 'horizontal'),\n    horizontal: Rectangle.intersects(fromBox, toBox, 'vertical'),\n  };\n\n  // 初始化偏移量\n  let offsetX: number = 0;\n  let offsetY: number = 0;\n\n  if (!intersection.horizontal) {\n    // 水平不相交，需要加垂直方向的偏移\n    if (isGreaterThan(toBox.center.y, fromBox.center.y)) {\n      // B在A下方\n      offsetY = neededOffset.y;\n    } else if (isLessThan(toBox.center.y, fromBox.center.y)) {\n      // B在A上方\n      offsetY = -neededOffset.y;\n    }\n  }\n\n  if (!intersection.vertical) {\n    // 垂直不相交，需要加水平方向的偏移\n    if (isGreaterThan(toBox.center.x, fromBox.center.x)) {\n      // B在A右侧\n      offsetX = neededOffset.x;\n    } else if (isLessThan(toBox.center.x, fromBox.center.x)) {\n      // B在A左侧\n      offsetX = -neededOffset.x;\n    }\n  }\n\n  return {\n    x: offsetX,\n    y: offsetY,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/update-sub-nodes-position.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPoint, PositionSchema } from '@flowgram.ai/utils';\nimport {\n  WorkflowNodeEntity,\n  WorkflowPortEntity,\n  WorkflowDragService,\n} from '@flowgram.ai/free-layout-core';\nimport { FreeOperationType, HistoryService } from '@flowgram.ai/free-history-plugin';\nimport { TransformData } from '@flowgram.ai/core';\n\nimport { getPortBox } from './get-port-box';\n\nexport type IUpdateSubSequentNodesPosition = (params: {\n  node: WorkflowNodeEntity;\n  subsequentNodes: WorkflowNodeEntity[];\n  fromPort: WorkflowPortEntity;\n  toPort: WorkflowPortEntity;\n  historyService: HistoryService;\n  dragService: WorkflowDragService;\n  containerNode?: WorkflowNodeEntity;\n  offset?: IPoint;\n}) => void;\n\n/** 更新后续节点位置 */\nexport const updateSubSequentNodesPosition: IUpdateSubSequentNodesPosition = (params) => {\n  const {\n    node,\n    subsequentNodes,\n    fromPort,\n    toPort,\n    containerNode,\n    offset,\n    historyService,\n    dragService,\n  } = params;\n  if (!offset || !toPort) {\n    return;\n  }\n  // 更新后续节点位置\n  const subsequentNodesPositions = subsequentNodes.map((node) => {\n    const nodeTrans = node.getData(TransformData);\n    return {\n      x: nodeTrans.position.x,\n      y: nodeTrans.position.y,\n    };\n  });\n  historyService.pushOperation({\n    type: FreeOperationType.dragNodes,\n    value: {\n      ids: subsequentNodes.map((node) => node.id),\n      value: subsequentNodesPositions.map((position) => ({\n        x: position.x + offset.x,\n        y: position.y + offset.y,\n      })),\n      oldValue: subsequentNodesPositions,\n    },\n  });\n  // 新增节点坐标需重新计算\n  const fromBox = getPortBox(fromPort);\n  const toBox = getPortBox(toPort, offset);\n  const nodeTrans = node.getData(TransformData);\n  let nodePos: PositionSchema = {\n    x: (fromBox.center.x + toBox.center.x) / 2,\n    y: (fromBox.y + toBox.y) / 2,\n  };\n  if (containerNode) {\n    nodePos = dragService.adjustSubNodePosition(\n      node.flowNodeType as string,\n      containerNode,\n      nodePos\n    );\n  }\n  historyService.pushOperation({\n    type: FreeOperationType.dragNodes,\n    value: {\n      ids: [node.id],\n      value: [nodePos],\n      oldValue: [\n        {\n          x: nodeTrans.position.x,\n          y: nodeTrans.position.y,\n        },\n      ],\n    },\n  });\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/src/utils/wait-node-render.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { delay } from '@flowgram.ai/free-layout-core';\n\nexport type IWaitNodeRender = () => Promise<void>;\n\nexport const waitNodeRender: IWaitNodeRender = async () => {\n  await delay(20);\n};\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-node-panel-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-snap-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSnapLayerOptions, WorkflowSnapServiceOptions } from './type';\n\nexport const SnapDefaultOptions: WorkflowSnapServiceOptions & WorkflowSnapLayerOptions = {\n  enableEdgeSnapping: true,\n  edgeThreshold: 7,\n  enableGridSnapping: false,\n  gridSize: 20,\n  enableMultiSnapping: true,\n  enableOnlyViewportSnapping: true,\n  edgeColor: '#4E40E5',\n  alignColor: '#4E40E5',\n  edgeLineWidth: 2,\n  alignLineWidth: 2,\n  alignCrossWidth: 16,\n};\n\nexport const Epsilon = 0.00001;\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/create-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport {\n  FreeSnapPluginOptions,\n  WorkflowSnapLayerOptions,\n  WorkflowSnapServiceOptions,\n} from './type';\nimport { WorkflowSnapService } from './service';\nimport { WorkflowSnapLayer } from './layer';\nimport { SnapDefaultOptions } from './constant';\n\nexport const createFreeSnapPlugin = definePluginCreator<FreeSnapPluginOptions>({\n  onBind({ bind }) {\n    bind(WorkflowSnapService).toSelf().inSingletonScope();\n  },\n  onInit(ctx, opts) {\n    const options: WorkflowSnapServiceOptions & WorkflowSnapLayerOptions = {\n      ...SnapDefaultOptions,\n      ...opts,\n    };\n    ctx.playground.registerLayer(WorkflowSnapLayer, options);\n    const snapService = ctx.get<WorkflowSnapService>(WorkflowSnapService);\n    snapService.init(options);\n  },\n  onDispose(ctx) {\n    const snapService = ctx.get<WorkflowSnapService>(WorkflowSnapService);\n    snapService.dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createFreeSnapPlugin } from './create-plugin';\nexport { WorkflowSnapService } from './service';\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { Rectangle } from '@flowgram.ai/utils';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport { Layer } from '@flowgram.ai/core';\n\nimport { isEqual, isGreaterThan, isNumber } from './utils';\nimport { AlignRect, SnapEvent, WorkflowSnapLayerOptions } from './type';\nimport { WorkflowSnapService } from './service';\n\ninterface SnapRenderLine {\n  className: string;\n  sourceNode: string;\n  top: number;\n  left: number;\n  width: number;\n  height: number;\n  dashed?: boolean;\n}\n\n@injectable()\nexport class WorkflowSnapLayer extends Layer<WorkflowSnapLayerOptions> {\n  public static type = 'WorkflowSnapLayer';\n\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  @inject(WorkflowSnapService) private readonly service: WorkflowSnapService;\n\n  public readonly node = domUtils.createDivWithClass(\n    'gedit-playground-layer gedit-flow-snap-layer'\n  );\n\n  private edgeLines: SnapRenderLine[] = [];\n\n  private alignLines: SnapRenderLine[] = [];\n\n  public onReady(): void {\n    this.node.style.zIndex = '9999';\n    this.toDispose.pushAll([\n      this.service.onSnap((event: SnapEvent) => {\n        this.edgeLines = this.calcEdgeLines(event);\n        this.alignLines = this.calcAlignLines(event);\n        this.render();\n      }),\n    ]);\n  }\n\n  public render(): JSX.Element {\n    return (\n      <>\n        {this.alignLines.length > 0 && (\n          <div className=\"workflow-snap-align-lines\">{this.renderAlignLines()}</div>\n        )}\n        {this.edgeLines.length > 0 && (\n          <div className=\"workflow-snap-edge-lines\">{this.renderEdgeLines()}</div>\n        )}\n      </>\n    );\n  }\n\n  public onZoom(scale: number): void {\n    this.node.style.transform = `scale(${scale})`;\n  }\n\n  private renderEdgeLines(): JSX.Element[] {\n    return this.edgeLines.map((renderLine: SnapRenderLine) => {\n      const { className, sourceNode, top, left, width, height, dashed } = renderLine;\n      const id = `${className}-${sourceNode}-${top}-${left}-${width}-${height}`;\n      const isHorizontal = width < height;\n      const border = `${this.options.edgeLineWidth}px ${dashed ? 'dashed' : 'solid'} ${\n        this.options.edgeColor\n      }`;\n      return (\n        <div\n          className={`workflow-snap-edge-line ${className}`}\n          data-testid=\"sdk.workflow.canvas.snap.edgeLine\"\n          data-snap-line-id={id}\n          data-snap-line-source-node={sourceNode}\n          key={id}\n          style={{\n            top,\n            left,\n            width,\n            height,\n            position: 'absolute',\n            borderLeft: isHorizontal ? border : 'none',\n            borderTop: !isHorizontal ? border : 'none',\n          }}\n        />\n      );\n    });\n  }\n\n  private renderAlignLines(): JSX.Element[] {\n    return this.alignLines.map((renderLine: SnapRenderLine) => {\n      const id = `${renderLine.className}-${renderLine.sourceNode}-${renderLine.top}-${renderLine.left}-${renderLine.width}-${renderLine.height}`;\n      const isHorizontal = isGreaterThan(renderLine.width, renderLine.height);\n      const alignLineWidth = this.options.alignLineWidth; // 整体线条粗细\n      const alignCrossWidth = this.options.alignCrossWidth; // 工字形横线的长度\n\n      // 调整渲染位置以保持居中\n      const adjustedTop = isHorizontal ? renderLine.top - alignLineWidth / 2 : renderLine.top;\n      const adjustedLeft = isHorizontal ? renderLine.left : renderLine.left - alignLineWidth / 2;\n\n      return (\n        <div\n          className={`workflow-snap-align-line ${renderLine.className}`}\n          data-testid=\"sdk.workflow.canvas.snap.alignLine\"\n          data-snap-line-id={id}\n          data-snap-line-source-node={renderLine.sourceNode}\n          key={id}\n          style={{\n            position: 'absolute',\n          }}\n        >\n          {/* 主线 */}\n          <div\n            style={{\n              position: 'absolute',\n              top: adjustedTop,\n              left: adjustedLeft,\n              width: isHorizontal ? renderLine.width : alignLineWidth,\n              height: isHorizontal ? alignLineWidth : renderLine.height,\n              backgroundColor: this.options.alignColor,\n            }}\n          />\n          {/* 左端或上端横线 */}\n          <div\n            style={{\n              position: 'absolute',\n              top: isHorizontal\n                ? adjustedTop - (alignCrossWidth - alignLineWidth) / 2\n                : adjustedTop,\n              left: isHorizontal\n                ? adjustedLeft\n                : adjustedLeft - (alignCrossWidth - alignLineWidth) / 2,\n              width: isHorizontal ? alignLineWidth : alignCrossWidth,\n              height: isHorizontal ? alignCrossWidth : alignLineWidth,\n              backgroundColor: this.options.alignColor,\n            }}\n          />\n          {/* 右端或下端横线 */}\n          <div\n            style={{\n              position: 'absolute',\n              top: isHorizontal\n                ? adjustedTop - (alignCrossWidth - alignLineWidth) / 2\n                : adjustedTop + renderLine.height - alignLineWidth,\n              left: isHorizontal\n                ? adjustedLeft + renderLine.width - alignLineWidth\n                : adjustedLeft - (alignCrossWidth - alignLineWidth) / 2,\n              width: isHorizontal ? alignLineWidth : alignCrossWidth,\n              height: isHorizontal ? alignCrossWidth : alignLineWidth,\n              backgroundColor: this.options.alignColor,\n            }}\n          />\n        </div>\n      );\n    });\n  }\n\n  private calcEdgeLines(event: SnapEvent): SnapRenderLine[] {\n    const { alignRects, snapRect, snapEdgeLines } = event;\n    const edgeLines: SnapRenderLine[] = [];\n\n    const topFullAlign = this.directionFullAlign({\n      alignRects: alignRects.top,\n      targetRect: snapRect,\n      isVertical: true,\n    });\n    const bottomFullAlign = this.directionFullAlign({\n      alignRects: alignRects.bottom,\n      targetRect: snapRect,\n      isVertical: true,\n    });\n    const leftFullAlign = this.directionFullAlign({\n      alignRects: alignRects.left,\n      targetRect: snapRect,\n      isVertical: false,\n    });\n    const rightFullAlign = this.directionFullAlign({\n      alignRects: alignRects.right,\n      targetRect: snapRect,\n      isVertical: false,\n    });\n\n    // 处理顶部对齐\n    if (topFullAlign) {\n      const top = topFullAlign.rect.top;\n      const height = bottomFullAlign\n        ? snapRect.bottom - snapRect.height / 2 - top\n        : snapRect.bottom - top;\n      const width = this.options.edgeLineWidth;\n      const lineData = {\n        top,\n        width,\n        height,\n      };\n      edgeLines.push({\n        className: 'edge-full-top-left',\n        sourceNode: topFullAlign.sourceNodeId,\n        left: snapRect.left,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-top-right',\n        sourceNode: topFullAlign.sourceNodeId,\n        left: snapRect.right - 1,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-top-mid',\n        sourceNode: topFullAlign.sourceNodeId,\n        left: snapRect.left + snapRect.width / 2,\n        dashed: true,\n        ...lineData,\n      });\n    }\n\n    // 处理底部对齐\n    if (bottomFullAlign) {\n      const top = topFullAlign ? snapRect.top + snapRect.height / 2 : snapRect.top;\n      const height = bottomFullAlign.rect.bottom - top;\n      const width = this.options.edgeLineWidth;\n      const lineData = {\n        top,\n        width,\n        height,\n      };\n      edgeLines.push({\n        className: 'edge-full-bottom-left',\n        sourceNode: bottomFullAlign.sourceNodeId,\n        left: snapRect.left,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-bottom-right',\n        sourceNode: bottomFullAlign.sourceNodeId,\n        left: snapRect.right - 1,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-bottom-mid',\n        sourceNode: bottomFullAlign.sourceNodeId,\n        left: snapRect.left + snapRect.width / 2,\n        dashed: true,\n        ...lineData,\n      });\n    }\n\n    // 处理左侧对齐\n    if (leftFullAlign) {\n      const left = leftFullAlign.rect.left;\n      const width = rightFullAlign\n        ? snapRect.right - snapRect.width / 2 - left\n        : snapRect.right - left;\n      const height = this.options.edgeLineWidth;\n      const lineData = {\n        left,\n        width,\n        height,\n      };\n      edgeLines.push({\n        className: 'edge-full-left-top',\n        sourceNode: leftFullAlign.sourceNodeId,\n        top: snapRect.top,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-left-bottom',\n        sourceNode: leftFullAlign.sourceNodeId,\n        top: snapRect.bottom - 1,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-left-mid',\n        sourceNode: leftFullAlign.sourceNodeId,\n        top: snapRect.top + snapRect.height / 2,\n        dashed: true,\n        ...lineData,\n      });\n    }\n\n    // 处理右侧对齐\n    if (rightFullAlign) {\n      const left = leftFullAlign ? snapRect.left + snapRect.width / 2 : snapRect.left;\n      const width = rightFullAlign.rect.right - left;\n      const height = this.options.edgeLineWidth;\n      const lineData = {\n        left,\n        width,\n        height,\n      };\n      edgeLines.push({\n        className: 'edge-full-right-top',\n        sourceNode: rightFullAlign.sourceNodeId,\n        top: snapRect.top,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-right-bottom',\n        sourceNode: rightFullAlign.sourceNodeId,\n        top: snapRect.bottom - 1,\n        ...lineData,\n      });\n      edgeLines.push({\n        className: 'edge-full-right-mid',\n        sourceNode: rightFullAlign.sourceNodeId,\n        top: snapRect.top + snapRect.height / 2,\n        dashed: true,\n        ...lineData,\n      });\n    }\n\n    const snappedEdgeLines = Object.entries(snapEdgeLines)\n      .map(([direction, snapLine]) => {\n        if (!snapLine) {\n          return;\n        }\n        const sourceNode = this.document.getNode(snapLine.sourceNodeId);\n        if (!sourceNode) {\n          return;\n        }\n        const nodeRect = sourceNode.getData(FlowNodeTransformData).bounds;\n        if (isNumber(snapLine.x)) {\n          // 垂直\n          const top = Math.min(nodeRect.top, snapRect.top);\n          const bottom = Math.max(nodeRect.bottom, snapRect.bottom);\n          const height = bottom - top;\n          const left = direction === 'right' ? snapLine.x - 1 : snapLine.x;\n          const width = this.options.edgeLineWidth;\n          const isMidX = direction === 'midVertical';\n          const lineData: SnapRenderLine = {\n            className: `edge-snapped-${direction}`,\n            sourceNode: snapLine.sourceNodeId,\n            top,\n            left,\n            width,\n            height,\n            dashed: isMidX,\n          };\n          const onTop = top === nodeRect.top;\n          if (onTop && topFullAlign) {\n            return;\n          }\n          if (!onTop && bottomFullAlign) {\n            return;\n          }\n          return lineData;\n        } else if (isNumber(snapLine.y)) {\n          // 水平\n          const left = Math.min(nodeRect.left, snapRect.left);\n          const right = Math.max(nodeRect.right, snapRect.right);\n          const width = right - left;\n          const top = direction === 'bottom' ? snapLine.y - 1 : snapLine.y;\n          const height = this.options.edgeLineWidth;\n          const isMidY = direction === 'midHorizontal';\n          const lineData: SnapRenderLine = {\n            className: `edge-snapped-${direction}`,\n            sourceNode: snapLine.sourceNodeId,\n            top,\n            left,\n            width,\n            height,\n            dashed: isMidY,\n          };\n          const onLeft = left === nodeRect.left;\n          if (onLeft && leftFullAlign) {\n            return;\n          }\n          if (!onLeft && rightFullAlign) {\n            return;\n          }\n          return lineData;\n        }\n      })\n      .filter(Boolean) as SnapRenderLine[];\n\n    edgeLines.push(...snappedEdgeLines);\n\n    return edgeLines;\n  }\n\n  private directionFullAlign(params: {\n    alignRects: AlignRect[];\n    targetRect: Rectangle;\n    isVertical: boolean;\n  }): AlignRect | undefined {\n    const { alignRects, targetRect, isVertical } = params;\n    let fullAlignIndex = -1;\n    for (let i = 0; i < alignRects.length; i++) {\n      const alignRect = alignRects[i];\n      const prevRect = alignRects[i - 1]?.rect ?? targetRect;\n      const isFullAlign = this.rectFullAlign(alignRect.rect, prevRect, isVertical);\n      if (!isFullAlign) {\n        // 未对齐则中断\n        break; // 用 for 循环 + break 反而比 Array.findIndex 实现可读性更好\n      }\n      fullAlignIndex = i;\n    }\n    const fullAlignRect = alignRects[fullAlignIndex];\n    return fullAlignRect;\n  }\n\n  private rectFullAlign(rectA: Rectangle, rectB: Rectangle, isVertical: boolean): boolean {\n    if (isVertical) {\n      return isEqual(rectA.left, rectB.left) && isEqual(rectA.right, rectB.right);\n    } else {\n      return isEqual(rectA.top, rectB.top) && isEqual(rectA.bottom, rectB.bottom);\n    }\n  }\n\n  private calcAlignLines(event: SnapEvent): SnapRenderLine[] {\n    const { alignRects, alignSpacing, snapRect } = event;\n\n    const topAlignLines = this.calcDirectionAlignLines({\n      alignRects: alignRects.top,\n      targetRect: snapRect,\n      isVertical: true,\n      spacing: alignSpacing.midVertical ?? alignSpacing.top,\n    });\n\n    const bottomAlignLines = this.calcDirectionAlignLines({\n      alignRects: alignRects.bottom,\n      targetRect: snapRect,\n      isVertical: true,\n      spacing: alignSpacing.midVertical ?? alignSpacing.bottom,\n    });\n\n    const leftAlignLines = this.calcDirectionAlignLines({\n      alignRects: alignRects.left,\n      targetRect: snapRect,\n      isVertical: false,\n      spacing: alignSpacing.midHorizontal ?? alignSpacing.left,\n    });\n\n    const rightAlignLines = this.calcDirectionAlignLines({\n      alignRects: alignRects.right,\n      targetRect: snapRect,\n      isVertical: false,\n      spacing: alignSpacing.midHorizontal ?? alignSpacing.right,\n    });\n\n    return [...topAlignLines, ...bottomAlignLines, ...leftAlignLines, ...rightAlignLines];\n  }\n\n  private calcDirectionAlignLines(params: {\n    alignRects: AlignRect[];\n    targetRect: Rectangle;\n    isVertical: boolean;\n    spacing?: number;\n  }) {\n    const { alignRects, targetRect, isVertical, spacing } = params;\n    const alignLines: SnapRenderLine[] = [];\n    if (!spacing) {\n      return alignLines;\n    }\n    for (let i = 0; i < alignRects.length; i++) {\n      const alignRect = alignRects[i];\n      const rect = alignRect.rect;\n      const prevRect = alignRects[i - 1]?.rect ?? targetRect;\n\n      const betweenSpacing = isVertical\n        ? Math.min(Math.abs(prevRect.top - rect.bottom), Math.abs(prevRect.bottom - rect.top))\n        : Math.min(Math.abs(prevRect.left - rect.right), Math.abs(prevRect.right - rect.left));\n      if (!isEqual(betweenSpacing, spacing)) {\n        // 不连续，需要中断\n        break; // 因为要用到 break，所以不能用 Array.map()\n      }\n      if (isVertical) {\n        const centerX = this.calcHorizontalIntersectionCenter(rect, targetRect);\n        alignLines.push({\n          className: 'align-vertical',\n          sourceNode: alignRect.sourceNodeId,\n          top: Math.min(rect.bottom, prevRect.bottom),\n          left: centerX,\n          width: 1,\n          height: spacing,\n        });\n      } else {\n        const centerY = this.calcVerticalIntersectionCenter(rect, targetRect);\n        alignLines.push({\n          className: 'align-horizontal',\n          sourceNode: alignRect.sourceNodeId,\n          top: centerY,\n          left: Math.min(rect.right, prevRect.right),\n          width: spacing,\n          height: 1,\n        });\n      }\n    }\n    return alignLines;\n  }\n\n  private calcVerticalIntersectionCenter(rectA: Rectangle, rectB: Rectangle): number {\n    const top = Math.max(rectA.top, rectB.top);\n    const bottom = Math.min(rectA.bottom, rectB.bottom);\n    return (top + bottom) / 2;\n  }\n\n  private calcHorizontalIntersectionCenter(rectA: Rectangle, rectB: Rectangle): number {\n    const left = Math.max(rectA.left, rectB.left);\n    const right = Math.min(rectA.right, rectB.right);\n    return (left + right) / 2;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Disposable, Emitter, Rectangle } from '@flowgram.ai/utils';\nimport { IPoint } from '@flowgram.ai/utils';\nimport { WorkflowNodeEntity, WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { WorkflowDragService } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { EntityManager, PlaygroundConfigEntity, TransformData } from '@flowgram.ai/core';\n\nimport { isEqual, isGreaterThan, isLessThan, isLessThanOrEqual, isNumber } from './utils';\nimport type {\n  SnapEvent,\n  SnapHorizontalLine,\n  SnapLines,\n  SnapMidHorizontalLine,\n  SnapMidVerticalLine,\n  SnapVerticalLine,\n  WorkflowSnapServiceOptions,\n  AlignRects,\n  AlignRect,\n  AlignSpacing,\n  SnapNodeRect,\n  SnapEdgeLines,\n} from './type';\nimport { SnapDefaultOptions } from './constant';\n\n@injectable()\nexport class WorkflowSnapService {\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  @inject(EntityManager) private readonly entityManager: EntityManager;\n\n  @inject(WorkflowDragService)\n  private readonly dragService: WorkflowDragService;\n\n  @inject(PlaygroundConfigEntity)\n  private readonly playgroundConfig: PlaygroundConfigEntity;\n\n  private disposers: Disposable[] = [];\n\n  private options: WorkflowSnapServiceOptions;\n\n  private snapEmitter = new Emitter<SnapEvent>();\n\n  public readonly onSnap = this.snapEmitter.event;\n\n  private _disabled = false;\n\n  public init(params: Partial<WorkflowSnapServiceOptions> = {}): void {\n    this.options = {\n      ...SnapDefaultOptions,\n      ...params,\n    };\n    this.mountListener();\n  }\n\n  public dispose(): void {\n    this.disposers.forEach((disposer) => disposer.dispose());\n  }\n\n  public get disabled(): boolean {\n    return this._disabled;\n  }\n\n  public disable(): void {\n    if (this._disabled) {\n      return;\n    }\n    this._disabled = true;\n    this.clear();\n  }\n\n  public enable(): void {\n    if (!this._disabled) {\n      return;\n    }\n    this._disabled = false;\n    this.clear();\n  }\n\n  private mountListener(): void {\n    const dragAdjusterDisposer = this.dragService.registerPosAdjuster((params) => {\n      const { selectedNodes: targetNodes, position } = params;\n      const isMultiSnapping = this.options.enableMultiSnapping ? false : targetNodes.length !== 1;\n      if (this._disabled || !this.options.enableEdgeSnapping || isMultiSnapping) {\n        return {\n          x: 0,\n          y: 0,\n        };\n      }\n      return this.snapping({\n        targetNodes,\n        position,\n      });\n    });\n    const dragEndDisposer = this.dragService.onNodesDrag((event) => {\n      if (event.type !== 'onDragEnd' || this._disabled) {\n        return;\n      }\n      if (this.options.enableGridSnapping) {\n        this.gridSnapping({\n          targetNodes: event.nodes,\n          gridSize: this.options.gridSize,\n        });\n      }\n      if (this.options.enableEdgeSnapping) {\n        this.clear();\n      }\n    });\n    this.disposers.push(dragAdjusterDisposer, dragEndDisposer);\n  }\n\n  private snapping(params: { targetNodes: WorkflowNodeEntity[]; position: IPoint }): IPoint {\n    const { targetNodes, position } = params;\n\n    const targetBounds = this.getBounds(targetNodes);\n\n    const targetRect = new Rectangle(\n      position.x,\n      position.y,\n      targetBounds.width,\n      targetBounds.height\n    );\n\n    const snapNodeRects = this.getSnapNodeRects({\n      targetNodes,\n      targetRect,\n    });\n\n    const { alignOffset, alignRects, alignSpacing } = this.calcAlignOffset({\n      targetRect,\n      alignThreshold: this.options.edgeThreshold,\n      snapNodeRects,\n    });\n\n    const { snapOffset, snapEdgeLines } = this.calcSnapOffset({\n      targetRect,\n      edgeThreshold: this.options.edgeThreshold,\n      snapNodeRects,\n    });\n\n    const offset: IPoint = {\n      x: snapOffset.x || alignOffset.x,\n      y: snapOffset.y || alignOffset.y,\n    };\n\n    const snapRect = new Rectangle(\n      position.x + offset.x,\n      position.y + offset.y,\n      targetRect.width,\n      targetRect.height\n    );\n\n    this.snapEmitter.fire({\n      snapRect,\n      snapEdgeLines,\n      alignRects,\n      alignSpacing,\n    });\n\n    return offset;\n  }\n\n  private calcSnapOffset(params: {\n    snapNodeRects: SnapNodeRect[];\n    targetRect: Rectangle;\n    edgeThreshold: number;\n  }): {\n    snapOffset: IPoint;\n    snapEdgeLines: SnapEdgeLines;\n  } {\n    const { snapNodeRects, edgeThreshold, targetRect } = params;\n\n    const snapLines = this.getSnapLines({\n      snapNodeRects,\n    });\n\n    // 找到最近的线条\n    const topYClosestLine = snapLines.horizontal.find((line) =>\n      isLessThanOrEqual(Math.abs(line.y - targetRect.top), edgeThreshold)\n    );\n    const bottomYClosestLine = snapLines.horizontal.find((line) =>\n      isLessThanOrEqual(Math.abs(line.y - targetRect.bottom), edgeThreshold)\n    );\n    const leftXClosestLine = snapLines.vertical.find((line) =>\n      isLessThanOrEqual(Math.abs(line.x - targetRect.left), edgeThreshold)\n    );\n    const rightXClosestLine = snapLines.vertical.find((line) =>\n      isLessThanOrEqual(Math.abs(line.x - targetRect.right), edgeThreshold)\n    );\n    const midYClosestLine = snapLines.midHorizontal.find((line) =>\n      isLessThanOrEqual(Math.abs(line.y - targetRect.center.y), edgeThreshold)\n    );\n    const midXClosestLine = snapLines.midVertical.find((line) =>\n      isLessThanOrEqual(Math.abs(line.x - targetRect.center.x), edgeThreshold)\n    );\n\n    // 计算最近坐标\n    const topYClosest = topYClosestLine?.y;\n    const bottomYClosest = isNumber(bottomYClosestLine?.y)\n      ? bottomYClosestLine!.y - targetRect.height\n      : undefined;\n    const leftXClosest = leftXClosestLine?.x;\n    const rightXClosest = isNumber(rightXClosestLine?.x)\n      ? rightXClosestLine!.x - targetRect.width\n      : undefined;\n    const midYClosest = isNumber(midYClosestLine?.y)\n      ? midYClosestLine!.y - targetRect.height / 2\n      : undefined;\n    const midXClosest = isNumber(midXClosestLine?.x)\n      ? midXClosestLine!.x - targetRect.width / 2\n      : undefined;\n\n    // 吸附后坐标，按优先级取值\n    const snappingPosition = {\n      x: midXClosest ?? leftXClosest ?? rightXClosest ?? targetRect.x,\n      y: midYClosest ?? topYClosest ?? bottomYClosest ?? targetRect.y,\n    };\n\n    // 吸附修正偏移量\n    const snapOffset: IPoint = {\n      x: snappingPosition.x - targetRect.x,\n      y: snappingPosition.y - targetRect.y,\n    };\n\n    // 生效的吸附线条\n    const snapEdgeLines: SnapEdgeLines = {\n      top: isEqual(topYClosest, snappingPosition.y) ? topYClosestLine : undefined,\n      bottom: isEqual(bottomYClosest, snappingPosition.y) ? bottomYClosestLine : undefined,\n      left: isEqual(leftXClosest, snappingPosition.x) ? leftXClosestLine : undefined,\n      right: isEqual(rightXClosest, snappingPosition.x) ? rightXClosestLine : undefined,\n      midVertical: isEqual(midXClosest, snappingPosition.x) ? midXClosestLine : undefined,\n      midHorizontal: isEqual(midYClosest, snappingPosition.y) ? midYClosestLine : undefined,\n    };\n\n    return { snapOffset, snapEdgeLines };\n  }\n\n  private gridSnapping(params: { gridSize: number; targetNodes: WorkflowNodeEntity[] }): void {\n    const { gridSize, targetNodes } = params;\n    const rect = this.getBounds(targetNodes);\n    const snap = (value: number) => Math.round(value / gridSize) * gridSize;\n    const snappedPosition: IPoint = {\n      x: snap(rect.x),\n      y: snap(rect.y),\n    };\n    const offset: IPoint = {\n      x: snappedPosition.x - rect.x,\n      y: snappedPosition.y - rect.y,\n    };\n    targetNodes.forEach((node) =>\n      this.updateNodePositionWithOffset({\n        node,\n        offset,\n      })\n    );\n  }\n\n  private clear() {\n    this.snapEmitter.fire({\n      snapEdgeLines: {},\n      snapRect: Rectangle.EMPTY,\n      alignRects: {\n        top: [],\n        bottom: [],\n        left: [],\n        right: [],\n      },\n      alignSpacing: {},\n    });\n  }\n\n  private getSnapLines(params: { snapNodeRects: SnapNodeRect[] }): SnapLines {\n    const { snapNodeRects } = params;\n    const horizontalLines: SnapHorizontalLine[] = [];\n    const verticalLines: SnapVerticalLine[] = [];\n    const midHorizontalLines: SnapMidHorizontalLine[] = [];\n    const midVerticalLines: SnapMidVerticalLine[] = [];\n\n    snapNodeRects.forEach((snapNodeRect) => {\n      const nodeBounds = snapNodeRect.rect;\n      const nodeCenter = nodeBounds.center;\n      // 边缘横线\n      const top: SnapHorizontalLine = {\n        y: nodeBounds.top,\n        sourceNodeId: snapNodeRect.id,\n      };\n      const bottom: SnapHorizontalLine = {\n        y: nodeBounds.bottom,\n        sourceNodeId: snapNodeRect.id,\n      };\n      // 边缘竖线\n      const left: SnapVerticalLine = {\n        x: nodeBounds.left,\n        sourceNodeId: snapNodeRect.id,\n      };\n      const right: SnapVerticalLine = {\n        x: nodeBounds.right,\n        sourceNodeId: snapNodeRect.id,\n      };\n      // 中间横线\n      const midHorizontal: SnapMidHorizontalLine = {\n        y: nodeCenter.y,\n        sourceNodeId: snapNodeRect.id,\n      };\n      // 中间竖线\n      const midVertical: SnapMidVerticalLine = {\n        x: nodeCenter.x,\n        sourceNodeId: snapNodeRect.id,\n      };\n      horizontalLines.push(top, bottom);\n      verticalLines.push(left, right);\n      midHorizontalLines.push(midHorizontal);\n      midVerticalLines.push(midVertical);\n    });\n\n    return {\n      horizontal: horizontalLines,\n      vertical: verticalLines,\n      midHorizontal: midHorizontalLines,\n      midVertical: midVerticalLines,\n    };\n  }\n\n  private getAvailableNodes(params: {\n    targetNodes: WorkflowNodeEntity[];\n    targetRect: Rectangle;\n  }): WorkflowNodeEntity[] {\n    const { targetNodes, targetRect } = params;\n\n    const targetCenter = targetRect.center;\n    const targetContainerId = targetNodes[0].parent?.id ?? this.document.root.id;\n\n    const disabledNodeIds = targetNodes.map((n) => n.id);\n    disabledNodeIds.push(FlowNodeBaseType.ROOT);\n    const availableNodes = this.nodes\n      .filter((n) => n.parent?.id === targetContainerId)\n      .filter((n) => !disabledNodeIds.includes(n.id))\n      .sort((nodeA, nodeB) => {\n        const nodeCenterA = nodeA.getData(FlowNodeTransformData)!.bounds.center;\n        const nodeCenterB = nodeB.getData(FlowNodeTransformData)!.bounds.center;\n        // 距离越近优先级越高\n        const distanceA =\n          Math.abs(nodeCenterA.x - targetCenter.x) + Math.abs(nodeCenterA.y - targetCenter.y);\n        const distanceB =\n          Math.abs(nodeCenterB.x - targetCenter.x) + Math.abs(nodeCenterB.y - targetCenter.y);\n        return distanceA - distanceB;\n      });\n    return availableNodes;\n  }\n\n  private viewRect(): Rectangle {\n    const { width, height, scrollX, scrollY, zoom } = this.playgroundConfig.config;\n    return new Rectangle(scrollX / zoom, scrollY / zoom, width / zoom, height / zoom);\n  }\n\n  private getSnapNodeRects(params: {\n    targetNodes: WorkflowNodeEntity[];\n    targetRect: Rectangle;\n  }): SnapNodeRect[] {\n    const availableNodes = this.getAvailableNodes(params);\n    const viewRect = this.viewRect();\n    return availableNodes\n      .map((node) => {\n        const snapNodeRect: SnapNodeRect = {\n          id: node.id,\n          rect: node.getData(FlowNodeTransformData).bounds,\n          entity: node,\n        };\n        if (\n          this.options.enableOnlyViewportSnapping &&\n          node.parent?.flowNodeType === FlowNodeBaseType.ROOT &&\n          !Rectangle.intersects(viewRect, snapNodeRect.rect)\n        ) {\n          // 最外层节点仅包含当前可见节点\n          return;\n        }\n        return snapNodeRect;\n      })\n      .filter(Boolean) as SnapNodeRect[];\n  }\n\n  private get nodes(): WorkflowNodeEntity[] {\n    return this.entityManager.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity);\n  }\n\n  private getBounds(nodes: WorkflowNodeEntity[]): Rectangle {\n    if (nodes.length === 0) {\n      return Rectangle.EMPTY;\n    }\n    return Rectangle.enlarge(nodes.map((n) => n.getData(FlowNodeTransformData)!.bounds));\n  }\n\n  private updateNodePositionWithOffset(params: { node: WorkflowNodeEntity; offset: IPoint }): void {\n    const { node, offset } = params;\n    const transform = node.getData(TransformData);\n    const positionWithOffset: IPoint = {\n      x: transform.position.x + offset.x,\n      y: transform.position.y + offset.y,\n    };\n    transform.update({\n      position: positionWithOffset,\n    });\n    this.document.layout.updateAffectedTransform(node);\n  }\n\n  private calcAlignOffset(params: {\n    snapNodeRects: SnapNodeRect[];\n    targetRect: Rectangle;\n    alignThreshold: number;\n  }): {\n    alignOffset: IPoint;\n    alignRects: AlignRects;\n    alignSpacing: AlignSpacing;\n  } {\n    const { snapNodeRects, targetRect, alignThreshold } = params;\n\n    const alignRects = this.getAlignRects({\n      targetRect,\n      snapNodeRects,\n    });\n\n    const alignSpacing = this.calcAlignSpacing({\n      targetRect,\n      alignRects,\n    });\n\n    let topY: number | undefined;\n    let bottomY: number | undefined;\n    let leftX: number | undefined;\n    let rightX: number | undefined;\n    let midY: number | undefined;\n    let midX: number | undefined;\n\n    if (alignSpacing.top) {\n      const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.top;\n      const isAlignTop = isLessThanOrEqual(Math.abs(targetRect.top - topAlignY), alignThreshold);\n      if (isAlignTop) {\n        // 生效\n        topY = topAlignY;\n      } else {\n        // 失效\n        alignSpacing.top = undefined;\n      }\n    }\n    if (alignSpacing.bottom) {\n      const bottomAlignY = alignRects.bottom[0].rect.top - alignSpacing.bottom;\n      const isAlignBottom = isLessThan(Math.abs(targetRect.bottom - bottomAlignY), alignThreshold);\n      if (isAlignBottom) {\n        bottomY = bottomAlignY - targetRect.height;\n      } else {\n        alignSpacing.bottom = undefined;\n      }\n    }\n    if (alignSpacing.left) {\n      const leftAlignX = alignRects.left[0].rect.right + alignSpacing.left;\n      const isAlignLeft = isLessThanOrEqual(Math.abs(targetRect.left - leftAlignX), alignThreshold);\n      if (isAlignLeft) {\n        leftX = leftAlignX;\n      } else {\n        alignSpacing.left = undefined;\n      }\n    }\n    if (alignSpacing.right) {\n      const rightAlignX = alignRects.right[0].rect.left - alignSpacing.right;\n      const isAlignRight = isLessThanOrEqual(\n        Math.abs(targetRect.right - rightAlignX),\n        alignThreshold\n      );\n      if (isAlignRight) {\n        rightX = rightAlignX - targetRect.width;\n      } else {\n        alignSpacing.right = undefined;\n      }\n    }\n    if (alignSpacing.midHorizontal) {\n      const leftAlignX = alignRects.left[0].rect.right + alignSpacing.midHorizontal;\n      const isAlignMidHorizontal = isLessThanOrEqual(\n        Math.abs(targetRect.left - leftAlignX),\n        alignThreshold\n      );\n      if (isAlignMidHorizontal) {\n        midX = leftAlignX;\n      } else {\n        alignSpacing.midHorizontal = undefined;\n      }\n    }\n    if (alignSpacing.midVertical) {\n      const topAlignY = alignRects.top[0].rect.bottom + alignSpacing.midVertical;\n      const isAlignMidVertical = isLessThanOrEqual(\n        Math.abs(targetRect.top - topAlignY),\n        alignThreshold\n      );\n      if (isAlignMidVertical) {\n        midY = topAlignY;\n      } else {\n        alignSpacing.midVertical = undefined;\n      }\n    }\n\n    const alignPosition: IPoint = {\n      x: midX ?? leftX ?? rightX ?? targetRect.x,\n      y: midY ?? topY ?? bottomY ?? targetRect.y,\n    };\n\n    const alignOffset: IPoint = {\n      x: alignPosition.x - targetRect.x,\n      y: alignPosition.y - targetRect.y,\n    };\n\n    return { alignOffset, alignRects, alignSpacing };\n  }\n\n  private calcAlignSpacing(params: {\n    targetRect: Rectangle;\n    alignRects: AlignRects;\n  }): AlignSpacing {\n    const { targetRect, alignRects } = params;\n\n    const topSpacing = this.getDirectionAlignSpacing({\n      rects: alignRects.top,\n      isHorizontal: false,\n    });\n    const bottomSpacing = this.getDirectionAlignSpacing({\n      rects: alignRects.bottom,\n      isHorizontal: false,\n    });\n    const leftSpacing = this.getDirectionAlignSpacing({\n      rects: alignRects.left,\n      isHorizontal: true,\n    });\n    const rightSpacing = this.getDirectionAlignSpacing({\n      rects: alignRects.right,\n      isHorizontal: true,\n    });\n    const midHorizontalSpacing = this.getMidAlignSpacing({\n      rectA: alignRects.left[0]?.rect,\n      rectB: alignRects.right[0]?.rect,\n      targetRect,\n      isHorizontal: true,\n    });\n    const midVerticalSpacing = this.getMidAlignSpacing({\n      rectA: alignRects.top[0]?.rect,\n      rectB: alignRects.bottom[0]?.rect,\n      targetRect,\n      isHorizontal: false,\n    });\n    return {\n      top: topSpacing,\n      bottom: bottomSpacing,\n      left: leftSpacing,\n      right: rightSpacing,\n      midHorizontal: midHorizontalSpacing,\n      midVertical: midVerticalSpacing,\n    };\n  }\n\n  private getAlignRects(params: {\n    targetRect: Rectangle;\n    snapNodeRects: SnapNodeRect[];\n  }): AlignRects {\n    const { targetRect, snapNodeRects } = params;\n\n    const topVerticalRects: AlignRect[] = [];\n    const bottomVerticalRects: AlignRect[] = [];\n    const leftHorizontalRects: AlignRect[] = [];\n    const rightHorizontalRects: AlignRect[] = [];\n\n    snapNodeRects.forEach((snapNodeRect) => {\n      const nodeRect = snapNodeRect.rect;\n      const { isVerticalIntersection, isHorizontalIntersection, isIntersection } =\n        this.intersection(nodeRect, targetRect);\n      if (isIntersection) {\n        // 忽略重叠的节点\n        return;\n      } else if (isVerticalIntersection) {\n        // 垂直重合\n        if (isGreaterThan(nodeRect.center.y, targetRect.center.y)) {\n          // 下方\n          bottomVerticalRects.push({\n            rect: nodeRect,\n            sourceNodeId: snapNodeRect.id,\n          });\n        } else {\n          // 上方\n          topVerticalRects.push({\n            rect: nodeRect,\n            sourceNodeId: snapNodeRect.id,\n          });\n        }\n      } else if (isHorizontalIntersection) {\n        // 水平重合\n        if (isGreaterThan(nodeRect.center.x, targetRect.center.x)) {\n          // 右方\n          rightHorizontalRects.push({\n            rect: nodeRect,\n            sourceNodeId: snapNodeRect.id,\n          });\n        } else {\n          // 左方\n          leftHorizontalRects.push({\n            rect: nodeRect,\n            sourceNodeId: snapNodeRect.id,\n          });\n        }\n      }\n    });\n\n    return {\n      top: topVerticalRects,\n      bottom: bottomVerticalRects,\n      left: leftHorizontalRects,\n      right: rightHorizontalRects,\n    };\n  }\n\n  private getMidAlignSpacing(params: {\n    rectA?: Rectangle;\n    rectB?: Rectangle;\n    targetRect: Rectangle;\n    isHorizontal: boolean;\n  }): number | undefined {\n    const { rectA, rectB, targetRect, isHorizontal } = params;\n    if (!rectA || !rectB) {\n      return;\n    }\n    const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(\n      rectA,\n      rectB\n    );\n    if (isIntersection) {\n      return;\n    }\n    if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) {\n      const betweenSpacing = Math.min(\n        Math.abs(rectA.left - rectB.right),\n        Math.abs(rectA.right - rectB.left)\n      );\n      return (betweenSpacing - targetRect.width) / 2;\n    } else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) {\n      const betweenSpacing = Math.min(\n        Math.abs(rectA.top - rectB.bottom),\n        Math.abs(rectA.bottom - rectB.top)\n      );\n      return (betweenSpacing - targetRect.height) / 2;\n    }\n  }\n\n  private getDirectionAlignSpacing(params: {\n    rects: AlignRect[];\n    isHorizontal: boolean;\n  }): number | undefined {\n    const { rects, isHorizontal } = params;\n    if (rects.length < 2) {\n      // 非法情况\n      return;\n    }\n    const rectA = rects[0].rect;\n    const rectB = rects[1].rect;\n\n    const { isVerticalIntersection, isHorizontalIntersection, isIntersection } = this.intersection(\n      rectA,\n      rectB\n    );\n\n    if (isIntersection) {\n      // 非法情况：重叠\n      return;\n    }\n    if (isHorizontal && isHorizontalIntersection && !isVerticalIntersection) {\n      return Math.min(Math.abs(rectA.left - rectB.right), Math.abs(rectA.right - rectB.left));\n    } else if (!isHorizontal && isVerticalIntersection && !isHorizontalIntersection) {\n      return Math.min(Math.abs(rectA.top - rectB.bottom), Math.abs(rectA.bottom - rectB.top));\n    }\n    return;\n  }\n\n  private intersection(\n    rectA: Rectangle,\n    rectB: Rectangle\n  ): {\n    isHorizontalIntersection: boolean;\n    isVerticalIntersection: boolean;\n    isIntersection: boolean;\n  } {\n    const isVerticalIntersection =\n      isLessThan(rectA.left, rectB.right) && isGreaterThan(rectA.right, rectB.left);\n    const isHorizontalIntersection =\n      isLessThan(rectA.top, rectB.bottom) && isGreaterThan(rectA.bottom, rectB.top);\n    const isIntersection = isHorizontalIntersection && isVerticalIntersection;\n\n    return {\n      isHorizontalIntersection,\n      isVerticalIntersection,\n      isIntersection,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\nimport type { Rectangle } from '@flowgram.ai/utils';\n\nexport interface SnapNodeRect {\n  id: string;\n  rect: Rectangle;\n  entity: WorkflowNodeEntity;\n}\n\nexport interface SnapLine {\n  x?: number;\n  y?: number;\n  sourceNodeId: string;\n}\n\nexport interface SnapHorizontalLine extends SnapLine {\n  y: number;\n}\n\nexport interface SnapVerticalLine extends SnapLine {\n  x: number;\n}\n\nexport interface SnapMidHorizontalLine extends SnapLine {\n  y: number;\n}\n\nexport interface SnapMidVerticalLine extends SnapLine {\n  x: number;\n}\n\nexport interface SnapLines {\n  horizontal: SnapHorizontalLine[];\n  vertical: SnapVerticalLine[];\n  midHorizontal: SnapMidHorizontalLine[];\n  midVertical: SnapMidVerticalLine[];\n}\n\nexport interface SnapEdgeLines {\n  top?: SnapHorizontalLine;\n  bottom?: SnapHorizontalLine;\n  left?: SnapVerticalLine;\n  right?: SnapVerticalLine;\n  midHorizontal?: SnapMidHorizontalLine;\n  midVertical?: SnapMidVerticalLine;\n}\n\nexport interface AlignRect {\n  rect: Rectangle;\n  sourceNodeId: string;\n}\n\nexport interface AlignRects {\n  top: AlignRect[];\n  bottom: AlignRect[];\n  left: AlignRect[];\n  right: AlignRect[];\n}\n\nexport interface AlignSpacing {\n  top?: number;\n  bottom?: number;\n  left?: number;\n  right?: number;\n  midHorizontal?: number;\n  midVertical?: number;\n}\n\nexport interface SnapEvent {\n  snapEdgeLines: SnapEdgeLines;\n  snapRect: Rectangle;\n  alignRects: AlignRects;\n  alignSpacing: AlignSpacing;\n}\n\nexport interface WorkflowSnapServiceOptions {\n  edgeThreshold: number;\n  gridSize: number;\n  enableGridSnapping: boolean;\n  enableEdgeSnapping: boolean;\n  enableMultiSnapping: boolean;\n  enableOnlyViewportSnapping: boolean;\n}\n\nexport interface WorkflowSnapLayerOptions {\n  edgeColor: string;\n  alignColor: string;\n  edgeLineWidth: number;\n  alignLineWidth: number;\n  alignCrossWidth: number;\n}\n\nexport type FreeSnapPluginOptions = Partial<WorkflowSnapServiceOptions & WorkflowSnapLayerOptions>;\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Epsilon } from './constant';\n\n/** 检查浮点数 a 是否等于 b */\nexport const isEqual = (a: number | undefined, b: number | undefined): boolean => {\n  if (a === undefined || b === undefined) {\n    return false;\n  }\n  // 检查 a 和 b 的差的绝对值是否小于 Epsilon\n  return Math.abs(a - b) < Epsilon;\n};\n\n/** 检查浮点数 a 是否小于 b */\nexport const isLessThan = (a: number | undefined, b: number | undefined): boolean => {\n  if (a === undefined || b === undefined) {\n    return false;\n  }\n  // 检查 a 是否显著小于 b\n  return b - a > Epsilon;\n};\n\n/** 检查浮点数 a 是否大于 b */\nexport const isGreaterThan = (a: number | undefined, b: number | undefined): boolean => {\n  if (a === undefined || b === undefined) {\n    return false;\n  }\n  return a - b > Epsilon;\n};\n\n/** 检查浮点数 a 是否小于等于 b */\nexport const isLessThanOrEqual = (a: number | undefined, b: number | undefined): boolean =>\n  isEqual(a, b) || isLessThan(a, b);\n\n/** 检查浮点数 a 是否大于等于 b */\nexport const isGreaterThanOrEqual = (a: number | undefined, b: number | undefined): boolean =>\n  isEqual(a, b) || isGreaterThan(a, b);\n\n/** 检查值是否是数字类型 */\nexport const isNumber = (value: unknown): value is number =>\n  typeof value === 'number' && !isNaN(value);\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-snap-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/__tests__/computing.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { it, expect, beforeEach, describe } from 'vitest';\nimport { interfaces } from 'inversify';\nimport {\n  WorkflowDocument,\n  WorkflowHoverService,\n  WorkflowLineEntity,\n  WorkflowLinesManager,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { StackingComputing } from '../src/stacking-computing';\nimport { StackingContextManager } from '../src/manager';\nimport { createWorkflowContainer, workflowJSON } from './utils.mock';\nimport { IStackingComputing, IStackingContextManager } from './type.mock';\n\nlet container: interfaces.Container;\nlet document: WorkflowDocument;\nlet stackingContextManager: IStackingContextManager;\nlet stackingComputing: IStackingComputing;\n\nbeforeEach(() => {\n  container = createWorkflowContainer();\n  container.bind(StackingContextManager).to(StackingContextManager);\n  document = container.get<WorkflowDocument>(WorkflowDocument);\n  stackingContextManager = container.get<StackingContextManager>(\n    StackingContextManager\n  ) as unknown as IStackingContextManager;\n  document.fromJSON(workflowJSON);\n  stackingContextManager.init();\n  stackingComputing = new StackingComputing() as unknown as IStackingComputing;\n});\n\ndescribe('StackingComputing compute', () => {\n  it('should create instance', () => {\n    const computing = new StackingComputing();\n    expect(computing).not.toBeUndefined();\n  });\n  it('should execute compute', () => {\n    const { nodeLevel, lineLevel, topLevel, maxLevel } = stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    expect(topLevel).toBe(8);\n    expect(maxLevel).toBe(16);\n    expect(Object.fromEntries(nodeLevel)).toEqual({\n      start_0: 1,\n      condition_0: 2,\n      end_0: 3,\n      loop_0: 4,\n      break_0: 6,\n      variable_0: 7,\n    });\n    expect(Object.fromEntries(lineLevel)).toEqual({\n      'start_0_-condition_0_': 0,\n      'start_0_-loop_0_': 0,\n      'condition_0_if-end_0_': 0,\n      'condition_0_else-end_0_': 0,\n      'loop_0_-end_0_': 0,\n      'break_0_-variable_0_': 5,\n    });\n  });\n  it('should put hovered line on max level', () => {\n    const hoverService = container.get<WorkflowHoverService>(WorkflowHoverService);\n    const hoveredLineId = 'start_0_-loop_0_';\n    hoverService.updateHoveredKey(hoveredLineId);\n    const { lineLevel, maxLevel } = stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    const hoveredLineLevel = lineLevel.get(hoveredLineId);\n    expect(hoveredLineLevel).toBe(maxLevel);\n  });\n  it('should put selected line on max level', () => {\n    const entityManager = container.get<EntityManager>(EntityManager);\n    const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);\n    const selectedLineId = 'start_0_-loop_0_';\n    const selectedLine = entityManager.getEntityById<WorkflowLineEntity>(selectedLineId)!;\n    selectService.selection = [selectedLine];\n    const { lineLevel, maxLevel } = stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    const selectedLineLevel = lineLevel.get(selectedLineId);\n    expect(selectedLineLevel).toBe(maxLevel);\n  });\n  it('should put drawing line on max level', () => {\n    const linesManager = container.get<WorkflowLinesManager>(WorkflowLinesManager);\n    const drawingLine = linesManager.createLine({\n      from: 'start_0',\n      drawingTo: { x: 100, y: 100, location: 'left' },\n    })!;\n    const { lineLevel, maxLevel } = stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    const drawingLineLevel = lineLevel.get(drawingLine.id);\n    expect(drawingLineLevel).toBe(maxLevel);\n  });\n  it('should put selected nodes on top level', () => {\n    const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);\n    const selectedNodeId = 'start_0';\n    const selectedNode = document.getNode(selectedNodeId)!;\n    selectService.selectNode(selectedNode);\n    const { nodeLevel, topLevel } = stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    const selectedNodeLevel = nodeLevel.get(selectedNodeId);\n    expect(selectedNodeLevel).toBe(topLevel);\n  });\n});\n\ndescribe('StackingComputing builtin methods', () => {\n  it('computeNodeIndexesMap', () => {\n    stackingComputing.compute({\n      root: document.root,\n      nodes: stackingContextManager.nodes,\n      context: stackingContextManager.context,\n    });\n    const nodeIndexes = stackingComputing.computeNodeIndexesMap(stackingContextManager.nodes);\n    expect(Object.fromEntries(nodeIndexes)).toEqual({\n      root: 0,\n      start_0: 1,\n      condition_0: 2,\n      end_0: 3,\n      loop_0: 4,\n      break_0: 5,\n      variable_0: 6,\n    });\n  });\n  it('computeTopLevel', () => {\n    const topLevel = stackingComputing.computeTopLevel(stackingContextManager.nodes);\n    expect(topLevel).toEqual(8);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/__tests__/manager.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { it, expect, beforeEach, describe, vi } from 'vitest';\nimport { debounce } from 'lodash-es';\nimport { interfaces } from 'inversify';\nimport {\n  delay,\n  WorkflowDocument,\n  WorkflowHoverService,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport {\n  EntityManager,\n  PipelineRegistry,\n  PipelineRenderer,\n  PlaygroundConfigEntity,\n} from '@flowgram.ai/core';\n\nimport { StackingContextManager } from '../src/manager';\nimport { createWorkflowContainer, workflowJSON } from './utils.mock';\nimport { IStackingContextManager } from './type.mock';\n\nlet container: interfaces.Container;\nlet document: WorkflowDocument;\nlet stackingContextManager: IStackingContextManager;\n\nbeforeEach(() => {\n  container = createWorkflowContainer();\n  container.bind(StackingContextManager).to(StackingContextManager);\n  document = container.get<WorkflowDocument>(WorkflowDocument);\n  stackingContextManager = container.get<StackingContextManager>(\n    StackingContextManager\n  ) as unknown as IStackingContextManager;\n  document.fromJSON(workflowJSON);\n});\n\ndescribe('StackingContextManager public methods', () => {\n  it('should create instance', () => {\n    const stackingContextManager = container.get<StackingContextManager>(StackingContextManager);\n    expect(stackingContextManager.node).toMatchInlineSnapshot(`\n    <div\n      class=\"gedit-playground-layer gedit-flow-render-layer\"\n    />\n    `);\n    expect(stackingContextManager).not.toBeUndefined();\n  });\n  it('should execute init', () => {\n    stackingContextManager.init();\n    const pipelineRenderer = container.get<PipelineRenderer>(PipelineRenderer);\n    expect(pipelineRenderer.node).toMatchInlineSnapshot(\n      `\n    <div\n      class=\"gedit-playground-pipeline\"\n    >\n      <div\n        class=\"gedit-playground-layer gedit-flow-render-layer\"\n      />\n    </div>\n      `\n    );\n    expect(stackingContextManager.disposers).toHaveLength(4);\n  });\n  it('should execute ready', () => {\n    stackingContextManager.compute = vi.fn();\n    stackingContextManager.ready();\n    expect(stackingContextManager.compute).toBeCalled();\n  });\n  it('should dispose', () => {\n    expect(stackingContextManager.disposers).toHaveLength(0);\n    stackingContextManager.init();\n    expect(stackingContextManager.disposers).toHaveLength(4);\n    const mockDispose = { dispose: vi.fn() };\n    stackingContextManager.disposers.push(mockDispose);\n    stackingContextManager.dispose();\n    expect(mockDispose.dispose).toBeCalled();\n  });\n});\n\ndescribe('StackingContextManager private methods', () => {\n  it('should compute with debounce', async () => {\n    const compute = vi.fn();\n    vi.spyOn(stackingContextManager, 'compute').mockImplementation(debounce(compute, 10));\n    stackingContextManager.compute();\n    await delay(1);\n    stackingContextManager.compute();\n    await delay(1);\n    stackingContextManager.compute();\n    await delay(1);\n    stackingContextManager.compute();\n    expect(compute).toBeCalledTimes(0);\n    await delay(20);\n    expect(compute).toBeCalledTimes(1);\n  });\n\n  it('should get nodes and lines', async () => {\n    const nodeIds = stackingContextManager.nodes.map((n) => n.id);\n    const lineIds = stackingContextManager.lines.map((l) => l.id);\n    expect(nodeIds).toEqual([\n      'root',\n      'start_0',\n      'condition_0',\n      'end_0',\n      'loop_0',\n      'break_0',\n      'variable_0',\n    ]);\n    expect(lineIds).toEqual([\n      'break_0_-variable_0_',\n      'start_0_-condition_0_',\n      'condition_0_if-end_0_',\n      'condition_0_else-end_0_',\n      'loop_0_-end_0_',\n      'start_0_-loop_0_',\n    ]);\n  });\n\n  it('should generate context', async () => {\n    const hoverService = container.get<WorkflowHoverService>(WorkflowHoverService);\n    const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);\n    expect(stackingContextManager.context).toStrictEqual({\n      hoveredEntityID: undefined,\n      selectedIDs: new Set(),\n      selectedNodes: [],\n      sortNodes: stackingContextManager.options.sortNodes,\n    });\n    hoverService.updateHoveredKey('start_0');\n    const breakNode = document.getNode('break_0')!;\n    const variableNode = document.getNode('variable_0')!;\n    selectService.selection = [breakNode, variableNode];\n    expect(stackingContextManager.context.hoveredEntityID).toEqual('start_0');\n    expect(stackingContextManager.context.selectedIDs).toEqual(new Set(['break_0', 'variable_0']));\n  });\n\n  it('should callback compute when onZoom trigger', () => {\n    const entityManager = container.get<EntityManager>(EntityManager);\n    const pipelineRegistry = container.get<PipelineRegistry>(PipelineRegistry);\n    const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {});\n    const playgroundConfig =\n      entityManager.getEntity<PlaygroundConfigEntity>(PlaygroundConfigEntity)!;\n    pipelineRegistry.ready();\n    stackingContextManager.mountListener();\n    playgroundConfig.updateConfig({\n      zoom: 1.5,\n    });\n    expect(stackingContextManager.node.style.transform).toBe('scale(1.5)');\n    playgroundConfig.updateConfig({\n      zoom: 2,\n    });\n    expect(stackingContextManager.node.style.transform).toBe('scale(2)');\n    playgroundConfig.updateConfig({\n      zoom: 1,\n    });\n    expect(stackingContextManager.node.style.transform).toBe('scale(1)');\n    expect(compute).toBeCalledTimes(3);\n  });\n\n  it('should callback compute when onHover trigger', () => {\n    const hoverService = container.get<WorkflowHoverService>(WorkflowHoverService);\n    const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {});\n    stackingContextManager.mountListener();\n    hoverService.updateHoveredKey('start_0');\n    hoverService.updateHoveredKey('end_0');\n    expect(compute).toBeCalledTimes(2);\n  });\n\n  it('should callback compute when onEntityChange trigger', () => {\n    const entityManager = container.get<EntityManager>(EntityManager);\n    const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {});\n    const node = document.getNode('start_0')!;\n    stackingContextManager.mountListener();\n    entityManager.fireEntityChanged(node);\n    expect(compute).toBeCalledTimes(1);\n  });\n\n  it('should callback compute when onSelect trigger', () => {\n    const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);\n    const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {});\n    stackingContextManager.mountListener();\n    const breakNode = document.getNode('break_0')!;\n    const variableNode = document.getNode('variable_0')!;\n    selectService.selectNode(breakNode);\n    selectService.selectNode(variableNode);\n    expect(compute).toBeCalledTimes(2);\n  });\n\n  it('should mount listeners', () => {\n    const hoverService = container.get<WorkflowHoverService>(WorkflowHoverService);\n    const selectService = container.get<WorkflowSelectService>(WorkflowSelectService);\n    const compute = vi.spyOn(stackingContextManager, 'compute').mockImplementation(() => {});\n    stackingContextManager.mountListener();\n    // onHover\n    hoverService.updateHoveredKey('start_0');\n    hoverService.updateHoveredKey('end_0');\n    expect(compute).toBeCalledTimes(2);\n    compute.mockReset();\n    // select callback\n    const breakNode = document.getNode('break_0')!;\n    const variableNode = document.getNode('variable_0')!;\n    selectService.selectNode(breakNode);\n    selectService.selectNode(variableNode);\n    expect(compute).toBeCalledTimes(2);\n  });\n\n  it('should trigger compute', async () => {\n    stackingContextManager.ready();\n    await delay(200);\n    const node = document.getNode('loop_0')!;\n    const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);\n    const element = nodeRenderData.node;\n    expect(element.style.zIndex).toBe('12');\n  });\n});\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/__tests__/type.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { Disposable } from '@flowgram.ai/utils';\nimport type {\n  WorkflowDocument,\n  WorkflowHoverService,\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport type { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core';\n\nimport type { StackContextManagerOptions, StackingContext } from '../src/type';\n\n/** mock类型便于测试内部方法 */\nexport interface IStackingContextManager {\n  document: WorkflowDocument;\n  entityManager: EntityManager;\n  pipelineRenderer: PipelineRenderer;\n  pipelineRegistry: PipelineRegistry;\n  hoverService: WorkflowHoverService;\n  selectService: WorkflowSelectService;\n  node: HTMLDivElement;\n  disposers: Disposable[];\n  options: StackContextManagerOptions;\n  init(): void;\n  ready(): void;\n  dispose(): void;\n  compute(): void;\n  _compute(): void;\n  stackingCompute(): void;\n  nodes: WorkflowNodeEntity[];\n  lines: WorkflowLineEntity[];\n  context: StackingContext;\n  mountListener(): void;\n  onZoom(): Disposable;\n  onHover(): Disposable;\n  onEntityChange(): Disposable;\n  onSelect(): Disposable;\n}\n\nexport interface IStackingComputing {\n  currentLevel: number;\n  topLevel: number;\n  maxLevel: number;\n  nodeIndexes: Map<string, number>;\n  nodeLevel: Map<string, number>;\n  lineLevel: Map<string, number>;\n  context: StackingContext;\n  compute(params: {\n    root: WorkflowNodeEntity;\n    nodes: WorkflowNodeEntity[];\n    context: StackingContext;\n  }): {\n    nodeLevel: Map<string, number>;\n    lineLevel: Map<string, number>;\n    topLevel: number;\n    maxLevel: number;\n  };\n  clearCache(): void;\n  computeNodeIndexesMap(nodes: WorkflowNodeEntity[]): Map<string, number>;\n  computeTopLevel(nodes: WorkflowNodeEntity[]): number;\n  layerHandler(nodes: WorkflowNodeEntity[], pinTop?: boolean): void;\n  getLevel(pinTop: boolean): number;\n  levelIncrease(): void;\n}\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/__tests__/utils.mock.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\nimport {\n  WorkflowJSON,\n  WorkflowDocumentContainerModule,\n  // WorkflowLinesManager,\n} from '@flowgram.ai/free-layout-core';\nimport { FlowDocumentContainerModule } from '@flowgram.ai/document';\nimport { PlaygroundMockTools } from '@flowgram.ai/core';\n\nexport function createWorkflowContainer(): interfaces.Container {\n  const container = PlaygroundMockTools.createContainer([\n    FlowDocumentContainerModule,\n    WorkflowDocumentContainerModule,\n  ]);\n  // const linesManager = container.get(WorkflowLinesManager);\n  // linesManager.registerContribution(WorkflowSimpleLineContribution);\n  // linesManager.switchLineType(WorkflowSimpleLineContribution.type);\n  return container;\n}\n\nexport const workflowJSON: WorkflowJSON = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: { x: 0, y: 0 },\n        testRun: {\n          showError: undefined,\n        },\n      },\n      data: undefined,\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: { x: 400, y: 0 },\n        testRun: {\n          showError: undefined,\n        },\n      },\n      data: undefined,\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: { x: 800, y: 0 },\n        testRun: {\n          showError: undefined,\n        },\n      },\n      data: undefined,\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: { x: 1200, y: 0 },\n        testRun: {\n          showError: undefined,\n        },\n      },\n      data: undefined,\n      blocks: [\n        {\n          id: 'break_0',\n          type: 'break',\n          meta: {\n            position: { x: 0, y: 0 },\n            testRun: {\n              showError: undefined,\n            },\n          },\n          data: undefined,\n        },\n        {\n          id: 'variable_0',\n          type: 'variable',\n          meta: {\n            position: { x: 400, y: 0 },\n            testRun: {\n              showError: undefined,\n            },\n          },\n          data: undefined,\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'break_0',\n          targetNodeID: 'variable_0',\n        },\n      ],\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'if',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      sourcePortID: 'else',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'loop_0',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'loop_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/free-stack-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@types/styled-components\": \"^5\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"styled-components\": \"^5\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\",\n    \"styled-components\": \">=5\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// 起始 z-index\nexport const BASE_Z_INDEX = 8;\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/create-free-stack-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { FreeStackPluginOptions } from './type';\nimport { StackingContextManager } from './manager';\n\nexport const createFreeStackPlugin = definePluginCreator<FreeStackPluginOptions>({\n  singleton: true,\n  onBind({ bind }) {\n    bind(StackingContextManager).toSelf().inSingletonScope();\n  },\n  onInit(ctx, options) {\n    const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);\n    stackingContextManager.init(options);\n  },\n  onReady(ctx) {\n    const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);\n    stackingContextManager.ready();\n  },\n  onDispose(ctx) {\n    const stackingContextManager = ctx.get<StackingContextManager>(StackingContextManager);\n    stackingContextManager.dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-free-stack-plugin';\nexport * from './manager';\nexport * from './constant';\nexport * from './stacking-computing';\nexport * from './type';\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { debounce } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport { Disposable } from '@flowgram.ai/utils';\nimport {\n  WorkflowHoverService,\n  WorkflowNodeEntity,\n  WorkflowSelectService,\n} from '@flowgram.ai/free-layout-core';\nimport { WorkflowLineEntity } from '@flowgram.ai/free-layout-core';\nimport { WorkflowDocument } from '@flowgram.ai/free-layout-core';\nimport { FlowNodeRenderData } from '@flowgram.ai/document';\nimport { EntityManager, PipelineRegistry, PipelineRenderer } from '@flowgram.ai/core';\n\nimport type { StackContextManagerOptions, StackingContext } from './type';\nimport { StackingComputing } from './stacking-computing';\nimport { BASE_Z_INDEX } from './constant';\n\n@injectable()\nexport class StackingContextManager {\n  @inject(WorkflowDocument) private readonly document: WorkflowDocument;\n\n  @inject(EntityManager) private readonly entityManager: EntityManager;\n\n  @inject(PipelineRenderer)\n  private readonly pipelineRenderer: PipelineRenderer;\n\n  @inject(PipelineRegistry)\n  private readonly pipelineRegistry: PipelineRegistry;\n\n  @inject(WorkflowHoverService)\n  private readonly hoverService: WorkflowHoverService;\n\n  @inject(WorkflowSelectService)\n  private readonly selectService: WorkflowSelectService;\n\n  public readonly node = domUtils.createDivWithClass(\n    'gedit-playground-layer gedit-flow-render-layer'\n  );\n\n  private options: StackContextManagerOptions = {\n    sortNodes: (nodes: WorkflowNodeEntity[]) => nodes,\n  };\n\n  private disposers: Disposable[] = [];\n\n  constructor() {}\n\n  public init(options: Partial<StackContextManagerOptions> = {}): void {\n    this.options = { ...this.options, ...options };\n    this.pipelineRenderer.node.appendChild(this.node);\n    this.mountListener();\n  }\n\n  public ready(): void {\n    this.compute();\n  }\n\n  public dispose(): void {\n    this.disposers.forEach((disposer) => disposer.dispose());\n  }\n\n  /**\n   * 触发计算\n   * 10ms内仅计算一次\n   */\n  private compute = debounce(this._compute, 10);\n\n  private _compute(): void {\n    const context = this.context;\n    const stackingComputing = new StackingComputing();\n    const { nodeLevel, lineLevel } = stackingComputing.compute({\n      root: this.document.root,\n      nodes: this.nodes,\n      context,\n    });\n    this.nodes.forEach((node) => {\n      const level = nodeLevel.get(node.id);\n      const nodeRenderData = node.getData<FlowNodeRenderData>(FlowNodeRenderData);\n      const element = nodeRenderData.node;\n      element.style.position = 'absolute';\n      if (level === undefined) {\n        nodeRenderData.stackIndex = 0;\n        element.style.zIndex = 'auto';\n        return;\n      }\n      nodeRenderData.stackIndex = level;\n      const zIndex = BASE_Z_INDEX + level;\n      element.style.zIndex = String(zIndex);\n    });\n    this.lines.forEach((line) => {\n      const level = lineLevel.get(line.id);\n      const element = line.node;\n      element.style.position = 'absolute';\n      if (level === undefined) {\n        line.stackIndex = 0;\n        element.style.zIndex = 'auto';\n        return;\n      }\n      line.stackIndex = level;\n      const zIndex = BASE_Z_INDEX + level;\n      element.style.zIndex = String(zIndex);\n    });\n  }\n\n  private get nodes(): WorkflowNodeEntity[] {\n    return this.entityManager.getEntities<WorkflowNodeEntity>(WorkflowNodeEntity);\n  }\n\n  private get lines(): WorkflowLineEntity[] {\n    return this.entityManager.getEntities<WorkflowLineEntity>(WorkflowLineEntity);\n  }\n\n  private get context(): StackingContext {\n    return {\n      hoveredEntityID: this.hoverService.someHovered?.id,\n      selectedNodes: this.selectService.selectedNodes,\n      selectedIDs: new Set(this.selectService.selection.map((entity) => entity.id)),\n      sortNodes: this.options.sortNodes,\n    };\n  }\n\n  private mountListener(): void {\n    const entityChangeDisposer = this.onEntityChange();\n    const zoomDisposer = this.onZoom();\n    const hoverDisposer = this.onHover();\n    const selectDisposer = this.onSelect();\n    this.disposers = [entityChangeDisposer, zoomDisposer, hoverDisposer, selectDisposer];\n  }\n\n  private onZoom(): Disposable {\n    return this.pipelineRegistry.onZoom((scale: number) => {\n      this.node.style.transform = `scale(${scale})`;\n    });\n  }\n\n  private onHover(): Disposable {\n    return this.hoverService.onHoveredChange(() => {\n      this.compute();\n    });\n  }\n\n  private onEntityChange(): Disposable {\n    return this.entityManager.onEntityChange(() => {\n      this.compute();\n    });\n  }\n\n  private onSelect(): Disposable {\n    return this.selectService.onSelectionChanged(() => {\n      this.compute();\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/stacking-computing.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowLineEntity,\n  WorkflowNodeEntity,\n  WorkflowNodeLinesData,\n} from '@flowgram.ai/free-layout-core';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\n\nimport type { StackingContext } from './type';\n\nexport class StackingComputing {\n  private currentLevel: number;\n\n  private topLevel: number;\n\n  private maxLevel: number;\n\n  private nodeIndexes: Map<string, number>;\n\n  private nodeLevel: Map<string, number>;\n\n  private lineLevel: Map<string, number>;\n\n  private selectedNodeParentSet: Set<string>;\n\n  private context: StackingContext;\n\n  public compute(params: {\n    root: WorkflowNodeEntity;\n    nodes: WorkflowNodeEntity[];\n    context: StackingContext;\n  }): {\n    /** 节点层级 */\n    nodeLevel: Map<string, number>;\n    /** 线条层级 */\n    lineLevel: Map<string, number>;\n    /** 正常渲染的最高层级 */\n    topLevel: number;\n    /** 选中计算叠加后可能计算出的最高层级 */\n    maxLevel: number;\n  } {\n    this.clearCache();\n    const { root, nodes, context } = params;\n    this.context = context;\n    this.nodeIndexes = this.computeNodeIndexesMap(nodes);\n    this.selectedNodeParentSet = this.computeSelectedNodeParentSet(nodes);\n    this.topLevel = this.computeTopLevel(nodes);\n    this.maxLevel = this.topLevel * 2;\n    this.layerHandler(root.blocks);\n    return {\n      nodeLevel: this.nodeLevel,\n      lineLevel: this.lineLevel,\n      topLevel: this.topLevel,\n      maxLevel: this.maxLevel,\n    };\n  }\n\n  private clearCache(): void {\n    this.currentLevel = 0;\n    this.topLevel = 0;\n    this.maxLevel = 0;\n    this.nodeIndexes = new Map();\n    this.nodeLevel = new Map();\n    this.lineLevel = new Map();\n  }\n\n  private computeNodeIndexesMap(nodes: WorkflowNodeEntity[]): Map<string, number> {\n    const nodeIndexMap = new Map<string, number>();\n    // 默认按照创建节点顺序排序\n    nodes.forEach((node, index) => {\n      nodeIndexMap.set(node.id, index);\n    });\n    return nodeIndexMap;\n  }\n\n  private computeSelectedNodeParentSet(nodes: WorkflowNodeEntity[]): Set<string> {\n    const selectedNodeParents = this.context.selectedNodes.flatMap((node) =>\n      this.getNodeParents(node)\n    );\n    return new Set(selectedNodeParents.map((node) => node.id));\n  }\n\n  private getNodeParents(node: WorkflowNodeEntity): WorkflowNodeEntity[] {\n    const nodes: WorkflowNodeEntity[] = [];\n    let currentNode: WorkflowNodeEntity | undefined = node;\n    while (currentNode && currentNode.flowNodeType !== FlowNodeBaseType.ROOT) {\n      nodes.unshift(currentNode);\n      currentNode = currentNode.parent;\n    }\n    return nodes;\n  }\n\n  private computeTopLevel(nodes: WorkflowNodeEntity[]): number {\n    const nodesWithoutRoot = nodes.filter((node) => node.id !== FlowNodeBaseType.ROOT);\n    const nodeHasChildren = nodesWithoutRoot.reduce((count, node) => {\n      if (node.blocks.length > 0) {\n        return count + 1;\n      } else {\n        return count;\n      }\n    }, 0);\n    // 最高层数 = 节点个数 + 容器节点个数（线条单独占一层） + 抬高一层\n    return nodesWithoutRoot.length + nodeHasChildren + 1;\n  }\n\n  private layerHandler(layerNodes: WorkflowNodeEntity[], pinTop: boolean = false): void {\n    const nodes = this.sortNodes(layerNodes);\n    const lines = this.getNodesAllLines(nodes);\n\n    // 线条统一设为当前层级最低\n    lines.forEach((line) => {\n      if (\n        line.isDrawing || // 正在绘制\n        this.context.hoveredEntityID === line.id || // hover\n        this.context.selectedIDs.has(line.id) // 选中\n      ) {\n        // 线条置顶条件：正在绘制 / hover / 选中\n        this.lineLevel.set(line.id, this.maxLevel);\n      } else {\n        this.lineLevel.set(line.id, this.getLevel(pinTop));\n      }\n    });\n    this.levelIncrease();\n    nodes.forEach((node) => {\n      const selected = this.context.selectedIDs.has(node.id);\n      if (selected) {\n        // 节点置顶条件：选中\n        this.nodeLevel.set(node.id, this.topLevel);\n      } else {\n        this.nodeLevel.set(node.id, this.getLevel(pinTop));\n      }\n      // 节点层级逐层增高\n      this.levelIncrease();\n      if (node.blocks.length > 0) {\n        // 子节点层级需低于后续兄弟节点，因此需要先进行计算\n        this.layerHandler(node.blocks, pinTop || selected);\n      }\n    });\n  }\n\n  private sortNodes(nodes: WorkflowNodeEntity[]): WorkflowNodeEntity[] {\n    const baseSortNodes = nodes.sort((a, b) => {\n      const aIndex = this.nodeIndexes.get(a.id);\n      const bIndex = this.nodeIndexes.get(b.id);\n      if (aIndex === undefined || bIndex === undefined) {\n        return 0;\n      }\n      return aIndex - bIndex;\n    });\n    const contextSortNodes = this.context.sortNodes(baseSortNodes);\n    return contextSortNodes.sort((a, b) => {\n      const aIsSelectedParent = this.selectedNodeParentSet.has(a.id);\n      const bIsSelectedParent = this.selectedNodeParentSet.has(b.id);\n      if (aIsSelectedParent && !bIsSelectedParent) {\n        return 1;\n      } else if (!aIsSelectedParent && bIsSelectedParent) {\n        return -1;\n      } else {\n        return 0;\n      }\n    });\n  }\n\n  private getNodesAllLines(nodes: WorkflowNodeEntity[]): WorkflowLineEntity[] {\n    const lines = nodes\n      .map((node) => {\n        const linesData = node.getData<WorkflowNodeLinesData>(WorkflowNodeLinesData);\n        const outputLines = linesData.outputLines.filter(Boolean);\n        const inputLines = linesData.inputLines.filter(Boolean);\n        return [...outputLines, ...inputLines];\n      })\n      .flat();\n\n    // 过滤出未计算层级的线条，以及高度优先（需要覆盖计算）的线条\n    const filteredLines = lines.filter(\n      (line) => this.lineLevel.get(line.id) === undefined || this.isHigherFirstLine(line)\n    );\n\n    return filteredLines;\n  }\n\n  private isHigherFirstLine(line: WorkflowLineEntity): boolean {\n    // 父子相连的线条，需要作为高度优先的线条，避免线条不可见\n    return line.to?.parent === line.from || line.from?.parent === line.to;\n  }\n\n  private getLevel(pinTop: boolean): number {\n    if (pinTop) {\n      return this.topLevel + this.currentLevel;\n    }\n    return this.currentLevel;\n  }\n\n  private levelIncrease(): void {\n    this.currentLevel += 1;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeEntity } from '@flowgram.ai/free-layout-core';\n\nexport interface StackingContext {\n  hoveredEntityID?: string;\n  selectedNodes: WorkflowNodeEntity[];\n  selectedIDs: Set<string>;\n  sortNodes: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n}\n\nexport interface StackContextManagerOptions {\n  sortNodes: (nodes: WorkflowNodeEntity[]) => WorkflowNodeEntity[];\n}\n\nexport type FreeStackPluginOptions = Partial<StackContextManagerOptions>;\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/free-stack-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/group-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/group-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/group-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/components/group-box.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport { useEffect, type CSSProperties } from 'react';\nimport React from 'react';\n\nimport type { Rectangle } from '@flowgram.ai/utils';\nimport { FlowGroupController } from '@flowgram.ai/document';\n\nimport { IGroupBox } from '../type';\nimport { useHover } from './hooks';\n\nexport const GroupBox: IGroupBox = (props) => {\n  const { groupNode } = props;\n  const groupController = FlowGroupController.create(groupNode)!;\n  const bounds: Rectangle = groupController.bounds;\n  const { hover, ref } = useHover();\n\n  const positionStyle: CSSProperties = {\n    position: 'absolute',\n    left: bounds.left,\n    top: bounds.top,\n    width: bounds.width,\n    height: bounds.height,\n  };\n\n  const defaultBackgroundStyle: CSSProperties = {\n    borderRadius: 10,\n    zIndex: -1,\n    outline: `${hover ? 2 : 1}px solid rgb(97, 69, 211)`,\n    backgroundColor: 'rgb(236 233 247)',\n  };\n\n  const backgroundStyle = props.backgroundStyle\n    ? props.backgroundStyle(groupController)\n    : defaultBackgroundStyle;\n\n  useEffect(() => {\n    groupController.hovered = hover;\n  }, [hover]);\n\n  if (!groupController || groupController.collapsed) {\n    return <></>;\n  }\n\n  return (\n    <div className=\"gedit-group-box\" data-group-id={groupNode.id}>\n      <div\n        className=\"gedit-group-background\"\n        ref={ref}\n        style={{\n          ...positionStyle,\n          ...backgroundStyle,\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/components/group-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\nimport React, { useCallback, useEffect, useState } from 'react';\n\nimport { FlowGroupController, FlowNodeEntity, FlowNodeRenderData } from '@flowgram.ai/document';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { useEntityFromContext, useService } from '@flowgram.ai/core';\nimport { delay, Rectangle } from '@flowgram.ai/utils';\n\nimport { IGroupRender } from '../type';\n\nfunction useCurrentDomNode(): HTMLDivElement {\n  const entity = useEntityFromContext<FlowNodeEntity>();\n  const renderData = entity.getData<FlowNodeRenderData>(FlowNodeRenderData);\n  return renderData.node;\n}\n\nexport const GroupRender: IGroupRender = props => {\n  const { groupNode, GroupNode, GroupBoxHeader } = props;\n  const container = useCurrentDomNode();\n  const document = useService<FlowDocument>(FlowDocument);\n  const groupController = FlowGroupController.create(groupNode);\n\n  const [key, setKey] = useState(0);\n  const [rendering, setRendering] = useState(true);\n  const [collapsedCache, setCollapsedCache] = useState(groupController?.collapsed ?? false);\n\n  const rerender = useCallback(async () => {\n    setRendering(true);\n    setKey(key + 1);\n    // 边框bounds计算会有延迟\n    await delay(50);\n    setKey(key + 1);\n    setRendering(false);\n  }, [key]);\n\n  // 监听 collapsed 变化触发重渲染\n  useEffect(() => {\n    const disposer = document.renderTree.onTreeChange(() => {\n      if (groupController?.collapsed !== collapsedCache) {\n        setCollapsedCache(groupController?.collapsed ?? false);\n        rerender();\n      }\n    });\n    return () => {\n      disposer.dispose();\n    };\n  }, [key]);\n\n  // 首次渲染时如果分组是展开状态，此时边框bounds计算会有延迟，需要强制重新渲染\n  useEffect(() => {\n    if (!groupController || groupController.collapsed) {\n      return;\n    }\n    rerender();\n  }, []);\n\n  if (!groupController) {\n    return <></>;\n  }\n\n  const groupNodeRender = (\n    <GroupNode key={key} groupNode={groupNode} groupController={groupController} />\n  );\n  const groupBoxHeader = (\n    <GroupBoxHeader key={key} groupController={groupController} groupNode={groupNode} />\n  );\n\n  if (groupController.collapsed) {\n    const positionStyle: Partial<CSSStyleDeclaration> = {\n      display: 'block',\n      zIndex: '0',\n      width: 'auto',\n      height: 'auto',\n    };\n    Object.assign(container.style, positionStyle);\n    return groupNodeRender;\n  } else if (!rendering) {\n    const bounds: Rectangle = groupController.bounds;\n    const positionStyle: Partial<CSSStyleDeclaration> = {\n      width: `${bounds.width}px`,\n    };\n    Object.assign(container.style, positionStyle);\n    return groupBoxHeader;\n  } else {\n    return <></>;\n  }\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/components/hooks.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useRef, useState } from 'react';\n\nexport const useHover = () => {\n  const ref = useRef<HTMLDivElement>(null);\n  const [hover, setHover] = useState(false);\n\n  const checkMouseOver = (event: MouseEvent) => {\n    if (!ref.current) {\n      return;\n    }\n    const { left, top, right, bottom } = ref.current.getBoundingClientRect();\n    const isOver =\n      event.clientX >= left &&\n      event.clientX <= right &&\n      event.clientY >= top &&\n      event.clientY <= bottom;\n\n    setHover(isOver);\n  };\n\n  useEffect(() => {\n    window.addEventListener('mousemove', checkMouseOver);\n\n    return () => {\n      window.removeEventListener('mousemove', checkMouseOver);\n    };\n  }, []);\n\n  return {\n    hover,\n    ref,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/components/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { GroupRender } from './group-render';\nexport { GroupBox } from './group-box';\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum GroupRenderer {\n  GroupRender = 'group_render',\n  GroupBox = 'group_box',\n}\n\nexport const PositionConfig = {\n  paddingWithNote: 50, // note 留白大小\n  padding: 10, // 无 label 的 padding\n  paddingWithAddLabel: 20, // 有 label 的padding，如要放添加按钮\n  headerHeight: 20, // 基础头部高度\n};\n\nexport enum GroupPluginRegister {\n  GroupNode = 'registerGroupNode',\n  Render = 'registerRender',\n  Layer = 'registerLayer',\n  CleanGroups = 'registerCleanGroups',\n}\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/create-group-plugin.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { CreateGroupPluginOptions } from './type';\nimport { groupRegisters } from './registers';\nimport { GroupPluginRegister } from './constant';\n\n/**\n * 分组插件\n */\nexport const createGroupPlugin = definePluginCreator<CreateGroupPluginOptions>({\n  onInit: (ctx: PluginContext, opts: CreateGroupPluginOptions) => {\n    const { registers: registerConfs = {} } = opts;\n    Object.entries(groupRegisters).forEach(([key, register]) => {\n      const registerName = key as GroupPluginRegister;\n      const registerConf = registerConfs[registerName];\n      if (registerConf === false) {\n        return;\n      }\n      if (typeof registerConf === 'function') {\n        registerConf(ctx, opts);\n        return;\n      }\n      register(ctx, opts);\n    });\n  },\n});\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/group-node-register.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IPoint, PaddingSchema, Point } from '@flowgram.ai/utils';\nimport {\n  FlowGroupController,\n  FlowNodeBaseType,\n  FlowNodeRegistry,\n  FlowNodeTransformData,\n  FlowTransitionLabelEnum,\n  type FlowTransitionLine,\n  FlowTransitionLineEnum,\n} from '@flowgram.ai/document';\n\nimport { GroupRenderer, PositionConfig } from './constant';\n\nexport const GroupRegister: FlowNodeRegistry = {\n  type: FlowNodeBaseType.GROUP,\n  meta: {\n    exportJSON: true,\n    renderKey: GroupRenderer.GroupRender,\n    positionConfig: PositionConfig,\n    padding: (transform: FlowNodeTransformData): PaddingSchema => {\n      const groupController = FlowGroupController.create(transform.entity);\n      if (!groupController || groupController.collapsed || groupController.nodes.length === 0) {\n        return {\n          top: 0,\n          bottom: 0,\n          left: 0,\n          right: 0,\n        };\n      }\n      if (transform.entity.isVertical) {\n        return {\n          top: PositionConfig.paddingWithNote,\n          bottom: PositionConfig.paddingWithAddLabel,\n          left: PositionConfig.padding,\n          right: PositionConfig.padding,\n        };\n      }\n      return {\n        top: PositionConfig.paddingWithNote,\n        bottom: PositionConfig.padding,\n        left: PositionConfig.padding,\n        right: PositionConfig.paddingWithAddLabel,\n      };\n    },\n  },\n  formMeta: {\n    render: () => React.createElement('div'),\n  },\n  getLines(transition) {\n    const { transform } = transition;\n    const lines: FlowTransitionLine[] = [];\n    if (transform.firstChild) {\n      lines.push({\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: transform.inputPoint,\n        to: transform.firstChild.inputPoint,\n      });\n    }\n    if (transform.next) {\n      lines.push({\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: transform.outputPoint,\n        to: transform.next.inputPoint,\n      });\n    } else {\n      lines.push({\n        type: FlowTransitionLineEnum.STRAIGHT_LINE,\n        from: transform.outputPoint,\n        to: transform.parent!.outputPoint,\n      });\n    }\n    return lines;\n  },\n  getDelta(transform: FlowNodeTransformData): IPoint | undefined {\n    const groupController = FlowGroupController.create(transform.entity);\n    if (!groupController || groupController.collapsed) {\n      return;\n    }\n    if (transform.entity.isVertical) {\n      return {\n        x: 0,\n        y: PositionConfig.paddingWithNote,\n      };\n    }\n    return {\n      x: PositionConfig.padding,\n      y: 0,\n    };\n  },\n  getInputPoint(transform: FlowNodeTransformData): IPoint {\n    const child = transform.firstChild;\n    if (!child) return transform.defaultInputPoint;\n    if (transform.entity.isVertical) {\n      return {\n        x: child.inputPoint.x,\n        y: transform.bounds.topCenter.y,\n      };\n    }\n    return {\n      x: transform.bounds.leftCenter.x,\n      y: child.inputPoint.y,\n    };\n  },\n  getOutputPoint(transform: FlowNodeTransformData): IPoint {\n    const child = transform.lastChild;\n    if (!child) return transform.defaultOutputPoint;\n    if (transform.entity.isVertical) {\n      return {\n        x: child.outputPoint.x,\n        y: child.outputPoint.y + PositionConfig.paddingWithAddLabel / 2,\n      };\n    }\n    return {\n      x: child.outputPoint.x + PositionConfig.paddingWithAddLabel / 2,\n      y: child.outputPoint.y,\n    };\n  },\n  getLabels(transition) {\n    const { transform } = transition;\n    if (transform.next) {\n      if (transform.entity.isVertical) {\n        return [\n          {\n            offset: Point.getMiddlePoint(\n              Point.move(transform.outputPoint, {\n                x: 0,\n                y: PositionConfig.paddingWithAddLabel / 2,\n              }),\n              transform.next.inputPoint\n            ),\n            type: FlowTransitionLabelEnum.ADDER_LABEL,\n          },\n        ];\n      }\n      return [\n        {\n          offset: Point.getMiddlePoint(\n            Point.move(transform.outputPoint, { x: PositionConfig.paddingWithAddLabel / 2, y: 0 }),\n            transform.next.inputPoint\n          ),\n          type: FlowTransitionLabelEnum.ADDER_LABEL,\n        },\n      ];\n    }\n    return [\n      {\n        offset: transform.parent!.outputPoint,\n        type: FlowTransitionLabelEnum.ADDER_LABEL,\n      },\n    ];\n  },\n  getOriginDeltaY(transform): number {\n    const { children } = transform;\n    if (children.length === 0) {\n      return -transform.size.height * transform.origin.y;\n    }\n    // 这里要加上 y 轴的偏移\n    return -transform.size.height * transform.origin.y - PositionConfig.paddingWithNote;\n  },\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/groups-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport {\n  FlowDocument,\n  FlowDocumentTransformerEntity,\n  FlowGroupController,\n  FlowGroupService,\n  FlowNodeEntity,\n  FlowNodeRenderData,\n  FlowNodeTransformData,\n} from '@flowgram.ai/document';\nimport { Layer, observeEntity, observeEntityDatas } from '@flowgram.ai/core';\nimport { domUtils } from '@flowgram.ai/utils';\n\nimport { GroupsLayerOptions, IGroupBox } from './type';\nimport { GroupRenderer } from './constant';\nimport { GroupBox } from './components';\n\n@injectable()\nexport class GroupsLayer extends Layer<GroupsLayerOptions> {\n  public readonly node: HTMLElement;\n\n  @inject(FlowDocument) protected document: FlowDocument;\n\n  @inject(FlowRendererRegistry)\n  protected readonly rendererRegistry: FlowRendererRegistry;\n\n  @inject(FlowGroupService)\n  protected readonly groupService: FlowGroupService;\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeRenderData)\n  renderStates: FlowNodeRenderData[];\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData)\n  transforms: FlowNodeTransformData[];\n\n  private readonly className = 'gedit-groups-layer';\n\n  constructor() {\n    super();\n    this.node = domUtils.createDivWithClass(this.className);\n    this.node.style.zIndex = '0';\n  }\n\n  /** 缩放 */\n  public onZoom(scale: number): void {\n    this.node!.style.transform = `scale(${scale})`;\n  }\n\n  public render(): JSX.Element {\n    if (this.documentTransformer.loading) return <></>;\n    this.documentTransformer.refresh();\n\n    return <>{this.renderGroups()}</>;\n  }\n\n  /** 渲染分组 */\n  protected renderGroups(): JSX.Element {\n    const Box = this.renderer || GroupBox;\n    return (\n      <>\n        {this.groups.map(group => (\n          <Box\n            key={group.groupNode.id}\n            groupNode={group.groupNode}\n            backgroundStyle={this.options.groupBoxStyle}\n          />\n        ))}\n      </>\n    );\n  }\n\n  /** 所有分组 */\n  protected get groups(): FlowGroupController[] {\n    return this.groupService.getAllGroups();\n  }\n\n  protected get renderer(): IGroupBox {\n    return this.rendererRegistry.tryToGetRendererComponent(GroupRenderer.GroupBox)\n      ?.renderer as IGroupBox;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowGroupController } from '@flowgram.ai/document';\nexport * from './groups-layer';\nexport * from './create-group-plugin';\nexport * from './type';\nexport * from './constant';\nexport * from './group-node-register';\nexport * from './components';\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/registers/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IGroupPluginRegister } from '../type';\nimport { GroupPluginRegister } from '../constant';\nimport { registerRender } from './register-render';\nimport { registerLayer } from './register-layer';\nimport { registerGroupNode } from './register-group-node';\nimport { registerCleanGroups } from './register-clean-groups';\n\nexport const groupRegisters: Record<GroupPluginRegister, IGroupPluginRegister> = {\n  [GroupPluginRegister.GroupNode]: registerGroupNode,\n  [GroupPluginRegister.Render]: registerRender,\n  [GroupPluginRegister.Layer]: registerLayer,\n  [GroupPluginRegister.CleanGroups]: registerCleanGroups,\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/registers/register-clean-groups.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { FlowGroupService } from '@flowgram.ai/document';\n\nimport { IGroupPluginRegister } from '../type';\n\n/** 注册清理分组逻辑 */\nexport const registerCleanGroups: IGroupPluginRegister = (ctx, opts) => {\n  const groupService = ctx.get<FlowGroupService>(FlowGroupService);\n  const document = ctx.get<FlowDocument>(FlowDocument);\n\n  const clearInvalidGroups = () => {\n    groupService.getAllGroups().forEach(group => {\n      if (group?.nodes.length !== 0) {\n        return;\n      }\n      if (!group.groupNode.pre) {\n        return;\n      }\n      groupService.deleteGroup(group.groupNode);\n    });\n  };\n\n  document.originTree.onTreeChange(() => {\n    setTimeout(() => {\n      clearInvalidGroups();\n    }, 0);\n  });\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/registers/register-group-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocument } from '@flowgram.ai/document';\n\nimport { IGroupPluginRegister } from '../type';\nimport { GroupRegister } from '../group-node-register';\n\n/** 注册分组节点 */\nexport const registerGroupNode: IGroupPluginRegister = ctx => {\n  const document = ctx.get<FlowDocument>(FlowDocument);\n  document.registerFlowNodes(GroupRegister);\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/registers/register-layer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IGroupPluginRegister } from '../type';\nimport { GroupsLayer } from '../groups-layer';\n\n/** 注册背景层 */\nexport const registerLayer: IGroupPluginRegister = (ctx, opts) => {\n  ctx.playground.registerLayer(GroupsLayer, opts);\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/registers/register-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { FC } from 'react';\n\nimport { FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { IGroupPluginRegister } from '../type';\nimport { GroupRenderer } from '../constant';\nimport { GroupRender } from '../components';\n\n/** 注册渲染组件 */\nexport const registerRender: IGroupPluginRegister = (ctx, opts) => {\n  const rendererRegistry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);\n  const renderer: FC<{ node: FlowNodeEntity }> = props => (\n    <GroupRender\n      groupNode={props.node}\n      GroupNode={opts.components!.GroupNode}\n      GroupBoxHeader={opts.components!.GroupBoxHeader}\n    />\n  );\n  rendererRegistry.registerReactComponent(GroupRenderer.GroupRender, renderer);\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CSSProperties, FC } from 'react';\n\nimport type { FlowNodeEntity } from '@flowgram.ai/document';\nimport type { FlowGroupController } from '@flowgram.ai/document';\nimport type { LayerOptions, PluginContext } from '@flowgram.ai/core';\n\nimport type { GroupPluginRegister } from './constant';\n\nexport type IGroupBox = FC<{\n  groupNode: FlowNodeEntity;\n  backgroundStyle?: (groupController: FlowGroupController) => CSSProperties;\n}>;\n\nexport type IGroupRender = FC<{\n  groupNode: FlowNodeEntity;\n  GroupNode: IGroupNode;\n  GroupBoxHeader: IGroupBoxHeader;\n}>;\n\nexport type IGroupNode = FC<{\n  groupNode: FlowNodeEntity;\n  groupController: FlowGroupController;\n}>;\n\nexport type IGroupBoxHeader = FC<{\n  groupNode: FlowNodeEntity;\n  groupController: FlowGroupController;\n}>;\n\nexport interface GroupsLayerOptions extends LayerOptions {\n  groupBoxStyle?: (groupController: FlowGroupController) => CSSProperties;\n}\n\nexport type IGroupPluginRegister = (ctx: PluginContext, opts: CreateGroupPluginOptions) => void;\n\nexport type CreateGroupPluginOptions = GroupsLayerOptions & {\n  components?: {\n    GroupNode: IGroupNode;\n    GroupBoxHeader: IGroupBoxHeader;\n  };\n  registers?: {\n    [key in GroupPluginRegister]?: IGroupPluginRegister | boolean;\n  };\n};\n"
  },
  {
    "path": "packages/plugins/group-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/group-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/group-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/__tests__/create-container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormModel } from '@flowgram.ai/form';\nimport { FlowDocumentContainerModule } from '@flowgram.ai/document';\nimport { loadPlugins, Playground, PlaygroundMockTools } from '@flowgram.ai/core';\nimport { createHistoryPlugin, HistoryService } from '@flowgram.ai/history';\n\nimport { attachFormValuesChange } from '../src/utils';\nimport { createHistoryNodePlugin } from '../src';\n\nexport const createContainer = () => {\n  const container = PlaygroundMockTools.createContainer([FlowDocumentContainerModule]);\n\n  const formModel = new FormModel();\n\n  const playground = container.get(Playground);\n\n  loadPlugins([createHistoryPlugin({ enable: true }), createHistoryNodePlugin({})], container);\n  playground.init();\n\n  const historyService = container.get(HistoryService);\n  historyService.context.source = container;\n\n  attachFormValuesChange(formModel as any, { id: 1 } as any, historyService);\n\n  return {\n    formModel,\n    container,\n    historyService,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/__tests__/form.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, it, expect, vi } from 'vitest';\nimport { FieldArrayModel, FormModel } from '@flowgram.ai/form';\nimport { HistoryService } from '@flowgram.ai/history';\n\nimport * as utils from '../src/utils';\nimport { createContainer } from './create-container';\n\nfunction delay(timeout: number) {}\n\ndescribe('form', () => {\n  let formModel: FormModel;\n  let historyService: HistoryService;\n\n  beforeEach(() => {\n    const container = createContainer();\n    formModel = container.formModel;\n    historyService = container.historyService;\n    vi.spyOn(utils, 'getFormModelV2').mockImplementation(() => formModel as any);\n    vi.useFakeTimers();\n  });\n\n  it('object set', async () => {\n    const obj = formModel.createField('obj');\n    const fieldA = formModel.createField('obj.a');\n    fieldA.value = 1;\n    vi.advanceTimersByTime(500);\n\n    fieldA.value = 2;\n    obj.value = { a: 3 };\n    await historyService.undo();\n    expect(obj.value).toEqual({ a: 1 });\n    await historyService.redo();\n    expect(obj.value).toEqual({ a: 3 });\n  });\n\n  it('array delete', async () => {\n    formModel.createFieldArray('arr');\n    const arrField = formModel.getField<FieldArrayModel>('arr')!;\n    expect(arrField.value).toEqual(undefined);\n    arrField.value = ['a', 'b', 'c'];\n    expect(arrField.value).toEqual(['a', 'b', 'c']);\n\n    vi.advanceTimersByTime(500);\n\n    arrField.delete(1);\n    expect(arrField.value).toEqual(['a', 'c']);\n    await historyService.undo();\n    expect(arrField.value).toEqual(['a', 'b', 'c']);\n    await historyService.redo();\n    expect(arrField.value).toEqual(['a', 'c']);\n  });\n\n  it('array append', async () => {\n    formModel.createFieldArray('arr');\n    const arrField = formModel.getField<FieldArrayModel>('arr')!;\n    arrField.value = ['a'];\n    expect(arrField.value).toEqual(['a']);\n    await historyService.undo();\n    expect(arrField.value).toEqual(undefined);\n    await historyService.redo();\n    expect(arrField.value).toEqual(['a']);\n\n    vi.advanceTimersByTime(500);\n\n    arrField.append('b');\n    expect(arrField.value).toEqual(['a', 'b']);\n    await historyService.undo();\n    expect(arrField.value).toEqual(['a']);\n    await historyService.redo();\n    expect(arrField.value).toEqual(['a', 'b']);\n  });\n\n  it('array set', async () => {\n    formModel.createFieldArray('arr');\n    const arrField = formModel.getField<FieldArrayModel>('arr')!;\n    arrField.value = ['a'];\n    expect(arrField.value).toEqual(['a']);\n\n    vi.advanceTimersByTime(500);\n\n    formModel.setValueIn('arr.0', undefined);\n    expect(arrField.value).toEqual([undefined]);\n    await historyService.undo();\n    expect(arrField.value).toEqual(['a']);\n    await historyService.redo();\n    expect(arrField.value).toEqual([undefined]);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/history-node-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/history\": \"workspace:*\",\n    \"@flowgram.ai/node\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/create-history-node-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { bindContributions, definePluginCreator } from '@flowgram.ai/core';\nimport { HistoryContainerModule, HistoryService, OperationContribution } from '@flowgram.ai/history';\n\nimport { attachFormValuesChange, getFormModelV2 } from './utils';\nimport { HistoryNodeRegisters } from './history-node-registers';\n\n/**\n * 表单历史插件\n */\nexport const createHistoryNodePlugin = definePluginCreator({\n  onBind: ({ bind }) => {\n    bindContributions(bind, HistoryNodeRegisters, [OperationContribution]);\n  },\n  onInit: (ctx, _opts) => {\n    const document = ctx.get<FlowDocument>(FlowDocument);\n    const historyService = ctx.get<HistoryService>(HistoryService);\n\n    document.onNodeCreate(({ node }) => {\n      const formModel = getFormModelV2(node);\n\n      if (!formModel) {\n        return;\n      }\n\n      attachFormValuesChange(formModel, node, historyService);\n    });\n  },\n  containerModules: [HistoryContainerModule],\n});\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/history-node-registers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { type OperationContribution, type OperationRegistry } from '@flowgram.ai/history';\n\nimport { operationMetas } from './operation-metas';\n\n/**\n * 表单历史操作\n */\n@injectable()\nexport class HistoryNodeRegisters implements OperationContribution {\n  registerOperationMeta(operationRegistry: OperationRegistry): void {\n    operationMetas.forEach(operationMeta => {\n      operationRegistry.registerOperationMeta(operationMeta);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-history-node-plugin';\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/operation-metas/change-form-values.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type OperationMeta } from '@flowgram.ai/history';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { type PluginContext } from '@flowgram.ai/core';\n\nimport { getFormModelV2, shouldChangeFormValuesMerge } from '../utils';\nimport { ChangeFormValuesOperationValue, NodeOperationType } from '../types';\n\n/**\n * 表单修改操作\n */\nexport const changeFormValueOperationMeta: OperationMeta<\n  ChangeFormValuesOperationValue,\n  PluginContext,\n  void\n> = {\n  type: NodeOperationType.changeFormValues,\n  inverse: (op) => ({\n    ...op,\n    value: {\n      ...op.value,\n      value: op.value.oldValue,\n      oldValue: op.value.value,\n    },\n  }),\n  apply: ({ value: { value, path, id } }, ctx: PluginContext) => {\n    const document = ctx.get<FlowDocument>(FlowDocument);\n    const formModel = getFormModelV2(document.getNode(id));\n\n    if (!formModel) {\n      return;\n    }\n    if (!path) {\n      formModel.updateFormValues(value);\n    } else {\n      formModel.setValueIn(path, value);\n    }\n  },\n  shouldMerge: shouldChangeFormValuesMerge as OperationMeta['shouldMerge'],\n};\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/operation-metas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OperationMeta } from '@flowgram.ai/history';\n\nimport { changeFormValueOperationMeta } from './change-form-values';\n\nexport const operationMetas: OperationMeta[] = [changeFormValueOperationMeta];\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum NodeOperationType {\n  changeFormValues = 'changeFormValues',\n}\n\nexport interface ChangeFormValuesOperationValue {\n  id: string;\n  path: string;\n  value: unknown;\n  oldValue: unknown;\n}\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/src/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get } from 'lodash-es';\nimport { FormModelV2, isFormModelV2 } from '@flowgram.ai/node';\nimport { HistoryService, Operation } from '@flowgram.ai/history';\nimport { StackOperation } from '@flowgram.ai/history';\nimport { FlowNodeFormData } from '@flowgram.ai/form-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { ChangeFormValuesOperationValue, NodeOperationType } from '../types';\n\n/**\n * 获取v2版本的formModel\n * @param node 节点\n * @returns\n */\nexport function getFormModelV2(node: FlowNodeEntity | undefined): FormModelV2 | undefined {\n  if (!node) {\n    return undefined;\n  }\n\n  const formModel = node?.getData(FlowNodeFormData)?.getFormModel<FormModelV2>();\n\n  if (!formModel || !isFormModelV2(formModel)) {\n    return undefined;\n  }\n\n  return formModel;\n}\n\n/**\n * 表单合并策略\n * @param op 操作\n * @param prev 上一个操作\n * @param element 操作栈元素\n * @returns\n */\nexport function shouldChangeFormValuesMerge(\n  op: Operation<ChangeFormValuesOperationValue | undefined>,\n  prev: Operation<ChangeFormValuesOperationValue | undefined>,\n  element: StackOperation\n) {\n  if (!prev) {\n    return false;\n  }\n\n  if (Date.now() - element.getTimestamp() < 500) {\n    if (\n      op.type === prev.type && // 相同类型\n      op.value?.id === prev.value?.id && // 相同节点\n      op.value?.path === prev.value?.path // 相同路径\n    ) {\n      return {\n        type: op.type,\n        value: {\n          ...op.value,\n          value: op.value?.value,\n          oldValue: prev.value?.oldValue,\n        },\n      };\n    }\n    return true;\n  }\n  return false;\n}\n\n/**\n * 监听表单值变化\n * @param formModel 表单模型\n * @param node 节点\n * @param historyService 历史服务\n */\nexport function attachFormValuesChange(\n  formModel: FormModelV2,\n  node: FlowNodeEntity,\n  historyService: HistoryService\n) {\n  formModel.onFormValuesChange((event) => {\n    historyService.pushOperation(\n      {\n        type: NodeOperationType.changeFormValues,\n        value: {\n          id: node.id,\n          path: event.name,\n          value: event.name ? get(event.values, event.name) : event.values,\n          oldValue: event.name ? get(event.prevValues, event.name) : event.prevValues,\n        },\n      },\n      { noApply: true }\n    );\n  });\n}\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/history-node-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/i18n-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/i18n\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {},\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/src/create-i18n-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { I18n, type I18nLanguage } from '@flowgram.ai/i18n';\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nexport interface I18nPluginOptions {\n  locale?: string;\n  /**\n   * use `locale` instead\n   * @deprecated\n   */\n  localLanguage?: string;\n  /**\n   * if missingStrictMode is true\n   *  expect(I18n.t('Unknown')).toEqual('[missing \"en-US.Unknown\" translation]')\n   * else\n   *  expect(I18n.t('Unknown')).toEqual('Unknown')\n   */\n  missingStrictMode?: boolean;\n  languages?: I18nLanguage[] | Record<string, Record<string, any>>;\n  onLanguageChange?: (languageId: string) => void;\n}\n/**\n * I18n Plugin\n */\nexport const createI18nPlugin = definePluginCreator<I18nPluginOptions>({\n  onInit: (ctx, _opts) => {\n    if (_opts.onLanguageChange) {\n      ctx.playground.toDispose.push(I18n.onLanguageChange(_opts.onLanguageChange));\n    }\n    if (_opts.languages) {\n      if (Array.isArray(_opts.languages)) {\n        I18n.addLanguages(_opts.languages);\n      } else {\n        I18n.addLanguages(\n          Object.keys(_opts.languages).map((key) => ({\n            languageId: key,\n            contents: (_opts.languages as any)![key],\n          }))\n        );\n      }\n    }\n    if (_opts.locale || _opts.localLanguage) {\n      I18n.locale = (_opts.locale || _opts.localLanguage)!;\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { I18n, type I18nLanguage } from '@flowgram.ai/i18n';\nexport { createI18nPlugin, type I18nPluginOptions } from './create-i18n-plugin';\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/i18n-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/materials-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/materials-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/materials-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/materials-plugin/src/create-materials-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { FlowRendererKey, FlowRendererRegistry } from '@flowgram.ai/renderer';\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nexport type MaterialReactComponent<T = any> = (props: T) => React.ReactNode | null;\n\nexport interface MaterialsPluginOptions {\n  /**\n   * 注册特定的 UI 组件\n   */\n  components?: Record<FlowRendererKey | string, MaterialReactComponent>;\n  /**\n   * 注册特定的节点渲染组件\n   */\n  renderNodes?: Record<string, MaterialReactComponent>;\n  /**\n   * 默认节点渲染\n   */\n  renderDefaultNode?: MaterialReactComponent;\n  /**\n   * 注册渲染的文字\n   */\n  renderTexts?: Record<string, string>;\n}\n\nexport const createMaterialsPlugin = definePluginCreator<MaterialsPluginOptions>({\n  onInit(ctx, opts) {\n    const registry = ctx.get<FlowRendererRegistry>(FlowRendererRegistry);\n    /**\n     * 注册默认节点渲染\n     */\n    registry.registerReactComponent(\n      FlowRendererKey.NODE_RENDER,\n      opts.renderDefaultNode || (() => null)\n    );\n\n    /**\n     * 注册文案\n     */\n    if (opts.renderTexts) {\n      registry.registerText(opts.renderTexts);\n    }\n    /**\n     * 注册组件\n     */\n    if (opts.components) {\n      Object.keys(opts.components).forEach((key) =>\n        registry.registerReactComponent(key, opts.components![key])\n      );\n    }\n    /**\n     * 注册单节点渲染\n     */\n    if (opts.renderNodes) {\n      Object.keys(opts.renderNodes).forEach((key) =>\n        registry.registerReactComponent(key, opts.renderNodes![key])\n      );\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/materials-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-materials-plugin';\n"
  },
  {
    "path": "packages/plugins/materials-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/materials-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/materials-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/minimap-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"lodash-es\": \"^4.17.21\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/component.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { CSSProperties, useEffect, useRef, useState } from 'react';\n\nimport { usePlaygroundContainer } from '@flowgram.ai/core';\n\nimport { MinimapInactiveStyle } from './type';\nimport { FlowMinimapService } from './service';\nimport { MinimapDefaultInactiveStyle } from './constant';\n\ninterface MinimapProps {\n  service?: FlowMinimapService;\n  panelStyles?: CSSProperties;\n  containerStyles?: CSSProperties;\n  inactiveStyle?: Partial<MinimapInactiveStyle>;\n}\n\nexport const MinimapRender: React.FC<MinimapProps> = (props) => {\n  const { panelStyles = {}, containerStyles = {}, inactiveStyle: customInactiveStyle = {} } = props;\n  const inactiveStyle = {\n    ...MinimapDefaultInactiveStyle,\n    ...customInactiveStyle,\n  };\n  const playgroundContainer = usePlaygroundContainer();\n  const service = props.service || playgroundContainer?.get(FlowMinimapService);\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [activated, setActivated] = useState<boolean>(false);\n\n  useEffect(() => {\n    const canvasContainer: HTMLDivElement | null = panelRef.current;\n    if (canvasContainer && service.canvas) {\n      canvasContainer.appendChild(service.canvas);\n    }\n    const disposer = service.onActive((activate: boolean) => {\n      setActivated(activate);\n    });\n    service.setVisible(true);\n    service.render();\n    return () => {\n      disposer.dispose();\n      service.setVisible(false);\n    };\n  }, [service]);\n\n  // 计算缩放比例和透明度\n  const scale: number = activated ? 1 : inactiveStyle.scale;\n  const opacity: number = activated ? 1 : inactiveStyle.opacity;\n\n  // 计算偏移量\n  const translateX: number = activated ? 0 : inactiveStyle.translateX; // 向右偏移的像素\n  const translateY: number = activated ? 0 : inactiveStyle.translateY; // 向下偏移的像素\n\n  return (\n    <div\n      className=\"minimap-container\"\n      style={{\n        position: 'fixed',\n        right: 30,\n        bottom: 70,\n        transition: 'all 0.3s ease', // 添加过渡效果\n        transform: `scale(${scale}) translate(${translateX}px, ${translateY}px)`,\n        opacity: opacity,\n        transformOrigin: 'bottom right', // 设置变换的原点\n        ...containerStyles,\n      }}\n    >\n      <div\n        className=\"minimap-panel\"\n        style={{\n          display: 'flex',\n          width: '100%',\n          height: '100%',\n          borderRadius: '10px',\n          backgroundColor: 'rgba(255, 255, 255, 1)',\n          border: '0.572px solid rgba(6, 7, 9, 0.10)',\n          overflow: 'hidden',\n          boxShadow:\n            '0px 2.289px 6.867px 0px rgba(0, 0, 0, 0.08), 0px 4.578px 13.733px 0px rgba(0, 0, 0, 0.04)',\n          boxSizing: 'border-box',\n          padding: 8,\n          ...panelStyles,\n        }}\n        data-flow-editor-selectable=\"false\"\n        ref={panelRef}\n        onMouseEnter={() => {\n          service.setActivate(true);\n        }}\n        onMouseLeave={() => {\n          service.setActivate(false);\n        }}\n        onTouchStartCapture={() => {\n          service.setActivate(true);\n        }}\n        onTouchEndCapture={() => {\n          service.setActivate(false);\n        }}\n      ></div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { MinimapCanvasStyle, MinimapInactiveStyle, MinimapServiceOptions } from './type';\n\nexport const MinimapDefaultCanvasStyle: MinimapCanvasStyle = {\n  canvasWidth: 250,\n  canvasHeight: 250,\n  canvasPadding: 50,\n  canvasBackground: 'rgba(242, 243, 245, 1)',\n  canvasBorderRadius: 10,\n  viewportBackground: 'rgba(255, 255, 255, 1)',\n  viewportBorderRadius: 4,\n  viewportBorderColor: 'rgba(6, 7, 9, 0.10)',\n  viewportBorderWidth: 1,\n  viewportBorderDashLength: undefined,\n  nodeColor: 'rgba(0, 0, 0, 0.10)',\n  nodeBorderRadius: 2,\n  nodeBorderWidth: 0.145,\n  nodeBorderColor: 'rgba(6, 7, 9, 0.10)',\n  overlayColor: 'rgba(255, 255, 255, 0.55)',\n};\n\nexport const MinimapDefaultInactiveStyle: MinimapInactiveStyle = {\n  scale: 0.7,\n  opacity: 1,\n  translateX: 15,\n  translateY: 15,\n};\n\nexport const MinimapDefaultOptions: MinimapServiceOptions = {\n  canvasStyle: MinimapDefaultCanvasStyle,\n  canvasClassName: 'gedit-minimap-canvas',\n  enableDisplayAllNodes: false,\n  activeThrottleTime: 0,\n  inactiveThrottleTime: 24,\n};\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/create-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator, PluginContext } from '@flowgram.ai/core';\n\nimport { CreateMinimapPluginOptions } from './type';\nimport { FlowMinimapService } from './service';\nimport { FlowMinimapLayer } from './layer';\n\nexport const createMinimapPlugin = definePluginCreator<CreateMinimapPluginOptions>({\n  onBind: ({ bind }) => {\n    bind(FlowMinimapService).toSelf().inSingletonScope();\n  },\n  onInit: (ctx: PluginContext, opts: CreateMinimapPluginOptions) => {\n    ctx.playground.registerLayer(FlowMinimapLayer, opts);\n    ctx.get(FlowMinimapService).init(opts);\n  },\n  onDispose: (ctx: PluginContext) => {\n    ctx.get(FlowMinimapService).dispose();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/draw.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IPoint, Rectangle } from '@flowgram.ai/utils';\n\nexport namespace MinimapDraw {\n  /** 矩形是否合法 */\n  const isRectValid = (rect: Rectangle): boolean => rect.width > 0 && rect.height > 0;\n\n  /** 清空画布 */\n  export const clear = (params: {\n    canvas: HTMLCanvasElement;\n    context: CanvasRenderingContext2D;\n  }) => {\n    const { canvas, context } = params;\n    context.clearRect(0, 0, canvas.width, canvas.height);\n  };\n\n  /** 设置背景色 */\n  export const backgroundColor = (params: {\n    canvas: HTMLCanvasElement;\n    context: CanvasRenderingContext2D;\n    color: string;\n  }) => {\n    const { canvas, context, color } = params;\n    context.fillStyle = color;\n    context.fillRect(0, 0, canvas.width, canvas.height);\n  };\n\n  /** 绘制矩形 */\n  export const rectangle = (params: {\n    context: CanvasRenderingContext2D;\n    rect: Rectangle;\n    color: string;\n  }): void => {\n    const { context, rect, color } = params;\n    if (!isRectValid(rect)) {\n      return;\n    }\n    context.fillStyle = color;\n    context.fillRect(rect.x, rect.y, rect.width, rect.height);\n  };\n\n  /** 绘制圆角矩形 */\n  export const roundRectangle = (params: {\n    context: CanvasRenderingContext2D;\n    rect: Rectangle;\n    color: string;\n    radius: number;\n    borderColor?: string;\n    borderWidth?: number;\n    borderDashLength?: number;\n  }): void => {\n    const { context, rect, color, radius, borderColor, borderDashLength, borderWidth = 0 } = params;\n    const { x, y, width, height } = rect;\n\n    if (!isRectValid(rect)) {\n      return;\n    }\n\n    // 开始新路径\n    context.beginPath();\n\n    // 绘制圆角矩形路径\n    const drawRoundedRectPath = (): void => {\n      context.moveTo(x + radius, y);\n      context.lineTo(x + width - radius, y);\n      context.quadraticCurveTo(x + width, y, x + width, y + radius);\n      context.lineTo(x + width, y + height - radius);\n      context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);\n      context.lineTo(x + radius, y + height);\n      context.quadraticCurveTo(x, y + height, x, y + height - radius);\n      context.lineTo(x, y + radius);\n      context.quadraticCurveTo(x, y, x + radius, y);\n      context.closePath();\n    };\n\n    drawRoundedRectPath();\n\n    // 填充矩形\n    context.fillStyle = color;\n    context.fill();\n\n    // 如果设置了边框，绘制边框\n    if (borderColor && borderWidth > 0) {\n      context.strokeStyle = borderColor;\n      context.lineWidth = borderWidth;\n\n      // 设置虚线样式\n      if (borderDashLength) {\n        context.setLineDash([borderDashLength, borderDashLength]);\n      } else {\n        context.setLineDash([]);\n      }\n\n      context.stroke();\n\n      // 重置虚线样式\n      context.setLineDash([]);\n    }\n  };\n\n  /** 绘制矩形外的蒙层 */\n  export const overlay = (params: {\n    canvas: HTMLCanvasElement;\n    context: CanvasRenderingContext2D;\n    offset: IPoint;\n    scale: number;\n    rect: Rectangle;\n    color: string;\n  }): void => {\n    const { canvas, context, offset, scale, rect, color } = params;\n\n    if (!isRectValid(rect)) {\n      return;\n    }\n\n    context.fillStyle = color;\n\n    // 上方蒙层\n    context.fillRect(0, 0, canvas.width, (rect.y + offset.y) * scale);\n\n    // 下方蒙层\n    context.fillRect(\n      0,\n      (rect.y + rect.height + offset.y) * scale,\n      canvas.width,\n      canvas.height - (rect.y + rect.height + offset.y) * scale\n    );\n\n    // 左侧蒙层\n    context.fillRect(\n      0,\n      (rect.y + offset.y) * scale,\n      (rect.x + offset.x) * scale,\n      rect.height * scale\n    );\n\n    // 右侧蒙层\n    context.fillRect(\n      (rect.x + rect.width + offset.x) * scale,\n      (rect.y + offset.y) * scale,\n      canvas.width - (rect.x + rect.width + offset.x) * scale,\n      rect.height * scale\n    );\n  };\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { MinimapRender } from './component';\nexport {\n  MinimapDefaultCanvasStyle,\n  MinimapDefaultInactiveStyle,\n  MinimapDefaultOptions,\n} from './constant';\nexport { createMinimapPlugin } from './create-plugin';\nexport { FlowMinimapLayer } from './layer';\nexport { FlowMinimapService } from './service';\nexport {\n  MinimapCanvasStyle,\n  MinimapInactiveStyle,\n  MinimapServiceOptions,\n  MinimapLayerOptions,\n  CreateMinimapPluginOptions,\n  MinimapRenderContext,\n} from './type';\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { inject, injectable } from 'inversify';\nimport { domUtils } from '@flowgram.ai/utils';\nimport {\n  FlowNodeEntity,\n  FlowNodeTransformData,\n  FlowDocumentTransformerEntity,\n} from '@flowgram.ai/document';\nimport {\n  Layer,\n  observeEntityDatas,\n  observeEntity,\n  PlaygroundConfigEntity,\n} from '@flowgram.ai/core';\n\nimport { MinimapLayerOptions } from './type';\nimport { FlowMinimapService } from './service';\nimport { MinimapRender } from './component';\n\n@injectable()\nexport class FlowMinimapLayer extends Layer<MinimapLayerOptions> {\n  public static type = 'FlowMinimapLayer';\n\n  @inject(FlowMinimapService) private readonly service: FlowMinimapService;\n\n  @observeEntityDatas(FlowNodeEntity, FlowNodeTransformData)\n  transformDatas: FlowNodeTransformData[];\n\n  @observeEntity(PlaygroundConfigEntity) configEntity: PlaygroundConfigEntity;\n\n  @observeEntity(FlowDocumentTransformerEntity)\n  readonly documentTransformer: FlowDocumentTransformerEntity;\n\n  public readonly node: HTMLElement;\n\n  private readonly className = 'gedit-minimap-layer gedit-playground-layer';\n\n  constructor() {\n    super();\n    this.node = domUtils.createDivWithClass(this.className);\n    this.node.style.zIndex = '9999';\n  }\n\n  public render(): JSX.Element {\n    if (this.documentTransformer.loading) return <></>;\n    this.documentTransformer.refresh();\n    this.service.render();\n    if (this.options.disableLayer) {\n      return <></>;\n    }\n    return (\n      <MinimapRender\n        service={this.service}\n        panelStyles={this.options.panelStyles}\n        containerStyles={this.options.containerStyles}\n        inactiveStyle={this.options.inactiveStyle}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { throttle } from 'lodash-es';\nimport { inject, injectable } from 'inversify';\nimport { Disposable, DisposableCollection, IPoint, Rectangle } from '@flowgram.ai/utils';\nimport { FlowNodeTransformData } from '@flowgram.ai/document';\nimport { FlowNodeBaseType } from '@flowgram.ai/document';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { MouseTouchEvent, PlaygroundConfigEntity } from '@flowgram.ai/core';\n\nimport type { MinimapRenderContext, MinimapServiceOptions, MinimapCanvasStyle } from './type';\nimport { MinimapDraw } from './draw';\nimport { MinimapDefaultCanvasStyle, MinimapDefaultOptions } from './constant';\n\n@injectable()\nexport class FlowMinimapService {\n  @inject(FlowDocument) private readonly document: FlowDocument;\n\n  @inject(PlaygroundConfigEntity)\n  private readonly playgroundConfig: PlaygroundConfigEntity;\n\n  public readonly canvas: HTMLCanvasElement;\n\n  public readonly context2D: CanvasRenderingContext2D;\n\n  public activated;\n\n  private onActiveCallbacks: Set<(activated: boolean) => void>;\n\n  private options: MinimapServiceOptions;\n\n  private toDispose: DisposableCollection;\n\n  private initialized;\n\n  private visible: boolean = false;\n\n  private isDragging;\n\n  private style: MinimapCanvasStyle;\n\n  private dragStart?: IPoint;\n\n  constructor() {\n    this.canvas = document.createElement('canvas');\n    this.context2D = this.canvas.getContext('2d')!;\n    this.initialized = !!this.context2D;\n    this.onActiveCallbacks = new Set();\n    this.toDispose = new DisposableCollection();\n    this.render = this._render;\n    this.activated = false;\n    this.isDragging = false;\n  }\n\n  public init(options?: Partial<MinimapServiceOptions>) {\n    this.options = MinimapDefaultOptions;\n    Object.assign(this.options, options);\n    this.setThrottle(this.options.inactiveThrottleTime);\n    this.initStyle();\n  }\n\n  public dispose(): void {\n    this.toDispose.dispose();\n    this.initialized = false;\n    this.activated = false;\n    this.removeEventListeners();\n  }\n\n  setVisible(visible: boolean) {\n    this.visible = visible;\n  }\n\n  public setActivate(activate: boolean): void {\n    if (activate === this.activated) {\n      return;\n    }\n    if (!activate && this.isDragging) {\n      // 拖拽时持续激活\n      return;\n    }\n    this.activated = activate;\n    if (activate) {\n      this.setThrottle(this.options.activeThrottleTime);\n      this.addEventListeners();\n    } else {\n      this.setThrottle(this.options.inactiveThrottleTime);\n      this.removeEventListeners();\n    }\n    this.render();\n    this.onActiveCallbacks.forEach((callback) => callback(activate));\n  }\n\n  public onActive = (callback: (activated: boolean) => void): Disposable => {\n    this.onActiveCallbacks.add(callback);\n    return {\n      dispose: () => {\n        this.onActiveCallbacks.delete(callback);\n      },\n    };\n  };\n\n  private initStyle() {\n    if (!this.initialized) {\n      return;\n    }\n    const { canvasClassName, canvasStyle } = this.options;\n    this.canvas.className = canvasClassName;\n    this.style = {\n      ...MinimapDefaultCanvasStyle,\n      ...canvasStyle,\n    };\n    this.canvas.width = this.style.canvasWidth;\n    this.canvas.height = this.style.canvasHeight;\n    this.canvas.style.borderRadius = this.style.canvasBorderRadius\n      ? `${this.style.canvasBorderRadius}px`\n      : 'unset';\n  }\n\n  private setThrottle(throttleTime: number) {\n    this.render = throttle(this._render, throttleTime);\n  }\n\n  /**\n   * 触发渲染\n   */\n  public render: () => void = this._render;\n\n  private _render(): void {\n    if (!this.initialized || !this.visible) {\n      return;\n    }\n    const renderContext = this.createRenderContext();\n    this.renderCanvas(renderContext);\n  }\n\n  private createRenderContext(): MinimapRenderContext {\n    const { canvas, context2D } = this;\n    const nodeTransforms: FlowNodeTransformData[] = this.transformVisibles;\n    const nodeRects: Rectangle[] = nodeTransforms.map((transform) => transform.bounds);\n    const viewRect: Rectangle = this.viewRect();\n    const renderRect: Rectangle = this.renderRect(nodeRects).withPadding({\n      top: this.style.canvasPadding,\n      bottom: this.style.canvasPadding,\n      left: this.style.canvasPadding,\n      right: this.style.canvasPadding,\n    });\n    const canvasRect: Rectangle = Rectangle.enlarge([viewRect, renderRect]);\n\n    const { scale, offset } = this.calculateScaleAndOffset({ canvasRect });\n\n    return {\n      canvas,\n      context2D,\n      nodeRects,\n      canvasRect,\n      viewRect,\n      renderRect,\n      scale,\n      offset,\n    };\n  }\n\n  private renderCanvas(renderContext: MinimapRenderContext) {\n    const { canvas, context2D, nodeRects, viewRect, scale, offset } = renderContext;\n\n    // 清空画布\n    MinimapDraw.clear({ canvas, context: context2D });\n\n    // 设置背景色\n    MinimapDraw.backgroundColor({\n      canvas,\n      context: context2D,\n      color: this.style.canvasBackground,\n    });\n\n    // 绘制视窗\n    MinimapDraw.roundRectangle({\n      context: context2D,\n      rect: this.rectOnCanvas({ rect: viewRect, scale, offset }),\n      color: this.style.viewportBackground,\n      radius: this.style.viewportBorderRadius as number,\n    });\n\n    // 绘制节点\n    nodeRects.forEach((nodeRect: Rectangle) => {\n      MinimapDraw.roundRectangle({\n        context: context2D,\n        rect: this.rectOnCanvas({ rect: nodeRect, scale, offset }),\n        color: this.style.nodeColor as string,\n        radius: this.style.nodeBorderRadius as number,\n        borderWidth: this.style.nodeBorderWidth as number,\n        borderColor: this.style.nodeBorderColor as string,\n      });\n    });\n\n    // 绘制视窗边框\n    MinimapDraw.roundRectangle({\n      context: context2D,\n      rect: this.rectOnCanvas({ rect: viewRect, scale, offset }),\n      color: 'rgba(255, 255, 255, 0)' as string,\n      radius: this.style.viewportBorderRadius as number,\n      borderColor: this.style.viewportBorderColor as string,\n      borderWidth: this.style.viewportBorderWidth as number,\n      borderDashLength: this.style.viewportBorderDashLength as number,\n    });\n\n    // 绘制视窗外的蒙层\n    MinimapDraw.overlay({\n      canvas,\n      context: context2D,\n      offset,\n      scale,\n      rect: viewRect,\n      color: this.style.overlayColor as string,\n    });\n  }\n\n  private calculateScaleAndOffset(params: { canvasRect: Rectangle }): {\n    scale: number;\n    offset: IPoint;\n  } {\n    const { canvasRect } = params;\n    const { width: canvasWidth, height: canvasHeight } = this.canvas;\n\n    // 计算缩放比例\n    const scaleX = canvasWidth / canvasRect.width;\n    const scaleY = canvasHeight / canvasRect.height;\n    const scale = Math.min(scaleX, scaleY);\n\n    // 计算缩放后的渲染区域尺寸\n    const scaledWidth = canvasRect.width * scale;\n    const scaledHeight = canvasRect.height * scale;\n\n    // 计算居中偏移量\n    const centerOffsetX = (canvasWidth - scaledWidth) / 2;\n    const centerOffsetY = (canvasHeight - scaledHeight) / 2;\n\n    // 计算最终偏移量\n    const offset = {\n      x: centerOffsetX / scale - canvasRect.x,\n      y: centerOffsetY / scale - canvasRect.y,\n    };\n\n    return { scale, offset };\n  }\n\n  private get transformVisibles(): FlowNodeTransformData[] {\n    const transformVisible = this.document.getRenderDatas<FlowNodeTransformData>(\n      FlowNodeTransformData,\n      false\n    );\n    return transformVisible.filter((transform) => {\n      const node = transform.entity;\n      // 去除不可见节点\n      if (node.hidden) return false;\n      // 去除根节点\n      if (node.flowNodeType === FlowNodeBaseType.ROOT) return;\n      // 去除非一级节点\n      if (\n        !this.options.enableDisplayAllNodes &&\n        node.parent &&\n        node.parent.flowNodeType !== FlowNodeBaseType.ROOT\n      )\n        return;\n      return true;\n    });\n  }\n\n  private renderRect(rects: Rectangle[]): Rectangle {\n    return Rectangle.enlarge(rects);\n  }\n\n  private viewRect(): Rectangle {\n    const { width, height, scrollX, scrollY, zoom } = this.playgroundConfig.config;\n    return new Rectangle(scrollX / zoom, scrollY / zoom, width / zoom, height / zoom);\n  }\n\n  /** 计算画布坐标系下的矩形 */\n  private rectOnCanvas(params: { rect: Rectangle; scale: number; offset: IPoint }): Rectangle {\n    const { rect, scale, offset } = params;\n    return new Rectangle(\n      (rect.x + offset.x) * scale,\n      (rect.y + offset.y) * scale,\n      rect.width * scale,\n      rect.height * scale\n    );\n  }\n\n  private isPointInRect(params: { point: IPoint; rect: Rectangle }): boolean {\n    const { point, rect } = params;\n    return (\n      point.x >= rect.x &&\n      point.x <= rect.x + rect.width &&\n      point.y >= rect.y &&\n      point.y <= rect.y + rect.height\n    );\n  }\n\n  private addEventListeners(): void {\n    this.canvas.addEventListener('wheel', this.handleWheel);\n    this.canvas.addEventListener('mousedown', this.handleStartDrag);\n    this.canvas.addEventListener('touchstart', this.handleStartDrag, { passive: false });\n    this.canvas.addEventListener('mousemove', this.handleCursor);\n  }\n\n  private removeEventListeners(): void {\n    this.canvas.removeEventListener('wheel', this.handleWheel);\n    this.canvas.removeEventListener('mousedown', this.handleStartDrag);\n    this.canvas.removeEventListener('touchstart', this.handleStartDrag);\n    this.canvas.removeEventListener('mousemove', this.handleCursor);\n  }\n\n  private handleWheel = (event: WheelEvent): void => {};\n\n  private handleStartDrag = (event: MouseEvent | TouchEvent): void => {\n    MouseTouchEvent.preventDefault(event);\n    event.stopPropagation();\n    const renderContext = this.createRenderContext();\n    const { viewRect, scale, offset } = renderContext;\n    const canvasRect = this.canvas.getBoundingClientRect();\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);\n    const mousePoint: IPoint = {\n      x: clientX - canvasRect.left,\n      y: clientY - canvasRect.top,\n    };\n\n    const viewRectOnCanvas = this.rectOnCanvas({\n      rect: viewRect,\n      scale,\n      offset,\n    });\n    if (!this.isPointInRect({ point: mousePoint, rect: viewRectOnCanvas })) {\n      return;\n    }\n    this.isDragging = true;\n    this.dragStart = mousePoint;\n    // click\n    document.addEventListener('mousemove', this.handleDragging);\n    document.addEventListener('mouseup', this.handleEndDrag);\n    // touch\n    document.addEventListener('touchmove', this.handleDragging, { passive: false });\n    document.addEventListener('touchend', this.handleEndDrag);\n    document.addEventListener('touchcancel', this.handleEndDrag);\n  };\n\n  private handleDragging = (event: MouseEvent | TouchEvent): void => {\n    if (!this.isDragging || !this.dragStart) return;\n    MouseTouchEvent.preventDefault(event);\n    event.stopPropagation();\n\n    const renderContext = this.createRenderContext();\n    const { scale } = renderContext;\n    const canvasRect = this.canvas.getBoundingClientRect();\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);\n    const mouseX = clientX - canvasRect.left;\n    const mouseY = clientY - canvasRect.top;\n\n    const deltaX = (mouseX - this.dragStart.x) / scale;\n    const deltaY = (mouseY - this.dragStart.y) / scale;\n\n    this.updateScrollPosition(deltaX, deltaY);\n\n    this.dragStart = { x: mouseX, y: mouseY };\n    this.render();\n  };\n\n  private handleEndDrag = (event: MouseEvent | TouchEvent): void => {\n    MouseTouchEvent.preventDefault(event);\n    event.stopPropagation();\n    // click\n    document.removeEventListener('mousemove', this.handleDragging);\n    document.removeEventListener('mouseup', this.handleEndDrag);\n    // touch\n    document.removeEventListener('touchmove', this.handleDragging);\n    document.removeEventListener('touchend', this.handleEndDrag);\n    document.removeEventListener('touchcancel', this.handleEndDrag);\n    this.isDragging = false;\n    this.dragStart = undefined;\n    this.setActivate(this.isMouseInCanvas(event));\n  };\n\n  private handleCursor = (event: MouseEvent): void => {\n    if (!this.activated) return;\n\n    const renderContext = this.createRenderContext();\n    const { viewRect, scale, offset } = renderContext;\n    const canvasRect = this.canvas.getBoundingClientRect();\n    const mousePoint: IPoint = {\n      x: event.clientX - canvasRect.left,\n      y: event.clientY - canvasRect.top,\n    };\n\n    const viewRectOnCanvas = this.rectOnCanvas({\n      rect: viewRect,\n      scale,\n      offset,\n    });\n\n    if (this.isPointInRect({ point: mousePoint, rect: viewRectOnCanvas })) {\n      // 鼠标在视窗框内\n      this.canvas.style.cursor = 'grab';\n    } else {\n      // 鼠标在视窗框外但在画布内\n      this.canvas.style.cursor = 'default';\n    }\n  };\n\n  private isMouseInCanvas(event: MouseEvent | TouchEvent): boolean {\n    const canvasRect = this.canvas.getBoundingClientRect();\n    const { clientX, clientY } = MouseTouchEvent.getEventCoord(event);\n    return (\n      clientX >= canvasRect.left &&\n      clientX <= canvasRect.right &&\n      clientY >= canvasRect.top &&\n      clientY <= canvasRect.bottom\n    );\n  }\n\n  private updateScrollPosition(deltaX: number, deltaY: number): void {\n    const { scrollX, scrollY, zoom } = this.playgroundConfig.config;\n    this.playgroundConfig.updateConfig({\n      scrollX: scrollX + deltaX * zoom,\n      scrollY: scrollY + deltaY * zoom,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/src/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { CSSProperties } from 'react';\n\nimport type { IPoint, Rectangle } from '@flowgram.ai/utils';\n\nexport interface MinimapCanvasStyle {\n  canvasWidth: number;\n  canvasHeight: number;\n  canvasPadding: number;\n  canvasBackground: string;\n  canvasBorderRadius: number | undefined;\n  viewportBackground: string;\n  viewportBorderRadius: number | undefined;\n  viewportBorderColor: string;\n  viewportBorderWidth: number;\n  viewportBorderDashLength: number | undefined;\n  nodeColor: string;\n  nodeBorderRadius: number | undefined;\n  nodeBorderWidth: number;\n  nodeBorderColor: string | undefined;\n  overlayColor: string;\n}\n\nexport interface MinimapInactiveStyle {\n  scale: number;\n  opacity: number;\n  translateX: number;\n  translateY: number;\n}\n\nexport interface MinimapServiceOptions {\n  canvasStyle: Partial<MinimapCanvasStyle>;\n  canvasClassName: string;\n  enableDisplayAllNodes: boolean;\n  activeThrottleTime: number;\n  inactiveThrottleTime: number;\n}\n\nexport interface MinimapLayerOptions {\n  disableLayer?: boolean;\n  panelStyles?: CSSProperties;\n  containerStyles?: CSSProperties;\n  inactiveStyle?: Partial<MinimapInactiveStyle>;\n}\n\nexport interface CreateMinimapPluginOptions\n  extends MinimapLayerOptions,\n    Partial<MinimapServiceOptions> {}\n\nexport interface MinimapRenderContext {\n  canvas: HTMLCanvasElement;\n  context2D: CanvasRenderingContext2D;\n  nodeRects: Rectangle[];\n  viewRect: Rectangle;\n  renderRect: Rectangle;\n  canvasRect: Rectangle;\n  scale: number;\n  offset: IPoint;\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/minimap-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/node-core-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/node\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/create-node-core-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FormModelV2 } from '@flowgram.ai/node';\nimport {\n  createNodeContainerModules,\n  createNodeEntityDatas,\n  FlowNodeFormData,\n  FormManager,\n  NodeManager,\n} from '@flowgram.ai/form-core';\nimport { FlowDocument, FlowNodeEntity } from '@flowgram.ai/document';\nimport { definePluginCreator, EntityManager } from '@flowgram.ai/core';\n\nimport { registerNodeMaterial } from './utils';\nimport { NodeEngineMaterialOptions } from './types';\n\nexport interface NodeCorePluginOptions {\n  materials?: NodeEngineMaterialOptions;\n}\n\nexport const createNodeCorePlugin = definePluginCreator<NodeCorePluginOptions>({\n  onInit(ctx, options) {\n    /**\n     * 注册NodeEngine 相关 EntityData 到flowDocument\n     */\n    ctx.get<FlowDocument>(FlowDocument).registerNodeDatas(...createNodeEntityDatas());\n\n    const formModelFactory = (entity: FlowNodeEntity) => new FormModelV2(entity);\n    const entityManager = ctx.get<EntityManager>(EntityManager);\n    entityManager.registerEntityData(\n      FlowNodeFormData,\n      () =>\n        ({\n          formModelFactory: formModelFactory,\n        } as any)\n    );\n\n    if (!options.materials) {\n      return;\n    }\n\n    const nodeManager = ctx.get<NodeManager>(NodeManager);\n    const formManager = ctx.get<FormManager>(FormManager);\n\n    if (!nodeManager || !formManager) {\n      throw new Error('NodeCorePlugin Error: nodeManager or formManager not found');\n    }\n\n    registerNodeMaterial({ nodeManager, formManager, material: options.materials! });\n  },\n  onDispose(ctx) {\n    ctx.get<FormManager>(FormManager)?.dispose();\n  },\n  containerModules: createNodeContainerModules(),\n  // onBind: ({ bind }) => {\n  //   bindContributions(bind, FormNodeContribution, [NodeContribution]);\n  // },\n});\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/form-node-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { NodeContribution, NodeManager, PLUGIN_KEY } from '@flowgram.ai/form-core';\n\nimport { FormRender } from './form-render';\n\n@injectable()\nexport class FormNodeContribution implements NodeContribution {\n  onRegister(nodeManager: NodeManager) {\n    nodeManager.registerPluginRender(PLUGIN_KEY.FORM, FormRender);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/form-render.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { FlowNodeFormData, FormModel } from '@flowgram.ai/form-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { PlaygroundContext, useRefresh } from '@flowgram.ai/core';\n\ninterface FormRenderProps {\n  node: FlowNodeEntity;\n  playgroundContext?: PlaygroundContext;\n}\n\nfunction getFormModelFromNode(node: FlowNodeEntity) {\n  return node.getData(FlowNodeFormData)?.getFormModel<FormModel>();\n}\n\nexport function FormRender({ node }: FormRenderProps): any {\n  const refresh = useRefresh();\n  const formModel = getFormModelFromNode(node);\n\n  useEffect(() => {\n    const disposable = formModel?.onInitialized(() => {\n      refresh();\n    });\n    return () => {\n      disposable.dispose();\n    };\n  }, [formModel]);\n\n  return formModel?.initialized ? formModel.render() : null;\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-node-core-plugin';\nexport * from './utils';\nexport * from './types';\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DecoratorExtension,\n  EffectExtension,\n  NodeErrorRender,\n  NodePlaceholderRender,\n  SetterExtension,\n  ValidationExtension,\n} from '@flowgram.ai/form-core';\n\nexport interface NodeEngineMaterialOptions {\n  /**\n   * 节点项的渲染物料\n   */\n  setters?: SetterExtension[];\n  /**\n   * 节点项的渲染装饰器物料\n   */\n  decorators?: DecoratorExtension[];\n  /**\n   * 副作用物料\n   */\n  effects?: EffectExtension[];\n  /**\n   * 校验物料\n   */\n  validators?: ValidationExtension[];\n  /**\n   * 节点内部报错的渲染组件\n   */\n  nodeErrorRender?: NodeErrorRender;\n  /**\n   * 节点无内容时的渲染组件\n   */\n  nodePlaceholderRender?: NodePlaceholderRender;\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  DecoratorAbility,\n  DecoratorExtension,\n  EffectAbility,\n  EffectExtension,\n  FormManager,\n  NodeManager,\n  registerNodeErrorRender,\n  registerNodePlaceholderRender,\n  SetterAbility,\n  SetterExtension,\n  ValidationAbility,\n  ValidationExtension,\n} from '@flowgram.ai/form-core';\n\nimport { NodeEngineMaterialOptions } from './types';\n\ninterface RegisterNodeMaterialProps {\n  nodeManager: NodeManager;\n  formManager: FormManager;\n  material: NodeEngineMaterialOptions;\n}\n\nexport function registerNodeMaterial({\n  nodeManager,\n  formManager,\n  material,\n}: RegisterNodeMaterialProps) {\n  const {\n    setters = [],\n    decorators = [],\n    effects = [],\n    validators = [],\n    nodeErrorRender,\n    nodePlaceholderRender,\n  } = material;\n\n  if (nodeErrorRender) {\n    registerNodeErrorRender(nodeManager, nodeErrorRender);\n  }\n  if (nodePlaceholderRender) {\n    registerNodePlaceholderRender(nodeManager, nodePlaceholderRender);\n  }\n  setters.forEach((setter: SetterExtension) => {\n    formManager.registerAbilityExtension(SetterAbility.type, setter);\n  });\n  decorators.forEach((decorator: DecoratorExtension) => {\n    formManager.registerAbilityExtension(DecoratorAbility.type, decorator);\n  });\n  effects.forEach((effect: EffectExtension) => {\n    formManager.registerAbilityExtension(EffectAbility.type, effect);\n  });\n  validators.forEach((validator: ValidationExtension) => {\n    formManager.registerAbilityExtension(ValidationAbility.type, validator);\n  });\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/node-core-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/node-variable-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/node\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@flowgram.ai/variable-plugin\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/bezier-js\": \"4.1.3\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/components/PrivateScopeProvider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { FlowNodeVariableData, type Scope, ScopeProvider } from '@flowgram.ai/variable-plugin';\nimport { useEntityFromContext } from '@flowgram.ai/core';\n\ninterface VariableProviderProps {\n  children: React.ReactElement;\n}\n\n/**\n * PrivateScopeProvider provides the private scope to its children via context.\n */\nexport const PrivateScopeProvider = ({ children }: VariableProviderProps) => {\n  const node = useEntityFromContext();\n\n  const privateScope: Scope = useMemo(() => {\n    const variableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);\n    if (!variableData.private) {\n      variableData.initPrivate();\n    }\n    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n    return variableData.private!;\n  }, [node]);\n\n  return <ScopeProvider scope={privateScope}>{children}</ScopeProvider>;\n};\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/components/PublicScopeProvider.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { useMemo } from 'react';\n\nimport { FlowNodeVariableData, type Scope, ScopeProvider } from '@flowgram.ai/variable-plugin';\nimport { useEntityFromContext } from '@flowgram.ai/core';\n\ninterface VariableProviderProps {\n  children: React.ReactElement;\n}\n\n/**\n * PublicScopeProvider provides the public scope to its children via context.\n */\nexport const PublicScopeProvider = ({ children }: VariableProviderProps) => {\n  const node = useEntityFromContext();\n\n  const publicScope: Scope = useMemo(() => node.getData(FlowNodeVariableData).public, [node]);\n\n  return <ScopeProvider scope={publicScope}>{children}</ScopeProvider>;\n};\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/create-node-variable-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// import { FormManager } from '@flowgram.ai/form-core';\nimport { NodeManager } from '@flowgram.ai/form-core';\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { withNodeVariables } from './with-node-variables';\n\n// import { withNodeVariables } from './with-node-variables';\n\nexport const createNodeVariablePlugin = definePluginCreator({\n  onInit(ctx) {\n    const nodeManager = ctx.get<NodeManager>(NodeManager);\n    nodeManager.registerNodeRenderHoc(withNodeVariables);\n  },\n});\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/form-v2/create-provider-effect.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeVariableData, type Scope, ASTKind } from '@flowgram.ai/variable-plugin';\nimport { DataEvent, type Effect, type EffectOptions } from '@flowgram.ai/node';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { type VariableProviderAbilityOptions } from '../types';\n\n/**\n * 根据 VariableProvider 生成 FormV2 的 Effect\n * @param options\n * @returns\n */\nexport function createEffectFromVariableProvider(\n  options: VariableProviderAbilityOptions\n): EffectOptions[] {\n  const getScope = (node: FlowNodeEntity): Scope => {\n    const variableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);\n\n    if (options.private || options.scope === 'private') {\n      return variableData.initPrivate();\n    }\n    return variableData.public;\n  };\n\n  const transformValueToAST: Effect = ({ value, name, context, formValues, form }) => {\n    if (!context) {\n      return;\n    }\n    const { node } = context;\n    const scope = getScope(node);\n\n    const parsedValue = options.parse(value, {\n      node,\n      scope,\n      options,\n      name,\n      formValues,\n      form,\n    });\n\n    // Fix: When parsedValue is not an array, transform it to array\n    scope.ast.set(options.namespace || name || '', {\n      kind: ASTKind.VariableDeclarationList,\n      declarations: Array.isArray(parsedValue) ? parsedValue : [parsedValue],\n    });\n  };\n\n  return [\n    {\n      event: DataEvent.onValueInit,\n      effect: ((params) => {\n        const { context } = params;\n\n        const scope = getScope(context.node);\n        const disposable = options.onInit?.({\n          node: context.node,\n          scope,\n          options,\n          name: params.name,\n          formValues: params.formValues,\n          form: params.form,\n        });\n\n        transformValueToAST(params);\n\n        return () => {\n          disposable?.dispose();\n        };\n      }) as Effect,\n    },\n    {\n      event: DataEvent.onValueChange,\n      effect: ((params) => {\n        transformValueToAST(params);\n      }) as Effect,\n    },\n  ];\n}\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/form-v2/create-variable-provider-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DataEvent, defineFormPluginCreator } from '@flowgram.ai/node';\n\nexport const createVariableProviderPlugin = defineFormPluginCreator({\n  name: 'VariableProviderPlugin',\n  onInit: (ctx, opts) => {\n    // todo\n    // console.log('>>> VariableProviderPlugin init', ctx, opts);\n  },\n  onSetupFormMeta({ mergeEffect }) {\n    mergeEffect({\n      arr: [\n        {\n          event: DataEvent.onValueInitOrChange,\n          effect: () => {\n            // todo\n            // console.log('>>> VariableProviderPlugin effect triggered');\n          },\n        },\n      ],\n    });\n  },\n  onDispose: (ctx, opts) => {\n    // todo\n    // console.log('>>> VariableProviderPlugin dispose', ctx, opts);\n  },\n});\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-node-variable-plugin';\nexport {\n  VariableProviderAbilityOptions,\n  VariableConsumerAbilityOptions,\n  VariableAbilityParseContext,\n} from './types';\nexport { PrivateScopeProvider } from './components/PrivateScopeProvider';\nexport { PublicScopeProvider } from './components/PublicScopeProvider';\nexport { createEffectFromVariableProvider } from './form-v2/create-provider-effect';\nexport { createVariableProviderPlugin } from './form-v2/create-variable-provider-plugin';\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Scope,\n  type ASTNodeJSON,\n  type VariableDeclarationJSON,\n} from '@flowgram.ai/variable-plugin';\nimport { Disposable } from '@flowgram.ai/utils';\nimport { EffectFuncProps } from '@flowgram.ai/node';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nexport interface VariableAbilityCommonContext {\n  node: FlowNodeEntity; // 节点\n  scope: Scope; // 作用域\n  options: VariableAbilityOptions;\n  name: string; // 表单字段名\n  formValues: EffectFuncProps['formValues']; // 表单值\n  form: EffectFuncProps['form'];\n}\n\nexport interface VariableAbilityInitCtx extends VariableAbilityCommonContext {}\n\nexport interface VariableAbilityOptions {\n  /**\n   * @deprecated use scope: 'private'\n   */\n  private?: boolean;\n  // 生成 AST 在抽象语法树中的索引，默认用 formItem 的 Path 作为 namespace\n  namespace?: string;\n  // 输出变量的作用域类型，默认为 public\n  scope?: 'private' | 'public';\n  // 初始化，可以进行额外的操作监听\n  onInit?: (ctx: VariableAbilityInitCtx) => Disposable | undefined;\n  // 扩展字段，可以在 ability onInit 时进行一些额外操作\n  [key: string]: any;\n}\n\nexport interface VariableAbilityParseContext extends VariableAbilityCommonContext {}\n\nexport interface VariableProviderAbilityOptions<V = any> extends VariableAbilityOptions {\n  // 解析变量协议\n  parse: (\n    v: V,\n    ctx: VariableAbilityParseContext\n  ) => VariableDeclarationJSON | VariableDeclarationJSON[];\n}\n\nexport interface VariableConsumerAbilityOptions<V = any> extends VariableAbilityOptions {\n  // 解析变量协议\n  parse: (v: V, ctx: VariableAbilityParseContext) => ASTNodeJSON | undefined;\n}\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/src/with-node-variables.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { NodeRenderHoc, type NodeRenderProps } from '@flowgram.ai/form-core';\n\nimport { PublicScopeProvider } from './components/PublicScopeProvider';\n\n// eslint-disable-next-line react/display-name\nexport const withNodeVariables: NodeRenderHoc = (Component) => (props: NodeRenderProps) =>\n  (\n    <PublicScopeProvider>\n      <Component {...props} />\n    </PublicScopeProvider>\n  );\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/node-variable-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n    'react/prop-types': 'off'\n  },\n});\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/panel-manager-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"inversify\": \"^6.0.1\",\n    \"clsx\": \"^1.1.1\",\n    \"nanoid\": \"^5.0.9\",\n    \"zustand\": \"^4.5.5\",\n    \"use-sync-external-store\": \"^1.6.0\",\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"react\": \"^18\",\n    \"react-dom\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react\": {\n      \"optional\": true\n    }\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/panel-layer/css.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const globalCSS = `\n  .gedit-flow-panel-layer-wrap * {\n    box-sizing: border-box;\n  }\n  .gedit-flow-panel-layer-wrap {\n    position: absolute;\n    top: 0;\n    left: 0;\n    display: flex;\n    width: 100%;\n    height: 100%;\n    overflow: hidden;\n  }\n  .gedit-flow-panel-layer-wrap-docked {\n\n  }\n  .gedit-flow-panel-layer-wrap-floating {\n    pointer-events: none;\n  }\n\n  .gedit-flow-panel-left-area {\n    width: 100%;\n    min-width: 0;\n    flex-grow: 0;\n    flex-shrink: 1;\n    display: flex;\n    flex-direction: column;\n  }\n  .gedit-flow-panel-right-area {\n    height: 100%;\n    flex-grow: 1;\n    flex-shrink: 0;\n    min-width: 0;\n    display: flex;\n    max-width: 100%;\n  }\n\n  .gedit-flow-panel-main-area {\n    position: relative;\n    overflow: hidden;\n    flex-grow: 0;\n    flex-shrink: 1;\n    width: 100%;\n    height: 100%;\n  }\n  .gedit-flow-panel-bottom-area {\n    flex-grow: 1;\n    flex-shrink: 0;\n    width: 100%;\n    min-height: 0;\n  }\n  .gedit-flow-panel-wrap {\n    pointer-events: auto;\n    overflow: auto;\n    position: relative;\n  }\n  .gedit-flow-panel-wrap.panel-horizontal {\n    height: 100%;\n  }\n  .gedit-flow-panel-wrap.panel-vertical {\n    width: 100%;\n  }\n`;\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/panel-layer/docked-panel-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport { PanelLayer, PanelLayerProps } from './panel-layer';\n\nexport type DockedPanelLayerProps = Omit<PanelLayerProps, 'mode'>;\n\nexport const DockedPanelLayer: React.FC<DockedPanelLayerProps> = (props) => (\n  <PanelLayer mode=\"docked\" {...props} />\n);\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/panel-layer/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PanelLayer, type PanelLayerProps } from './panel-layer';\nexport { DockedPanelLayer, type DockedPanelLayerProps } from './docked-panel-layer';\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/panel-layer/panel-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nimport clsx from 'clsx';\n\nimport { useGlobalCSS } from '../../hooks/use-global-css';\nimport { PanelArea } from './panel';\nimport { globalCSS } from './css';\n\nexport type PanelLayerProps = React.PropsWithChildren<{\n  /** 模式：悬浮｜挤压 */\n  mode?: 'floating' | 'docked';\n  className?: string;\n  style?: React.CSSProperties;\n}>;\n\nexport const PanelLayer: React.FC<PanelLayerProps> = ({\n  mode = 'floating',\n  className,\n  style,\n  children,\n}) => {\n  useGlobalCSS({\n    cssText: globalCSS,\n    id: 'flow-panel-layer-css',\n  });\n\n  return (\n    <div\n      className={clsx(\n        'gedit-flow-panel-layer-wrap',\n        mode === 'docked' && 'gedit-flow-panel-layer-wrap-docked',\n        mode === 'floating' && 'gedit-flow-panel-layer-wrap-floating',\n        className\n      )}\n      style={style}\n    >\n      <div className=\"gedit-flow-panel-left-area\">\n        <div className=\"gedit-flow-panel-main-area\">{children}</div>\n        <div className=\"gedit-flow-panel-bottom-area\">\n          <PanelArea area={mode === 'docked' ? 'docked-bottom' : 'bottom'} />\n        </div>\n      </div>\n      <div className=\"gedit-flow-panel-right-area\">\n        <PanelArea area={mode === 'docked' ? 'docked-right' : 'right'} />\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/panel-layer/panel.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nconst { useEffect, useState, useRef } = React;\n\nimport { clsx } from 'clsx';\n\nimport { Area } from '../../types';\nimport { PanelEntity } from '../../services/panel-factory';\nimport { usePanelManager } from '../../hooks/use-panel-manager';\nimport { usePanelStore } from '../../hooks/use-panel';\nimport { PanelContext } from '../../contexts';\n\nconst PanelItem: React.FC<{ panel: PanelEntity; hidden?: boolean }> = ({ panel, hidden }) => {\n  const panelManager = usePanelManager();\n  const ref = useRef<HTMLDivElement>(null);\n\n  const isHorizontal = ['right', 'docked-right'].includes(panel.area);\n\n  const { size, fullscreen } = usePanelStore((s) => ({\n    size: s.size,\n    fullscreen: s.fullscreen,\n  }));\n\n  const [layerSize, setLayerSize] = useState(size);\n\n  const currentSize = fullscreen ? layerSize : size;\n\n  const sizeStyle = isHorizontal ? { width: currentSize } : { height: currentSize };\n  const handleResize = (next: number) => {\n    let nextSize = next;\n    if (typeof panel.factory.maxSize === 'number' && nextSize > panel.factory.maxSize) {\n      nextSize = panel.factory.maxSize;\n    } else if (typeof panel.factory.minSize === 'number' && nextSize < panel.factory.minSize) {\n      nextSize = panel.factory.minSize;\n    }\n    panel.store.setState({ size: nextSize });\n  };\n\n  useEffect(() => {\n    /** The set size may be illegal and needs to be updated according to the real element rendered for the first time. */\n    if (ref.current && !fullscreen) {\n      const { width, height } = ref.current.getBoundingClientRect();\n      const realSize = isHorizontal ? width : height;\n      panel.store.setState({ size: realSize });\n    }\n  }, [fullscreen]);\n\n  useEffect(() => {\n    if (!fullscreen) {\n      return;\n    }\n    const layer = panel.layer;\n    if (!layer) {\n      return;\n    }\n    const observer = new ResizeObserver(([entry]) => {\n      const { width, height } = entry.contentRect;\n      setLayerSize(isHorizontal ? width : height);\n    });\n    observer.observe(layer);\n    return () => observer.disconnect();\n  }, [fullscreen]);\n\n  return (\n    <div\n      className={clsx(\n        'gedit-flow-panel-wrap',\n        isHorizontal ? 'panel-horizontal' : 'panel-vertical'\n      )}\n      key={panel.id}\n      ref={ref}\n      style={{\n        display: hidden ? 'none' : 'block',\n        ...panel.factory.style,\n        ...panel.config.style,\n        ...sizeStyle,\n      }}\n    >\n      {panel.resizable &&\n        panelManager.config.resizeBarRender({\n          size,\n          direction: isHorizontal ? 'vertical' : 'horizontal',\n          onResize: handleResize,\n        })}\n      {panel.renderer}\n    </div>\n  );\n};\n\nexport const PanelArea: React.FC<{ area: Area }> = ({ area }) => {\n  const panelManager = usePanelManager();\n  const [panels, setPanels] = useState(panelManager.getPanels(area));\n\n  useEffect(() => {\n    let pending: number;\n\n    function startTransition(fn: () => void) {\n      clearTimeout(pending);\n      pending = setTimeout(fn, 0);\n    }\n    const dispose = panelManager.onPanelsChange(() => {\n      const r: any = { ...React };\n\n      const start = r['startTransition'] as undefined | ((cb: () => void) => void);\n      if (typeof start === 'function') {\n        start(() => setPanels(panelManager.getPanels(area)));\n      } else {\n        startTransition(() => setPanels(panelManager.getPanels(area)));\n      }\n    });\n    return () => dispose.dispose();\n  }, []);\n\n  return (\n    <>\n      {panels.map((panel) => (\n        <PanelContext.Provider value={panel} key={panel.id}>\n          <PanelItem panel={panel} hidden={panel.keepDOM && !panel.visible} />\n        </PanelContext.Provider>\n      ))}\n    </>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/components/resize-bar/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport * as React from 'react';\n\nconst { useRef, useState } = React;\n\ninterface Props {\n  size: number;\n  direction?: 'vertical' | 'horizontal';\n  onResize: (w: number) => void;\n}\n\nexport const ResizeBar: React.FC<Props> = ({ onResize, size, direction }) => {\n  const currentPoint = useRef<null | number>(null);\n  const [isDragging, setIsDragging] = useState(false);\n  const [isHovered, setIsHovered] = useState(false);\n\n  const isVertical = direction === 'vertical';\n\n  return (\n    <div\n      onMouseDown={(e) => {\n        currentPoint.current = isVertical ? e.clientX : e.clientY;\n        e.stopPropagation();\n        e.preventDefault();\n        setIsDragging(true);\n        const mouseUp = () => {\n          currentPoint.current = null;\n          document.body.removeEventListener('mouseup', mouseUp);\n          document.body.removeEventListener('mousemove', mouseMove);\n          setIsDragging(false);\n        };\n        const mouseMove = (e: MouseEvent) => {\n          const delta = currentPoint.current! - (isVertical ? e.clientX : e.clientY);\n          onResize(size + delta);\n        };\n        document.body.addEventListener('mouseup', mouseUp);\n        document.body.addEventListener('mousemove', mouseMove);\n      }}\n      onMouseEnter={() => setIsHovered(true)}\n      onMouseLeave={() => setIsHovered(false)}\n      style={{\n        position: 'absolute',\n        top: 0,\n        left: 0,\n        zIndex: 999,\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center',\n        pointerEvents: 'auto',\n        ...(isVertical\n          ? {\n              cursor: 'ew-resize',\n              height: '100%',\n              marginLeft: -5,\n              width: 10,\n            }\n          : {\n              cursor: 'ns-resize',\n              width: '100%',\n              marginTop: -5,\n              height: 10,\n            }),\n      }}\n    >\n      <div\n        style={{\n          ...(isVertical\n            ? {\n                width: 3,\n                height: '100%',\n              }\n            : {\n                height: 3,\n                width: '100%',\n              }),\n          backgroundColor: isDragging || isHovered ? 'var(--g-playground-line)' : 'transparent',\n        }}\n      />\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/contexts.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createContext } from 'react';\n\nimport type { PanelEntity } from './services/panel-factory';\n\nexport const PanelContext = createContext({} as PanelEntity);\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/create-panel-manager-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport {\n  PanelEntityFactory,\n  PanelEntity,\n  PanelEntityFactoryConstant,\n  PanelEntityConfigConstant,\n} from './services/panel-factory';\nimport { defineConfig } from './services/panel-config';\nimport {\n  PanelManager,\n  PanelManagerConfig,\n  PanelLayer,\n  PanelRestore,\n  PanelRestoreImpl,\n} from './services';\n\nexport const createPanelManagerPlugin = definePluginCreator<Partial<PanelManagerConfig>>({\n  onBind: ({ bind }, opt) => {\n    bind(PanelManager).to(PanelManager).inSingletonScope();\n    bind(PanelRestore).to(PanelRestoreImpl).inSingletonScope();\n    bind(PanelManagerConfig).toConstantValue(defineConfig(opt));\n    bind(PanelEntityFactory).toFactory(\n      (context) =>\n        ({\n          factory,\n          config,\n        }: {\n          factory: PanelEntityFactoryConstant;\n          config: PanelEntityConfigConstant;\n        }) => {\n          const container = context.container.createChild();\n          container.bind(PanelEntityFactoryConstant).toConstantValue(factory);\n          container.bind(PanelEntityConfigConstant).toConstantValue(config);\n          const panel = container.resolve(PanelEntity);\n          panel.init();\n          return panel;\n        }\n    );\n  },\n  onInit(ctx) {\n    ctx.playground.registerLayer(PanelLayer);\n    const panelManager = ctx.container.get<PanelManager>(PanelManager);\n    panelManager.init();\n  },\n});\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/hooks/use-global-css.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\ninterface UseGlobalCSSOptions {\n  cssText: string;\n  id: string;\n  cleanup?: boolean;\n}\n\nexport const useGlobalCSS = ({ cssText, id, cleanup }: UseGlobalCSSOptions) => {\n  useEffect(() => {\n    /** SSR safe */\n    if (typeof document === 'undefined') return;\n\n    if (document.getElementById(id)) return;\n\n    const style = document.createElement('style');\n    style.id = id;\n    style.textContent = cssText;\n    document.head.appendChild(style);\n\n    return () => {\n      const existing = document.getElementById(id);\n      if (existing && cleanup) existing.remove();\n    };\n  }, [id]);\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/hooks/use-panel-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService } from '@flowgram.ai/core';\n\nimport { PanelManager } from '../services/panel-manager';\n\nexport const usePanelManager = () => useService<PanelManager>(PanelManager);\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/hooks/use-panel.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { useStoreWithEqualityFn } from 'zustand/traditional';\nimport { shallow } from 'zustand/shallow';\n\nimport { PanelEntityState } from '../services/panel-factory';\nimport { PanelContext } from '../contexts';\n\nexport const usePanel = () => useContext(PanelContext);\n\nexport const usePanelStore = <T>(selector: (s: PanelEntityState) => T) => {\n  const panel = usePanel();\n  return useStoreWithEqualityFn(panel.store, selector, shallow);\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/** create plugin function */\nexport { createPanelManagerPlugin } from './create-panel-manager-plugin';\n\n/** services */\nexport { PanelManager, PanelRestore, type PanelManagerConfig } from './services';\n\n/** react hooks */\nexport { usePanelManager } from './hooks/use-panel-manager';\nexport { usePanel } from './hooks/use-panel';\n\nexport { DockedPanelLayer, type DockedPanelLayerProps } from './components/panel-layer';\nexport { ResizeBar } from './components/resize-bar';\n\n/** types */\nexport type { Area, PanelFactory } from './types';\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { PanelManager } from './panel-manager';\nexport { PanelManagerConfig } from './panel-config';\nexport { PanelLayer } from './panel-layer';\nexport { PanelRestore, PanelRestoreImpl } from './panel-restore';\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/panel-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PluginContext } from '@flowgram.ai/core';\n\nimport type { PanelFactory, PanelConfig } from '../types';\nimport { ResizeBar } from '../components/resize-bar';\nimport type { PanelLayerProps } from '../components/panel-layer';\n\nexport interface PanelManagerConfig {\n  factories: PanelFactory<any>[];\n  right: PanelConfig;\n  bottom: PanelConfig;\n  dockedRight: PanelConfig;\n  dockedBottom: PanelConfig;\n  /** Resizable, and multi-panel options mutually exclusive */\n  autoResize: boolean;\n  layerProps: PanelLayerProps;\n  resizeBarRender: ({\n    size,\n  }: {\n    size: number;\n    direction?: 'vertical' | 'horizontal';\n    onResize: (size: number) => void;\n  }) => React.ReactNode;\n  getPopupContainer: (ctx: PluginContext) => HTMLElement; // default playground.node.parentElement\n}\n\nexport const PanelManagerConfig = Symbol('PanelManagerConfig');\n\nexport const defineConfig = (config: Partial<PanelManagerConfig>) => {\n  const defaultConfig: PanelManagerConfig = {\n    right: {\n      max: 1,\n    },\n    bottom: {\n      max: 1,\n    },\n    dockedRight: {\n      max: 1,\n    },\n    dockedBottom: {\n      max: 1,\n    },\n    factories: [],\n    autoResize: true,\n    layerProps: {},\n    resizeBarRender: ResizeBar,\n    getPopupContainer: (ctx: PluginContext) => ctx.playground.node.parentNode as HTMLElement,\n  };\n  return {\n    ...defaultConfig,\n    ...config,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/panel-factory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createStore, StoreApi } from 'zustand/vanilla';\nimport { nanoid } from 'nanoid';\nimport { inject, injectable } from 'inversify';\n\nimport type { PanelFactory, PanelEntityConfig, Area } from '../types';\nimport { PanelRestore } from './panel-restore';\nimport { PanelManagerConfig } from './panel-config';\nimport { merge } from '../utils';\n\nexport const PanelEntityFactory = Symbol('PanelEntityFactory');\nexport type PanelEntityFactory = (options: {\n  factory: PanelEntityFactoryConstant;\n  config: PanelEntityConfigConstant;\n}) => PanelEntity;\n\nexport const PanelEntityFactoryConstant = Symbol('PanelEntityFactoryConstant');\nexport type PanelEntityFactoryConstant = PanelFactory<any>;\nexport const PanelEntityConfigConstant = Symbol('PanelEntityConfigConstant');\nexport type PanelEntityConfigConstant = PanelEntityConfig<any> & {\n  area: Area;\n};\n\nconst PANEL_SIZE_DEFAULT = 400;\n\nexport interface PanelEntityState {\n  size: number;\n  fullscreen: boolean;\n  visible: boolean;\n}\n\n@injectable()\nexport class PanelEntity {\n  @inject(PanelRestore) restore: PanelRestore;\n\n  /** 面板工厂 */\n  @inject(PanelEntityFactoryConstant) public factory: PanelEntityFactoryConstant;\n\n  @inject(PanelEntityConfigConstant) public config: PanelEntityConfigConstant;\n\n  @inject(PanelManagerConfig) readonly globalConfig: PanelManagerConfig;\n\n  private initialized = false;\n\n  /** 实例唯一标识 */\n  id: string = nanoid();\n\n  /** 渲染缓存 */\n  node: React.ReactNode = null;\n\n  store: StoreApi<PanelEntityState>;\n\n  get area() {\n    return this.config.area;\n  }\n\n  get mode() {\n    return this.config.area.startsWith('docked') ? 'docked' : 'floating';\n  }\n\n  get key() {\n    return this.factory.key;\n  }\n\n  get renderer() {\n    if (!this.node) {\n      this.node = this.factory.render(this.config.props);\n    }\n    return this.node;\n  }\n\n  get fullscreen() {\n    return this.store.getState().fullscreen;\n  }\n\n  set fullscreen(next: boolean) {\n    this.store.setState({ fullscreen: next });\n  }\n\n  get resizable() {\n    if (this.fullscreen) {\n      return false;\n    }\n    return this.factory.resize !== undefined ? this.factory.resize : this.globalConfig.autoResize;\n  }\n\n  get keepDOM() {\n    return this.factory.keepDOM;\n  }\n\n  get visible() {\n    return this.store.getState().visible;\n  }\n\n  set visible(next: boolean) {\n    this.store.setState({ visible: next });\n  }\n\n  get layer() {\n    return document.querySelector(\n      this.mode ? '.gedit-flow-panel-layer-wrap-docked' : '.gedit-flow-panel-layer-wrap-floating'\n    );\n  }\n\n  init() {\n    if (this.initialized) {\n      return;\n    }\n    this.initialized = true;\n    const cache = this.restore.restore<PanelEntityState>(this.key);\n\n    const initialState = merge<PanelEntityState>(\n      {\n        size: this.config.defaultSize,\n        fullscreen: this.config.fullscreen,\n      },\n      cache ? cache : {},\n      {\n        size: this.factory.defaultSize || PANEL_SIZE_DEFAULT,\n        fullscreen: this.factory.fullscreen || false,\n        ...(this.factory.keepDOM ? { visible: true } : {}),\n      }\n    );\n\n    this.store = createStore<PanelEntityState>(() => initialState);\n  }\n\n  mergeState() {}\n\n  dispose() {\n    this.restore.store(this.key, this.store.getState());\n  }\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/panel-layer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport ReactDOM from 'react-dom';\nimport { createElement } from 'react';\n\nimport { injectable, inject } from 'inversify';\nimport { domUtils, Disposable } from '@flowgram.ai/utils';\nimport { Layer, PluginContext } from '@flowgram.ai/core';\n\nimport { PanelLayer as PanelLayerComp } from '../components/panel-layer';\nimport { PanelManagerConfig } from './panel-config';\n\n@injectable()\nexport class PanelLayer extends Layer {\n  @inject(PanelManagerConfig) private readonly panelConfig: PanelManagerConfig;\n\n  @inject(PluginContext) private readonly pluginContext: PluginContext;\n\n  readonly panelRoot = domUtils.createDivWithClass('gedit-flow-panel-layer');\n\n  layout: JSX.Element | null = null;\n\n  onReady(): void {\n    this.panelConfig.getPopupContainer(this.pluginContext).appendChild(this.panelRoot);\n    this.toDispose.push(\n      Disposable.create(() => {\n        // Remove from PopupContainer\n        this.panelRoot.remove();\n      })\n    );\n    const commonStyle = {\n      pointerEvents: 'none',\n      width: '100%',\n      height: '100%',\n      position: 'absolute',\n      left: 0,\n      top: 0,\n      zIndex: 100,\n    };\n    domUtils.setStyle(this.panelRoot, commonStyle);\n  }\n\n  render(): JSX.Element {\n    if (!this.layout) {\n      const { children, ...layoutProps } = this.panelConfig.layerProps;\n      this.layout = createElement(PanelLayerComp, layoutProps, children);\n    }\n    return ReactDOM.createPortal(this.layout, this.panelRoot);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/panel-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, inject } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { PanelManagerConfig } from './panel-config';\nimport type { Area, PanelEntityConfig, PanelFactory } from '../types';\nimport { PanelEntity, PanelEntityFactory } from './panel-factory';\n\n@injectable()\nexport class PanelManager {\n  @inject(PanelManagerConfig) readonly config: PanelManagerConfig;\n\n  @inject(PanelEntityFactory) readonly createPanel: PanelEntityFactory;\n\n  readonly panelRegistry = new Map<string, PanelFactory<any>>();\n\n  private panels = new Map<string, PanelEntity>();\n\n  private onPanelsChangeEvent = new Emitter<void>();\n\n  public onPanelsChange = this.onPanelsChangeEvent.event;\n\n  init() {\n    this.config.factories.forEach((factory) => this.register(factory));\n  }\n\n  /** registry panel factory */\n  register<T extends any>(factory: PanelFactory<T>) {\n    this.panelRegistry.set(factory.key, factory);\n  }\n\n  /** open panel */\n  public open(key: string, area: Area = 'right', options?: PanelEntityConfig) {\n    const factory = this.panelRegistry.get(key);\n    if (!factory) {\n      return;\n    }\n\n    const sameKeyPanels = this.getPanels(area).filter((p) => p.key === key);\n\n    if (factory.keepDOM && sameKeyPanels.length) {\n      const [panel] = sameKeyPanels;\n      // move to last\n      this.panels.delete(panel.id);\n      this.panels.set(panel.id, panel);\n      panel.visible = true;\n    } else {\n      if (!factory.allowDuplicates && sameKeyPanels.length) {\n        sameKeyPanels.forEach((p) => this.remove(p.id));\n      }\n      const panel = this.createPanel({\n        factory,\n        config: {\n          area,\n          ...options,\n        },\n      });\n\n      this.panels.set(panel.id, panel);\n    }\n\n    this.trim(area);\n    this.onPanelsChangeEvent.fire();\n  }\n\n  /** close panel */\n  public close(key?: string) {\n    const panels = this.getPanels();\n    const closedPanels = key ? panels.filter((p) => p.key === key) : panels;\n    closedPanels.forEach((panel) => {\n      this.remove(panel.id);\n    });\n    this.onPanelsChangeEvent.fire();\n  }\n\n  private trim(area: Area) {\n    /** 1. general panel; 2. keepDOM visible panel */\n    const panels = this.getPanels(area).filter((p) => !p.keepDOM || p.visible);\n    const areaConfig = this.getAreaConfig(area);\n    while (panels.length > areaConfig.max) {\n      const removed = panels.shift();\n      if (removed) {\n        this.remove(removed.id);\n      }\n    }\n  }\n\n  private remove(id: string) {\n    const panel = this.panels.get(id);\n    if (!panel) {\n      return;\n    }\n    if (panel.keepDOM) {\n      panel.visible = false;\n    } else {\n      panel.dispose();\n      this.panels.delete(id);\n    }\n  }\n\n  getPanels(area?: Area) {\n    const panels: PanelEntity[] = [];\n    this.panels.forEach((panel) => {\n      if (!area || panel.area === area) {\n        panels.push(panel);\n      }\n    });\n    return panels;\n  }\n\n  getAreaConfig(area: Area) {\n    switch (area) {\n      case 'docked-bottom':\n        return this.config.dockedBottom;\n      case 'docked-right':\n        return this.config.dockedRight;\n      case 'bottom':\n        return this.config.bottom;\n      case 'right':\n      default:\n        return this.config.right;\n    }\n  }\n\n  dispose() {\n    this.onPanelsChangeEvent.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/services/panel-restore.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\n\nexport const PanelRestore = Symbol('PanelRestore');\nexport interface PanelRestore {\n  store: (k: string, v: any) => void;\n  restore: <T>(k: string) => T | undefined;\n}\n\n@injectable()\nexport class PanelRestoreImpl implements PanelRestore {\n  map = new Map<string, any>();\n\n  store(k: string, v: any) {\n    this.map.set(k, v);\n  }\n\n  restore<T>(k: string): T | undefined {\n    return this.map.get(k) as T;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type Area = 'right' | 'bottom' | 'docked-right' | 'docked-bottom';\n\nexport interface PanelConfig {\n  /** max panel */\n  max: number;\n}\n\nexport interface PanelFactory<T extends any> {\n  key: string;\n  defaultSize: number;\n  fullscreen?: boolean;\n  maxSize?: number;\n  minSize?: number;\n  style?: React.CSSProperties;\n  /** Allows multiple panels with the same key to be rendered simultaneously  */\n  allowDuplicates?: boolean;\n  resize?: boolean;\n  keepDOM?: boolean;\n  render: (props: T) => React.ReactNode;\n}\n\nexport interface PanelEntityConfig<T extends any = any> {\n  defaultSize?: number;\n  fullscreen?: boolean;\n  style?: React.CSSProperties;\n  props?: T;\n}\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const merge = <T>(...objs: Partial<T>[]) => {\n  const result: any = {};\n\n  for (const obj of objs) {\n    if (!obj || typeof obj !== 'object') continue;\n\n    for (const key of Object.keys(obj)) {\n      const value = (obj as any)[key];\n\n      if (result[key] === undefined) {\n        result[key] = value;\n      }\n    }\n  }\n\n  return result as T;\n};\n"
  },
  {
    "path": "packages/plugins/panel-manager-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react\"\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/redux-devtool-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/variable-core\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/connectors/base.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, postConstruct } from 'inversify';\n\n/**\n * 参考：https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Methods.md\n */\n@injectable()\nexport abstract class BaseConnector {\n  devTools;\n\n  abstract getName(): string;\n\n  abstract getState(): any;\n\n  abstract onInit(): any;\n\n  constructor() {\n    this.devTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__?.connect({\n      name: this.getName(),\n    });\n  }\n\n  @postConstruct()\n  init() {\n    if (this.devTools) {\n      this.devTools.init(this.getState());\n      this.devTools.subscribe((_msg: any) => {\n        // COMMIT 清空数据\n        if (_msg?.payload?.type === 'COMMIT') {\n          this.devTools.init(this.getState());\n        }\n      });\n      this.onInit();\n    }\n  }\n\n  protected send(action: any, state?: any) {\n    if (this.devTools) {\n      this.devTools.send(action, state || this.getState());\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/connectors/ecs-connector.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { BaseConnector } from './base';\n\n@injectable()\nexport class ECSConnector extends BaseConnector {\n  @inject(EntityManager) protected entityManager: EntityManager;\n\n  getName(): string {\n    return '@flowgram.ai/EntityManager';\n  }\n\n  getState() {\n    return this.entityManager.storeState({ configOnly: false });\n  }\n\n  onInit() {\n    this.entityManager.onEntityLifeCycleChange(action => {\n      this.send(`${action.type}/${action.entity.type}/${action.entity.id}`);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/connectors/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ECSConnector } from './ecs-connector';\nexport { VariableConnector } from './variable-connector';\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/connectors/variable-connector.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Scope, VariableEngine } from '@flowgram.ai/variable-core';\n\nimport { BaseConnector } from './base';\n\n@injectable()\nexport class VariableConnector extends BaseConnector {\n  @inject(VariableEngine) protected variableEngine: VariableEngine;\n\n  /**\n   * 缓存变量状态\n   */\n  scopes: Record<string, any> = {};\n\n  getName(): string {\n    return '@flowgram.ai/VariableEngine';\n  }\n\n  getState() {\n    return { scopes: this.scopes, variables: this.variableEngine.globalVariableTable.variables };\n  }\n\n  getScopeState(scope: Scope) {\n    return {\n      ast: scope?.ast.toJSON(),\n      output: scope.output.variables,\n      available: scope.available.variables,\n    };\n  }\n\n  onInit() {\n    this.variableEngine.onScopeChange((action) => {\n      const { scope, type } = action;\n\n      if (type === 'delete') {\n        delete this.scopes[String(scope.id)];\n      } else {\n        this.scopes = {\n          ...this.scopes,\n          [scope.id]: this.getScopeState(scope),\n        };\n      }\n\n      this.send(`${type}/${String(scope.id)}`);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/create-redux-devtool-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { ECSConnector, VariableConnector } from './connectors';\n\nexport interface ReduxDevToolPluginOptions {\n  enable?: boolean;\n  // 需要监听的内容\n  ecs?: boolean;\n  variable?: boolean;\n}\n\nexport const createReduxDevToolPlugin = definePluginCreator<ReduxDevToolPluginOptions>({\n  onBind({ bind }, opts) {\n    const { enable } = opts;\n    if (!enable) {\n      return;\n    }\n\n    bind(ECSConnector).toSelf().inSingletonScope();\n\n    bind(VariableConnector).toSelf().inSingletonScope();\n  },\n  onInit(ctx, opts) {\n    const { enable, ecs = true, variable = false } = opts;\n    if (!enable) {\n      return;\n    }\n\n    if (ecs) {\n      ctx.get(ECSConnector);\n    }\n\n    if (variable) {\n      ctx.get(VariableConnector);\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-redux-devtool-plugin';\nexport * from './connectors/ecs-connector';\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/redux-devtool-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/select-box-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/renderer\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/src/create-select-box-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowSelectorBoundsLayer,\n  FlowSelectorBoundsLayerOptions,\n  FlowSelectorBoxLayer,\n  FlowSelectorBoxOptions,\n  type SelectorBoxPopoverProps,\n} from '@flowgram.ai/renderer';\nimport { definePluginCreator } from '@flowgram.ai/core';\n\n// import { SelectorBounds } from './selector-bounds';\n\nexport { type SelectorBoxPopoverProps };\nexport interface SelectBoxPluginOptions\n  extends FlowSelectorBoundsLayerOptions,\n    FlowSelectorBoxOptions {\n  enable?: boolean;\n}\n\nexport const createSelectBoxPlugin = definePluginCreator<SelectBoxPluginOptions>({\n  onInit(ctx, opts): void {\n    // 默认可用，所以强制判断 false\n    if (opts.enable !== false) {\n      ctx.playground.registerLayer<FlowSelectorBoundsLayer>(FlowSelectorBoundsLayer, opts);\n      ctx.playground.registerLayer<FlowSelectorBoxLayer>(FlowSelectorBoxLayer, {\n        canSelect: opts.canSelect,\n      });\n    }\n  },\n});\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-select-box-plugin';\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/select-box-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/shortcuts-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/create-shortcuts-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { bindContributionProvider, definePluginCreator } from '@flowgram.ai/core';\n\nimport { ShortcutsRegistry, ShortcutsContribution } from './shortcuts-contribution';\nimport { ShortcutsLayer } from './layers';\n\n/**\n * @param opts\n *\n * createShortcutsPlugin({\n *   registerShortcuts(registry) {\n *   }\n * })\n */\nexport const createShortcutsPlugin = definePluginCreator<ShortcutsContribution>({\n  onBind: ({ bind }) => {\n    bind(ShortcutsRegistry).toSelf().inSingletonScope();\n    bindContributionProvider(bind, ShortcutsContribution);\n  },\n  onInit: (ctx) => {\n    ctx.playground.registerLayer(ShortcutsLayer);\n  },\n  contributionKeys: [ShortcutsContribution],\n});\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-shortcuts-plugin';\nexport * from './shortcuts-contribution';\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/layers/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './shortcuts-layer';\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/layers/shortcuts-layer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Layer, SelectionService, Command } from '@flowgram.ai/core';\n\nimport { isShortcutsMatch } from '../shortcuts-utils';\nimport { ShortcutsRegistry } from '../shortcuts-contribution';\n\n@injectable()\nexport class ShortcutsLayer extends Layer<object> {\n  static type = 'ShortcutsLayer';\n\n  @inject(ShortcutsRegistry) shortcuts: ShortcutsRegistry;\n\n  @inject(SelectionService) selection: SelectionService;\n\n  onReady(): void {\n    this.shortcuts.addHandlersIfNotFound(\n      /**\n       * 放大\n       */\n      {\n        commandId: Command.Default.ZOOM_IN,\n        shortcuts: ['meta =', 'ctrl ='],\n        execute: () => {\n          // TODO 这里要判断 CurrentEditor\n          this.config.zoomin();\n        },\n      },\n      /**\n       * 缩小\n       */\n      {\n        commandId: Command.Default.ZOOM_OUT,\n        shortcuts: ['meta -', 'ctrl -'],\n        execute: () => {\n          // TODO 这里要判断 CurrentEditor\n          this.config.zoomout();\n        },\n      },\n    );\n    this.toDispose.pushAll([\n      // 监听画布鼠标移动事件\n      this.listenPlaygroundEvent('keydown', (e: KeyboardEvent) => {\n        if (!this.isFocused || e.target !== this.playgroundNode) {\n          return;\n        }\n        this.shortcuts.shortcutsHandlers.some(shortcutsHandler => {\n          if (\n            isShortcutsMatch(e, shortcutsHandler.shortcuts) &&\n            (!shortcutsHandler.isEnabled || shortcutsHandler.isEnabled(e))\n          ) {\n            shortcutsHandler.execute(e);\n            e.preventDefault();\n            return true;\n          }\n        });\n      }),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/shortcuts-contribution.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, named, optional, postConstruct } from 'inversify';\nimport { Command, CommandRegistry, ContributionProvider } from '@flowgram.ai/core';\n\nexport interface ShortcutsHandler {\n  commandId: string;\n  commandDetail?: Omit<Command, 'id'>;\n  shortcuts: string[];\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  isEnabled?: (...args: any[]) => boolean;\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  execute: (...args: any[]) => void;\n}\n\nexport const ShortcutsContribution = Symbol('ShortcutsContribution');\n\nexport interface ShortcutsContribution {\n  registerShortcuts: (registry: ShortcutsRegistry) => void;\n}\n\n@injectable()\nexport class ShortcutsRegistry {\n  @inject(ContributionProvider)\n  @named(ShortcutsContribution)\n  @optional()\n  protected contribs: ContributionProvider<ShortcutsContribution>;\n\n  @inject(CommandRegistry) protected commandRegistry: CommandRegistry;\n\n  shortcutsHandlers: ShortcutsHandler[] = [];\n\n  addHandlers(...handlers: ShortcutsHandler[]): void {\n    // 注册 command\n    handlers.forEach((handler) => {\n      if (!this.commandRegistry.getCommand(handler.commandId)) {\n        this.commandRegistry.registerCommand(\n          { id: handler.commandId, ...(handler.commandDetail || {}) },\n          { execute: handler.execute, isEnabled: handler.isEnabled }\n        );\n      } else {\n        this.commandRegistry.registerHandler(handler.commandId, {\n          execute: handler.execute,\n          isEnabled: handler.isEnabled,\n        });\n      }\n    });\n    // Insert before for override pre handlers\n    this.shortcutsHandlers.unshift(...handlers);\n  }\n\n  addHandlersIfNotFound(...handlers: ShortcutsHandler[]): void {\n    handlers.forEach((handler) => {\n      if (!this.has(handler.commandId)) {\n        this.addHandlers(handler);\n      }\n    });\n  }\n\n  removeHandler(commandId: string): void {\n    this.shortcutsHandlers = this.shortcutsHandlers.filter(\n      (handler) => handler.commandId !== commandId\n    );\n  }\n\n  has(commandId: string): boolean {\n    return this.shortcutsHandlers.some((handler) => handler.commandId === commandId);\n  }\n\n  @postConstruct()\n  protected init(): void {\n    this.contribs?.forEach((contrib) => contrib.registerShortcuts(this));\n  }\n}\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/src/shortcuts-utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst isAppleDevice = /(mac|iphone|ipod|ipad)/i.test(\n  typeof navigator !== 'undefined' ? navigator?.platform : '',\n);\n// 键盘事件 keyCode 别名\nconst aliasKeyCodeMap: Record<string, number | number[]> = {\n  '0': 48,\n  '1': 49,\n  '2': 50,\n  '3': 51,\n  '4': 52,\n  '5': 53,\n  '6': 54,\n  '7': 55,\n  '8': 56,\n  '9': 57,\n  backspace: 8,\n  tab: 9,\n  enter: 13,\n  shift: 16,\n  ctrl: 17,\n  alt: 18,\n  pausebreak: 19,\n  capslock: 20,\n  esc: 27,\n  space: 32,\n  pageup: 33,\n  pagedown: 34,\n  end: 35,\n  home: 36,\n  leftarrow: 37,\n  uparrow: 38,\n  rightarrow: 39,\n  downarrow: 40,\n  insert: 45,\n  delete: 46,\n  a: 65,\n  b: 66,\n  c: 67,\n  d: 68,\n  e: 69,\n  f: 70,\n  g: 71,\n  h: 72,\n  i: 73,\n  j: 74,\n  k: 75,\n  l: 76,\n  m: 77,\n  n: 78,\n  o: 79,\n  p: 80,\n  q: 81,\n  r: 82,\n  s: 83,\n  t: 84,\n  u: 85,\n  v: 86,\n  w: 87,\n  x: 88,\n  y: 89,\n  z: 90,\n  leftwindowkey: 91,\n  rightwindowkey: 92,\n  meta: isAppleDevice ? [91, 93] : [91, 92],\n  selectkey: 93,\n  numpad0: 96,\n  numpad1: 97,\n  numpad2: 98,\n  numpad3: 99,\n  numpad4: 100,\n  numpad5: 101,\n  numpad6: 102,\n  numpad7: 103,\n  numpad8: 104,\n  numpad9: 105,\n  multiply: 106,\n  add: 107,\n  subtract: 109,\n  decimalpoint: 110,\n  divide: 111,\n  f1: 112,\n  f2: 113,\n  f3: 114,\n  f4: 115,\n  f5: 116,\n  f6: 117,\n  f7: 118,\n  f8: 119,\n  f9: 120,\n  f10: 121,\n  f11: 122,\n  f12: 123,\n  numlock: 144,\n  scrolllock: 145,\n  semicolon: 186,\n  equalsign: 187,\n  '=': 187,\n  comma: 188,\n  dash: 189,\n  '-': 189,\n  period: 190,\n  forwardslash: 191,\n  graveaccent: 192,\n  openbracket: 219,\n  backslash: 220,\n  closebracket: 221,\n  singlequote: 222,\n};\n\nconst modifierKey: any = {\n  ctrl: (event: KeyboardEvent) => event.ctrlKey,\n  shift: (event: KeyboardEvent) => event.shiftKey,\n  alt: (event: KeyboardEvent) => event.altKey,\n  meta: (event: KeyboardEvent) => {\n    if (event.type === 'keyup') {\n      return (aliasKeyCodeMap.meta as number[]).includes(event.keyCode);\n    }\n    return event.metaKey;\n  },\n};\n\n// 根据 event 计算激活键数量\nfunction countKeyByEvent(event: KeyboardEvent): number {\n  const countOfModifier = Object.keys(modifierKey).reduce((total, key) => {\n    if (modifierKey[key](event)) {\n      return total + 1;\n    }\n\n    return total;\n  }, 0);\n\n  // 16 17 18 91 92 是修饰键的 keyCode，如果 keyCode 是修饰键，那么激活数量就是修饰键的数量，如果不是，那么就需要 +1\n  return [16, 17, 18, 91, 92].includes(event.keyCode) ? countOfModifier : countOfModifier + 1;\n}\n\n/**\n *\n * @param event\n * @param keyString 'ctrl.s' 'meta.s'\n * @param exactMatch\n */\nfunction isKeyStringMatch(event: KeyboardEvent, keyString: string, exactMatch = true): boolean {\n  // 浏览器自动补全 input 的时候，会触发 keyDown、keyUp 事件，但此时 event.key 等为空\n  if (!event.key || !keyString) {\n    return false;\n  }\n  // 字符串依次判断是否有组合键\n  const genArr = keyString.split(/\\s+/);\n  let genLen = 0;\n\n  for (const key of genArr) {\n    // 组合键\n    const genModifier = modifierKey[key];\n    // keyCode 别名\n    const aliasKeyCode: number | number[] = aliasKeyCodeMap[key.toLowerCase()];\n\n    if ((genModifier && genModifier(event)) || (aliasKeyCode && aliasKeyCode === event.keyCode)) {\n      genLen++;\n    }\n  }\n\n  /**\n   * 需要判断触发的键位和监听的键位完全一致，判断方法就是触发的键位里有且等于监听的键位\n   * genLen === genArr.length 能判断出来触发的键位里有监听的键位\n   * countKeyByEvent(event) === genArr.length 判断出来触发的键位数量里有且等于监听的键位数量\n   * 主要用来防止按组合键其子集也会触发的情况，例如监听 ctrl+a 会触发监听 ctrl 和 a 两个键的事件。\n   */\n  if (exactMatch) {\n    return genLen === genArr.length && countKeyByEvent(event) === genArr.length;\n  }\n  return genLen === genArr.length;\n}\n\n/**\n * 匹配指定的快捷键\n * @param event\n * @param shortcuts\n */\nexport function isShortcutsMatch(event: KeyboardEvent, shortcuts: string[]): boolean {\n  return shortcuts.some(keyString => isKeyStringMatch(event, keyString));\n}\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/shortcuts-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  rules: {\n    'no-console': 'off',\n    'react/no-deprecated': 'off',\n    '@flowgram.ai/e2e-data-testid': 'off',\n    'react/prop-types': 'off'\n  },\n});\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/test-run-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"inversify\": \"^6.0.1\",\n    \"nanoid\": \"^5.0.9\",\n    \"zustand\": \"^4.5.5\",\n    \"@flowgram.ai/form-core\": \"workspace:*\",\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/form\": \"workspace:*\",\n    \"@flowgram.ai/reactive\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"react\": \"^18\",\n    \"@types/react\": \"^18\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react\": {\n      \"optional\": true\n    }\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/create-test-run-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { definePluginCreator } from '@flowgram.ai/core';\n\nimport { TestRunFormEntity, TestRunFormFactory, TestRunFormManager } from './services/form';\nimport {\n  TestRunService,\n  TestRunPipelineEntity,\n  TestRunPipelineFactory,\n  TestRunConfig,\n  defineConfig,\n} from './services';\n\nexport const createTestRunPlugin = definePluginCreator<Partial<TestRunConfig>>({\n  onBind: ({ bind }, opt) => {\n    /** service */\n    bind(TestRunService).toSelf().inSingletonScope();\n    /** config */\n    bind(TestRunConfig).toConstantValue(defineConfig(opt));\n    /** form manager */\n    bind(TestRunFormManager).toSelf().inSingletonScope();\n    /** form entity */\n    bind<TestRunFormFactory>(TestRunFormFactory).toFactory<TestRunFormEntity>((context) => () => {\n      const e = context.container.resolve(TestRunFormEntity);\n      return e;\n    });\n    /** pipeline entity */\n    bind<TestRunPipelineFactory>(TestRunPipelineFactory).toFactory<TestRunPipelineEntity>(\n      (context) => () => {\n        const e = context.container.resolve(TestRunPipelineEntity);\n        e.container = context.container.createChild();\n        return e;\n      }\n    );\n  },\n});\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/contexts.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createContext } from 'react';\n\nimport type { FormComponents } from './types';\nimport type { FormSchemaModel } from './model';\n\n/** Model context for each form item */\nexport const FieldModelContext = createContext<FormSchemaModel>({} as any);\n/** The form's model context */\nexport const FormModelContext = createContext<FormSchemaModel>({} as any);\n/** Context of material component map */\nexport const ComponentsContext = createContext<FormComponents>({});\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/create-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PropsWithChildren } from 'react';\n\nimport { SchemaField, type SchemaFieldProps } from './schema-field';\nimport { FormComponents } from '../types';\n\ntype InnerSchemaFieldProps = Omit<SchemaFieldProps, 'components'> &\n  Pick<Partial<SchemaFieldProps>, 'components'>;\n\nexport interface CreateSchemaFieldOptions {\n  components?: FormComponents;\n}\nexport const createSchemaField = (options: CreateSchemaFieldOptions) => {\n  const InnerSchemaField: React.FC<PropsWithChildren<InnerSchemaFieldProps>> = ({\n    components,\n    ...props\n  }) => (\n    <SchemaField\n      components={{\n        ...options.components,\n        ...components,\n      }}\n      {...props}\n    />\n  );\n\n  return InnerSchemaField;\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/general-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Field } from '@flowgram.ai/form';\n\nimport { ReactiveField } from './reactive-field';\nimport type { FormSchemaModel } from '../model';\nimport { FieldModelContext } from '../contexts';\n\nexport interface GeneralFieldProps {\n  model: FormSchemaModel;\n}\n\nexport const GeneralField: React.FC<GeneralFieldProps> = ({ model }) => (\n  <FieldModelContext.Provider value={model}>\n    <Field\n      name={model.uniqueName}\n      defaultValue={model.defaultValue}\n      render={({ field, fieldState }) => (\n        <ReactiveField\n          componentProps={{\n            value: field.value,\n            onChange: field.onChange,\n            onFocus: field.onFocus,\n            onBlur: field.onBlur,\n            ...fieldState,\n          }}\n          decoratorProps={fieldState}\n        />\n      )}\n    />\n  </FieldModelContext.Provider>\n);\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/** field components */\nexport { SchemaField, type SchemaFieldProps } from './schema-field';\n/** field functions */\nexport { createSchemaField, type CreateSchemaFieldOptions } from './create-field';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/object-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FormSchemaModel } from '../model';\nimport { FieldModelContext } from '../contexts';\nimport { ReactiveField } from './reactive-field';\n\nexport interface ObjectFieldProps {\n  model: FormSchemaModel;\n}\n\nexport const ObjectField: React.FC<React.PropsWithChildren<ObjectFieldProps>> = ({\n  model,\n  children,\n}) => (\n  <FieldModelContext.Provider value={model}>\n    <ReactiveField>{children}</ReactiveField>\n  </FieldModelContext.Provider>\n);\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/reactive-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\nimport React from 'react';\n\nimport { useFormState } from '../hooks/use-form';\nimport { useFieldModel, useFieldState } from '../hooks/use-field';\nimport { ComponentsContext } from '../contexts';\n\ninterface ReactiveFieldProps {\n  componentProps?: Record<string, unknown>;\n  decoratorProps?: Record<string, unknown>;\n}\n\nexport const ReactiveField: React.FC<React.PropsWithChildren<ReactiveFieldProps>> = (props) => {\n  const formState = useFormState();\n  const model = useFieldModel();\n  const modelState = useFieldState();\n  const components = useContext(ComponentsContext);\n\n  const disabled = modelState.disabled || formState.disabled;\n  const componentRender = () => {\n    if (!model.componentType || !components[model.componentType]) {\n      return props.children;\n    }\n    return React.createElement(\n      components[model.componentType],\n      {\n        disabled,\n        ...model.componentProps,\n        ...props.componentProps,\n      },\n      props.children\n    );\n  };\n\n  const decoratorRender = (children: React.ReactNode) => {\n    if (!model.decoratorType || !components[model.decoratorType]) {\n      return <>{children}</>;\n    }\n    return React.createElement(\n      components[model.decoratorType],\n      {\n        type: model.type,\n        required: model.required,\n        ...model.decoratorProps,\n        ...props.decoratorProps,\n      },\n      children\n    );\n  };\n\n  return decoratorRender(componentRender());\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/recursion-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useMemo } from 'react';\n\nimport { FormSchemaModel } from '../model';\nimport { ObjectField } from './object-field';\nimport { GeneralField } from './general-field';\n\ninterface RecursionFieldProps {\n  model: FormSchemaModel;\n}\n\nexport const RecursionField: React.FC<RecursionFieldProps> = ({ model }) => {\n  const properties = useMemo(() => model.getPropertyList(), [model]);\n\n  /** general field has no children */\n  if (model.type !== 'object') {\n    return <GeneralField model={model} />;\n  }\n\n  return (\n    <ObjectField model={model}>\n      {properties.map((item) => (\n        <RecursionField key={item.uniqueName} model={item} />\n      ))}\n    </ObjectField>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/fields/schema-field.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useState } from 'react';\n\nimport type { FormComponents } from '../types';\nimport { FormSchemaModel } from '../model';\nimport { ComponentsContext, FormModelContext } from '../contexts';\nimport { RecursionField } from './recursion-field';\n\nexport interface SchemaFieldProps {\n  model: FormSchemaModel;\n  components: FormComponents;\n}\nexport const SchemaField: React.FC<React.PropsWithChildren<SchemaFieldProps>> = ({\n  components,\n  model,\n  children,\n}) => {\n  /** Only initialized once, dynamic is not supported */\n  const [innerComponents] = useState(() => components);\n  return (\n    <ComponentsContext.Provider value={innerComponents}>\n      <FormModelContext.Provider value={model}>\n        <RecursionField model={model} />\n        {children}\n      </FormModelContext.Provider>\n    </ComponentsContext.Provider>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/form/form.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Form } from '@flowgram.ai/form';\n\nimport { FormSchema, FormComponents } from '../types';\nimport { useCreateForm, type UseCreateFormOptions } from '../hooks';\nimport { createSchemaField } from '../fields';\n\nconst SchemaField = createSchemaField({});\n\nexport type FormEngineProps = React.PropsWithChildren<\n  {\n    /** Form schema */\n    schema: FormSchema;\n    /** form material map */\n    components?: FormComponents;\n  } & UseCreateFormOptions\n>;\n\nexport const FormEngine: React.FC<FormEngineProps> = ({\n  schema,\n  components,\n  children,\n  ...props\n}) => {\n  const { model, control } = useCreateForm(schema, props);\n\n  return (\n    <Form control={control}>\n      <SchemaField model={model} components={components}>\n        {children}\n      </SchemaField>\n    </Form>\n  );\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/form/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FormEngine, type FormEngineProps } from './form';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useCreateForm, type UseCreateFormOptions } from './use-create-form';\nexport { useFieldModel, useFieldState } from './use-field';\nexport { useFormModel, useFormState } from './use-form';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/hooks/use-create-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo } from 'react';\n\nimport type { OnFormValuesChangePayload } from '@flowgram.ai/form-core';\nimport { createForm, ValidateTrigger, type IForm } from '@flowgram.ai/form';\n\nimport { createValidate } from '../utils';\nimport { FormSchema, FormSchemaValidate } from '../types';\nimport { FormSchemaModel } from '../model';\n\nexport interface FormInstance {\n  model: FormSchemaModel;\n  form: IForm;\n}\nexport interface UseCreateFormOptions {\n  defaultValues?: any;\n  validate?: Record<string, FormSchemaValidate>;\n  validateTrigger?: ValidateTrigger;\n  onMounted?: (form: FormInstance) => void;\n  onFormValuesChange?: (payload: OnFormValuesChangePayload) => void;\n  onUnmounted?: () => void;\n}\n\nexport const useCreateForm = (schema: FormSchema, options: UseCreateFormOptions = {}) => {\n  const { form, control } = useMemo(\n    () =>\n      createForm({\n        validate: {\n          ...createValidate(schema),\n          ...options.validate,\n        },\n        validateTrigger: options.validateTrigger ?? ValidateTrigger.onBlur,\n      }),\n    [schema]\n  );\n\n  const model = useMemo(\n    () => new FormSchemaModel({ type: 'object', ...schema, defaultValue: options.defaultValues }),\n    [schema]\n  );\n\n  /** Lifecycle and event binding */\n  useEffect(() => {\n    if (options.onMounted) {\n      options.onMounted({ model, form });\n    }\n    const disposable = control._formModel.onFormValuesChange((payload) => {\n      if (options.onFormValuesChange) {\n        options.onFormValuesChange(payload);\n      }\n    });\n    return () => {\n      disposable.dispose();\n      if (options.onUnmounted) {\n        options.onUnmounted();\n      }\n    };\n  }, [control]);\n\n  return {\n    form,\n    control,\n    model,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/hooks/use-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { useObserve } from '@flowgram.ai/reactive';\n\nimport { FieldModelContext } from '../contexts';\n\nexport const useFieldModel = () => useContext(FieldModelContext);\nexport const useFieldState = () => useObserve(useFieldModel().state.value);\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/hooks/use-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useContext } from 'react';\n\nimport { useObserve } from '@flowgram.ai/reactive';\n\nimport { FormModelContext } from '../contexts';\n\nexport const useFormModel = () => useContext(FormModelContext);\nexport const useFormState = () => useObserve(useFormModel().state.value);\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FormEngine, type FormEngineProps } from './form';\nexport { FormSchemaModel } from './model';\n/** utils */\nexport { connect, isFormEmpty } from './utils';\n\n/** types */\nexport type {\n  FormSchema,\n  FormSchemaValidate,\n  FormComponents,\n  FormComponent,\n  FormComponentProps,\n} from './types';\nexport type { FormInstance } from './hooks/use-create-form';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/model/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ReactiveState } from '@flowgram.ai/reactive';\n\nimport { getUniqueFieldName, mergeFieldPath } from '../utils';\nimport { FormSchema, FormSchemaType, FormSchemaModelState } from '../types';\n\nexport class FormSchemaModel implements FormSchema {\n  name?: string;\n\n  type?: FormSchemaType;\n\n  defaultValue?: any;\n\n  properties?: Record<string, FormSchema>;\n\n  ['x-index']?: number;\n\n  ['x-component']?: string;\n\n  ['x-component-props']?: Record<string, unknown>;\n\n  ['x-decorator']?: string;\n\n  ['x-decorator-props']?: Record<string, unknown>;\n\n  [key: string]: any;\n\n  path: string[] = [];\n\n  state = new ReactiveState<FormSchemaModelState>({ disabled: false });\n\n  get componentType() {\n    return this['x-component'];\n  }\n\n  get componentProps() {\n    return this['x-component-props'];\n  }\n\n  get decoratorType() {\n    return this['x-decorator'];\n  }\n\n  get decoratorProps() {\n    return this['x-decorator-props'];\n  }\n\n  get uniqueName() {\n    return getUniqueFieldName(...this.path);\n  }\n\n  constructor(json: FormSchema, path: string[] = []) {\n    this.fromJSON(json);\n    this.path = path;\n  }\n\n  private fromJSON(json: FormSchema) {\n    Object.entries(json).forEach(([key, value]) => {\n      this[key] = value;\n    });\n  }\n\n  getPropertyList() {\n    const orderProperties: FormSchemaModel[] = [];\n    const unOrderProperties: FormSchemaModel[] = [];\n    Object.entries(this.properties || {}).forEach(([key, item]) => {\n      const index = item['x-index'];\n      const defaultValues = this.defaultValue;\n      /**\n       * The upper layer's default value has a higher priority than its own default value,\n       * because the upper layer's default value ultimately comes from the outside world.\n       */\n      if (typeof defaultValues === 'object' && defaultValues !== null && key in defaultValues) {\n        item.defaultValue = defaultValues[key];\n      }\n      const current = new FormSchemaModel(item, mergeFieldPath(this.path, key));\n      if (index !== undefined && !isNaN(index)) {\n        orderProperties[index] = current;\n      } else {\n        unOrderProperties.push(current);\n      }\n    });\n    return orderProperties.concat(unOrderProperties).filter((item) => !!item);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport type { Validate, FieldState } from '@flowgram.ai/form';\n\n/** field type */\nexport type FormSchemaType = 'string' | 'number' | 'boolean' | 'object' | string;\n\nexport type FormSchemaValidate = Validate;\n\nexport interface FormSchema {\n  /** core */\n  name?: string;\n  type?: FormSchemaType;\n  defaultValue?: any;\n\n  /** children */\n  properties?: Record<string, FormSchema>;\n\n  /** ui */\n  title?: string | React.ReactNode;\n  description?: string | React.ReactNode;\n  ['x-index']?: number;\n  ['x-visible']?: boolean;\n  ['x-hidden']?: boolean;\n  ['x-component']?: string;\n  ['x-component-props']?: Record<string, unknown>;\n  ['x-decorator']?: string;\n  ['x-decorator-props']?: Record<string, unknown>;\n\n  /** rule */\n  required?: boolean;\n  ['x-validator']?: FormSchemaValidate;\n\n  /** custom */\n  [key: string]: any;\n}\n\nexport type FormComponentProps = {\n  type?: FormSchemaType;\n  disabled?: boolean;\n  [key: string]: any;\n} & FormSchema['x-component-props'] &\n  FormSchema['x-decorator-props'] &\n  Partial<FieldState>;\nexport type FormComponent = React.FunctionComponent<any>;\nexport type FormComponents = Record<string, FormComponent>;\n\n/** ui state */\nexport interface FormSchemaModelState {\n  disabled: boolean;\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/form-engine/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createElement } from 'react';\n\nimport type { FormSchema, FormSchemaValidate, FormComponentProps } from './types';\n\n/** Splice form item unique name */\nexport const getUniqueFieldName = (...args: (string | undefined)[]) =>\n  args.filter((path) => path).join('.');\n\nexport const mergeFieldPath = (path?: string[], name?: string) =>\n  [...(path || []), name].filter((i): i is string => Boolean(i));\n\n/** Create validation rules */\nexport const createValidate = (schema: FormSchema) => {\n  const rules: Record<string, FormSchemaValidate> = {};\n\n  visit(schema);\n\n  return rules;\n\n  function visit(current: FormSchema, name?: string) {\n    if (name && current['x-validator']) {\n      rules[name] = current['x-validator'];\n    }\n    if (current.type === 'object' && current.properties) {\n      Object.entries(current.properties).forEach(([key, value]) => {\n        visit(value, getUniqueFieldName(name, key));\n      });\n    }\n  }\n};\n\nexport const connect = <T = any>(\n  Component: React.FunctionComponent<any>,\n  mapProps: (p: FormComponentProps) => T\n) => {\n  const Connected = (props: FormComponentProps) => {\n    const mappedProps = mapProps(props);\n    return createElement(Component, mappedProps, (mappedProps as any).children);\n  };\n\n  return Connected;\n};\n\nexport const isFormEmpty = (schema: FormSchema) => {\n  /** is not general field and not has children */\n  const isEmpty = (s: FormSchema): boolean => {\n    if (!s.type || s.type === 'object' || !s.name) {\n      return Object.entries(schema.properties || {})\n        .map(([key, value]) => ({\n          name: key,\n          ...value,\n        }))\n        .every(isFormEmpty);\n    }\n    return false;\n  };\n\n  return isEmpty(schema);\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { createTestRunPlugin } from './create-test-run-plugin';\nexport { useCreateForm, useTestRunService } from './reactive';\n\nexport {\n  FormEngine,\n  connect,\n  type FormInstance,\n  type FormEngineProps,\n  type FormSchema,\n  type FormComponentProps,\n} from './form-engine';\n\nexport {\n  type TestRunPipelinePlugin,\n  TestRunPipelineEntity,\n  type TestRunPipelineEntityCtx,\n  type TestRunConfig,\n} from './services';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/reactive/hooks/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useCreateForm } from './use-create-form';\nexport { useTestRunService } from './use-test-run-service';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/reactive/hooks/use-create-form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect, useMemo, useState } from 'react';\n\nimport { DisposableCollection } from '@flowgram.ai/utils';\nimport type { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { TestRunFormEntity } from '../../services/form/form';\nimport { FormEngineProps, isFormEmpty } from '../../form-engine';\nimport { useTestRunService } from './use-test-run-service';\n\ninterface UseFormOptions {\n  node?: FlowNodeEntity;\n  /** form loading */\n  loadingRenderer?: React.ReactNode;\n  /** form empty */\n  emptyRenderer?: React.ReactNode;\n  defaultValues?: FormEngineProps['defaultValues'];\n  onMounted?: FormEngineProps['onMounted'];\n  onUnmounted?: FormEngineProps['onUnmounted'];\n  onFormValuesChange?: FormEngineProps['onFormValuesChange'];\n}\n\nexport const useCreateForm = ({\n  node,\n  loadingRenderer,\n  emptyRenderer,\n  defaultValues,\n  onMounted,\n  onUnmounted,\n  onFormValuesChange,\n}: UseFormOptions) => {\n  const testRun = useTestRunService();\n  const [loading, setLoading] = useState(false);\n  const [form, setForm] = useState<TestRunFormEntity | null>(null);\n  const renderer = useMemo(() => {\n    if (loading || !form) {\n      return loadingRenderer;\n    }\n\n    const isEmpty = isFormEmpty(form.schema);\n\n    return form.render({\n      defaultValues,\n      onFormValuesChange,\n      children: isEmpty ? emptyRenderer : null,\n    });\n  }, [form, loading]);\n\n  const compute = async () => {\n    if (!node) {\n      return;\n    }\n    try {\n      setLoading(true);\n      const formEntity = await testRun.createForm(node);\n      setForm(formEntity);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  useEffect(() => {\n    compute();\n  }, [node]);\n\n  useEffect(() => {\n    if (!form) {\n      return;\n    }\n    const disposable = new DisposableCollection(\n      form.onFormMounted((data) => {\n        onMounted?.(data);\n      }),\n      form.onFormUnmounted(() => {\n        onUnmounted?.();\n      })\n    );\n    return () => disposable.dispose();\n  }, [form]);\n\n  return {\n    renderer,\n    loading,\n    form,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/reactive/hooks/use-test-run-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useService } from '@flowgram.ai/core';\n\nimport { TestRunService } from '../../services/test-run';\n\nexport const useTestRunService = () => useService<TestRunService>(TestRunService);\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/reactive/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { useCreateForm, useTestRunService } from './hooks';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeType, FlowNodeEntity } from '@flowgram.ai/document';\n\nimport type { MaybePromise } from '../types';\nimport type { FormSchema, FormComponents } from '../form-engine';\nimport type { TestRunPipelinePlugin } from './pipeline';\n\ntype PropertiesFunctionParams = {\n  node: FlowNodeEntity;\n};\nexport type NodeMap = Record<FlowNodeType, NodeTestConfig>;\nexport interface NodeTestConfig {\n  /** Enable node TestRun */\n  enabled?: boolean;\n  /** Input schema properties */\n  properties?:\n    | Record<string, FormSchema>\n    | ((params: PropertiesFunctionParams) => MaybePromise<Record<string, FormSchema>>);\n}\n\nexport interface TestRunConfig {\n  components: FormComponents;\n  nodes: NodeMap;\n  plugins: (new () => TestRunPipelinePlugin)[];\n}\n\nexport const TestRunConfig = Symbol('TestRunConfig');\nexport const defineConfig = (config: Partial<TestRunConfig>) => {\n  const defaultConfig: TestRunConfig = {\n    components: {},\n    nodes: {},\n    plugins: [],\n  };\n  return {\n    ...defaultConfig,\n    ...config,\n  };\n};\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/form/factory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { TestRunFormEntity } from './form';\n\nexport const TestRunFormFactory = Symbol('TestRunFormFactory');\nexport type TestRunFormFactory = () => TestRunFormEntity;\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/form/form.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createElement, type ReactNode } from 'react';\n\nimport { nanoid } from 'nanoid';\nimport { injectable, inject } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { TestRunConfig } from '../config';\nimport { FormSchema, FormEngine, type FormInstance, type FormEngineProps } from '../../form-engine';\n\nexport type FormRenderProps = Omit<\n  FormEngineProps,\n  'schema' | 'components' | 'onMounted' | 'onUnmounted'\n>;\n\n@injectable()\nexport class TestRunFormEntity {\n  @inject(TestRunConfig) private readonly config: TestRunConfig;\n\n  private _schema: FormSchema;\n\n  private initialized = false;\n\n  id = nanoid();\n\n  form: FormInstance | null = null;\n\n  onFormMountedEmitter = new Emitter<FormInstance>();\n\n  onFormMounted = this.onFormMountedEmitter.event;\n\n  onFormUnmountedEmitter = new Emitter<void>();\n\n  onFormUnmounted = this.onFormUnmountedEmitter.event;\n\n  get schema() {\n    return this._schema;\n  }\n\n  init(options: { schema: FormSchema }) {\n    if (this.initialized) return;\n\n    this._schema = options.schema;\n    this.initialized = true;\n  }\n\n  render(props?: FormRenderProps): ReactNode {\n    if (!this.initialized) {\n      return null;\n    }\n    const { children, ...restProps } = props || {};\n    return createElement(\n      FormEngine,\n      {\n        schema: this.schema,\n        components: this.config.components,\n        onMounted: (instance) => {\n          this.form = instance;\n          this.onFormMountedEmitter.fire(instance);\n        },\n        onUnmounted: this.onFormUnmountedEmitter.fire.bind(this.onFormUnmountedEmitter),\n        ...restProps,\n      },\n      children\n    );\n  }\n\n  dispose() {\n    this._schema = {};\n    this.form = null;\n    this.onFormMountedEmitter.dispose();\n    this.onFormUnmountedEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/form/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { TestRunFormEntity } from './form';\nexport { TestRunFormFactory } from './factory';\nexport { TestRunFormManager } from './manager';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/form/manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\n\nimport type { TestRunFormEntity } from './form';\nimport { TestRunFormFactory } from './factory';\n\n@injectable()\nexport class TestRunFormManager {\n  @inject(TestRunFormFactory) private readonly factory: TestRunFormFactory;\n\n  private entities = new Map<string, TestRunFormEntity>();\n\n  createForm() {\n    return this.factory();\n  }\n\n  getForm(id: string) {\n    return this.entities.get(id);\n  }\n\n  getAllForm() {\n    return Array.from(this.entities);\n  }\n\n  disposeForm(id: string) {\n    const form = this.entities.get(id);\n    if (!form) {\n      return;\n    }\n    form.dispose();\n    this.entities.delete(id);\n  }\n\n  disposeAllForm() {\n    for (const id of this.entities.keys()) {\n      this.disposeForm(id);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { TestRunService } from './test-run';\nexport { TestRunFormEntity, TestRunFormFactory } from './form';\nexport {\n  TestRunPipelineEntity,\n  TestRunPipelineFactory,\n  type TestRunPipelinePlugin,\n  type TestRunPipelineEntityCtx,\n} from './pipeline';\nexport { TestRunConfig, defineConfig } from './config';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/pipeline/factory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { TestRunPipelineEntity } from './pipeline';\n\nexport const TestRunPipelineFactory = Symbol('TestRunPipelineFactory');\nexport type TestRunPipelineFactory = () => TestRunPipelineEntity;\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/pipeline/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { TestRunPipelineFactory } from './factory';\nexport {\n  TestRunPipelineEntity,\n  type TestRunPipelineEntityOptions,\n  type TestRunPipelineEntityCtx,\n} from './pipeline';\nexport { TestRunPipelinePlugin } from './plugin';\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/pipeline/pipeline.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { StoreApi } from 'zustand';\nimport { nanoid } from 'nanoid';\nimport { injectable, interfaces } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { Tap } from './tap';\nimport type { TestRunPipelinePlugin } from './plugin';\nimport { StoreService } from '../store';\nexport interface TestRunPipelineEntityOptions {\n  plugins: (new () => TestRunPipelinePlugin)[];\n}\n\ninterface TestRunPipelineEntityState<T = any> {\n  status: 'idle' | 'preparing' | 'executing' | 'canceled' | 'finished' | 'disposed';\n  data?: T;\n  result?: any;\n  getData: () => T;\n  setData: (next: any) => void;\n}\n\nexport interface TestRunPipelineEntityCtx<T = any> {\n  id: string;\n  store: StoreApi<TestRunPipelineEntityState<T>>;\n  operate: {\n    update: (data: any) => void;\n    cancel: () => void;\n  };\n}\n\nconst initialState: Omit<TestRunPipelineEntityState, 'getData' | 'setData'> = {\n  status: 'idle',\n  data: {},\n};\n\n@injectable()\nexport class TestRunPipelineEntity extends StoreService<TestRunPipelineEntityState> {\n  container: interfaces.Container | undefined;\n\n  id = nanoid();\n\n  plugins: TestRunPipelinePlugin[] = [];\n\n  prepare = new Tap<TestRunPipelineEntityCtx>();\n\n  private execute?: (ctx: TestRunPipelineEntityCtx) => Promise<void> | void;\n\n  private progress?: (ctx: TestRunPipelineEntityCtx) => Promise<void> | void;\n\n  get status() {\n    return this.getState().status;\n  }\n\n  set status(next: TestRunPipelineEntityState['status']) {\n    this.setState({ status: next });\n  }\n\n  onProgressEmitter = new Emitter<any>();\n\n  onProgress = this.onProgressEmitter.event;\n\n  onFinishedEmitter = new Emitter();\n\n  onFinished = this.onFinishedEmitter.event;\n\n  constructor() {\n    super((set, get) => ({\n      ...initialState,\n      getData: () => get().data || {},\n      setData: (next: any) => set((state) => ({ ...state, data: { ...state.data, ...next } })),\n    }));\n  }\n\n  public init(options: TestRunPipelineEntityOptions) {\n    if (!this.container) {\n      return;\n    }\n    const { plugins } = options;\n    for (const PluginClass of plugins) {\n      const plugin = this.container.resolve<TestRunPipelinePlugin>(PluginClass);\n      plugin.apply(this);\n      this.plugins.push(plugin);\n    }\n  }\n\n  public registerExecute(fn: (ctx: TestRunPipelineEntityCtx) => Promise<void> | void) {\n    this.execute = fn;\n  }\n\n  public registerProgress(fn: (ctx: TestRunPipelineEntityCtx) => Promise<void> | void) {\n    this.progress = fn;\n  }\n\n  async start<T>(options?: { data: T }) {\n    const { data } = options || {};\n    if (this.status !== 'idle') {\n      return;\n    }\n    /** initialization data */\n    this.setState({ data });\n    const ctx: TestRunPipelineEntityCtx = {\n      id: this.id,\n      store: this.store,\n      operate: {\n        update: this.update.bind(this),\n        cancel: this.cancel.bind(this),\n      },\n    };\n\n    this.status = 'preparing';\n    await this.prepare.call(ctx);\n    if (this.status !== 'preparing') {\n      return;\n    }\n\n    this.status = 'executing';\n    if (this.execute) {\n      await this.execute(ctx);\n    }\n    if (this.progress) {\n      await this.progress(ctx);\n    }\n    if (this.status === 'executing') {\n      this.status = 'finished';\n      this.onFinishedEmitter.fire(this.getState().result);\n    }\n  }\n\n  update(result: any) {\n    this.setState({ result });\n    this.onProgressEmitter.fire(result);\n  }\n\n  cancel() {\n    if ((this.status = 'preparing')) {\n      this.prepare.freeze();\n    }\n    this.status = 'canceled';\n  }\n\n  dispose() {\n    this.status = 'disposed';\n    this.plugins.forEach((p) => {\n      if (p.dispose) {\n        p.dispose();\n      }\n    });\n    this.onProgressEmitter.dispose();\n    this.onFinishedEmitter.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/pipeline/plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { TestRunPipelineEntity } from './pipeline';\n\nexport interface TestRunPipelinePlugin {\n  name: string;\n  apply(pipeline: TestRunPipelineEntity): void;\n\n  dispose?: () => void;\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/pipeline/tap.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { MaybePromise } from '../../types';\n\ninterface TapValue<T> {\n  name: string;\n  fn: (arg: T) => MaybePromise<void>;\n}\n\nexport class Tap<T> {\n  private taps: TapValue<T>[] = [];\n\n  private frozen = false;\n\n  tap(name: string, fn: TapValue<T>['fn']) {\n    this.taps.push({ name, fn });\n  }\n\n  async call(ctx: T) {\n    for (const tap of this.taps) {\n      if (this.frozen) {\n        return;\n      }\n      await tap.fn(ctx);\n    }\n  }\n\n  freeze() {\n    this.frozen = true;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createStore } from 'zustand/vanilla';\nimport type { StoreApi, StateCreator } from 'zustand';\nimport { injectable, unmanaged } from 'inversify';\n/**\n * 包含 Store 的 Service\n */\n@injectable()\nexport class StoreService<State> {\n  store: StoreApi<State>;\n\n  get getState() {\n    return this.store.getState.bind(this.store);\n  }\n\n  get setState() {\n    return this.store.setState.bind(this.store);\n  }\n\n  constructor(@unmanaged() stateCreator: StateCreator<State>) {\n    this.store = createStore(stateCreator);\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/services/test-run.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';\nimport type { FlowNodeEntity, FlowNodeType } from '@flowgram.ai/document';\n\nimport { TestRunPipelineFactory } from './pipeline/factory';\nimport { TestRunFormManager } from './form';\nimport { FormSchema } from '../form-engine';\nimport { TestRunPipelineEntity, type TestRunPipelineEntityOptions } from './pipeline';\nimport { TestRunConfig } from './config';\n\n@injectable()\nexport class TestRunService {\n  @inject(TestRunConfig) private readonly config: TestRunConfig;\n\n  @inject(TestRunPipelineFactory) private readonly pipelineFactory: TestRunPipelineFactory;\n\n  @inject(TestRunFormManager) readonly formManager: TestRunFormManager;\n\n  pipelineEntities = new Map<string, TestRunPipelineEntity>();\n\n  pipelineBindings = new Map<string, Disposable>();\n\n  onPipelineProgressEmitter = new Emitter();\n\n  onPipelineProgress = this.onPipelineProgressEmitter.event;\n\n  onPipelineFinishedEmitter = new Emitter();\n\n  onPipelineFinished = this.onPipelineFinishedEmitter.event;\n\n  public isEnabled(nodeType: FlowNodeType) {\n    const config = this.config.nodes[nodeType];\n    return config && config?.enabled !== false;\n  }\n\n  async toSchema(node: FlowNodeEntity) {\n    const nodeType = node.flowNodeType;\n    const config = this.config.nodes[nodeType];\n    if (!this.isEnabled(nodeType)) {\n      return {};\n    }\n    const properties =\n      typeof config.properties === 'function'\n        ? await config.properties({ node })\n        : config.properties;\n\n    return {\n      type: 'object',\n      properties,\n    };\n  }\n\n  createFormWithSchema(schema: FormSchema) {\n    const form = this.formManager.createForm();\n    form.init({ schema });\n    return form;\n  }\n\n  async createForm(node: FlowNodeEntity) {\n    const schema = await this.toSchema(node);\n    return this.createFormWithSchema(schema);\n  }\n\n  createPipeline(options: TestRunPipelineEntityOptions) {\n    const pipeline = this.pipelineFactory();\n    this.pipelineEntities.set(pipeline.id, pipeline);\n    pipeline.init(options);\n    return pipeline;\n  }\n\n  disposePipeline(id: string) {\n    const pipeline = this.pipelineEntities.get(id);\n    if (pipeline) {\n      this.pipelineEntities.delete(id);\n      pipeline.dispose();\n    }\n  }\n\n  connectPipeline(pipeline: TestRunPipelineEntity) {\n    if (this.pipelineBindings.get(pipeline.id)) {\n      return;\n    }\n    const disposable = new DisposableCollection(\n      pipeline.onProgress(this.onPipelineProgressEmitter.fire.bind(this.onPipelineProgressEmitter)),\n      pipeline.onFinished(this.onPipelineFinishedEmitter.fire.bind(this.onPipelineFinishedEmitter))\n    );\n    this.pipelineBindings.set(pipeline.id, disposable);\n  }\n\n  disconnectPipeline(id: string) {\n    if (this.pipelineBindings.has(id)) {\n      const disposable = this.pipelineBindings.get(id);\n      disposable?.dispose();\n      this.pipelineBindings.delete(id);\n    }\n  }\n\n  disconnectAllPipeline() {\n    for (const id of this.pipelineBindings.keys()) {\n      this.disconnectPipeline(id);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { FlowNodeType, FlowNodeEntity } from '@flowgram.ai/document';\n\nimport type { FormSchema, FormComponents } from './form-engine';\n\nexport type MaybePromise<T> = T | Promise<T>;\n\ntype PropertiesFunctionParams = {\n  node: FlowNodeEntity;\n};\n\nexport interface NodeTestConfig {\n  /** Enable node TestRun */\n  enabled?: boolean;\n  /** Input schema properties */\n  properties?:\n    | Record<string, FormSchema>\n    | ((params: PropertiesFunctionParams) => MaybePromise<Record<string, FormSchema>>);\n}\nexport type NodeMap = Record<FlowNodeType, NodeTestConfig>;\n\nexport interface TestRunPluginConfig {\n  components?: FormComponents;\n  nodes?: NodeMap;\n}\n"
  },
  {
    "path": "packages/plugins/test-run-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/variable-plugin/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/plugins/variable-plugin/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/variable-plugin\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/variable-core\": \"workspace:*\",\n    \"@flowgram.ai/variable-layout\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/variable-plugin/src/create-variable-plugin.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowNodeVariableData,\n  FreeLayoutScopeChain,\n  FixedLayoutScopeChain,\n  VariableChainConfig,\n  bindGlobalScope,\n  ScopeChainTransformService,\n} from '@flowgram.ai/variable-layout';\nimport {\n  VariableContainerModule,\n  ASTNodeRegistry,\n  ASTRegisters,\n  VariableEngine,\n  ScopeChain,\n} from '@flowgram.ai/variable-core';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { PluginContext, definePluginCreator } from '@flowgram.ai/core';\nimport { EntityManager } from '@flowgram.ai/core';\n\n/**\n * @deprecated 请使用 @injectToAst(XXXService) declare xxxService: XXXService 实现外部依赖注入\n */\ntype Injector = (ctx: PluginContext) => Record<string, any>;\n\nexport interface VariablePluginOptions {\n  enable?: boolean;\n  /**\n   * Custom Extends ASTNode\n   */\n  extendASTNodes?: (ASTNodeRegistry | [ASTNodeRegistry] | [ASTNodeRegistry, Injector])[];\n  /**\n   * Layout method\n   */\n  layout?: 'fixed' | 'free';\n  /**\n   * @deprecated use chainConfig instead\n   */\n  layoutConfig?: VariableChainConfig;\n  /**\n   * Configuration for scope chain\n   */\n  chainConfig?: VariableChainConfig;\n}\n\nexport const createVariablePlugin = definePluginCreator<VariablePluginOptions>({\n  onBind({ bind }, opts) {\n    const { layout, layoutConfig, chainConfig } = opts;\n\n    bind(ScopeChainTransformService).toSelf().inSingletonScope();\n\n    if (layout === 'free') {\n      bind(ScopeChain).to(FreeLayoutScopeChain).inSingletonScope();\n    }\n    if (layout === 'fixed') {\n      bind(ScopeChain).to(FixedLayoutScopeChain).inSingletonScope();\n    }\n    if (chainConfig) {\n      bind(VariableChainConfig).toConstantValue(chainConfig || {});\n    } else if (layoutConfig) {\n      console.warn(`Layout Config deprecated, use chainConfig instead`);\n      bind(VariableChainConfig).toConstantValue(layoutConfig || {});\n    }\n\n    bindGlobalScope(bind);\n  },\n  onInit(ctx, opts) {\n    const { extendASTNodes } = opts || {};\n\n    const variableEngine = ctx.get<VariableEngine>(VariableEngine);\n    const astRegisters = ctx.get<ASTRegisters>(ASTRegisters);\n    const entityManager = ctx.get<EntityManager>(EntityManager);\n    const document = ctx.get<FlowDocument>(FlowDocument);\n\n    /**\n     * 注册扩展 AST 节点\n     */\n    (extendASTNodes || []).forEach((info) => {\n      if (Array.isArray(info)) {\n        const [extendASTNode, injector] = info;\n\n        astRegisters.registerAST(extendASTNode, injector ? () => injector(ctx) : undefined);\n\n        return;\n      }\n\n      astRegisters.registerAST(info);\n    });\n\n    /**\n     * 扩展 FlowNodeVariableData\n     */\n    entityManager.registerEntityData(FlowNodeVariableData, () => ({ variableEngine } as any));\n    document.registerNodeDatas(FlowNodeVariableData);\n  },\n  containerModules: [VariableContainerModule],\n});\n"
  },
  {
    "path": "packages/plugins/variable-plugin/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './create-variable-plugin';\nexport * from '@flowgram.ai/variable-core';\nexport {\n  FlowNodeVariableData,\n  GlobalScope,\n  ScopeChainTransformService,\n  getNodeScope,\n  getNodePrivateScope,\n  FlowNodeScopeType,\n  type FlowNodeScopeMeta,\n  type FlowNodeScope,\n} from '@flowgram.ai/variable-layout';\n"
  },
  {
    "path": "packages/plugins/variable-plugin/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"./src\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/plugins/variable-plugin/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/plugins/variable-plugin/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/runtime/interface/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineFlatConfig } from '@flowgram.ai/eslint-config';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineFlatConfig({\n  preset: 'base',\n  packageRoot: __dirname,\n});\n\n"
  },
  {
    "path": "packages/runtime/interface/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/runtime-interface\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"npm run watch\",\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\"\n  },\n  \"dependencies\": {\n    \"zod\": \"^3.24.4\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/api/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum FlowGramAPIMethod {\n  GET = 'GET',\n  POST = 'POST',\n  PUT = 'PUT',\n  DELETE = 'DELETE',\n  PATCH = 'PATCH',\n}\n\nexport enum FlowGramAPIName {\n  ServerInfo = 'ServerInfo',\n  TaskRun = 'TaskRun',\n  TaskReport = 'TaskReport',\n  TaskResult = 'TaskResult',\n  TaskCancel = 'TaskCancel',\n  TaskValidate = 'TaskValidate',\n}\n\nexport enum FlowGramAPIModule {\n  Info = 'Info',\n  Task = 'Task',\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/api/define.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramAPIDefines } from './type';\nimport { TaskValidateDefine } from './task-validate';\nimport { TaskRunDefine } from './task-run';\nimport { TaskResultDefine } from './task-result';\nimport { TaskReportDefine } from './task-report';\nimport { TaskCancelDefine } from './task-cancel';\nimport { ServerInfoDefine } from './server-info';\nimport { FlowGramAPIName } from './constant';\n\nexport const FlowGramAPIs: FlowGramAPIDefines = {\n  [FlowGramAPIName.ServerInfo]: ServerInfoDefine,\n  [FlowGramAPIName.TaskRun]: TaskRunDefine,\n  [FlowGramAPIName.TaskReport]: TaskReportDefine,\n  [FlowGramAPIName.TaskResult]: TaskResultDefine,\n  [FlowGramAPIName.TaskCancel]: TaskCancelDefine,\n  [FlowGramAPIName.TaskValidate]: TaskValidateDefine,\n};\n\nexport const FlowGramAPINames = Object.keys(FlowGramAPIs) as FlowGramAPIName[];\n"
  },
  {
    "path": "packages/runtime/interface/src/api/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './type';\nexport * from './define';\nexport * from './constant';\n\nexport * from './task-run';\nexport * from './server-info';\nexport * from './task-report';\nexport * from './task-validate';\nexport * from './task-result';\nexport * from './task-cancel';\n"
  },
  {
    "path": "packages/runtime/interface/src/api/schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nconst WorkflowIOZodSchema = z.record(z.string(), z.any());\n\nconst WorkflowSnapshotZodSchema = z.object({\n  id: z.string(),\n  nodeID: z.string(),\n  inputs: WorkflowIOZodSchema,\n  outputs: WorkflowIOZodSchema.optional(),\n  data: WorkflowIOZodSchema,\n  branch: z.string().optional(),\n});\n\nconst WorkflowStatusZodShape = {\n  status: z.string(),\n  terminated: z.boolean(),\n  startTime: z.number(),\n  endTime: z.number().optional(),\n  timeCost: z.number(),\n};\nconst WorkflowStatusZodSchema = z.object(WorkflowStatusZodShape);\n\nconst WorkflowNodeReportZodSchema = z.object({\n  id: z.string(),\n  ...WorkflowStatusZodShape,\n  snapshots: z.array(WorkflowSnapshotZodSchema),\n});\n\nconst WorkflowReportsZodSchema = z.record(z.string(), WorkflowNodeReportZodSchema);\n\nconst WorkflowMessageZodSchema = z.object({\n  id: z.string(),\n  type: z.enum(['log', 'info', 'debug', 'error', 'warning']),\n  message: z.string(),\n  nodeID: z.string().optional(),\n  timestamp: z.number(),\n});\n\nconst WorkflowMessagesZodSchema = z.record(\n  z.enum(['log', 'info', 'debug', 'error', 'warning']),\n  z.array(WorkflowMessageZodSchema)\n);\n\nexport const WorkflowZodSchema = {\n  Inputs: WorkflowIOZodSchema,\n  Outputs: WorkflowIOZodSchema,\n  Status: WorkflowStatusZodSchema,\n  Snapshot: WorkflowSnapshotZodSchema,\n  Reports: WorkflowReportsZodSchema,\n  Messages: WorkflowMessagesZodSchema,\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/server-info/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { type FlowGramAPIDefine } from '@api/type';\nimport { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from '@api/constant';\n\nexport interface ServerInfoInput {}\n\nexport interface ServerInfoOutput {\n  name: string;\n  title: string;\n  description: string;\n  runtime: string;\n  version: string;\n  time: string;\n}\n\nexport const ServerInfoDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.ServerInfo,\n  method: FlowGramAPIMethod.GET,\n  path: '/info',\n  module: FlowGramAPIModule.Info,\n  schema: {\n    input: z.undefined(),\n    output: z.object({\n      name: z.string(),\n      runtime: z.string(),\n      version: z.string(),\n      time: z.string(),\n    }),\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/task-cancel/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { FlowGramAPIDefine } from '@api/type';\nimport { FlowGramAPIName, FlowGramAPIMethod, FlowGramAPIModule } from '@api/constant';\n\nexport interface TaskCancelInput {\n  taskID: string;\n}\n\nexport type TaskCancelOutput = {\n  success: boolean;\n};\n\nexport const TaskCancelDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.TaskCancel,\n  method: FlowGramAPIMethod.PUT,\n  path: '/task/cancel',\n  module: FlowGramAPIModule.Task,\n  schema: {\n    input: z.object({\n      taskID: z.string(),\n    }),\n    output: z.object({\n      success: z.boolean(),\n    }),\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/task-report/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { IReport } from '@runtime/index';\nimport { FlowGramAPIDefine } from '@api/type';\nimport { WorkflowZodSchema } from '@api/schema';\nimport { FlowGramAPIName, FlowGramAPIMethod, FlowGramAPIModule } from '@api/constant';\n\nexport interface TaskReportInput {\n  taskID: string;\n}\n\nexport type TaskReportOutput = IReport | undefined;\n\nexport const TaskReportDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.TaskReport,\n  method: FlowGramAPIMethod.GET,\n  path: '/task/report',\n  module: FlowGramAPIModule.Task,\n  schema: {\n    input: z.object({\n      taskID: z.string(),\n    }),\n    output: z.object({\n      id: z.string(),\n      inputs: WorkflowZodSchema.Inputs,\n      outputs: WorkflowZodSchema.Outputs,\n      workflowStatus: WorkflowZodSchema.Status,\n      reports: WorkflowZodSchema.Reports,\n      messages: WorkflowZodSchema.Messages,\n    }),\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/task-result/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { WorkflowOutputs } from '@runtime/index';\nimport { FlowGramAPIDefine } from '@api/type';\nimport { WorkflowZodSchema } from '@api/schema';\nimport { FlowGramAPIName, FlowGramAPIMethod, FlowGramAPIModule } from '@api/constant';\n\nexport interface TaskResultInput {\n  taskID: string;\n}\n\nexport type TaskResultOutput = WorkflowOutputs | undefined;\n\nexport const TaskResultDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.TaskResult,\n  method: FlowGramAPIMethod.GET,\n  path: '/task/result',\n  module: FlowGramAPIModule.Task,\n  schema: {\n    input: z.object({\n      taskID: z.string(),\n    }),\n    output: WorkflowZodSchema.Outputs,\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/task-run/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { WorkflowInputs } from '@runtime/index';\nimport { FlowGramAPIDefine } from '@api/type';\nimport { WorkflowZodSchema } from '@api/schema';\nimport { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from '@api/constant';\n\nexport interface TaskRunInput {\n  inputs: WorkflowInputs;\n  schema: string;\n}\n\nexport interface TaskRunOutput {\n  taskID: string;\n}\n\nexport const TaskRunDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.TaskRun,\n  method: FlowGramAPIMethod.POST,\n  path: '/task/run',\n  module: FlowGramAPIModule.Task,\n  schema: {\n    input: z.object({\n      schema: z.string(),\n      inputs: WorkflowZodSchema.Inputs,\n    }),\n    output: z.object({\n      taskID: z.string(),\n    }),\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/task-validate/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\n\nimport { ValidationResult, WorkflowInputs } from '@runtime/index';\nimport { FlowGramAPIDefine } from '@api/type';\nimport { WorkflowZodSchema } from '@api/schema';\nimport { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from '@api/constant';\n\nexport interface TaskValidateInput {\n  inputs: WorkflowInputs;\n  schema: string;\n}\n\nexport interface TaskValidateOutput extends ValidationResult {}\n\nexport const TaskValidateDefine: FlowGramAPIDefine = {\n  name: FlowGramAPIName.TaskValidate,\n  method: FlowGramAPIMethod.POST,\n  path: '/task/validate',\n  module: FlowGramAPIModule.Task,\n  schema: {\n    input: z.object({\n      schema: z.string(),\n      inputs: WorkflowZodSchema.Inputs,\n    }),\n    output: z.object({\n      valid: z.boolean(),\n      errors: z.array(z.string()).optional(),\n    }),\n  },\n};\n"
  },
  {
    "path": "packages/runtime/interface/src/api/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type z from 'zod';\n\nimport { FlowGramAPIMethod, FlowGramAPIModule, FlowGramAPIName } from './constant';\n\nexport interface FlowGramAPIDefine {\n  name: FlowGramAPIName;\n  method: FlowGramAPIMethod;\n  path: `/${string}`;\n  module: FlowGramAPIModule;\n  schema: {\n    input: z.ZodFirstPartySchemaTypes;\n    output: z.ZodFirstPartySchemaTypes;\n  };\n}\n\nexport interface FlowGramAPIDefines {\n  [key: string]: FlowGramAPIDefine;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/client/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type {\n  FlowGramAPIName,\n  TaskCancelInput,\n  TaskCancelOutput,\n  TaskReportInput,\n  TaskReportOutput,\n  TaskResultInput,\n  TaskResultOutput,\n  TaskRunInput,\n  TaskRunOutput,\n  TaskValidateInput,\n  TaskValidateOutput,\n} from '@api/index';\n\nexport interface IRuntimeClient {\n  [FlowGramAPIName.TaskRun]: (input: TaskRunInput) => Promise<TaskRunOutput | undefined>;\n  [FlowGramAPIName.TaskReport]: (input: TaskReportInput) => Promise<TaskReportOutput | undefined>;\n  [FlowGramAPIName.TaskResult]: (input: TaskResultInput) => Promise<TaskResultOutput | undefined>;\n  [FlowGramAPIName.TaskCancel]: (input: TaskCancelInput) => Promise<TaskCancelOutput | undefined>;\n  [FlowGramAPIName.TaskValidate]: (\n    input: TaskValidateInput\n  ) => Promise<TaskValidateOutput | undefined>;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './api';\nexport * from './schema';\nexport * from './node';\nexport * from './runtime';\nexport * from './client';\n"
  },
  {
    "path": "packages/runtime/interface/src/node/break/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { FlowGramNode } from '@node/constant';\n\ninterface BreakNodeData {}\n\nexport type BreakNodeSchema = WorkflowNodeSchema<FlowGramNode.Break, BreakNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/code/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { IJsonSchema } from '@schema/json-schema';\nimport { FlowGramNode } from '@node/constant';\n\ninterface CodeNodeData {\n  title: string;\n  inputsValues: Record<string, IFlowValue>;\n  inputs: IJsonSchema<'object'>;\n  outputs: IJsonSchema<'object'>;\n  script: {\n    language: 'javascript';\n    content: string;\n  };\n}\n\nexport type CodeNodeSchema = WorkflowNodeSchema<FlowGramNode.Code, CodeNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/condition/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum ConditionOperator {\n  /** Equal */\n  EQ = 'eq',\n  /** Not Equal */\n  NEQ = 'neq',\n  /** Greater Than */\n  GT = 'gt',\n  /** Greater Than or Equal */\n  GTE = 'gte',\n  /** Less Than */\n  LT = 'lt',\n  /** Less Than or Equal */\n  LTE = 'lte',\n  /** In */\n  IN = 'in',\n  /** Not In */\n  NIN = 'nin',\n  /** Contains */\n  CONTAINS = 'contains',\n  /** Not Contains */\n  NOT_CONTAINS = 'not_contains',\n  /** Is Empty */\n  IS_EMPTY = 'is_empty',\n  /** Is Not Empty */\n  IS_NOT_EMPTY = 'is_not_empty',\n  /** Is True */\n  IS_TRUE = 'is_true',\n  /** Is False */\n  IS_FALSE = 'is_false',\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/node/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue, IFlowRefValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { FlowGramNode } from '@node/constant';\nimport { ConditionOperator } from './constant';\n\nexport { ConditionOperator };\n\nexport interface ConditionItem {\n  key: string;\n  value: {\n    left: IFlowRefValue;\n    operator: ConditionOperator;\n    right: IFlowConstantRefValue;\n  };\n}\n\ninterface ConditionNodeData {\n  title: string;\n  conditions: ConditionItem[];\n}\n\nexport type ConditionNodeSchema = WorkflowNodeSchema<FlowGramNode.Condition, ConditionNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum FlowGramNode {\n  Root = 'root',\n  Start = 'start',\n  End = 'end',\n  LLM = 'llm',\n  Code = 'code',\n  Condition = 'condition',\n  Loop = 'loop',\n  Comment = 'comment',\n  Group = 'group',\n  BlockStart = 'block-start',\n  BlockEnd = 'block-end',\n  HTTP = 'http',\n  Break = 'break',\n  Continue = 'continue',\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/node/continue/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { FlowGramNode } from '@node/constant';\n\ninterface ContinueNodeData {}\n\nexport type ContinueNodeSchema = WorkflowNodeSchema<FlowGramNode.Continue, ContinueNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { IJsonSchema } from '@schema/json-schema';\nimport { FlowGramNode } from '@node/constant';\n\ninterface EndNodeData {\n  title: string;\n  inputs: IJsonSchema<'object'>;\n  inputsValues: Record<string, IFlowConstantRefValue>;\n}\n\nexport type EndNodeSchema = WorkflowNodeSchema<FlowGramNode.End, EndNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/http/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum HTTPMethod {\n  Get = 'GET',\n  Post = 'POST',\n  Put = 'PUT',\n  Delete = 'DELETE',\n  Patch = 'PATCH',\n  Head = 'HEAD',\n}\n\nexport enum HTTPBodyType {\n  None = 'none',\n  FormData = 'form-data',\n  XWwwFormUrlencoded = 'x-www-form-urlencoded',\n  RawText = 'raw-text',\n  JSON = 'JSON',\n  Binary = 'binary',\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/node/http/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue, IFlowTemplateValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { IJsonSchema } from '@schema/json-schema';\nimport { FlowGramNode } from '@node/constant';\nimport { HTTPBodyType, HTTPMethod } from './constant';\n\ninterface HTTPNodeData {\n  title: string;\n  outputs: IJsonSchema<'object'>;\n  api: {\n    method: HTTPMethod;\n    url: IFlowTemplateValue;\n  };\n  headers: IJsonSchema<'object'>;\n  headersValues: Record<string, IFlowConstantRefValue>;\n  params: IJsonSchema<'object'>;\n  paramsValues: Record<string, IFlowConstantRefValue>;\n  body: {\n    bodyType: HTTPBodyType;\n    json?: IFlowTemplateValue;\n    formData?: IJsonSchema<'object'>;\n    formDataValues?: Record<string, IFlowConstantRefValue>;\n    rawText?: IFlowTemplateValue;\n    binary?: IFlowTemplateValue;\n    xWwwFormUrlencoded?: IJsonSchema<'object'>;\n    xWwwFormUrlencodedValues?: Record<string, IFlowConstantRefValue>;\n  };\n  timeout: {\n    retryTimes: number;\n    timeout: number;\n  };\n}\nexport { HTTPMethod, HTTPBodyType };\nexport type HTTPNodeSchema = WorkflowNodeSchema<FlowGramNode.HTTP, HTTPNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowGramNode } from './constant';\nexport { EndNodeSchema } from './end';\nexport { LLMNodeSchema } from './llm';\nexport { StartNodeSchema } from './start';\nexport { LoopNodeSchema } from './loop';\nexport { ConditionNodeSchema, ConditionOperator, ConditionItem } from './condition';\nexport { HTTPNodeSchema, HTTPMethod, HTTPBodyType } from './http';\nexport { CodeNodeSchema } from './code';\nexport { BreakNodeSchema } from './break';\nexport { ContinueNodeSchema } from './continue';\n"
  },
  {
    "path": "packages/runtime/interface/src/node/llm/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowConstantRefValue, IFlowConstantValue, IFlowTemplateValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { IJsonSchema } from '@schema/json-schema';\nimport { FlowGramNode } from '@node/constant';\n\ninterface LLMNodeData {\n  title: string;\n  inputs: IJsonSchema<'object'>;\n  outputs: IJsonSchema<'object'>;\n  inputValues: {\n    apiKey: IFlowConstantRefValue;\n    modelType: IFlowConstantRefValue;\n    baseURL: IFlowConstantRefValue;\n    temperature: IFlowConstantRefValue;\n    systemPrompt: IFlowConstantValue | IFlowTemplateValue;\n    prompt: IFlowConstantValue | IFlowTemplateValue;\n  };\n}\n\nexport type LLMNodeSchema = WorkflowNodeSchema<FlowGramNode.LLM, LLMNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowRefValue } from '@schema/value';\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { FlowGramNode } from '@node/constant';\n\ninterface LoopNodeData {\n  title: string;\n  loopFor: IFlowRefValue;\n  loopOutputs: Record<string, IFlowRefValue>;\n}\n\nexport type LoopNodeSchema = WorkflowNodeSchema<FlowGramNode.Loop, LoopNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/node/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeSchema } from '@schema/node';\nimport { IJsonSchema } from '@schema/json-schema';\nimport { FlowGramNode } from '@node/constant';\n\ninterface StartNodeData {\n  title: string;\n  outputs: IJsonSchema<'object'>;\n}\n\nexport type StartNodeSchema = WorkflowNodeSchema<FlowGramNode.Start, StartNodeData>;\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/base/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type { VOData } from './value-object';\nexport type { InvokeParams, WorkflowRuntimeInvoke } from './invoke';\nexport type { WorkflowInputs, WorkflowOutputs } from './inputs-outputs';\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/base/inputs-outputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type WorkflowInputs = Record<string, any>;\nexport type WorkflowOutputs = Record<string, any>;\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/base/invoke.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@schema/index';\nimport { WorkflowInputs } from './inputs-outputs';\n\nexport interface InvokeParams {\n  schema: WorkflowSchema;\n  inputs: WorkflowInputs;\n}\n\nexport type WorkflowRuntimeInvoke = (params: InvokeParams) => Promise<WorkflowInputs>;\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/base/value-object.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type VOData<T> = Omit<T, 'id'>;\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/cache/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface ICache<K = string, V = any> {\n  init(): void;\n  dispose(): void;\n  get(key: K): V;\n  set(key: K, value: V): this;\n  delete(key: K): boolean;\n  has(key: K): boolean;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/container/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type ContainerService = any;\n\nexport interface IContainer {\n  get<T = ContainerService>(key: any): T;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IVariableStore } from '@runtime/variable';\nimport { IStatusCenter } from '@runtime/status';\nimport { IState } from '@runtime/state';\nimport { ISnapshotCenter } from '@runtime/snapshot';\nimport { IReporter } from '@runtime/reporter';\nimport { IMessageCenter } from '@runtime/message';\nimport { IIOCenter } from '@runtime/io-center';\nimport { IDocument } from '@runtime/document';\nimport { ICache } from '@runtime/cache';\nimport { InvokeParams } from '@runtime/base';\n\nexport interface ContextData {\n  cache: ICache;\n  variableStore: IVariableStore;\n  state: IState;\n  document: IDocument;\n  ioCenter: IIOCenter;\n  snapshotCenter: ISnapshotCenter;\n  statusCenter: IStatusCenter;\n  messageCenter: IMessageCenter;\n  reporter: IReporter;\n}\n\nexport interface IContext extends ContextData {\n  id: string;\n  init(params: InvokeParams): void;\n  dispose(): void;\n  sub(): IContext;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/document/document.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@schema/index';\nimport { INode } from './node';\nimport { IEdge } from './edge';\n\nexport interface IDocument {\n  id: string;\n  nodes: INode[];\n  edges: IEdge[];\n  root: INode;\n  start: INode;\n  end: INode;\n  init(schema: WorkflowSchema): void;\n  dispose(): void;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/document/edge.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IPort } from './port';\nimport { INode } from './node';\n\nexport interface IEdge {\n  id: string;\n  from: INode;\n  to: INode;\n  fromPort: IPort;\n  toPort: IPort;\n}\n\nexport interface CreateEdgeParams {\n  id: string;\n  from: INode;\n  to: INode;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/document/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type { IDocument } from './document';\nexport type { IEdge, CreateEdgeParams } from './edge';\nexport type { NodeDeclare as NodeVariable, INode, CreateNodeParams } from './node';\nexport type { IPort, CreatePortParams } from './port';\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/document/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IFlowValue, IJsonSchema, PositionSchema } from '@schema/index';\nimport { FlowGramNode } from '@node/constant';\nimport { IPort } from './port';\nimport { IEdge } from './edge';\n\nexport interface NodeDeclare {\n  inputsValues?: Record<string, IFlowValue>;\n  inputs?: IJsonSchema;\n  outputs?: IJsonSchema;\n}\n\nexport interface INode<T = any> {\n  id: string;\n  type: FlowGramNode;\n  name: string;\n  position: PositionSchema;\n  declare: NodeDeclare;\n  data: T;\n  ports: {\n    inputs: IPort[];\n    outputs: IPort[];\n  };\n  edges: {\n    inputs: IEdge[];\n    outputs: IEdge[];\n  };\n  parent: INode | null;\n  children: INode[];\n  prev: INode[];\n  next: INode[];\n  successors: INode[];\n  predecessors: INode[];\n  isBranch: boolean;\n}\n\nexport interface CreateNodeParams {\n  id: string;\n  type: FlowGramNode;\n  name: string;\n  position: PositionSchema;\n  variable?: NodeDeclare;\n  data?: any;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/document/port.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowPortType } from '@schema/index';\nimport { INode } from './node';\nimport { IEdge } from './edge';\n\nexport interface IPort {\n  id: string;\n  node: INode;\n  edges: IEdge[];\n  type: WorkflowPortType;\n}\n\nexport interface CreatePortParams {\n  id: string;\n  node: INode;\n  type: WorkflowPortType;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/engine/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IValidation } from '@runtime/validation';\nimport { ITask } from '../task';\nimport { IExecutor } from '../executor';\nimport { INode } from '../document';\nimport { IContext } from '../context';\nimport { InvokeParams } from '../base';\n\nexport interface EngineServices {\n  Validation: IValidation;\n  Executor: IExecutor;\n}\n\nexport interface IEngine {\n  invoke(params: InvokeParams): ITask;\n  executeNode(params: { context: IContext; node: INode }): Promise<void>;\n}\n\nexport const IEngine = Symbol.for('Engine');\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/executor/executor.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ExecutionContext, ExecutionResult, INodeExecutor } from './node-executor';\n\nexport interface IExecutor {\n  execute: (context: ExecutionContext) => Promise<ExecutionResult>;\n  register: (executor: INodeExecutor) => void;\n}\n\nexport const IExecutor = Symbol.for('Executor');\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/executor/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { IExecutor } from './executor';\nexport type {\n  ExecutionContext,\n  ExecutionResult,\n  INodeExecutor,\n  INodeExecutorFactory,\n} from './node-executor';\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/executor/node-executor.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramNode } from '@node/index';\nimport { ISnapshot } from '../snapshot';\nimport { INode } from '../document';\nimport { IContext } from '../context';\nimport { IContainer } from '../container';\nimport { WorkflowInputs, WorkflowOutputs } from '../base';\n\nexport interface ExecutionContext {\n  node: INode;\n  inputs: WorkflowInputs;\n  container: IContainer;\n  runtime: IContext;\n  snapshot: ISnapshot;\n}\n\nexport interface ExecutionResult {\n  outputs: WorkflowOutputs;\n  branch?: string;\n}\n\nexport interface INodeExecutor {\n  type: FlowGramNode;\n  execute: (context: ExecutionContext) => Promise<ExecutionResult>;\n}\n\nexport interface INodeExecutorFactory {\n  new (): INodeExecutor;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './container';\nexport * from './base';\nexport * from './engine';\nexport * from './context';\nexport * from './document';\nexport * from './executor';\nexport * from './io-center';\nexport * from './snapshot';\nexport * from './reporter';\nexport * from './state';\nexport * from './status';\nexport * from './task';\nexport * from './validation';\nexport * from './variable';\nexport * from './message';\nexport * from './cache';\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/io-center/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowInputs, WorkflowOutputs } from '../base';\n\nexport interface IOData {\n  inputs: WorkflowInputs;\n  outputs: WorkflowOutputs;\n}\n\n/** Input & Output */\nexport interface IIOCenter {\n  inputs: WorkflowInputs;\n  outputs: WorkflowOutputs;\n  setInputs(inputs: WorkflowInputs): void;\n  setOutputs(outputs: WorkflowOutputs): void;\n  init(inputs: WorkflowInputs): void;\n  dispose(): void;\n  export(): IOData;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/message/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowMessageType {\n  Log = 'log',\n  Info = 'info',\n  Debug = 'debug',\n  Error = 'error',\n  Warn = 'warning',\n}\n\nexport interface MessageData {\n  message: string;\n  nodeID?: string;\n  timestamp?: number;\n}\n\nexport interface IMessage extends MessageData {\n  id: string;\n  type: WorkflowMessageType;\n  timestamp: number;\n}\n\nexport type WorkflowMessages = Record<WorkflowMessageType, IMessage[]>;\n\nexport interface IMessageCenter {\n  init(): void;\n  dispose(): void;\n  log(data: MessageData): IMessage;\n  info(data: MessageData): IMessage;\n  debug(data: MessageData): IMessage;\n  error(data: MessageData): IMessage;\n  warn(data: MessageData): IMessage;\n  export(): WorkflowMessages;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/reporter/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMessageCenter, WorkflowMessages } from '@runtime/message';\nimport { StatusData, IStatusCenter } from '../status';\nimport { Snapshot, ISnapshotCenter } from '../snapshot';\nimport { WorkflowInputs, WorkflowOutputs } from '../base';\n\nexport interface NodeReport extends StatusData {\n  id: string;\n  snapshots: Snapshot[];\n}\n\nexport type WorkflowReports = Record<string, NodeReport>;\n\nexport interface IReport {\n  id: string;\n  inputs: WorkflowInputs;\n  outputs: WorkflowOutputs;\n  workflowStatus: StatusData;\n  reports: WorkflowReports;\n  messages: WorkflowMessages;\n}\n\nexport interface IReporter {\n  snapshotCenter: ISnapshotCenter;\n  statusCenter: IStatusCenter;\n  messageCenter: IMessageCenter;\n  init(): void;\n  dispose(): void;\n  export(): IReport;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/snapshot/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport type { ISnapshot, Snapshot, SnapshotData } from './snapshot';\nexport type { ISnapshotCenter } from './snapshot-center';\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/snapshot/snapshot-center.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ISnapshot, Snapshot, SnapshotData } from './snapshot';\n\nexport interface ISnapshotCenter {\n  id: string;\n  create(snapshot: Partial<SnapshotData>): ISnapshot;\n  exportAll(): Snapshot[];\n  export(): Record<string, Snapshot[]>;\n  init(): void;\n  dispose(): void;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/snapshot/snapshot.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowInputs, WorkflowOutputs } from '../base';\n\nexport interface SnapshotData {\n  nodeID: string;\n  inputs: WorkflowInputs;\n  outputs: WorkflowOutputs;\n  data: any;\n  branch?: string;\n  error?: string;\n}\n\nexport interface Snapshot extends SnapshotData {\n  id: string;\n}\n\nexport interface ISnapshot {\n  id: string;\n  data: Partial<SnapshotData>;\n  update(data: Partial<SnapshotData>): void;\n  validate(): boolean;\n  export(): Snapshot;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/state/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  IFlowValue,\n  IFlowRefValue,\n  WorkflowVariableType,\n  IFlowTemplateValue,\n  IJsonSchema,\n  WorkflowSchema,\n} from '@schema/index';\nimport { IVariableParseResult, IVariableStore } from '../variable';\nimport { INode } from '../document';\nimport { WorkflowInputs, WorkflowOutputs } from '../base';\n\nexport interface IState {\n  id: string;\n  variableStore: IVariableStore;\n  init(schema?: WorkflowSchema): void;\n  dispose(): void;\n  getNodeInputs(node: INode): WorkflowInputs;\n  setNodeOutputs(params: { node: INode; outputs: WorkflowOutputs }): void;\n  parseInputs(params: { values: Record<string, IFlowValue>; declare: IJsonSchema }): WorkflowInputs;\n  parseRef<T = unknown>(ref: IFlowRefValue): IVariableParseResult<T> | null;\n  parseTemplate(template: IFlowTemplateValue): IVariableParseResult<string> | null;\n  parseFlowValue<T = unknown>(params: {\n    flowValue: IFlowValue;\n    declareType: WorkflowVariableType;\n  }): IVariableParseResult<T> | null;\n  isExecutedNode(node: INode): boolean;\n  addExecutedNode(node: INode): void;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/status/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowStatus {\n  Pending = 'pending',\n  Processing = 'processing',\n  Succeeded = 'succeeded',\n  Failed = 'failed',\n  Cancelled = 'canceled',\n}\n\nexport interface StatusData {\n  status: WorkflowStatus;\n  terminated: boolean;\n  startTime: number;\n  endTime?: number;\n  timeCost: number;\n}\n\nexport interface IStatus extends StatusData {\n  id: string;\n  process(): void;\n  success(): void;\n  fail(): void;\n  cancel(): void;\n  export(): StatusData;\n}\n\nexport interface IStatusCenter {\n  workflow: IStatus;\n  nodeStatus(nodeID: string): IStatus;\n  init(): void;\n  dispose(): void;\n  getStatusNodeIDs(status: WorkflowStatus): string[];\n  exportNodeStatus(): Record<string, StatusData>;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/task/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IContext } from '../context';\nimport { WorkflowOutputs } from '../base';\n\nexport interface ITask {\n  id: string;\n  processing: Promise<WorkflowOutputs>;\n  context: IContext;\n  cancel(): void;\n}\n\nexport interface TaskParams {\n  processing: Promise<WorkflowOutputs>;\n  context: IContext;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/validation/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { InvokeParams } from '@runtime/base';\n\nexport interface ValidationResult {\n  valid: boolean;\n  errors?: string[];\n}\n\nexport interface IValidation {\n  invoke(params: InvokeParams): ValidationResult;\n}\n\nexport const IValidation = Symbol.for('Validation');\n"
  },
  {
    "path": "packages/runtime/interface/src/runtime/variable/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowVariableType } from '@schema/index';\n\ninterface VariableTypeInfo {\n  type: WorkflowVariableType;\n  itemsType?: WorkflowVariableType;\n}\n\nexport interface IVariable<T = Object> extends VariableTypeInfo {\n  id: string;\n  nodeID: string;\n  key: string;\n  value: T;\n}\n\nexport interface IVariableParseResult<T = unknown> extends VariableTypeInfo {\n  value: T;\n  type: WorkflowVariableType;\n}\n\nexport interface IVariableStore {\n  id: string;\n  store: Map<string, Map<string, IVariable>>;\n  setParent(parent: IVariableStore): void;\n  setVariable(\n    params: {\n      nodeID: string;\n      key: string;\n      value: unknown;\n    } & VariableTypeInfo\n  ): void;\n  setValue(params: {\n    nodeID: string;\n    variableKey: string;\n    variablePath?: string[];\n    value: unknown;\n  }): void;\n  getValue<T = unknown>(params: {\n    nodeID: string;\n    variableKey: string;\n    variablePath?: string[];\n  }): IVariableParseResult<T> | null;\n  init(): void;\n  dispose(): void;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport enum WorkflowPortType {\n  Input = 'input',\n  Output = 'output',\n}\n\nexport enum WorkflowVariableType {\n  String = 'string',\n  Integer = 'integer',\n  Number = 'number',\n  Boolean = 'boolean',\n  Object = 'object',\n  Array = 'array',\n  Map = 'map',\n  DateTime = 'date-time',\n  Null = 'null',\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/edge.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface WorkflowEdgeSchema {\n  sourceNodeID: string;\n  targetNodeID: string;\n  sourcePortID?: string;\n  targetPortID?: string;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/group.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeSchema } from './node';\n\nexport interface WorkflowGroupSchema extends WorkflowNodeSchema {\n  data: {\n    title?: string;\n    color?: string;\n    parentID: string;\n    blockIDs: string[];\n  };\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowEdgeSchema } from './edge';\nexport { JsonSchemaBasicType, IJsonSchema, IBasicJsonSchema } from './json-schema';\nexport { WorkflowNodeMetaSchema } from './node-meta';\nexport { WorkflowNodeSchema } from './node';\nexport { WorkflowSchema } from './workflow';\nexport { XYSchema, PositionSchema } from './xy';\nexport { WorkflowPortType, WorkflowVariableType } from './constant';\nexport {\n  IFlowConstantRefValue,\n  IFlowConstantValue,\n  IFlowRefValue,\n  IFlowValue,\n  IFlowTemplateValue,\n} from './value';\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/json-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// TODO copy packages/materials/form-materials/src/typings/json-schema/index.ts\n\nexport type JsonSchemaBasicType =\n  | 'boolean'\n  | 'string'\n  | 'integer'\n  | 'number'\n  | 'object'\n  | 'array'\n  | 'map';\n\nexport interface IJsonSchema<T = string> {\n  type: T;\n  default?: any;\n  title?: string;\n  description?: string;\n  enum?: (string | number)[];\n  properties?: Record<string, IJsonSchema<T>>;\n  additionalProperties?: IJsonSchema<T>;\n  items?: IJsonSchema<T>;\n  required?: string[];\n  $ref?: string;\n  extra?: {\n    index?: number;\n    // Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak\n    weak?: boolean;\n    // Set the render component\n    formComponent?: string;\n    [key: string]: any;\n  };\n}\n\nexport type IBasicJsonSchema = IJsonSchema<JsonSchemaBasicType>;\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/node-meta.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { PositionSchema } from './xy';\n\nexport interface WorkflowNodeMetaSchema {\n  position: PositionSchema;\n  canvasPosition?: PositionSchema;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { IFlowValue } from './value';\nimport type { WorkflowNodeMetaSchema } from './node-meta';\nimport { IJsonSchema } from './json-schema';\nimport type { WorkflowEdgeSchema } from './edge';\n\nexport interface WorkflowNodeSchema<T = string, D = any> {\n  id: string;\n  type: T;\n  meta: WorkflowNodeMetaSchema;\n  data: D & {\n    title?: string;\n    inputsValues?: Record<string, IFlowValue>;\n    inputs?: IJsonSchema;\n    outputs?: IJsonSchema;\n    [key: string]: any;\n  };\n  blocks?: WorkflowNodeSchema[];\n  edges?: WorkflowEdgeSchema[];\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/value.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n// TODO copy packages/materials/form-materials/src/typings/flow-value/index.ts\n\nexport interface IFlowConstantValue {\n  type: 'constant';\n  content?: string | number | boolean;\n}\n\nexport interface IFlowRefValue {\n  type: 'ref';\n  content?: string[];\n}\n\nexport interface IFlowExpressionValue {\n  type: 'expression';\n  content?: string;\n}\n\nexport interface IFlowTemplateValue {\n  type: 'template';\n  content?: string;\n}\n\nexport type IFlowValue =\n  | IFlowConstantValue\n  | IFlowRefValue\n  | IFlowExpressionValue\n  | IFlowTemplateValue;\n\nexport type IFlowConstantRefValue = IFlowConstantValue | IFlowRefValue;\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/workflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowNodeSchema } from './node';\nimport type { IJsonSchema } from './json-schema';\nimport type { WorkflowGroupSchema } from './group';\nimport type { WorkflowEdgeSchema } from './edge';\n\nexport interface WorkflowSchema {\n  nodes: WorkflowNodeSchema[];\n  edges: WorkflowEdgeSchema[];\n  groups?: WorkflowGroupSchema[];\n  globalVariable?: IJsonSchema;\n}\n"
  },
  {
    "path": "packages/runtime/interface/src/schema/xy.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport interface XYSchema {\n  x: number;\n  y: number;\n}\n\nexport type PositionSchema = XYSchema;\n"
  },
  {
    "path": "packages/runtime/interface/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@api/*\": [\n        \"api/*\"\n      ],\n      \"@node/*\": [\n        \"node/*\"\n      ],\n      \"@runtime/*\": [\n        \"runtime/*\"\n      ],\n      \"@schema/*\": [\n        \"schema/*\"\n      ],\n      \"@client/*\": [\n        \"client/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/runtime/js-core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineFlatConfig } from '@flowgram.ai/eslint-config';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineFlatConfig({\n  preset: 'base',\n  packageRoot: __dirname,\n});\n"
  },
  {
    "path": "packages/runtime/js-core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/runtime-js\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"npm run watch\",\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\",\n    \"lint\": \"eslint ./src --cache\",\n    \"lint:fix\": \"eslint ./src --fix\"\n  },\n  \"dependencies\": {\n    \"@langchain/openai\": \"0.5.18\",\n    \"@langchain/core\": \"^0.3.58\",\n    \"lodash-es\": \"^4.17.21\",\n    \"quickjs-emscripten\": \"^0.32.0\",\n    \"uuid\": \"^9.0.0\",\n    \"zod\": \"^3.24.4\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/runtime-interface\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/uuid\": \"^9.0.1\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"dotenv\": \"~16.5.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramAPIName } from '@flowgram.ai/runtime-interface';\n\nimport { TaskValidateAPI } from './task-validate';\nimport { TaskRunAPI } from './task-run';\nimport { TaskResultAPI } from './task-result';\nimport { TaskReportAPI } from './task-report';\nimport { TaskCancelAPI } from './task-cancel';\n\nexport { TaskRunAPI, TaskResultAPI, TaskReportAPI, TaskCancelAPI, TaskValidateAPI };\n\nexport const WorkflowRuntimeAPIs: Record<FlowGramAPIName, (i: any) => any> = {\n  [FlowGramAPIName.ServerInfo]: () => {}, // TODO\n  [FlowGramAPIName.TaskRun]: TaskRunAPI,\n  [FlowGramAPIName.TaskReport]: TaskReportAPI,\n  [FlowGramAPIName.TaskResult]: TaskResultAPI,\n  [FlowGramAPIName.TaskCancel]: TaskCancelAPI,\n  [FlowGramAPIName.TaskValidate]: TaskValidateAPI,\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/task-cancel.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TaskCancelInput, TaskCancelOutput } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowApplication } from '@application/workflow';\n\nexport const TaskCancelAPI = async (input: TaskCancelInput): Promise<TaskCancelOutput> => {\n  const app = WorkflowApplication.instance;\n  const { taskID } = input;\n  const success = app.cancel(taskID);\n  const output: TaskCancelOutput = {\n    success,\n  };\n  return output;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/task-report.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable no-console */\nimport {\n  TaskReportInput,\n  TaskReportOutput,\n  TaskReportDefine,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowApplication } from '@application/workflow';\n\nexport const TaskReportAPI = async (input: TaskReportInput): Promise<TaskReportOutput> => {\n  const app = WorkflowApplication.instance;\n  const { taskID } = input;\n  const output: TaskReportOutput = app.report(taskID);\n  try {\n    TaskReportDefine.schema.output.parse(output);\n  } catch (e) {\n    console.log('> TaskReportAPI - output: ', JSON.stringify(output));\n    console.error(e);\n  }\n  return output;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/task-result.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TaskResultInput, TaskResultOutput } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowApplication } from '@application/workflow';\n\nexport const TaskResultAPI = async (input: TaskResultInput): Promise<TaskResultOutput> => {\n  const app = WorkflowApplication.instance;\n  const { taskID } = input;\n  const output: TaskResultOutput = app.result(taskID);\n  return output;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/task-run.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TaskRunInput, TaskRunOutput } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowApplication } from '@application/workflow';\n\nexport const TaskRunAPI = async (input: TaskRunInput): Promise<TaskRunOutput> => {\n  const app = WorkflowApplication.instance;\n  const { schema: stringSchema, inputs } = input;\n  const schema = JSON.parse(stringSchema);\n  const taskID = app.run({\n    schema,\n    inputs,\n  });\n  const output: TaskRunOutput = {\n    taskID,\n  };\n  return output;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/api/task-validate.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { TaskValidateInput, TaskValidateOutput } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowApplication } from '@application/workflow';\n\nexport const TaskValidateAPI = async (input: TaskValidateInput): Promise<TaskValidateOutput> => {\n  const app = WorkflowApplication.instance;\n  const { schema: stringSchema, inputs } = input;\n  const schema = JSON.parse(stringSchema);\n  const result = app.validate({\n    schema,\n    inputs,\n  });\n  const output: TaskValidateOutput = result;\n  return output;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/application/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowApplication } from './workflow';\n"
  },
  {
    "path": "packages/runtime/js-core/src/application/workflow.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable no-console */\nimport {\n  InvokeParams,\n  IContainer,\n  IEngine,\n  ITask,\n  IReport,\n  WorkflowOutputs,\n  IValidation,\n  ValidationResult,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeContainer } from '@workflow/container';\n\nexport class WorkflowApplication {\n  private container: IContainer;\n\n  public tasks: Map<string, ITask>;\n\n  constructor() {\n    this.container = WorkflowRuntimeContainer.instance;\n    this.tasks = new Map();\n  }\n\n  public run(params: InvokeParams): string {\n    const engine = this.container.get<IEngine>(IEngine);\n    const task = engine.invoke(params);\n    this.tasks.set(task.id, task);\n    console.log('> POST TaskRun - taskID: ', task.id);\n    console.log(params.inputs);\n    task.processing.then((output) => {\n      console.log('> LOG Task finished: ', task.id);\n      console.log(output);\n    });\n    return task.id;\n  }\n\n  public cancel(taskID: string): boolean {\n    console.log('> PUT TaskCancel - taskID: ', taskID);\n    const task = this.tasks.get(taskID);\n    if (!task) {\n      return false;\n    }\n    task.cancel();\n    return true;\n  }\n\n  public report(taskID: string): IReport | undefined {\n    const task = this.tasks.get(taskID);\n    console.log('> GET TaskReport - taskID: ', taskID);\n    if (!task) {\n      return;\n    }\n    return task.context.reporter.export();\n  }\n\n  public result(taskID: string): WorkflowOutputs | undefined {\n    console.log('> GET TaskResult - taskID: ', taskID);\n    const task = this.tasks.get(taskID);\n    if (!task) {\n      return;\n    }\n    if (!task.context.statusCenter.workflow.terminated) {\n      return;\n    }\n    return task.context.ioCenter.outputs;\n  }\n\n  public validate(params: InvokeParams): ValidationResult {\n    const validation = this.container.get<IValidation>(IValidation);\n    const result = validation.invoke(params);\n    console.log('> POST TaskValidate - valid: ', result.valid);\n    return result;\n  }\n\n  private static _instance: WorkflowApplication;\n\n  public static get instance(): WorkflowApplication {\n    if (this._instance) {\n      return this._instance;\n    }\n    this._instance = new WorkflowApplication();\n    return this._instance;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const ENABLE_REAL_LLM = false;\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/executor/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { INodeExecutorFactory } from '@flowgram.ai/runtime-interface';\n\nimport { StartExecutor } from '@nodes/start';\nimport { EndExecutor } from '@nodes/end';\nimport { ConditionExecutor } from '@nodes/condition';\nimport { MockLLMExecutor } from './llm';\n\nexport const MockWorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [\n  StartExecutor,\n  EndExecutor,\n  MockLLMExecutor,\n  ConditionExecutor,\n];\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/executor/llm.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ExecutionContext, ExecutionResult } from '@flowgram.ai/runtime-interface';\n\nimport { LLMExecutor, LLMExecutorInputs } from '@nodes/llm';\nimport { delay } from '@infra/utils';\n\nexport class MockLLMExecutor extends LLMExecutor {\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = context.inputs as LLMExecutorInputs;\n    this.checkInputs(inputs);\n    await delay(10); // TODO mock node run\n    const result = `Hi, I am an AI model, my name is ${inputs.modelName}, temperature is ${inputs.temperature}, system prompt is \"${inputs.systemPrompt}\", prompt is \"${inputs.prompt}\"`;\n    return {\n      outputs: {\n        result,\n      },\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/basic.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime basic schema', () => {\n  it('should execute a workflow with input', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.basicSchema,\n      inputs: {\n        model_name: 'ai-model',\n        llm_settings: {\n          temperature: 0.5,\n        },\n        work: {\n          role: 'Chat',\n          task: 'Tell me a story about love',\n        },\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      llm_res: `Hi, I am an AI model, my name is ai-model, temperature is 0.5, system prompt is \"You are a helpful AI assistant.\", prompt is \"<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>\"`,\n      llm_task: 'Tell me a story about love',\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: {\n          model_name: 'ai-model',\n          llm_settings: { temperature: 0.5 },\n          work: { role: 'Chat', task: 'Tell me a story about love' },\n        },\n        data: {},\n      },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'ai-model',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.5,\n          prompt: '<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>',\n          systemPrompt: 'You are a helpful AI assistant.',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is ai-model, temperature is 0.5, system prompt is \"You are a helpful AI assistant.\", prompt is \"<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>\"',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          llm_res:\n            'Hi, I am an AI model, my name is ai-model, temperature is 0.5, system prompt is \"You are a helpful AI assistant.\", prompt is \"<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>\"',\n          llm_task: 'Tell me a story about love',\n        },\n        outputs: {\n          llm_res:\n            'Hi, I am an AI model, my name is ai-model, temperature is 0.5, system prompt is \"You are a helpful AI assistant.\", prompt is \"<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>\"',\n          llm_task: 'Tell me a story about love',\n        },\n        data: {},\n      },\n    ]);\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/basic.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const basicSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 171.6,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            model_name: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            },\n            llm_settings: {\n              type: 'object',\n              extra: {\n                index: 1,\n              },\n              properties: {\n                temperature: {\n                  type: 'number',\n                  extra: {\n                    index: 1,\n                  },\n                },\n              },\n              required: [],\n            },\n            work: {\n              type: 'object',\n              extra: {\n                index: 2,\n              },\n              properties: {\n                role: {\n                  type: 'string',\n                  extra: {\n                    index: 0,\n                  },\n                },\n                task: {\n                  type: 'string',\n                  extra: {\n                    index: 1,\n                  },\n                },\n              },\n              required: ['role', 'task'],\n            },\n          },\n          required: ['model_name', 'work'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1124.4,\n          y: 171.6,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          llm_res: {\n            type: 'ref',\n            content: ['llm_0', 'result'],\n          },\n          llm_task: {\n            type: 'ref',\n            content: ['start_0', 'work', 'task'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            llm_res: {\n              type: 'string',\n            },\n            llm_task: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_0',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 652.2,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n        inputsValues: {\n          modelName: {\n            type: 'ref',\n            content: ['start_0', 'model_name'],\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'ref',\n            content: ['start_0', 'llm_settings', 'temperature'],\n          },\n          prompt: {\n            type: 'template',\n            content: '<Role>{{start_0.work.role}}</Role>\\n\\n<Task>\\n{{start_0.work.task}}\\n</Task>',\n          },\n          systemPrompt: {\n            type: 'constant',\n            content: 'You are a helpful AI assistant.',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'llm_0',\n    },\n    {\n      sourceNodeID: 'llm_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/branch-two-layers.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime branch schema', () => {\n  it('should execute a workflow with branch 1', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.branchTwoLayersSchema,\n      inputs: {\n        model_id: 1,\n        prompt: 'Tell me a joke',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      m3_res:\n        'Hi, I am an AI model, my name is AI_MODEL_3, temperature is 0.5, system prompt is \"I\\'m Model 3\", prompt is \"Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I\\'m Model 1\", prompt is \"Tell me a joke\"\"',\n    });\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_1.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_3.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_2).toBeUndefined();\n    expect(report.reports.llm_4).toBeUndefined();\n  });\n\n  it('should execute a workflow with branch 2', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.branchTwoLayersSchema,\n      inputs: {\n        model_id: 2,\n        prompt: 'Tell me a story',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      m4_res:\n        'Hi, I am an AI model, my name is AI_MODEL_4, temperature is 0.5, system prompt is \"I\\'m Model 4\", prompt is \"Hi, I am an AI model, my name is AI_MODEL_2, temperature is 0.6, system prompt is \"I\\'m Model 2\", prompt is \"Tell me a story\"\"',\n    });\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_2.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_4.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_1).toBeUndefined();\n    expect(report.reports.llm_3).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/branch-two-layers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const branchTwoLayersSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 368.3,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            model_id: {\n              type: 'integer',\n              extra: {\n                index: 0,\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                index: 1,\n              },\n            },\n          },\n          required: [],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 2020,\n          y: 368.29999999999995,\n        },\n      },\n      data: {\n        title: 'End',\n        inputs: {\n          type: 'object',\n          properties: {\n            m3_res: {\n              type: 'string',\n            },\n            m4_res: {\n              type: 'string',\n            },\n          },\n        },\n        inputsValues: {\n          m3_res: {\n            type: 'ref',\n            content: ['llm_3', 'result'],\n          },\n          m4_res: {\n            type: 'ref',\n            content: ['llm_4', 'result'],\n          },\n        },\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 640,\n          y: 304.8,\n        },\n      },\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'model_id'],\n              },\n              operator: 'eq',\n              right: {\n                type: 'constant',\n                content: 1,\n              },\n            },\n            key: 'if_1',\n          },\n          {\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'model_id'],\n              },\n              operator: 'eq',\n              right: {\n                type: 'constant',\n                content: 2,\n              },\n            },\n            key: 'if_2',\n          },\n        ],\n      },\n    },\n    {\n      id: 'llm_1',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1100,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_1',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: \"I'm Model 1\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.prompt}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_2',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1100,\n          y: 459.3,\n        },\n      },\n      data: {\n        title: 'LLM_2',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_2',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.6,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: \"I'm Model 2\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.prompt}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_3',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1560,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_3',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_3',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: \"I'm Model 3\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{llm_1.result}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_4',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1560,\n          y: 459.8,\n        },\n      },\n      data: {\n        title: 'LLM_4',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_4',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: \"I'm Model 4\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{llm_2.result}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'llm_3',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_4',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_1',\n      sourcePortID: 'if_1',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_2',\n      sourcePortID: 'if_2',\n    },\n    {\n      sourceNodeID: 'llm_1',\n      targetNodeID: 'llm_3',\n    },\n    {\n      sourceNodeID: 'llm_2',\n      targetNodeID: 'llm_4',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/branch.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime branch schema', () => {\n  it('should execute a workflow with branch 1', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.branchSchema,\n      inputs: {\n        model_id: 1,\n        prompt: 'Tell me a joke',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      m1_res: `Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I'm Model 1.\", prompt is \"Tell me a joke\"`,\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: { model_id: 1, prompt: 'Tell me a joke' },\n        data: {},\n      },\n      {\n        nodeID: 'condition_0',\n        inputs: {},\n        outputs: {},\n        data: {\n          conditions: [\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 1 },\n              },\n              key: 'if_1',\n            },\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 2 },\n              },\n              key: 'if_2',\n            },\n          ],\n        },\n        branch: 'if_1',\n      },\n      {\n        nodeID: 'llm_1',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.5,\n          systemPrompt: \"I'm Model 1.\",\n          prompt: 'Tell me a joke',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I\\'m Model 1.\", prompt is \"Tell me a joke\"',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          m1_res:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I\\'m Model 1.\", prompt is \"Tell me a joke\"',\n        },\n        outputs: {\n          m1_res:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I\\'m Model 1.\", prompt is \"Tell me a joke\"',\n        },\n        data: {},\n      },\n    ]);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_1.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_2).toBeUndefined();\n    expect(report.reports.llm_3).toBeUndefined();\n  });\n\n  it('should execute a workflow with branch 2', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.branchSchema,\n      inputs: {\n        model_id: 2,\n        prompt: 'Tell me a story',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      m2_res: `Hi, I am an AI model, my name is AI_MODEL_2, temperature is 0.6, system prompt is \"I'm Model 2.\", prompt is \"Tell me a story\"`,\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: { model_id: 2, prompt: 'Tell me a story' },\n        data: {},\n      },\n      {\n        nodeID: 'condition_0',\n        inputs: {},\n        outputs: {},\n        data: {\n          conditions: [\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 1 },\n              },\n              key: 'if_1',\n            },\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 2 },\n              },\n              key: 'if_2',\n            },\n          ],\n        },\n        branch: 'if_2',\n      },\n      {\n        nodeID: 'llm_2',\n        inputs: {\n          modelName: 'AI_MODEL_2',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: \"I'm Model 2.\",\n          prompt: 'Tell me a story',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_2, temperature is 0.6, system prompt is \"I\\'m Model 2.\", prompt is \"Tell me a story\"',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          m2_res:\n            'Hi, I am an AI model, my name is AI_MODEL_2, temperature is 0.6, system prompt is \"I\\'m Model 2.\", prompt is \"Tell me a story\"',\n        },\n        outputs: {\n          m2_res:\n            'Hi, I am an AI model, my name is AI_MODEL_2, temperature is 0.6, system prompt is \"I\\'m Model 2.\", prompt is \"Tell me a story\"',\n        },\n        data: {},\n      },\n    ]);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_2.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_1).toBeUndefined();\n    expect(report.reports.llm_3).toBeUndefined();\n  });\n\n  it('should execute a workflow with branch else', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.branchSchema,\n      inputs: {\n        model_id: 3,\n        prompt: 'Tell me a movie',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      m3_res: `Hi, I am an AI model, my name is AI_MODEL_3, temperature is 0.7, system prompt is \"I'm Model 3.\", prompt is \"Tell me a movie\"`,\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: { model_id: 3, prompt: 'Tell me a movie' },\n        data: {},\n      },\n      {\n        nodeID: 'condition_0',\n        inputs: {},\n        outputs: {},\n        data: {\n          conditions: [\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 1 },\n              },\n              key: 'if_1',\n            },\n            {\n              value: {\n                left: { type: 'ref', content: ['start_0', 'model_id'] },\n                operator: 'eq',\n                right: { type: 'constant', content: 2 },\n              },\n              key: 'if_2',\n            },\n          ],\n        },\n        branch: 'else',\n      },\n      {\n        nodeID: 'llm_3',\n        inputs: {\n          modelName: 'AI_MODEL_3',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.7,\n          systemPrompt: \"I'm Model 3.\",\n          prompt: 'Tell me a movie',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_3, temperature is 0.7, system prompt is \"I\\'m Model 3.\", prompt is \"Tell me a movie\"',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          m3_res:\n            'Hi, I am an AI model, my name is AI_MODEL_3, temperature is 0.7, system prompt is \"I\\'m Model 3.\", prompt is \"Tell me a movie\"',\n        },\n        outputs: {\n          m3_res:\n            'Hi, I am an AI model, my name is AI_MODEL_3, temperature is 0.7, system prompt is \"I\\'m Model 3.\", prompt is \"Tell me a movie\"',\n        },\n        data: {},\n      },\n    ]);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_3.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_1).toBeUndefined();\n    expect(report.reports.llm_2).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/branch.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const branchSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 614.7,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            model_id: {\n              type: 'integer',\n              extra: {\n                index: 0,\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                index: 1,\n              },\n            },\n          },\n          required: [],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1560,\n          y: 614.7,\n        },\n      },\n      data: {\n        title: 'End',\n        inputs: {\n          type: 'object',\n          properties: {\n            m1_res: {\n              type: 'string',\n            },\n            m2_res: {\n              type: 'string',\n            },\n            m3_res: {\n              type: 'string',\n            },\n          },\n        },\n        inputsValues: {\n          m1_res: {\n            type: 'ref',\n            content: ['llm_1', 'result'],\n            extra: {\n              index: 0,\n            },\n          },\n          m2_res: {\n            type: 'ref',\n            content: ['llm_2', 'result'],\n            extra: {\n              index: 1,\n            },\n          },\n          m3_res: {\n            type: 'ref',\n            content: ['llm_3', 'result'],\n            extra: {\n              index: 2,\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'condition_0',\n      type: 'condition',\n      meta: {\n        position: {\n          x: 640,\n          y: 526.7,\n        },\n      },\n      data: {\n        title: 'Condition',\n        conditions: [\n          {\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'model_id'],\n              },\n              operator: 'eq',\n              right: {\n                type: 'constant',\n                content: 1,\n              },\n            },\n            key: 'if_1',\n          },\n          {\n            value: {\n              left: {\n                type: 'ref',\n                content: ['start_0', 'model_id'],\n              },\n              operator: 'eq',\n              right: {\n                type: 'constant',\n                content: 2,\n              },\n            },\n            key: 'if_2',\n          },\n        ],\n      },\n    },\n    {\n      id: 'llm_1',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1100,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_1',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.5,\n          },\n          systemPrompt: {\n            type: 'constant',\n            content: \"I'm Model 1.\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.prompt}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_2',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1100,\n          y: 467.8,\n        },\n      },\n      data: {\n        title: 'LLM_2',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_2',\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.6,\n          },\n          systemPrompt: {\n            type: 'constant',\n            content: \"I'm Model 2.\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.prompt}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_3',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 1100,\n          y: 935.6,\n        },\n      },\n      data: {\n        title: 'LLM_3',\n        inputsValues: {\n          modelName: {\n            type: 'constant',\n            content: 'AI_MODEL_3',\n            schema: {\n              type: 'string',\n            },\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'constant',\n            content: 0.7,\n          },\n          systemPrompt: {\n            type: 'template',\n            content: \"I'm Model 3.\",\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.prompt}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'condition_0',\n    },\n    {\n      sourceNodeID: 'llm_1',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_2',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_3',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_1',\n      sourcePortID: 'if_1',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_2',\n      sourcePortID: 'if_2',\n    },\n    {\n      sourceNodeID: 'condition_0',\n      targetNodeID: 'llm_3',\n      sourcePortID: 'else',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/code.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime code schema', () => {\n  it('should execute a workflow with code node', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.codeSchema,\n      inputs: {\n        input: 'hello~',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result structure based on code schema output\n    expect(result).toStrictEqual({\n      input: 'hello~',\n      output_key0: 'hello~hello~', // Concatenated input\n      output_key1: ['hello', 'world'], // Array output\n      output_key2: {\n        // Object output\n        key21: 'hi',\n      },\n    });\n\n    // Verify snapshots\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: {\n          input: 'hello~',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'code_0',\n        inputs: {\n          input: 'hello~',\n        },\n        outputs: {\n          key0: 'hello~hello~',\n          key1: ['hello', 'world'],\n          key2: {\n            key21: 'hi',\n          },\n        },\n        data: {\n          script: {\n            language: 'javascript',\n            content:\n              '// Here, you can use \\'params\\' to access the input variables in the node and use \\'output\\' to output the result\\n// \\'params\\'  have already been properly injected into the environment\\n// Below is an example of retrieving the value of the parameter \\'input\\' from the node\\'s input:\\n// const input = params.input; \\n// Below is an example of outputting a \\'ret\\' object containing multiple data types:\\n// const output = { \"name\": \\'Jack\\', \"hobbies\": [\"reading\", \"traveling\"] };\\nasync function main({ params }) {\\n    // Construct the output object\\n    const output = {\\n        \"key0\": params.input + params.input, // Concatenate the value of the two input parameters\\n        \"key1\": [\"hello\", \"world\"], // Output an array\\n        \"key2\": { // Output an Object\\n            \"key21\": \"hi\"\\n        },\\n    };\\n    return output;\\n}',\n          },\n        },\n      },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          input: 'hello~',\n          output_key0: 'hello~hello~',\n          output_key1: ['hello', 'world'],\n          output_key2: {\n            key21: 'hi',\n          },\n        },\n        outputs: {\n          input: 'hello~',\n          output_key0: 'hello~hello~',\n          output_key1: ['hello', 'world'],\n          output_key2: {\n            key21: 'hi',\n          },\n        },\n        data: {},\n      },\n    ]);\n  });\n\n  it('should handle different input types in code node', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.codeSchema,\n      inputs: {\n        input: 'test123',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result with different input\n    expect(result).toStrictEqual({\n      input: 'test123',\n      output_key0: 'test123test123', // Concatenated input\n      output_key1: ['hello', 'world'], // Static array output\n      output_key2: {\n        // Static object output\n        key21: 'hi',\n      },\n    });\n  });\n\n  it('should handle empty string input in code node', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.codeSchema,\n      inputs: {\n        input: '',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result with empty input\n    expect(result).toStrictEqual({\n      input: '',\n      output_key0: '', // Empty string concatenated\n      output_key1: ['hello', 'world'], // Static array output\n      output_key2: {\n        // Static object output\n        key21: 'hi',\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/code.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const codeSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 171.6,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            input: {\n              type: 'string',\n            },\n          },\n          required: ['input'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1124.4,\n          y: 171.6,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          input: {\n            type: 'ref',\n            content: ['start_0', 'input'],\n          },\n          output_key0: {\n            type: 'ref',\n            content: ['code_0', 'key0'],\n          },\n          output_key1: {\n            type: 'ref',\n            content: ['code_0', 'key1'],\n          },\n          output_key2: {\n            type: 'ref',\n            content: ['code_0', 'key2'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            input: {\n              type: 'string',\n            },\n            output_key0: {\n              type: 'string',\n            },\n            output_key1: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            output_key2: {\n              type: 'object',\n              properties: {\n                type: 'string',\n              },\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'code_0',\n      type: 'code',\n      meta: {\n        position: {\n          x: 652.2,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'Code_0',\n        inputsValues: {\n          input: {\n            type: 'ref',\n            content: ['start_0', 'input'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['input'],\n          properties: {\n            input: {\n              type: 'string',\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            key0: {\n              type: 'string',\n            },\n            key1: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            key2: {\n              type: 'object',\n              properties: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        script: {\n          language: 'javascript',\n          content:\n            '// Here, you can use \\'params\\' to access the input variables in the node and use \\'output\\' to output the result\\n// \\'params\\'  have already been properly injected into the environment\\n// Below is an example of retrieving the value of the parameter \\'input\\' from the node\\'s input:\\n// const input = params.input; \\n// Below is an example of outputting a \\'ret\\' object containing multiple data types:\\n// const output = { \"name\": \\'Jack\\', \"hobbies\": [\"reading\", \"traveling\"] };\\nasync function main({ params }) {\\n    // Construct the output object\\n    const output = {\\n        \"key0\": params.input + params.input, // Concatenate the value of the two input parameters\\n        \"key1\": [\"hello\", \"world\"], // Output an array\\n        \"key2\": { // Output an Object\\n            \"key21\": \"hi\"\\n        },\\n    };\\n    return output;\\n}',\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'code_0',\n    },\n    {\n      sourceNodeID: 'code_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/end-constant.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime end constant schema', () => {\n  it('should execute a workflow', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.endConstantSchema,\n      inputs: {},\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      str: 'ABC',\n      num: 123.123,\n      int: 123,\n      bool: false,\n      obj: {\n        key_str: 'value',\n        key_int: 123,\n        key_bool: true,\n      },\n      map: {\n        key: 'value',\n      },\n      arr_str: ['AAA', 'BBB', 'CCC'],\n      date: '2000-01-01T00:00:00.000Z',\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      { nodeID: 'start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          str: 'ABC',\n          num: 123.123,\n          int: 123,\n          bool: false,\n          obj: { key_str: 'value', key_int: 123, key_bool: true },\n          map: { key: 'value' },\n          arr_str: ['AAA', 'BBB', 'CCC'],\n          date: '2000-01-01T00:00:00.000Z',\n        },\n        outputs: {\n          str: 'ABC',\n          num: 123.123,\n          int: 123,\n          bool: false,\n          obj: { key_str: 'value', key_int: 123, key_bool: true },\n          map: { key: 'value' },\n          arr_str: ['AAA', 'BBB', 'CCC'],\n          date: '2000-01-01T00:00:00.000Z',\n        },\n        data: {},\n      },\n    ]);\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/end-constant.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const endConstantSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 22.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {},\n          required: [],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          str: {\n            type: 'constant',\n            content: 'ABC',\n          },\n          num: {\n            type: 'constant',\n            content: 123.123,\n          },\n          int: {\n            type: 'constant',\n            content: 123,\n          },\n          bool: {\n            type: 'constant',\n            content: false,\n          },\n          obj: {\n            type: 'constant',\n            content: '{\"key_str\": \"value\",\"key_int\": 123,\"key_bool\": true}',\n          },\n          map: {\n            type: 'constant',\n            content: '{\"key\": \"value\"}',\n          },\n          arr_str: {\n            type: 'constant',\n            content: '[\"AAA\", \"BBB\", \"CCC\"]',\n          },\n          date: {\n            type: 'constant',\n            content: '2000-01-01T00:00:00.000Z',\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            str: {\n              type: 'string',\n            },\n            num: {\n              type: 'number',\n            },\n            int: {\n              type: 'integer',\n            },\n            bool: {\n              type: 'boolean',\n            },\n            obj: {\n              type: 'object',\n            },\n            map: {\n              type: 'map',\n            },\n            arr_str: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            date: {\n              type: 'date-time',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/global-variable.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime global variable schema', () => {\n  it('should execute a workflow', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.globalVariableSchema,\n      inputs: {},\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      g_str: 'ABC',\n      g_num: 123.123,\n      g_int: 123,\n      g_bool: false,\n      g_obj: {\n        key_str: 'value',\n        key_int: 123,\n        key_bool: true,\n      },\n      g_map: {\n        key: 'value',\n      },\n      g_arr_str: ['AAA', 'BBB', 'CCC'],\n      g_date: '2000-01-01T00:00:00.000Z',\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      { nodeID: 'start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          g_str: 'ABC',\n          g_num: 123.123,\n          g_int: 123,\n          g_bool: false,\n          g_obj: { key_str: 'value', key_int: 123, key_bool: true },\n          g_arr_str: ['AAA', 'BBB', 'CCC'],\n          g_map: { key: 'value' },\n          g_date: '2000-01-01T00:00:00.000Z',\n        },\n        outputs: {\n          g_str: 'ABC',\n          g_num: 123.123,\n          g_int: 123,\n          g_bool: false,\n          g_obj: { key_str: 'value', key_int: 123, key_bool: true },\n          g_arr_str: ['AAA', 'BBB', 'CCC'],\n          g_map: { key: 'value' },\n          g_date: '2000-01-01T00:00:00.000Z',\n        },\n        data: {},\n      },\n    ]);\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/global-variable.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const globalVariableSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 22.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {},\n          required: [],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          g_str: {\n            type: 'ref',\n            content: ['global', 'str'],\n          },\n          g_num: {\n            type: 'ref',\n            content: ['global', 'num'],\n          },\n          g_int: {\n            type: 'ref',\n            content: ['global', 'int'],\n          },\n          g_bool: {\n            type: 'ref',\n            content: ['global', 'bool'],\n          },\n          g_obj: {\n            type: 'ref',\n            content: ['global', 'obj'],\n          },\n          g_arr_str: {\n            type: 'ref',\n            content: ['global', 'arr_str'],\n          },\n          g_map: {\n            type: 'ref',\n            content: ['global', 'map'],\n          },\n          g_date: {\n            type: 'ref',\n            content: ['global', 'date'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            g_str: {\n              type: 'string',\n            },\n            g_num: {\n              type: 'number',\n            },\n            g_int: {\n              type: 'integer',\n            },\n            g_bool: {\n              type: 'boolean',\n            },\n            g_obj: {\n              type: 'object',\n              required: [],\n              properties: {\n                key_str: {\n                  type: 'string',\n                },\n                key_int: {\n                  type: 'integer',\n                },\n                key_bool: {\n                  type: 'boolean',\n                },\n              },\n            },\n            g_arr_str: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            g_map: {\n              type: 'map',\n            },\n            g_date: {\n              type: 'date-time',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n  globalVariable: {\n    type: 'object',\n    required: [],\n    properties: {\n      str: {\n        type: 'string',\n        default: 'ABC',\n      },\n      num: {\n        type: 'number',\n        default: 123.123,\n      },\n      int: {\n        type: 'integer',\n        default: 123,\n      },\n      bool: {\n        type: 'boolean',\n        default: false,\n      },\n      obj: {\n        type: 'object',\n        required: [],\n        properties: {\n          key_str: {\n            type: 'string',\n          },\n          key_int: {\n            type: 'integer',\n          },\n          key_bool: {\n            type: 'boolean',\n          },\n        },\n        default: '{\"key_str\": \"value\",\"key_int\": 123,\"key_bool\": true}',\n      },\n      arr_str: {\n        type: 'array',\n        items: {\n          type: 'string',\n        },\n        default: '[\"AAA\", \"BBB\", \"CCC\"]',\n      },\n      map: {\n        type: 'map',\n        default: '{\"key\": \"value\"}',\n      },\n      date: {\n        type: 'date-time',\n        default: '2000-01-01T00:00:00.000Z',\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/http-real.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime http schema', () => {\n  it('should execute a workflow with HTTP request', async () => {\n    if (process.env.ENABLE_REAL_TESTS !== 'true') {\n      return;\n    }\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'httpbin.org',\n        path: '/post',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result structure\n    expect(result).toHaveProperty('res');\n    expect(result).toHaveProperty('code');\n    expect(typeof result.res).toBe('string');\n    expect(typeof result.code).toBe('number');\n    expect(result.code).toBe(200);\n\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toHaveLength(3);\n\n    // Verify start node snapshot\n    expect(snapshots[0]).toMatchObject({\n      nodeID: 'start_0',\n      inputs: {},\n      outputs: {\n        host: 'httpbin.org',\n        path: '/post',\n      },\n      data: {},\n    });\n\n    // Verify http node snapshot\n    expect(snapshots[1]).toMatchObject({\n      nodeID: 'http_0',\n      inputs: {\n        method: 'POST',\n        url: 'https://httpbin.org/post',\n        body: '{}',\n        headers: {},\n        params: {},\n        timeout: 10000,\n        retryTimes: 1,\n      },\n      data: {},\n    });\n    expect(snapshots[1].outputs).toHaveProperty('body');\n    expect(snapshots[1].outputs).toHaveProperty('headers');\n    expect(snapshots[1].outputs).toHaveProperty('statusCode');\n    expect(snapshots[1].outputs.statusCode).toBe(200);\n\n    // Verify end node snapshot\n    expect(snapshots[2]).toMatchObject({\n      nodeID: 'end_0',\n      data: {},\n    });\n    expect(snapshots[2].inputs).toHaveProperty('res');\n    expect(snapshots[2].inputs).toHaveProperty('code');\n    expect(snapshots[2].outputs).toHaveProperty('res');\n    expect(snapshots[2].outputs).toHaveProperty('code');\n    expect(snapshots[2].outputs.code).toBe(200);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.http_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n\n  it('should handle HTTP request with different inputs', async () => {\n    if (process.env.ENABLE_REAL_TESTS !== 'true') {\n      return;\n    }\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'jsonplaceholder.typicode.com',\n        path: '/posts',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result structure\n    expect(result).toHaveProperty('res');\n    expect(result).toHaveProperty('code');\n    expect(typeof result.res).toBe('string');\n    expect(typeof result.code).toBe('number');\n    expect(result.code).toBe(201);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.http_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/http.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it, vi, beforeEach } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\n// Mock global fetch function\nconst mockFetch = vi.fn();\nglobal.fetch = mockFetch;\n\ndescribe('WorkflowRuntime http schema', () => {\n  beforeEach(() => {\n    // Reset mock before each test\n    mockFetch.mockReset();\n  });\n\n  it('should execute a workflow with HTTP request', async () => {\n    // Mock successful HTTP response\n    mockFetch.mockResolvedValueOnce({\n      ok: true,\n      status: 200,\n      statusText: 'OK',\n      headers: new Headers({\n        'content-type': 'application/json',\n      }),\n      text: async () =>\n        JSON.stringify({\n          url: 'https://api.example.com/post',\n          json: {},\n          headers: {\n            'Content-Type': 'application/json',\n          },\n        }),\n    });\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'api.example.com',\n        path: '/post',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result structure\n    expect(result).toHaveProperty('res');\n    expect(result).toHaveProperty('code');\n    expect(typeof result.res).toBe('string');\n    expect(typeof result.code).toBe('number');\n    expect(result.code).toBe(200);\n\n    // Verify fetch was called with correct parameters\n    expect(mockFetch).toHaveBeenCalledTimes(1);\n    expect(mockFetch).toHaveBeenCalledWith(\n      'https://api.example.com/post',\n      expect.objectContaining({\n        method: 'POST',\n        body: '{}',\n        headers: expect.any(Object),\n      })\n    );\n\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toHaveLength(3);\n\n    // Verify start node snapshot\n    expect(snapshots[0]).toMatchObject({\n      nodeID: 'start_0',\n      inputs: {},\n      outputs: {\n        host: 'api.example.com',\n        path: '/post',\n      },\n      data: {},\n    });\n\n    // Verify http node snapshot\n    expect(snapshots[1]).toMatchObject({\n      nodeID: 'http_0',\n      inputs: {\n        method: 'POST',\n        url: 'https://api.example.com/post',\n        body: '{}',\n        headers: {},\n        params: {},\n        timeout: 10000,\n        retryTimes: 1,\n      },\n      data: {},\n    });\n    expect(snapshots[1].outputs).toHaveProperty('body');\n    expect(snapshots[1].outputs).toHaveProperty('headers');\n    expect(snapshots[1].outputs).toHaveProperty('statusCode');\n    expect(snapshots[1].outputs.statusCode).toBe(200);\n\n    // Verify end node snapshot\n    expect(snapshots[2]).toMatchObject({\n      nodeID: 'end_0',\n      data: {},\n    });\n    expect(snapshots[2].inputs).toHaveProperty('res');\n    expect(snapshots[2].inputs).toHaveProperty('code');\n    expect(snapshots[2].outputs).toHaveProperty('res');\n    expect(snapshots[2].outputs).toHaveProperty('code');\n    expect(snapshots[2].outputs.code).toBe(200);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.http_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n\n  it('should handle HTTP request with different inputs and status codes', async () => {\n    // Mock HTTP response with 201 status\n    mockFetch.mockResolvedValueOnce({\n      ok: true,\n      status: 201,\n      statusText: 'Created',\n      headers: new Headers({\n        'content-type': 'application/json',\n      }),\n      text: async () =>\n        JSON.stringify({\n          id: 101,\n          title: 'foo',\n          body: 'bar',\n          userId: 1,\n        }),\n    });\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'api.test.com',\n        path: '/posts',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify the result structure\n    expect(result).toHaveProperty('res');\n    expect(result).toHaveProperty('code');\n    expect(typeof result.res).toBe('string');\n    expect(typeof result.code).toBe('number');\n    expect(result.code).toBe(201);\n\n    // Verify fetch was called with correct URL\n    expect(mockFetch).toHaveBeenCalledTimes(1);\n    expect(mockFetch).toHaveBeenCalledWith(\n      'https://api.test.com/posts',\n      expect.objectContaining({\n        method: 'POST',\n      })\n    );\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.http_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n\n  it('should handle HTTP request failure', async () => {\n    // Mock HTTP error response\n    mockFetch.mockResolvedValueOnce({\n      ok: false,\n      status: 404,\n      statusText: 'Not Found',\n      headers: new Headers(),\n      text: async () => 'Not Found',\n    });\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'api.mock.com',\n        path: '/nonexistent',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n\n    // The workflow should still succeed but with error status code\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toHaveProperty('res');\n    expect(result).toHaveProperty('code');\n    expect(result.code).toBe(404);\n\n    // Verify fetch was called\n    expect(mockFetch).toHaveBeenCalledTimes(1);\n    expect(mockFetch).toHaveBeenCalledWith(\n      'https://api.mock.com/nonexistent',\n      expect.objectContaining({\n        method: 'POST',\n      })\n    );\n  });\n\n  it('should handle network error', async () => {\n    // Mock network error\n    mockFetch.mockRejectedValueOnce(new Error('Network error'));\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.httpSchema,\n      inputs: {\n        host: 'api.invalid.test',\n        path: '/test',\n      },\n    });\n\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    await processing;\n\n    // The workflow should fail due to network error\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Failed);\n    expect(report.reports.http_0.status).toBe(WorkflowStatus.Failed);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/http.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const httpSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 125.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            host: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            },\n            path: {\n              type: 'string',\n              extra: {\n                index: 1,\n              },\n            },\n          },\n          required: ['host', 'path'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1100,\n          y: 125.5,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          res: {\n            type: 'ref',\n            content: ['http_0', 'body'],\n          },\n          code: {\n            type: 'ref',\n            content: ['http_0', 'statusCode'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            res: {\n              type: 'string',\n            },\n            code: {\n              type: 'integer',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'http_0',\n      type: 'http',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'HTTP_0',\n        api: {\n          method: 'POST',\n          url: {\n            type: 'template',\n            content: 'https://{{start_0.host}}{{start_0.path}}',\n          },\n        },\n        body: {\n          bodyType: 'JSON',\n          json: {\n            type: 'template',\n            content: '{}',\n          },\n        },\n        headers: {},\n        params: {},\n        outputs: {\n          type: 'object',\n          properties: {\n            body: {\n              type: 'string',\n            },\n            headers: {\n              type: 'object',\n            },\n            statusCode: {\n              type: 'integer',\n            },\n          },\n        },\n        timeout: {\n          timeout: 10000,\n          retryTimes: 1,\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'http_0',\n    },\n    {\n      sourceNodeID: 'http_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { validateInputsSchema } from './validate-inputs';\nimport { twoLLMSchema } from './two-llm';\nimport { startDefaultSchema } from './start-default';\nimport { loopBreakContinueSchema } from './loop-break-continue';\nimport { loopSchema } from './loop';\nimport { llmRealSchema } from './llm-real';\nimport { httpSchema } from './http';\nimport { globalVariableSchema } from './global-variable';\nimport { endConstantSchema } from './end-constant';\nimport { codeSchema } from './code';\nimport { branchTwoLayersSchema } from './branch-two-layers';\nimport { branchSchema } from './branch';\nimport { basicSchema } from './basic';\n\nexport const TestSchemas = {\n  twoLLMSchema,\n  basicSchema,\n  branchSchema,\n  llmRealSchema,\n  loopSchema,\n  loopBreakContinueSchema,\n  branchTwoLayersSchema,\n  validateInputsSchema,\n  httpSchema,\n  codeSchema,\n  endConstantSchema,\n  startDefaultSchema,\n  globalVariableSchema,\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/llm-real.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, IExecutor, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { LLMExecutor } from '@nodes/llm';\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nlet container: IContainer;\n\nbeforeEach(() => {\n  container = WorkflowRuntimeContainer.instance;\n  const executor = container.get<IExecutor>(IExecutor);\n  executor.register(new LLMExecutor());\n});\n\ndescribe('workflow runtime real llm test', () => {\n  it('should execute workflow', async () => {\n    if (process.env.ENABLE_REAL_TESTS !== 'true') {\n      return;\n    }\n    if (!process.env.MODEL_NAME || !process.env.API_KEY || !process.env.API_HOST) {\n      throw new Error('Missing environment variables');\n    }\n    const engine = container.get<IEngine>(IEngine);\n    const modelName = process.env.MODEL_NAME;\n    const apiKey = process.env.API_KEY;\n    const apiHost = process.env.API_HOST;\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.llmRealSchema,\n      inputs: {\n        model_name: modelName,\n        api_key: apiKey,\n        api_host: apiHost,\n        formula: '1+1',\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      answer: '2',\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: {\n          model_name: modelName,\n          api_key: apiKey,\n          api_host: apiHost,\n          formula: '1+1',\n        },\n        data: {},\n      },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: modelName,\n          apiKey: apiKey,\n          apiHost: apiHost,\n          temperature: 0,\n          prompt: 'Just give me the answer of \"1+1=?\", just one number, no other words',\n          systemPrompt: 'You are a \"math formula\" calculator.',\n        },\n        outputs: { result: '2' },\n        data: {},\n      },\n      {\n        nodeID: 'end_0',\n        inputs: { answer: '2' },\n        outputs: { answer: '2' },\n        data: {},\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/llm-real.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const llmRealSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 152.2,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            model_name: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            },\n            api_key: {\n              type: 'string',\n              extra: {\n                index: 1,\n              },\n            },\n            api_host: {\n              type: 'string',\n              extra: {\n                index: 2,\n              },\n            },\n            formula: {\n              type: 'string',\n              extra: {\n                index: 3,\n              },\n            },\n          },\n          required: ['model_name', 'api_key', 'api_host', 'formula'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1124.4,\n          y: 152.2,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          answer: {\n            type: 'ref',\n            content: ['llm_0', 'result'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            answer: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_0',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 652.2,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_0',\n        inputsValues: {\n          modelName: {\n            type: 'ref',\n            content: ['start_0', 'model_name'],\n          },\n          apiKey: {\n            type: 'ref',\n            content: ['start_0', 'api_key'],\n          },\n          apiHost: {\n            type: 'ref',\n            content: ['start_0', 'api_host'],\n          },\n          temperature: {\n            type: 'constant',\n            content: 0,\n          },\n          prompt: {\n            type: 'template',\n            content:\n              'Just give me the answer of \"{{start_0.formula}}=?\", just one number, no other words',\n          },\n          systemPrompt: {\n            type: 'template',\n            content: 'You are a \"math formula\" calculator.',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'llm_0',\n    },\n    {\n      sourceNodeID: 'llm_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/loop-break-continue.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime loop break continue schema', () => {\n  it('should execute a workflow with break and continue logic', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.loopBreakContinueSchema,\n      inputs: {\n        tasks: [\n          'TASK - A', // index 0, continue\n          'TASK - B', // index 1, continue\n          'TASK - C', // index 2, continue\n          'TASK - D', // index 3, execute\n          'TASK - E', // index 4, execute\n          'TASK - F', // index 5, execute\n          'TASK - G', // index 6, execute\n          'TASK - H', // index 7, break\n          'TASK - I', // index 8, should not reach\n        ],\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n\n    // Only tasks with index 3-6 should be processed (index > 2 and <= 6)\n    expect(result).toStrictEqual({\n      outputs: [\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n      ],\n    });\n\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n\n    // Verify that start node executed correctly\n    const startSnapshot = snapshots.find((s) => s.nodeID === 'start_0');\n    expect(startSnapshot).toBeDefined();\n    expect(startSnapshot?.outputs.tasks).toEqual([\n      'TASK - A',\n      'TASK - B',\n      'TASK - C',\n      'TASK - D',\n      'TASK - E',\n      'TASK - F',\n      'TASK - G',\n      'TASK - H',\n      'TASK - I',\n    ]);\n\n    // Verify that loop node executed correctly\n    const loopSnapshot = snapshots.find((s) => s.nodeID === 'loop_0');\n    expect(loopSnapshot).toBeDefined();\n    expect(loopSnapshot?.outputs.results).toEqual([\n      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n      'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n    ]);\n\n    // Verify that only the expected items and indexes were processed\n    expect(loopSnapshot?.outputs.items).toEqual(['TASK - D', 'TASK - E', 'TASK - F', 'TASK - G']);\n    expect(loopSnapshot?.outputs.indexes).toEqual([3, 4, 5, 6]);\n\n    // Verify that LLM node was executed exactly 4 times (for indexes 3-6)\n    const llmSnapshots = snapshots.filter((s) => s.nodeID === 'llm_0');\n    expect(llmSnapshots).toHaveLength(4);\n\n    // Verify the LLM executions\n    expect(llmSnapshots[0].inputs.systemPrompt).toBe('You are a helpful assistant No.3');\n    expect(llmSnapshots[0].inputs.prompt).toBe('TASK - D');\n    expect(llmSnapshots[1].inputs.systemPrompt).toBe('You are a helpful assistant No.4');\n    expect(llmSnapshots[1].inputs.prompt).toBe('TASK - E');\n    expect(llmSnapshots[2].inputs.systemPrompt).toBe('You are a helpful assistant No.5');\n    expect(llmSnapshots[2].inputs.prompt).toBe('TASK - F');\n    expect(llmSnapshots[3].inputs.systemPrompt).toBe('You are a helpful assistant No.6');\n    expect(llmSnapshots[3].inputs.prompt).toBe('TASK - G');\n\n    // Verify that continue and break nodes were executed\n    const continueSnapshots = snapshots.filter((s) => s.nodeID === 'continue_0');\n    const breakSnapshots = snapshots.filter((s) => s.nodeID === 'break_0');\n\n    // Continue should be executed 3 times (for indexes 0, 1, 2)\n    expect(continueSnapshots).toHaveLength(3);\n    // Break should be executed 1 time (for index 7)\n    expect(breakSnapshots).toHaveLength(1);\n\n    // Verify workflow status\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.loop_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.condition_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.continue_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.break_0.status).toBe(WorkflowStatus.Succeeded);\n\n    // Verify execution counts\n    expect(report.reports.llm_0.snapshots.length).toBe(4);\n    expect(report.reports.condition_0.snapshots.length).toBe(8); // Condition checked for each iteration\n    expect(report.reports.continue_0.snapshots.length).toBe(3);\n    expect(report.reports.break_0.snapshots.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/loop-break-continue.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const loopBreakContinueSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 181,\n          y: 337.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            tasks: {\n              type: 'array',\n              extra: {\n                index: 0,\n              },\n              items: {\n                type: 'string',\n              },\n            },\n          },\n          required: ['tasks'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 2017,\n          y: 337.4,\n        },\n      },\n      data: {\n        title: 'End',\n        inputs: {\n          type: 'object',\n          properties: {\n            outputs: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        inputsValues: {\n          outputs: {\n            type: 'ref',\n            content: ['loop_0', 'results'],\n          },\n        },\n      },\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 520,\n          y: 90,\n        },\n      },\n      data: {\n        title: 'Loop_1',\n        loopFor: {\n          type: 'ref',\n          content: ['start_0', 'tasks'],\n        },\n        loopOutputs: {\n          results: {\n            type: 'ref',\n            content: ['llm_0', 'result'],\n          },\n          items: {\n            type: 'ref',\n            content: ['loop_0_locals', 'item'],\n          },\n          indexes: {\n            type: 'ref',\n            content: ['loop_0_locals', 'index'],\n          },\n        },\n      },\n      blocks: [\n        {\n          id: 'block_start_0',\n          type: 'block-start',\n          meta: {\n            position: {\n              x: 32,\n              y: 149,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'block_end_0',\n          type: 'block-end',\n          meta: {\n            position: {\n              x: 1126,\n              y: 371,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'llm_0',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 804,\n              y: 213,\n            },\n          },\n          data: {\n            title: 'LLM_0',\n            inputsValues: {\n              modelName: {\n                type: 'constant',\n                content: 'AI_MODEL_1',\n              },\n              apiKey: {\n                type: 'constant',\n                content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n              },\n              apiHost: {\n                type: 'constant',\n                content: 'https://mock-ai-url/api/v3',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.6,\n              },\n              systemPrompt: {\n                type: 'template',\n                content: 'You are a helpful assistant No.{{loop_0_locals.index}}',\n              },\n              prompt: {\n                type: 'template',\n                content: '{{loop_0_locals.item}}',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n              properties: {\n                modelName: {\n                  type: 'string',\n                },\n                apiKey: {\n                  type: 'string',\n                },\n                apiHost: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n                prompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n        {\n          id: 'condition_0',\n          type: 'condition',\n          meta: {\n            position: {\n              x: 344,\n              y: 45,\n            },\n          },\n          data: {\n            title: 'Condition',\n            conditions: [\n              {\n                value: {\n                  left: {\n                    type: 'ref',\n                    content: ['loop_0_locals', 'index'],\n                  },\n                  operator: 'lte',\n                  right: {\n                    type: 'constant',\n                    content: 2,\n                  },\n                },\n                key: 'if_1',\n              },\n              {\n                value: {\n                  left: {\n                    type: 'ref',\n                    content: ['loop_0_locals', 'index'],\n                  },\n                  operator: 'gt',\n                  right: {\n                    type: 'constant',\n                    content: 6,\n                  },\n                },\n                key: 'if_2',\n              },\n              {\n                value: {\n                  type: 'expression',\n                  content: '',\n                  left: {\n                    type: 'ref',\n                    content: ['loop_0_locals', 'index'],\n                  },\n                  operator: 'is_not_empty',\n                },\n                key: 'if_3',\n              },\n            ],\n          },\n        },\n        {\n          id: 'continue_0',\n          type: 'continue',\n          meta: {\n            position: {\n              x: 804,\n              y: 84.3,\n            },\n          },\n          data: {\n            title: 'Continue_0',\n          },\n        },\n        {\n          id: 'break_0',\n          type: 'break',\n          meta: {\n            position: {\n              x: 804,\n              y: 149,\n            },\n          },\n          data: {\n            title: 'Break_0',\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'block_start_0',\n          targetNodeID: 'condition_0',\n        },\n        {\n          sourceNodeID: 'llm_0',\n          targetNodeID: 'block_end_0',\n        },\n        {\n          sourceNodeID: 'condition_0',\n          targetNodeID: 'llm_0',\n          sourcePortID: 'if_3',\n        },\n        {\n          sourceNodeID: 'condition_0',\n          targetNodeID: 'continue_0',\n          sourcePortID: 'if_1',\n        },\n        {\n          sourceNodeID: 'condition_0',\n          targetNodeID: 'break_0',\n          sourcePortID: 'if_2',\n        },\n      ],\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'loop_0',\n    },\n    {\n      sourceNodeID: 'loop_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/loop.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime loop schema', () => {\n  it('should execute a workflow with input', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.loopSchema,\n      inputs: {\n        tasks: [\n          'TASK - A',\n          'TASK - B',\n          'TASK - C',\n          'TASK - D',\n          'TASK - E',\n          'TASK - F',\n          'TASK - G',\n          'TASK - H',\n        ],\n      },\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      outputs: [\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.0\", prompt is \"TASK - A\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.1\", prompt is \"TASK - B\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.2\", prompt is \"TASK - C\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n        'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.7\", prompt is \"TASK - H\"',\n      ],\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      {\n        nodeID: 'start_0',\n        inputs: {},\n        outputs: {\n          tasks: [\n            'TASK - A',\n            'TASK - B',\n            'TASK - C',\n            'TASK - D',\n            'TASK - E',\n            'TASK - F',\n            'TASK - G',\n            'TASK - H',\n          ],\n        },\n        data: {},\n      },\n      {\n        nodeID: 'loop_0',\n        inputs: {},\n        outputs: {\n          results: [\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.0\", prompt is \"TASK - A\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.1\", prompt is \"TASK - B\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.2\", prompt is \"TASK - C\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.7\", prompt is \"TASK - H\"',\n          ],\n          items: [\n            'TASK - A',\n            'TASK - B',\n            'TASK - C',\n            'TASK - D',\n            'TASK - E',\n            'TASK - F',\n            'TASK - G',\n            'TASK - H',\n          ],\n          indexes: [0, 1, 2, 3, 4, 5, 6, 7],\n        },\n        data: {\n          loopFor: { type: 'ref', content: ['start_0', 'tasks'] },\n          loopOutputs: {\n            results: { type: 'ref', content: ['llm_0', 'result'] },\n            items: { type: 'ref', content: ['loop_0_locals', 'item'] },\n            indexes: { type: 'ref', content: ['loop_0_locals', 'index'] },\n          },\n        },\n      },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.0',\n          prompt: 'TASK - A',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.0\", prompt is \"TASK - A\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.1',\n          prompt: 'TASK - B',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.1\", prompt is \"TASK - B\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.2',\n          prompt: 'TASK - C',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.2\", prompt is \"TASK - C\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.3',\n          prompt: 'TASK - D',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.4',\n          prompt: 'TASK - E',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.5',\n          prompt: 'TASK - F',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.6',\n          prompt: 'TASK - G',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      { nodeID: 'block_start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'llm_0',\n        inputs: {\n          modelName: 'AI_MODEL_1',\n          apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          apiHost: 'https://mock-ai-url/api/v3',\n          temperature: 0.6,\n          systemPrompt: 'You are a helpful assistant No.7',\n          prompt: 'TASK - H',\n        },\n        outputs: {\n          result:\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.7\", prompt is \"TASK - H\"',\n        },\n        data: {},\n      },\n      { nodeID: 'block_end_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          outputs: [\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.0\", prompt is \"TASK - A\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.1\", prompt is \"TASK - B\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.2\", prompt is \"TASK - C\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.7\", prompt is \"TASK - H\"',\n          ],\n        },\n        outputs: {\n          outputs: [\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.0\", prompt is \"TASK - A\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.1\", prompt is \"TASK - B\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.2\", prompt is \"TASK - C\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.3\", prompt is \"TASK - D\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.4\", prompt is \"TASK - E\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.5\", prompt is \"TASK - F\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.6\", prompt is \"TASK - G\"',\n            'Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.6, system prompt is \"You are a helpful assistant No.7\", prompt is \"TASK - H\"',\n          ],\n        },\n        data: {},\n      },\n    ]);\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.loop_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.llm_0.snapshots.length).toBe(8);\n    expect(report.reports.block_start_0.snapshots.length).toBe(8);\n    expect(report.reports.block_end_0.snapshots.length).toBe(8);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/loop.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const loopSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 230,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            tasks: {\n              type: 'array',\n              extra: {\n                index: 0,\n              },\n              items: {\n                type: 'string',\n              },\n            },\n          },\n          required: ['tasks'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1628,\n          y: 230,\n        },\n      },\n      data: {\n        title: 'End',\n        inputs: {\n          type: 'object',\n          properties: {\n            outputs: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n          },\n        },\n        inputsValues: {\n          outputs: {\n            type: 'ref',\n            content: ['loop_0', 'results'],\n          },\n        },\n      },\n    },\n    {\n      id: 'loop_0',\n      type: 'loop',\n      meta: {\n        position: {\n          x: 560,\n          y: 120,\n        },\n      },\n      data: {\n        title: 'Loop_1',\n        loopFor: {\n          type: 'ref',\n          content: ['start_0', 'tasks'],\n        },\n        loopOutputs: {\n          results: {\n            type: 'ref',\n            content: ['llm_0', 'result'],\n          },\n          items: {\n            type: 'ref',\n            content: ['loop_0_locals', 'item'],\n          },\n          indexes: {\n            type: 'ref',\n            content: ['loop_0_locals', 'index'],\n          },\n        },\n      },\n      blocks: [\n        {\n          id: 'block_start_0',\n          type: 'block-start',\n          meta: {\n            position: {\n              x: 32,\n              y: 149.5,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'block_end_0',\n          type: 'block-end',\n          meta: {\n            position: {\n              x: 656,\n              y: 149.5,\n            },\n          },\n          data: {},\n        },\n        {\n          id: 'llm_0',\n          type: 'llm',\n          meta: {\n            position: {\n              x: 344,\n              y: -8.4,\n            },\n          },\n          data: {\n            title: 'LLM_0',\n            inputsValues: {\n              modelName: {\n                type: 'constant',\n                content: 'AI_MODEL_1',\n              },\n              apiKey: {\n                type: 'constant',\n                content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n              },\n              apiHost: {\n                type: 'constant',\n                content: 'https://mock-ai-url/api/v3',\n              },\n              temperature: {\n                type: 'constant',\n                content: 0.6,\n              },\n              systemPrompt: {\n                type: 'template',\n                content: 'You are a helpful assistant No.{{loop_0_locals.index}}',\n              },\n              prompt: {\n                type: 'template',\n                content: '{{loop_0_locals.item}}',\n              },\n            },\n            inputs: {\n              type: 'object',\n              required: ['modelName', 'apiKey', 'apiHost', 'temperature', 'prompt'],\n              properties: {\n                modelName: {\n                  type: 'string',\n                },\n                apiKey: {\n                  type: 'string',\n                },\n                apiHost: {\n                  type: 'string',\n                },\n                temperature: {\n                  type: 'number',\n                },\n                systemPrompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n                prompt: {\n                  type: 'string',\n                  extra: {\n                    formComponent: 'prompt-editor',\n                  },\n                },\n              },\n            },\n            outputs: {\n              type: 'object',\n              properties: {\n                result: {\n                  type: 'string',\n                },\n              },\n            },\n          },\n        },\n      ],\n      edges: [\n        {\n          sourceNodeID: 'block_start_0',\n          targetNodeID: 'llm_0',\n        },\n        {\n          sourceNodeID: 'llm_0',\n          targetNodeID: 'block_end_0',\n        },\n      ],\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'loop_0',\n    },\n    {\n      sourceNodeID: 'loop_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/start-default.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { snapshotsToVOData } from '../utils';\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime start default schema', () => {\n  it('should execute a workflow', async () => {\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.startDefaultSchema,\n      inputs: {},\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual({\n      s_str: 'ABC',\n      s_num: 123.123,\n      s_int: 123,\n      s_bool: false,\n      s_obj: {\n        key_str: 'value',\n        key_int: 123,\n        key_bool: true,\n      },\n      s_map: {\n        key: 'value',\n      },\n      s_arr_str: ['AAA', 'BBB', 'CCC'],\n      s_date: '2000-01-01T00:00:00.000Z',\n    });\n    const snapshots = snapshotsToVOData(context.snapshotCenter.exportAll());\n    expect(snapshots).toStrictEqual([\n      { nodeID: 'start_0', inputs: {}, outputs: {}, data: {} },\n      {\n        nodeID: 'end_0',\n        inputs: {\n          s_str: 'ABC',\n          s_num: 123.123,\n          s_int: 123,\n          s_bool: false,\n          s_obj: { key_str: 'value', key_int: 123, key_bool: true },\n          s_arr_str: ['AAA', 'BBB', 'CCC'],\n          s_map: { key: 'value' },\n          s_date: '2000-01-01T00:00:00.000Z',\n        },\n        outputs: {\n          s_str: 'ABC',\n          s_num: 123.123,\n          s_int: 123,\n          s_bool: false,\n          s_obj: { key_str: 'value', key_int: 123, key_bool: true },\n          s_arr_str: ['AAA', 'BBB', 'CCC'],\n          s_map: { key: 'value' },\n          s_date: '2000-01-01T00:00:00.000Z',\n        },\n        data: {},\n      },\n    ]);\n    const report = context.reporter.export();\n    expect(report.workflowStatus.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.start_0.status).toBe(WorkflowStatus.Succeeded);\n    expect(report.reports.end_0.status).toBe(WorkflowStatus.Succeeded);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/start-default.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const startDefaultSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            str: {\n              type: 'string',\n              default: 'ABC',\n            },\n            num: {\n              type: 'number',\n              default: 123.123,\n            },\n            int: {\n              type: 'integer',\n              default: 123,\n            },\n            bool: {\n              type: 'boolean',\n              default: false,\n            },\n            obj: {\n              type: 'object',\n              required: [],\n              properties: {\n                key_str: {\n                  type: 'string',\n                },\n                key_int: {\n                  type: 'integer',\n                },\n                key_bool: {\n                  type: 'boolean',\n                },\n              },\n              default: '{\"key_str\": \"value\",\"key_int\": 123,\"key_bool\": true}',\n            },\n            arr_str: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n              default: '[\"AAA\", \"BBB\", \"CCC\"]',\n            },\n            map: {\n              type: 'map',\n              default: '{\"key\": \"value\"}',\n            },\n            date: {\n              type: 'date-time',\n              default: '2000-01-01T00:00:00.000Z',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'End',\n        inputsValues: {\n          s_str: {\n            type: 'ref',\n            content: ['start_0', 'str'],\n          },\n          s_num: {\n            type: 'ref',\n            content: ['start_0', 'num'],\n          },\n          s_int: {\n            type: 'ref',\n            content: ['start_0', 'int'],\n          },\n          s_bool: {\n            type: 'ref',\n            content: ['start_0', 'bool'],\n          },\n          s_obj: {\n            type: 'ref',\n            content: ['start_0', 'obj'],\n          },\n          s_arr_str: {\n            type: 'ref',\n            content: ['start_0', 'arr_str'],\n          },\n          s_map: {\n            type: 'ref',\n            content: ['start_0', 'map'],\n          },\n          s_date: {\n            type: 'ref',\n            content: ['start_0', 'date'],\n          },\n        },\n        inputs: {\n          type: 'object',\n          properties: {\n            s_str: {\n              type: 'string',\n            },\n            s_num: {\n              type: 'number',\n            },\n            s_int: {\n              type: 'integer',\n            },\n            s_bool: {\n              type: 'boolean',\n            },\n            s_obj: {\n              type: 'object',\n              required: [],\n              properties: {\n                key_str: {\n                  type: 'string',\n                },\n                key_int: {\n                  type: 'integer',\n                },\n                key_bool: {\n                  type: 'boolean',\n                },\n              },\n            },\n            s_arr_str: {\n              type: 'array',\n              items: {\n                type: 'string',\n              },\n            },\n            s_map: {\n              type: 'map',\n            },\n            s_date: {\n              type: 'date-time',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/two-llm.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const twoLLMSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 222.5,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            query: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            },\n            enable: {\n              type: 'boolean',\n              extra: {\n                index: 1,\n              },\n            },\n            array_obj: {\n              type: 'array',\n              items: {\n                type: 'object',\n                properties: {\n                  int: {\n                    type: 'number',\n                  },\n                  str: {\n                    type: 'string',\n                  },\n                },\n              },\n              extra: {\n                index: 2,\n              },\n            },\n            num: {\n              type: 'number',\n              extra: {\n                index: 3,\n              },\n            },\n            model: {\n              type: 'string',\n              extra: {\n                index: 5,\n              },\n            },\n          },\n          required: [],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 1100,\n          y: 235.5,\n        },\n      },\n      data: {\n        title: 'End',\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_2',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 640,\n          y: 327,\n        },\n      },\n      data: {\n        title: 'LLM_2',\n        inputsValues: {\n          systemPrompt: {\n            type: 'constant',\n            content: 'BBBB',\n          },\n          modelName: {\n            type: 'ref',\n            content: ['start_0', 'model'],\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'ref',\n            content: ['start_0', 'num'],\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.query}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n    {\n      id: 'llm_1',\n      type: 'llm',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'LLM_1',\n        inputsValues: {\n          modelName: {\n            type: 'ref',\n            content: ['start_0', 'model'],\n          },\n          apiKey: {\n            type: 'constant',\n            content: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',\n          },\n          apiHost: {\n            type: 'constant',\n            content: 'https://mock-ai-url/api/v3',\n          },\n          temperature: {\n            type: 'ref',\n            content: ['start_0', 'num'],\n          },\n          systemPrompt: {\n            type: 'constant',\n            content: 'AAAA',\n          },\n          prompt: {\n            type: 'template',\n            content: '{{start_0.query}}',\n          },\n        },\n        inputs: {\n          type: 'object',\n          required: ['modelName', 'temperature', 'prompt'],\n          properties: {\n            modelName: {\n              type: 'string',\n            },\n            apiKey: {\n              type: 'string',\n            },\n            apiHost: {\n              type: 'string',\n            },\n            temperature: {\n              type: 'number',\n            },\n            systemPrompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n            prompt: {\n              type: 'string',\n              extra: {\n                formComponent: 'prompt-editor',\n              },\n            },\n          },\n        },\n        outputs: {\n          type: 'object',\n          properties: {\n            result: {\n              type: 'string',\n            },\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'llm_1',\n    },\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'llm_2',\n    },\n    {\n      sourceNodeID: 'llm_2',\n      targetNodeID: 'end_0',\n    },\n    {\n      sourceNodeID: 'llm_1',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IContainer, IEngine, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeContainer } from '../../container';\nimport { ValidateInputsSchemaInputs } from './validate-inputs';\nimport { TestSchemas } from '.';\n\nconst container: IContainer = WorkflowRuntimeContainer.instance;\n\ndescribe('WorkflowRuntime validate inputs success', () => {\n  it('basic inputs', async () => {\n    const inputs: ValidateInputsSchemaInputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: {\n          CEA: 'nested string',\n        },\n        CF: ['item1', 'item2', 'item3'],\n      },\n      DD: [\n        {\n          DA: 'optional string',\n          DB: {\n            DBA: 'deep nested',\n          },\n        },\n      ],\n      EE: {\n        EA: {\n          EAA: 'required nested',\n        },\n        EB: 'optional string',\n      },\n    };\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual(inputs);\n  });\n  it('complex inputs', async () => {\n    const inputs: ValidateInputsSchemaInputs = {\n      AA: 'complex example',\n      BB: -999,\n      CC: {\n        CA: 'test',\n        CB: 0,\n        CC: 999999,\n        CD: false,\n        CE: {\n          CEA: 'another nested value',\n        },\n        CF: ['a', 'b', 'c', 'd', 'e'],\n      },\n      DD: [\n        {\n          DA: 'first item',\n          DB: {\n            DBA: 'first nested',\n          },\n        },\n        {\n          DA: 'second item',\n          // DB is optional, omitted here\n        },\n        {\n          // DA is optional, omitted here\n          DB: {\n            DBA: 'third nested',\n          },\n        },\n      ],\n      EE: {\n        EA: {\n          EAA: 'required value',\n        },\n        // EB is optional, omitted here\n      },\n    };\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual(inputs);\n  });\n  it('min inputs', async () => {\n    const inputs: ValidateInputsSchemaInputs = {\n      AA: '',\n      BB: 0,\n      CC: {\n        CA: '',\n        CB: 0,\n        CC: 0,\n        CD: false,\n        CE: {\n          CEA: '',\n        },\n        CF: [],\n      },\n      DD: [],\n      EE: {\n        EA: {\n          EAA: '',\n        },\n      },\n    };\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual(inputs);\n  });\n  it('full inputs', async () => {\n    const inputs: ValidateInputsSchemaInputs = {\n      AA: 'full example',\n      BB: 12345,\n      CC: {\n        CA: 'complete',\n        CB: 500,\n        CC: 600,\n        CD: true,\n        CE: {\n          CEA: 'full nested',\n        },\n        CF: ['full', 'array', 'example'],\n      },\n      DD: [\n        {\n          DA: 'with all fields',\n          DB: {\n            DBA: 'complete nested',\n          },\n        },\n      ],\n      EE: {\n        EA: {\n          EAA: 'all required',\n        },\n        EB: 'with optional field',\n      },\n    };\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual(inputs);\n  });\n  it('edge inputs', async () => {\n    const inputs: ValidateInputsSchemaInputs = {\n      AA: 'a', // single character\n      BB: Number.MAX_SAFE_INTEGER,\n      CC: {\n        CA: 'very long string that tests the boundaries of what might be acceptable in real world scenarios',\n        CB: Number.MIN_SAFE_INTEGER,\n        CC: 1,\n        CD: true,\n        CE: {\n          CEA: 'boundary test',\n        },\n        CF: ['single'],\n      },\n      DD: Array(100).fill({\n        DA: 'repeated',\n        DB: {\n          DBA: 'many items',\n        },\n      }),\n      EE: {\n        EA: {\n          EAA: 'boundary',\n        },\n        EB: '',\n      },\n    };\n\n    const engine = container.get<IEngine>(IEngine);\n    const { context, processing } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Processing);\n    const result = await processing;\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Succeeded);\n    expect(result).toStrictEqual(inputs);\n  });\n});\n\ndescribe('WorkflowRuntime validate inputs failed', () => {\n  it('missing required property \"AA\"', async () => {\n    const inputs = {\n      // AA: \"missing\", // ❌ missing required property AA\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Missing required property \"AA\" at root'\n    );\n  });\n  it('property \"AA\" expected to be string, not number', async () => {\n    const inputs = {\n      AA: 123, // ❌ AA should be string, not number\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Expected string at AA, but got: number'\n    );\n  });\n  it('property \"BB\" expected to be number, not string', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 'test-string', // ❌ BB should be number, not string\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Expected integer at BB, but got: \"test-string\"'\n    );\n  });\n  it('property \"CC.CA\" expected to be string, not number', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        // CA: 123, // ❌ CA should be string, not number\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Missing required property \"CA\" at CC'\n    );\n  });\n  it('missing required property \"CC.CEA\"', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: {\n          // CEA: \"missing\" // ❌ missing required property CEA\n        },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Missing required property \"CEA\" at CC.CE'\n    );\n  });\n  it('xxxxxxxxxxxxxxxxx', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: [1, 2, 3], // ❌ CF should be string[], not number[]\n      },\n      DD: [],\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Expected string at CC.CF[0], but got: number'\n    );\n  });\n  it('property \"DD\" expected to be array, not string', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: 'not an array', // ❌ DD should be array, not string\n      EE: {\n        EA: { EAA: 'required' },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Expected array at DD, but got: string'\n    );\n  });\n  it('missing required property \"EE\"', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      // EE: { ... } // ❌ missing required property EE\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Missing required property \"EE\" at root'\n    );\n  });\n  it('property \"EE.EA.EAA\" expected to be string, not boolean', async () => {\n    const inputs = {\n      AA: 'hello',\n      BB: 42,\n      CC: {\n        CA: 'world',\n        CB: 100,\n        CC: 200,\n        CD: true,\n        CE: { CEA: 'nested' },\n        CF: ['item1'],\n      },\n      DD: [],\n      EE: {\n        EA: {\n          EAA: true, // ❌ EAA should be string, not boolean\n        },\n      },\n    };\n    const engine = container.get<IEngine>(IEngine);\n    const { context } = engine.invoke({\n      schema: TestSchemas.validateInputsSchema,\n      inputs,\n    });\n    expect(context.statusCenter.workflow.status).toBe(WorkflowStatus.Failed);\n    const report = context.reporter.export();\n    expect(report.messages.error[0].message).toBe(\n      'JSON Schema validation failed: Expected string at EE.EA.EAA, but got: boolean'\n    );\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/schemas/validate-inputs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport interface ValidateInputsSchemaInputs {\n  AA: string;\n  BB: number;\n  CC?: {\n    CA: string;\n    CB: number;\n    CC: number;\n    CD: boolean;\n    CE: {\n      CEA: string;\n    };\n    CF: string[];\n  };\n  DD?: Array<{\n    DA?: string;\n    DB?: {\n      DBA: string;\n    };\n  }>;\n  EE: {\n    EA: {\n      EAA: string;\n    };\n    EB?: string;\n  };\n}\n\nexport const validateInputsSchema: WorkflowSchema = {\n  nodes: [\n    {\n      id: 'start_0',\n      type: 'start',\n      meta: {\n        position: {\n          x: 180,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'Start',\n        outputs: {\n          type: 'object',\n          properties: {\n            AA: {\n              type: 'string',\n              extra: {\n                index: 0,\n              },\n            },\n            BB: {\n              type: 'integer',\n              extra: {\n                index: 1,\n              },\n            },\n            CC: {\n              type: 'object',\n              extra: {\n                index: 2,\n              },\n              properties: {\n                CA: {\n                  type: 'string',\n                  extra: {\n                    index: 0,\n                  },\n                },\n                CB: {\n                  type: 'integer',\n                  extra: {\n                    index: 1,\n                  },\n                },\n                CC: {\n                  type: 'number',\n                  extra: {\n                    index: 3,\n                  },\n                },\n                CD: {\n                  type: 'boolean',\n                  extra: {\n                    index: 4,\n                  },\n                },\n                CE: {\n                  type: 'object',\n                  extra: {\n                    index: 5,\n                  },\n                  properties: {\n                    CEA: {\n                      type: 'string',\n                      extra: {\n                        index: 1,\n                      },\n                    },\n                  },\n                  required: ['CEA'],\n                },\n                CF: {\n                  type: 'array',\n                  extra: {\n                    index: 6,\n                  },\n                  items: {\n                    type: 'string',\n                  },\n                },\n              },\n              required: ['CA', 'CB', 'CC', 'CD', 'CE', 'CF'],\n            },\n            DD: {\n              type: 'array',\n              extra: {\n                index: 3,\n              },\n              items: {\n                type: 'object',\n                properties: {\n                  DA: {\n                    type: 'string',\n                    extra: {\n                      index: 1,\n                    },\n                  },\n                  DB: {\n                    type: 'object',\n                    extra: {\n                      index: 2,\n                    },\n                    properties: {\n                      DBA: {\n                        type: 'string',\n                        extra: {\n                          index: 1,\n                        },\n                      },\n                    },\n                    required: ['DBA'],\n                  },\n                },\n                required: [],\n              },\n            },\n            EE: {\n              type: 'object',\n              extra: {\n                index: 4,\n              },\n              properties: {\n                EA: {\n                  type: 'object',\n                  extra: {\n                    index: 1,\n                  },\n                  properties: {\n                    EAA: {\n                      type: 'string',\n                      extra: {\n                        index: 1,\n                      },\n                    },\n                  },\n                  required: ['EAA'],\n                },\n                EB: {\n                  type: 'string',\n                  extra: {\n                    index: 2,\n                  },\n                },\n              },\n              required: ['EA'],\n            },\n          },\n          required: ['AA', 'EE'],\n        },\n      },\n    },\n    {\n      id: 'end_0',\n      type: 'end',\n      meta: {\n        position: {\n          x: 640,\n          y: 0,\n        },\n      },\n      data: {\n        title: 'End',\n        inputs: {\n          type: 'object',\n          properties: {\n            AA: {\n              type: 'string',\n            },\n            BB: {\n              type: 'integer',\n            },\n            CC: {\n              type: 'object',\n            },\n            DD: {\n              type: 'array',\n            },\n            EE: {\n              type: 'object',\n            },\n          },\n        },\n        inputsValues: {\n          AA: {\n            type: 'ref',\n            content: ['start_0', 'AA'],\n          },\n          BB: {\n            type: 'ref',\n            content: ['start_0', 'BB'],\n          },\n          CC: {\n            type: 'ref',\n            content: ['start_0', 'CC'],\n          },\n          DD: {\n            type: 'ref',\n            content: ['start_0', 'DD'],\n          },\n          EE: {\n            type: 'ref',\n            content: ['start_0', 'EE'],\n          },\n        },\n      },\n    },\n  ],\n  edges: [\n    {\n      sourceNodeID: 'start_0',\n      targetNodeID: 'end_0',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IExecutor } from '@flowgram.ai/runtime-interface';\n\nimport { MockLLMExecutor } from './executor/llm';\nimport { WorkflowRuntimeContainer } from '../container';\n\nconst container = WorkflowRuntimeContainer.instance;\nconst executor = container.get<IExecutor>(IExecutor);\nexecutor.register(new MockLLMExecutor());\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/utils/array-vo-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { VOData } from '@flowgram.ai/runtime-interface';\n\nexport const arrayVOData = <T>(arr: T[]): Array<VOData<T>> =>\n  arr.map((item: any) => {\n    const { id, ...data } = item;\n    return data;\n  });\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { snapshotsToVOData } from './snapshot';\nexport { arrayVOData } from './array-vo-data';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/__tests__/utils/snapshot.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Snapshot, VOData } from '@flowgram.ai/runtime-interface';\n\nexport const snapshotsToVOData = (snapshots: Snapshot[]): VOData<Snapshot>[] =>\n  snapshots.map((snapshot) => {\n    const { nodeID, inputs, outputs, data, branch } = snapshot;\n    const newSnapshot: VOData<Snapshot> = {\n      nodeID,\n      inputs,\n      outputs,\n      data,\n    };\n    if (branch) {\n      newSnapshot.branch = branch;\n    }\n    return newSnapshot;\n  });\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/cache/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ICache } from '@flowgram.ai/runtime-interface';\n\nexport class WorkflowRuntimeCache implements ICache {\n  private map: Map<string, any>;\n\n  public init(): void {\n    this.map = new Map();\n  }\n\n  public dispose(): void {\n    this.map.clear();\n  }\n\n  public get(key: string): any {\n    return this.map.get(key);\n  }\n\n  public set(key: string, value: any): this {\n    this.map.set(key, value);\n    return this;\n  }\n\n  public delete(key: string): boolean {\n    return this.map.delete(key);\n  }\n\n  public has(key: string): boolean {\n    return this.map.has(key);\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/container/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IEngine, IExecutor, IValidation } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeContainer } from './index';\n\ndescribe('WorkflowRuntimeContainer', () => {\n  it('should create a container instance', () => {\n    const container = new WorkflowRuntimeContainer({});\n    expect(container).toBeDefined();\n    expect(container).toBeInstanceOf(WorkflowRuntimeContainer);\n  });\n\n  it('should get services correctly', () => {\n    const mockServices = {\n      [IValidation]: { id: 'validation' },\n      [IExecutor]: { id: 'executor' },\n      [IEngine]: { id: 'engine' },\n    };\n\n    const container = new WorkflowRuntimeContainer(mockServices);\n\n    expect(container.get(IValidation)).toEqual({ id: 'validation' });\n    expect(container.get(IExecutor)).toEqual({ id: 'executor' });\n    expect(container.get(IEngine)).toEqual({ id: 'engine' });\n  });\n\n  it('should maintain singleton instance', () => {\n    const instance1 = WorkflowRuntimeContainer.instance;\n    const instance2 = WorkflowRuntimeContainer.instance;\n\n    expect(instance1).toBeDefined();\n    expect(instance2).toBeDefined();\n    expect(instance1).toBe(instance2);\n  });\n\n  it('should create services correctly', () => {\n    const services = (WorkflowRuntimeContainer as any).create();\n\n    expect(services[IValidation]).toBeDefined();\n    expect(services[IExecutor]).toBeDefined();\n    expect(services[IEngine]).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/container/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ContainerService,\n  IContainer,\n  IEngine,\n  IExecutor,\n  IValidation,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeNodeExecutors } from '@nodes/index';\nimport { WorkflowRuntimeValidation } from '../validation';\nimport { WorkflowRuntimeExecutor } from '../executor';\nimport { WorkflowRuntimeEngine } from '../engine';\n\nexport class WorkflowRuntimeContainer implements IContainer {\n  constructor(private readonly services: Record<string, ContainerService>) {}\n\n  public get<T = ContainerService>(key: any): T {\n    return this.services[key] as T;\n  }\n\n  private static _instance: IContainer;\n\n  public static get instance(): IContainer {\n    if (this._instance) {\n      return this._instance;\n    }\n    const services = this.create();\n    this._instance = new WorkflowRuntimeContainer(services);\n    return this._instance;\n  }\n\n  private static create(): Record<symbol, ContainerService> {\n    // services\n    const Validation = new WorkflowRuntimeValidation();\n    const Executor = new WorkflowRuntimeExecutor(WorkflowRuntimeNodeExecutors);\n    const Engine = new WorkflowRuntimeEngine({\n      Validation,\n      Executor,\n    });\n\n    return {\n      [IValidation]: Validation,\n      [IExecutor]: Executor,\n      [IEngine]: Engine,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/context/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  InvokeParams,\n  IContext,\n  IDocument,\n  IState,\n  ISnapshotCenter,\n  IVariableStore,\n  IStatusCenter,\n  IReporter,\n  IIOCenter,\n  ContextData,\n  IMessageCenter,\n  ICache,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeMessageCenter } from '@workflow/message';\nimport { WorkflowRuntimeCache } from '@workflow/cache';\nimport { uuid } from '@infra/utils';\nimport { WorkflowRuntimeVariableStore } from '../variable';\nimport { WorkflowRuntimeStatusCenter } from '../status';\nimport { WorkflowRuntimeState } from '../state';\nimport { WorkflowRuntimeSnapshotCenter } from '../snapshot';\nimport { WorkflowRuntimeReporter } from '../report';\nimport { WorkflowRuntimeIOCenter } from '../io-center';\nimport { WorkflowRuntimeDocument } from '../document';\n\nexport class WorkflowRuntimeContext implements IContext {\n  public readonly id: string;\n\n  public readonly cache: ICache;\n\n  public readonly document: IDocument;\n\n  public readonly variableStore: IVariableStore;\n\n  public readonly state: IState;\n\n  public readonly ioCenter: IIOCenter;\n\n  public readonly snapshotCenter: ISnapshotCenter;\n\n  public readonly statusCenter: IStatusCenter;\n\n  public readonly messageCenter: IMessageCenter;\n\n  public readonly reporter: IReporter;\n\n  private subContexts: IContext[] = [];\n\n  constructor(data: ContextData) {\n    this.id = uuid();\n    this.cache = data.cache;\n    this.document = data.document;\n    this.variableStore = data.variableStore;\n    this.state = data.state;\n    this.ioCenter = data.ioCenter;\n    this.snapshotCenter = data.snapshotCenter;\n    this.statusCenter = data.statusCenter;\n    this.messageCenter = data.messageCenter;\n    this.reporter = data.reporter;\n  }\n\n  public init(params: InvokeParams): void {\n    const { schema, inputs } = params;\n    this.cache.init();\n    this.document.init(schema);\n    this.variableStore.init();\n    this.state.init(schema);\n    this.ioCenter.init(inputs);\n    this.snapshotCenter.init();\n    this.statusCenter.init();\n    this.messageCenter.init();\n    this.reporter.init();\n  }\n\n  public dispose(): void {\n    this.subContexts.forEach((subContext) => {\n      subContext.dispose();\n    });\n    this.subContexts = [];\n    this.cache.dispose();\n    this.document.dispose();\n    this.variableStore.dispose();\n    this.state.dispose();\n    this.ioCenter.dispose();\n    this.snapshotCenter.dispose();\n    this.statusCenter.dispose();\n    this.messageCenter.dispose();\n    this.reporter.dispose();\n  }\n\n  public sub(): IContext {\n    const cache = new WorkflowRuntimeCache();\n    const variableStore = new WorkflowRuntimeVariableStore();\n    variableStore.setParent(this.variableStore);\n    const state = new WorkflowRuntimeState(variableStore);\n    const contextData: ContextData = {\n      cache,\n      document: this.document,\n      ioCenter: this.ioCenter,\n      snapshotCenter: this.snapshotCenter,\n      statusCenter: this.statusCenter,\n      messageCenter: this.messageCenter,\n      reporter: this.reporter,\n      variableStore,\n      state,\n    };\n    const subContext = new WorkflowRuntimeContext(contextData);\n    this.subContexts.push(subContext);\n    subContext.cache.init();\n    subContext.variableStore.init();\n    subContext.state.init();\n    return subContext;\n  }\n\n  public static create(): IContext {\n    const cache = new WorkflowRuntimeCache();\n    const document = new WorkflowRuntimeDocument();\n    const variableStore = new WorkflowRuntimeVariableStore();\n    const state = new WorkflowRuntimeState(variableStore);\n    const ioCenter = new WorkflowRuntimeIOCenter();\n    const snapshotCenter = new WorkflowRuntimeSnapshotCenter();\n    const statusCenter = new WorkflowRuntimeStatusCenter();\n    const messageCenter = new WorkflowRuntimeMessageCenter();\n    const reporter = new WorkflowRuntimeReporter(\n      ioCenter,\n      snapshotCenter,\n      statusCenter,\n      messageCenter\n    );\n    return new WorkflowRuntimeContext({\n      cache,\n      document,\n      variableStore,\n      state,\n      ioCenter,\n      snapshotCenter,\n      statusCenter,\n      messageCenter,\n      reporter,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/create-store.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { FlowGramNode, WorkflowPortType } from '@flowgram.ai/runtime-interface';\n\nimport { createStore } from './create-store';\n\ndescribe('createStore', () => {\n  it('should create an empty store', () => {\n    const store = createStore({\n      flattenSchema: { nodes: [], edges: [] },\n      nodeBlocks: new Map(),\n      nodeEdges: new Map(),\n    });\n\n    expect(store.nodes.size).toBe(1); // 只有 root 节点\n    expect(store.edges.size).toBe(0);\n    expect(store.ports.size).toBe(0);\n\n    const rootNode = store.nodes.get(FlowGramNode.Root);\n    expect(rootNode).toBeDefined();\n    expect(rootNode?.type).toBe(FlowGramNode.Root);\n    expect(rootNode?.position).toEqual({ x: 0, y: 0 });\n  });\n\n  it('should create store with nodes and edges', () => {\n    const store = createStore({\n      flattenSchema: {\n        nodes: [\n          {\n            id: 'node1',\n            type: 'TestNode',\n            meta: { position: { x: 100, y: 100 } },\n            data: {\n              title: 'Test Node 1',\n              inputsValues: { test: 'value' },\n              inputs: ['input1'],\n              outputs: ['output1'],\n            },\n          },\n          {\n            id: 'node2',\n            type: 'TestNode',\n            meta: { position: { x: 200, y: 200 } },\n            data: {\n              title: 'Test Node 2',\n            },\n          },\n        ],\n        edges: [\n          {\n            sourceNodeID: 'node1',\n            targetNodeID: 'node2',\n            sourcePortID: 'output1',\n            targetPortID: 'input1',\n          },\n        ],\n      },\n      nodeBlocks: new Map([['root', ['node1', 'node2']]]),\n      nodeEdges: new Map([['root', ['node1-node2']]]),\n    });\n\n    // 验证节点创建\n    expect(store.nodes.size).toBe(3); // root + 2个测试节点\n    const node1 = store.nodes.get('node1');\n    expect(node1?.type).toBe('TestNode');\n    expect(node1?.name).toBe('Test Node 1');\n    expect(node1?.position).toEqual({ x: 100, y: 100 });\n    expect(node1?.declare).toEqual({\n      inputsValues: { test: 'value' },\n      inputs: ['input1'],\n      outputs: ['output1'],\n    });\n\n    // 验证边创建\n    expect(store.edges.size).toBe(1);\n    const edge = Array.from(store.edges.values())[0];\n    expect(edge.from).toBe(node1);\n    expect(edge.to).toBe(store.nodes.get('node2'));\n\n    // 验证端口创建\n    expect(store.ports.size).toBe(2); // 输入端口和输出端口\n    const outputPort = store.ports.get('output1');\n    const inputPort = store.ports.get('input1');\n    expect(outputPort?.type).toBe(WorkflowPortType.Output);\n    expect(inputPort?.type).toBe(WorkflowPortType.Input);\n\n    // 验证节点关系\n    const rootNode = store.nodes.get(FlowGramNode.Root);\n    expect(rootNode?.children).toHaveLength(2);\n    expect(node1?.parent).toBe(rootNode);\n  });\n\n  it('should throw error for invalid edge', () => {\n    expect(() =>\n      createStore({\n        flattenSchema: {\n          nodes: [],\n          edges: [\n            {\n              sourceNodeID: 'nonexistent1',\n              targetNodeID: 'nonexistent2',\n            },\n          ],\n        },\n        nodeBlocks: new Map(),\n        nodeEdges: new Map(),\n      })\n    ).toThrow('Invalid edge schema ID');\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/create-store.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowGramNode,\n  WorkflowPortType,\n  type CreateEdgeParams,\n  type CreateNodeParams,\n  type CreatePortParams,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeEdge, WorkflowRuntimeNode, WorkflowRuntimePort } from '../entity';\nimport { FlattenData } from './flat-schema';\n\nexport interface DocumentStore {\n  nodes: Map<string, WorkflowRuntimeNode>;\n  edges: Map<string, WorkflowRuntimeEdge>;\n  ports: Map<string, WorkflowRuntimePort>;\n}\n\nconst createNode = (store: DocumentStore, params: CreateNodeParams): WorkflowRuntimeNode => {\n  const node = new WorkflowRuntimeNode(params);\n  store.nodes.set(node.id, node);\n  return node;\n};\n\nconst createEdge = (store: DocumentStore, params: CreateEdgeParams): WorkflowRuntimeEdge => {\n  const edge = new WorkflowRuntimeEdge(params);\n  store.edges.set(edge.id, edge);\n  return edge;\n};\n\nconst getOrCreatePort = (store: DocumentStore, params: CreatePortParams): WorkflowRuntimePort => {\n  const createdPort = store.ports.get(params.id);\n  if (createdPort) {\n    return createdPort as WorkflowRuntimePort;\n  }\n  const port = new WorkflowRuntimePort(params);\n  store.ports.set(port.id, port);\n  return port;\n};\n\nexport const createStore = (params: FlattenData): DocumentStore => {\n  const { flattenSchema, nodeBlocks } = params;\n  const { nodes, edges } = flattenSchema;\n  const store: DocumentStore = {\n    nodes: new Map(),\n    edges: new Map(),\n    ports: new Map(),\n  };\n  // create root node\n  createNode(store, {\n    id: FlowGramNode.Root,\n    type: FlowGramNode.Root,\n    name: FlowGramNode.Root,\n    position: { x: 0, y: 0 },\n  });\n  // create nodes\n  nodes.forEach((nodeSchema) => {\n    const id = nodeSchema.id;\n    const type = nodeSchema.type as FlowGramNode;\n    const {\n      title = `${type}-${id}-untitled`,\n      inputsValues,\n      inputs,\n      outputs,\n      ...data\n    } = nodeSchema.data ?? {};\n    createNode(store, {\n      id,\n      type,\n      name: title,\n      position: nodeSchema.meta.position,\n      variable: { inputsValues, inputs, outputs },\n      data,\n    });\n  });\n  // create node relations\n  nodeBlocks.forEach((blockIDs, parentID) => {\n    const parent = store.nodes.get(parentID) as WorkflowRuntimeNode;\n    const children = blockIDs\n      .map((id) => store.nodes.get(id))\n      .filter(Boolean) as WorkflowRuntimeNode[];\n    children.forEach((child) => {\n      child.parent = parent;\n      parent.addChild(child);\n    });\n  });\n  // create edges & ports\n  edges.forEach((edgeSchema) => {\n    const id = WorkflowRuntimeEdge.createID(edgeSchema);\n    const {\n      sourceNodeID,\n      targetNodeID,\n      sourcePortID = 'defaultOutput',\n      targetPortID = 'defaultInput',\n    } = edgeSchema;\n    const from = store.nodes.get(sourceNodeID);\n    const to = store.nodes.get(targetNodeID);\n    if (!from || !to) {\n      throw new Error(`Invalid edge schema ID: ${id}, from: ${sourceNodeID}, to: ${targetNodeID}`);\n    }\n    const edge = createEdge(store, {\n      id,\n      from,\n      to,\n    });\n\n    // create from port\n    const fromPort = getOrCreatePort(store, {\n      node: from,\n      id: sourcePortID,\n      type: WorkflowPortType.Output,\n    });\n\n    // build relation\n    fromPort.addEdge(edge);\n    edge.fromPort = fromPort;\n    from.addPort(fromPort);\n    from.addOutputEdge(edge);\n\n    // create to port\n    const toPort = getOrCreatePort(store, {\n      node: to,\n      id: targetPortID,\n      type: WorkflowPortType.Input,\n    });\n\n    // build relation\n    toPort.addEdge(edge);\n    edge.toPort = toPort;\n    to.addPort(toPort);\n    to.addInputEdge(edge);\n  });\n  return store;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/flat-schema.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { FlowGramNode, WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nimport { flatSchema } from './flat-schema';\n\ndescribe('flatSchema', () => {\n  it('should handle empty schema', () => {\n    const result = flatSchema({});\n    expect(result.flattenSchema.nodes).toEqual([]);\n    expect(result.flattenSchema.edges).toEqual([]);\n    expect(result.nodeBlocks.get(FlowGramNode.Root)).toEqual([]);\n    expect(result.nodeEdges.get(FlowGramNode.Root)).toEqual([]);\n  });\n\n  it('should handle basic schema without nested blocks', () => {\n    const schema = {\n      nodes: [\n        { id: 'node1', type: 'test' },\n        { id: 'node2', type: 'test' },\n      ],\n      edges: [{ sourceNodeID: 'node1', targetNodeID: 'node2' }],\n    } as unknown as WorkflowSchema;\n\n    const result = flatSchema(schema);\n    expect(result.flattenSchema.nodes).toEqual(schema.nodes);\n    expect(result.flattenSchema.edges).toEqual(schema.edges);\n    expect(result.nodeBlocks.get(FlowGramNode.Root)).toEqual(['node1', 'node2']);\n    expect(result.nodeEdges.get(FlowGramNode.Root)).toEqual(['node1-node2']);\n  });\n\n  it('should flatten nested blocks and edges', () => {\n    const schema = {\n      nodes: [\n        {\n          id: 'parent',\n          type: 'container',\n          blocks: [\n            { id: 'child1', type: 'test' },\n            {\n              id: 'child2',\n              type: 'test',\n              blocks: [{ id: 'grandchild', type: 'test' }],\n              edges: [{ sourceNodeID: 'child2', targetNodeID: 'grandchild' }],\n            },\n          ],\n          edges: [{ sourceNodeID: 'child1', targetNodeID: 'child2' }],\n        },\n      ],\n      edges: [],\n    } as unknown as WorkflowSchema;\n\n    const result = flatSchema(schema);\n\n    // 验证节点被正确展平\n    expect(result.flattenSchema.nodes.map((n) => n.id)).toEqual([\n      'parent',\n      'child1',\n      'child2',\n      'grandchild',\n    ]);\n\n    // 验证边被正确展平\n    expect(result.flattenSchema.edges.length).toBe(2);\n\n    // 验证节点关系映射\n    expect(result.nodeBlocks.get('parent')).toEqual(['child1', 'child2']);\n    expect(result.nodeBlocks.get('child2')).toEqual(['grandchild']);\n\n    // 验证边关系映射\n    expect(result.nodeEdges.get('parent')).toEqual(['child1-child2']);\n    expect(result.nodeEdges.get('child2')).toEqual(['child2-grandchild']);\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/flat-schema.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowNodeSchema, WorkflowSchema, FlowGramNode } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeEdge } from '../entity';\n\nexport interface FlattenData {\n  flattenSchema: WorkflowSchema;\n  nodeBlocks: Map<string, string[]>;\n  nodeEdges: Map<string, string[]>;\n}\n\ntype FlatSchema = (json: Partial<WorkflowSchema>) => FlattenData;\n\nconst flatLayer = (data: FlattenData, nodeSchema: WorkflowNodeSchema) => {\n  const { blocks, edges } = nodeSchema;\n  if (blocks) {\n    data.flattenSchema.nodes.push(...blocks);\n    const blockIDs: string[] = [];\n    blocks.forEach((block) => {\n      blockIDs.push(block.id);\n      // 递归处理子节点的 blocks 和 edges\n      if (block.blocks) {\n        flatLayer(data, block);\n      }\n    });\n    data.nodeBlocks.set(nodeSchema.id, blockIDs);\n    delete nodeSchema.blocks;\n  }\n  if (edges) {\n    data.flattenSchema.edges.push(...edges);\n    const edgeIDs: string[] = [];\n    edges.forEach((edge) => {\n      const edgeID = WorkflowRuntimeEdge.createID(edge);\n      edgeIDs.push(edgeID);\n    });\n    data.nodeEdges.set(nodeSchema.id, edgeIDs);\n    delete nodeSchema.edges;\n  }\n};\n\n/**\n * flat the tree json structure, extract the structure information to map\n */\nexport const flatSchema: FlatSchema = (schema = { nodes: [], edges: [] }) => {\n  const rootNodes = schema.nodes ?? [];\n  const rootEdges = schema.edges ?? [];\n\n  const data: FlattenData = {\n    flattenSchema: {\n      nodes: [],\n      edges: [],\n    },\n    nodeBlocks: new Map(),\n    nodeEdges: new Map(),\n  };\n\n  const root: WorkflowNodeSchema = {\n    id: FlowGramNode.Root,\n    type: FlowGramNode.Root,\n    blocks: rootNodes,\n    edges: rootEdges,\n    meta: {\n      position: {\n        x: 0,\n        y: 0,\n      },\n    },\n    data: {},\n  };\n\n  flatLayer(data, root);\n\n  return data;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\n\nimport { TestSchemas } from '@workflow/__tests__/schemas';\nimport { WorkflowRuntimeDocument } from './index';\n\ndescribe('WorkflowRuntimeDocument create', () => {\n  it('should create instance', () => {\n    const document = new WorkflowRuntimeDocument();\n    expect(document).toBeDefined();\n    expect(document.id).toBeDefined();\n  });\n\n  it('should has root', () => {\n    const schema = {\n      nodes: [],\n      edges: [],\n    };\n    const document = new WorkflowRuntimeDocument();\n    document.init(schema);\n    expect(document.root).toBeDefined();\n  });\n\n  it('should init', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.basicSchema);\n    const nodeIDs = document.nodes.map((n) => n.id);\n    const edgeIDs = document.edges.map((e) => e.id);\n    expect(nodeIDs).toEqual(['root', 'start_0', 'end_0', 'llm_0']);\n    expect(edgeIDs).toEqual(['start_0-llm_0', 'llm_0-end_0']);\n  });\n\n  it('should dispose', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.basicSchema);\n    expect(document.nodes.length).toBe(4);\n    expect(document.edges.length).toBe(2);\n    document.dispose();\n    expect(document.nodes.length).toBe(0);\n    expect(document.edges.length).toBe(0);\n  });\n\n  it('should has start & end', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.basicSchema);\n    expect(document.start.id).toBe('start_0');\n    expect(document.end.id).toBe('end_0');\n  });\n\n  it('should get node by id', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.basicSchema);\n\n    const node = document.getNode('llm_0');\n    expect(node).toBeDefined();\n    expect(node?.id).toBe('llm_0');\n    expect(node?.type).toBe('llm');\n\n    const nonExistNode = document.getNode('non_exist');\n    expect(nonExistNode).toBeNull();\n  });\n\n  it('should get edge by id', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.basicSchema);\n\n    const edge = document.getEdge('start_0-llm_0');\n    expect(edge).toBeDefined();\n    expect(edge?.id).toBe('start_0-llm_0');\n\n    const nonExistEdge = document.getEdge('non_exist');\n    expect(nonExistEdge).toBeNull();\n  });\n\n  it('should init with two LLM schema', () => {\n    const document = new WorkflowRuntimeDocument();\n    document.init(TestSchemas.twoLLMSchema);\n\n    expect(document.nodes.length).toBeGreaterThan(4); // 包含 root, start, end 和至少两个 LLM 节点\n    expect(document.edges.length).toBeGreaterThan(2); // 至少有 3 条边连接这些节点\n\n    // 验证所有必需的节点都存在\n    expect(document.root).toBeDefined();\n    expect(document.start).toBeDefined();\n    expect(document.end).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/document/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type WorkflowSchema,\n  FlowGramNode,\n  type IDocument,\n  type IEdge,\n  type INode,\n} from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\nimport { flatSchema } from './flat-schema';\nimport { createStore, DocumentStore } from './create-store';\n\nexport class WorkflowRuntimeDocument implements IDocument {\n  public readonly id: string;\n\n  private store: DocumentStore;\n\n  constructor() {\n    this.id = uuid();\n  }\n\n  public get root(): INode {\n    const rootNode = this.getNode(FlowGramNode.Root);\n    if (!rootNode) {\n      throw new Error('Root node not found');\n    }\n    return rootNode;\n  }\n\n  public get start(): INode {\n    const startNode = this.nodes.find((n) => n.type === FlowGramNode.Start);\n    if (!startNode) {\n      throw new Error('Start node not found');\n    }\n    return startNode;\n  }\n\n  public get end(): INode {\n    const endNode = this.nodes.find((n) => n.type === FlowGramNode.End);\n    if (!endNode) {\n      throw new Error('End node not found');\n    }\n    return endNode;\n  }\n\n  public getNode(id: string): INode | null {\n    return this.store.nodes.get(id) ?? null;\n  }\n\n  public getEdge(id: string): IEdge | null {\n    return this.store.edges.get(id) ?? null;\n  }\n\n  public get nodes(): INode[] {\n    return Array.from(this.store.nodes.values());\n  }\n\n  public get edges(): IEdge[] {\n    return Array.from(this.store.edges.values());\n  }\n\n  public init(schema: WorkflowSchema): void {\n    const flattenSchema = flatSchema(schema);\n    this.store = createStore(flattenSchema);\n  }\n\n  public dispose(): void {\n    this.store.edges.clear();\n    this.store.nodes.clear();\n    this.store.ports.clear();\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/edge/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { WorkflowEdgeSchema, CreateEdgeParams, INode, IPort } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeEdge } from '.';\n\ndescribe('WorkflowRuntimeEdge', () => {\n  let edge: WorkflowRuntimeEdge;\n  let mockFromNode: INode;\n  let mockToNode: INode;\n  let mockParams: CreateEdgeParams;\n\n  beforeEach(() => {\n    mockFromNode = {\n      id: 'from-node',\n    } as INode;\n\n    mockToNode = {\n      id: 'to-node',\n    } as INode;\n\n    mockParams = {\n      id: 'test-edge',\n      from: mockFromNode,\n      to: mockToNode,\n    };\n\n    edge = new WorkflowRuntimeEdge(mockParams);\n  });\n\n  describe('constructor', () => {\n    it('should initialize with provided params', () => {\n      expect(edge.id).toBe(mockParams.id);\n      expect(edge.from).toBe(mockParams.from);\n      expect(edge.to).toBe(mockParams.to);\n    });\n  });\n\n  describe('ports', () => {\n    let mockFromPort: IPort;\n    let mockToPort: IPort;\n\n    beforeEach(() => {\n      mockFromPort = { id: 'from-port' } as IPort;\n      mockToPort = { id: 'to-port' } as IPort;\n    });\n\n    it('should set and get fromPort correctly', () => {\n      edge.fromPort = mockFromPort;\n      expect(edge.fromPort).toBe(mockFromPort);\n    });\n\n    it('should set and get toPort correctly', () => {\n      edge.toPort = mockToPort;\n      expect(edge.toPort).toBe(mockToPort);\n    });\n  });\n\n  describe('createID', () => {\n    it('should create ID with port IDs', () => {\n      const schema: WorkflowEdgeSchema = {\n        sourceNodeID: 'source',\n        sourcePortID: 'sourcePort',\n        targetNodeID: 'target',\n        targetPortID: 'targetPort',\n      };\n\n      const id = WorkflowRuntimeEdge.createID(schema);\n      expect(id).toBe('source:sourcePort-target:targetPort');\n    });\n\n    it('should create ID without port IDs', () => {\n      const schema: WorkflowEdgeSchema = {\n        sourceNodeID: 'source',\n        targetNodeID: 'target',\n      };\n\n      const id = WorkflowRuntimeEdge.createID(schema);\n      expect(id).toBe('source-target');\n    });\n\n    it('should create ID with mixed port IDs', () => {\n      const schemaWithSourcePort: WorkflowEdgeSchema = {\n        sourceNodeID: 'source',\n        sourcePortID: 'sourcePort',\n        targetNodeID: 'target',\n      };\n\n      const schemaWithTargetPort: WorkflowEdgeSchema = {\n        sourceNodeID: 'source',\n        targetNodeID: 'target',\n        targetPortID: 'targetPort',\n      };\n\n      expect(WorkflowRuntimeEdge.createID(schemaWithSourcePort)).toBe('source:sourcePort-target');\n      expect(WorkflowRuntimeEdge.createID(schemaWithTargetPort)).toBe('source-target:targetPort');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/edge/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowEdgeSchema,\n  CreateEdgeParams,\n  IEdge,\n  INode,\n  IPort,\n} from '@flowgram.ai/runtime-interface';\n\nexport class WorkflowRuntimeEdge implements IEdge {\n  public readonly id: string;\n\n  public readonly from: INode;\n\n  public readonly to: INode;\n\n  private _fromPort: IPort;\n\n  private _toPort: IPort;\n\n  constructor(params: CreateEdgeParams) {\n    const { id, from, to } = params;\n    this.id = id;\n    this.from = from;\n    this.to = to;\n  }\n\n  public get fromPort() {\n    return this._fromPort;\n  }\n\n  public set fromPort(port: IPort) {\n    this._fromPort = port;\n  }\n\n  public get toPort() {\n    return this._toPort;\n  }\n\n  public set toPort(port: IPort) {\n    this._toPort = port;\n  }\n\n  public static createID(schema: WorkflowEdgeSchema): string {\n    const { sourceNodeID, sourcePortID, targetNodeID, targetPortID } = schema;\n    const sourcePart = sourcePortID ? `${sourceNodeID}:${sourcePortID}` : sourceNodeID;\n    const targetPart = targetPortID ? `${targetNodeID}:${targetPortID}` : targetNodeID;\n    return `${sourcePart}-${targetPart}`;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeEdge } from './edge';\nexport { WorkflowRuntimeNode } from './node';\nexport { WorkflowRuntimePort } from './port';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/node/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport {\n  FlowGramNode,\n  WorkflowPortType,\n  CreateNodeParams,\n  IEdge,\n  INode,\n  IPort,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeNode } from './index';\n\ndescribe('WorkflowRuntimeNode', () => {\n  let node: WorkflowRuntimeNode;\n  let mockParams: CreateNodeParams;\n\n  beforeEach(() => {\n    mockParams = {\n      id: 'test-node',\n      type: FlowGramNode.Start,\n      name: 'Test Node',\n      position: { x: 0, y: 0 },\n      variable: {},\n      data: { testData: 'data' },\n    };\n    node = new WorkflowRuntimeNode(mockParams);\n  });\n\n  describe('constructor', () => {\n    it('should initialize with provided params', () => {\n      expect(node.id).toBe(mockParams.id);\n      expect(node.type).toBe(mockParams.type);\n      expect(node.name).toBe(mockParams.name);\n      expect(node.position).toBe(mockParams.position);\n      expect(node.declare).toEqual(mockParams.variable);\n      expect(node.data).toEqual(mockParams.data);\n    });\n\n    it('should initialize with default values when optional params are not provided', () => {\n      const minimalParams = {\n        id: 'test-node',\n        type: FlowGramNode.Start,\n        name: 'Test Node',\n        position: { x: 0, y: 0 },\n      };\n      const minimalNode = new WorkflowRuntimeNode(minimalParams);\n      expect(minimalNode.declare).toEqual({});\n      expect(minimalNode.data).toEqual({});\n    });\n  });\n\n  describe('ports', () => {\n    let inputPort: IPort;\n    let outputPort: IPort;\n\n    beforeEach(() => {\n      inputPort = { id: 'input-1', type: WorkflowPortType.Input, node, edges: [] };\n      outputPort = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] };\n      node.addPort(inputPort);\n      node.addPort(outputPort);\n    });\n\n    it('should correctly categorize input and output ports', () => {\n      const { inputs, outputs } = node.ports;\n      expect(inputs).toHaveLength(1);\n      expect(outputs).toHaveLength(1);\n      expect(inputs[0]).toBe(inputPort);\n      expect(outputs[0]).toBe(outputPort);\n    });\n  });\n\n  describe('edges', () => {\n    let inputEdge: IEdge;\n    let outputEdge: IEdge;\n    let fromNode: INode;\n    let toNode: INode;\n\n    beforeEach(() => {\n      fromNode = new WorkflowRuntimeNode({ ...mockParams, id: 'from-node' });\n      toNode = new WorkflowRuntimeNode({ ...mockParams, id: 'to-node' });\n      inputEdge = {\n        id: 'input-edge',\n        from: fromNode,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      outputEdge = {\n        id: 'output-edge',\n        from: node,\n        to: toNode,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      node.addInputEdge(inputEdge);\n      node.addOutputEdge(outputEdge);\n    });\n\n    it('should correctly store input and output edges', () => {\n      const { inputs, outputs } = node.edges;\n      expect(inputs).toHaveLength(1);\n      expect(outputs).toHaveLength(1);\n      expect(inputs[0]).toBe(inputEdge);\n      expect(outputs[0]).toBe(outputEdge);\n    });\n\n    it('should update prev and next nodes when adding edges', () => {\n      expect(node.prev).toHaveLength(1);\n      expect(node.next).toHaveLength(1);\n      expect(node.prev[0]).toBe(fromNode);\n      expect(node.next[0]).toBe(toNode);\n    });\n  });\n\n  describe('parent and children', () => {\n    let parentNode: INode;\n    let childNode: INode;\n\n    beforeEach(() => {\n      parentNode = new WorkflowRuntimeNode({ ...mockParams, id: 'parent-node' });\n      childNode = new WorkflowRuntimeNode({ ...mockParams, id: 'child-node' });\n    });\n\n    it('should handle parent-child relationships', () => {\n      node.parent = parentNode;\n      node.addChild(childNode);\n\n      expect(node.parent).toBe(parentNode);\n      expect(node.children).toHaveLength(1);\n      expect(node.children[0]).toBe(childNode);\n    });\n  });\n\n  describe('isBranch', () => {\n    it('should return true when node has multiple output ports', () => {\n      const outputPort1 = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] };\n      const outputPort2 = { id: 'output-2', type: WorkflowPortType.Output, node, edges: [] };\n      node.addPort(outputPort1);\n      node.addPort(outputPort2);\n\n      expect(node.isBranch).toBe(true);\n    });\n\n    it('should return false when node has one or zero output ports', () => {\n      const outputPort = { id: 'output-1', type: WorkflowPortType.Output, node, edges: [] };\n      node.addPort(outputPort);\n\n      expect(node.isBranch).toBe(false);\n    });\n  });\n\n  describe('successors', () => {\n    it('should return empty array when node has no successors', () => {\n      expect(node.successors).toEqual([]);\n    });\n\n    it('should return direct successors', () => {\n      const successor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-1' });\n      const successor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-2' });\n\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: successor1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: node,\n        to: successor2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      node.addOutputEdge(edge2);\n\n      const { successors } = node;\n      expect(successors).toHaveLength(2);\n      expect(successors).toContain(successor1);\n      expect(successors).toContain(successor2);\n    });\n\n    it('should return all nested successors recursively', () => {\n      const successor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-1' });\n      const successor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-2' });\n      const successor3 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-3' });\n\n      // node -> successor1 -> successor2 -> successor3\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: successor1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: successor1,\n        to: successor2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: successor2,\n        to: successor3,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      successor1.addOutputEdge(edge2);\n      successor2.addOutputEdge(edge3);\n\n      const { successors } = node;\n      expect(successors).toHaveLength(3);\n      expect(successors).toContain(successor1);\n      expect(successors).toContain(successor2);\n      expect(successors).toContain(successor3);\n    });\n\n    it('should handle circular references without infinite loop', () => {\n      const successor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-1' });\n      const successor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'successor-2' });\n\n      // Create a cycle: node -> successor1 -> successor2 -> node\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: successor1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: successor1,\n        to: successor2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: successor2,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      successor1.addOutputEdge(edge2);\n      successor2.addOutputEdge(edge3);\n\n      const { successors } = node;\n      // In a circular reference, we should get all nodes in the cycle except the starting node\n      expect(successors).toHaveLength(3);\n      expect(successors).toContain(successor1);\n      expect(successors).toContain(successor2);\n      expect(successors).toContain(node); // node will be visited when traversing from successor2\n    });\n  });\n\n  describe('predecessors', () => {\n    it('should return empty array when node has no predecessors', () => {\n      expect(node.predecessors).toEqual([]);\n    });\n\n    it('should return direct predecessors', () => {\n      const predecessor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-1' });\n      const predecessor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-2' });\n\n      const edge1 = {\n        id: 'edge-1',\n        from: predecessor1,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: predecessor2,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addInputEdge(edge1);\n      node.addInputEdge(edge2);\n\n      const { predecessors } = node;\n      expect(predecessors).toHaveLength(2);\n      expect(predecessors).toContain(predecessor1);\n      expect(predecessors).toContain(predecessor2);\n    });\n\n    it('should return all nested predecessors recursively', () => {\n      const predecessor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-1' });\n      const predecessor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-2' });\n      const predecessor3 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-3' });\n\n      // predecessor3 -> predecessor2 -> predecessor1 -> node\n      const edge1 = {\n        id: 'edge-1',\n        from: predecessor3,\n        to: predecessor2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: predecessor2,\n        to: predecessor1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: predecessor1,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      predecessor2.addInputEdge(edge1);\n      predecessor1.addInputEdge(edge2);\n      node.addInputEdge(edge3);\n\n      const { predecessors } = node;\n      expect(predecessors).toHaveLength(3);\n      expect(predecessors).toContain(predecessor1);\n      expect(predecessors).toContain(predecessor2);\n      expect(predecessors).toContain(predecessor3);\n    });\n\n    it('should handle circular references without infinite loop', () => {\n      const predecessor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-1' });\n      const predecessor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'predecessor-2' });\n\n      // Create a cycle: node -> predecessor1 -> predecessor2 -> node\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: predecessor1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: predecessor1,\n        to: predecessor2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: predecessor2,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      predecessor1.addOutputEdge(edge2);\n      node.addInputEdge(edge3);\n\n      const { predecessors } = node;\n      expect(predecessors).toHaveLength(1);\n      expect(predecessors).toContain(predecessor2);\n      // node itself should not be included in predecessors\n      expect(predecessors).not.toContain(node);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/node/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  FlowGramNode,\n  PositionSchema,\n  CreateNodeParams,\n  IEdge,\n  INode,\n  IPort,\n  NodeVariable,\n  WorkflowPortType,\n} from '@flowgram.ai/runtime-interface';\n\nimport { traverseNodes } from '@infra/index';\n\nexport class WorkflowRuntimeNode<T = any> implements INode {\n  public readonly id: string;\n\n  public readonly type: FlowGramNode;\n\n  public readonly name: string;\n\n  public readonly position: PositionSchema;\n\n  public readonly declare: NodeVariable;\n\n  public readonly data: T;\n\n  private _parent: INode | null;\n\n  private readonly _children: INode[];\n\n  private readonly _ports: IPort[];\n\n  private readonly _inputEdges: IEdge[];\n\n  private readonly _outputEdges: IEdge[];\n\n  private readonly _prev: INode[];\n\n  private readonly _next: INode[];\n\n  constructor(params: CreateNodeParams) {\n    const { id, type, name, position, variable, data } = params;\n    this.id = id;\n    this.type = type;\n    this.name = name;\n    this.position = position;\n    this.declare = variable ?? {};\n    this.data = data ?? {};\n    this._parent = null;\n    this._children = [];\n    this._ports = [];\n    this._inputEdges = [];\n    this._outputEdges = [];\n    this._prev = [];\n    this._next = [];\n  }\n\n  public get ports() {\n    const inputs = this._ports.filter((port) => port.type === WorkflowPortType.Input);\n    const outputs = this._ports.filter((port) => port.type === WorkflowPortType.Output);\n    return {\n      inputs,\n      outputs,\n    };\n  }\n\n  public get edges() {\n    return {\n      inputs: this._inputEdges,\n      outputs: this._outputEdges,\n    };\n  }\n\n  public get parent() {\n    return this._parent;\n  }\n\n  public set parent(parent: INode | null) {\n    this._parent = parent;\n  }\n\n  public get children() {\n    return this._children;\n  }\n\n  public addChild(child: INode) {\n    this._children.push(child);\n  }\n\n  public addPort(port: IPort) {\n    this._ports.push(port);\n  }\n\n  public addInputEdge(edge: IEdge) {\n    this._inputEdges.push(edge);\n    this._prev.push(edge.from);\n  }\n\n  public addOutputEdge(edge: IEdge) {\n    this._outputEdges.push(edge);\n    this._next.push(edge.to);\n  }\n\n  public get prev() {\n    return this._prev;\n  }\n\n  public get next() {\n    return this._next;\n  }\n\n  public get successors(): INode[] {\n    return traverseNodes(this, (node) => node.next);\n  }\n\n  public get predecessors(): INode[] {\n    return traverseNodes(this, (node) => node.prev);\n  }\n\n  public get isBranch() {\n    return this.ports.outputs.length > 1;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/port/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { WorkflowPortType, CreatePortParams, IEdge, INode } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimePort } from '.';\n\ndescribe('WorkflowRuntimePort', () => {\n  let port: WorkflowRuntimePort;\n  let mockNode: INode;\n  let mockParams: CreatePortParams;\n\n  beforeEach(() => {\n    mockNode = {\n      id: 'test-node',\n    } as INode;\n\n    mockParams = {\n      id: 'test-port',\n      node: mockNode,\n      type: WorkflowPortType.Input,\n    };\n\n    port = new WorkflowRuntimePort(mockParams);\n  });\n\n  describe('constructor', () => {\n    it('should initialize with provided params', () => {\n      expect(port.id).toBe(mockParams.id);\n      expect(port.node).toBe(mockParams.node);\n      expect(port.type).toBe(mockParams.type);\n      expect(port.edges).toEqual([]);\n    });\n\n    it('should initialize with different port types', () => {\n      const inputPort = new WorkflowRuntimePort({\n        ...mockParams,\n        type: WorkflowPortType.Input,\n      });\n      expect(inputPort.type).toBe(WorkflowPortType.Input);\n\n      const outputPort = new WorkflowRuntimePort({\n        ...mockParams,\n        type: WorkflowPortType.Output,\n      });\n      expect(outputPort.type).toBe(WorkflowPortType.Output);\n    });\n  });\n\n  describe('edges management', () => {\n    it('should add edge correctly', () => {\n      const mockEdge: IEdge = {\n        id: 'test-edge',\n      } as IEdge;\n\n      port.addEdge(mockEdge);\n      expect(port.edges).toHaveLength(1);\n      expect(port.edges[0]).toBe(mockEdge);\n    });\n\n    it('should maintain edge order when adding multiple edges', () => {\n      const mockEdge1: IEdge = { id: 'edge-1' } as IEdge;\n      const mockEdge2: IEdge = { id: 'edge-2' } as IEdge;\n      const mockEdge3: IEdge = { id: 'edge-3' } as IEdge;\n\n      port.addEdge(mockEdge1);\n      port.addEdge(mockEdge2);\n      port.addEdge(mockEdge3);\n\n      expect(port.edges).toHaveLength(3);\n      expect(port.edges[0]).toBe(mockEdge1);\n      expect(port.edges[1]).toBe(mockEdge2);\n      expect(port.edges[2]).toBe(mockEdge3);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/entity/port/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowPortType,\n  CreatePortParams,\n  IEdge,\n  INode,\n  IPort,\n} from '@flowgram.ai/runtime-interface';\n\nexport class WorkflowRuntimePort implements IPort {\n  public readonly id: string;\n\n  public readonly node: INode;\n\n  public readonly type: WorkflowPortType;\n\n  private readonly _edges: IEdge[];\n\n  constructor(params: CreatePortParams) {\n    const { id, node } = params;\n    this.id = id;\n    this.node = node;\n    this.type = params.type;\n    this._edges = [];\n  }\n\n  public get edges() {\n    return this._edges;\n  }\n\n  public addEdge(edge: IEdge) {\n    this._edges.push(edge);\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/document/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeDocument } from './document';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/engine/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\n\nimport { WorkflowRuntimeValidation } from '@workflow/validation';\nimport { TestSchemas } from '@workflow/__tests__/schemas';\nimport { MockWorkflowRuntimeNodeExecutors } from '@workflow/__tests__/executor';\nimport { WorkflowRuntimeExecutor } from '../executor';\nimport { WorkflowRuntimeEngine } from './index';\n\nlet engine: WorkflowRuntimeEngine;\n\nbeforeEach(() => {\n  const Validation = new WorkflowRuntimeValidation();\n  const Executor = new WorkflowRuntimeExecutor(MockWorkflowRuntimeNodeExecutors);\n  engine = new WorkflowRuntimeEngine({\n    Validation,\n    Executor,\n  });\n});\n\ndescribe('WorkflowRuntimeEngine', () => {\n  it('should create a WorkflowRuntimeEngine', () => {\n    expect(engine).toBeDefined();\n  });\n\n  it('should execute a workflow with input', async () => {\n    const { processing } = engine.invoke({\n      schema: TestSchemas.basicSchema,\n      inputs: {\n        model_name: 'ai-model',\n        llm_settings: {\n          temperature: 0.5,\n        },\n        work: {\n          role: 'Chat',\n          task: 'Tell me a story about love',\n        },\n      },\n    });\n    const result = await processing;\n    expect(result).toStrictEqual({\n      llm_res: `Hi, I am an AI model, my name is ai-model, temperature is 0.5, system prompt is \"You are a helpful AI assistant.\", prompt is \"<Role>Chat</Role>\\n\\n<Task>\\nTell me a story about love\\n</Task>\"`,\n      llm_task: 'Tell me a story about love',\n    });\n  });\n\n  it('should execute a workflow with branch', async () => {\n    const { processing } = engine.invoke({\n      schema: TestSchemas.branchSchema,\n      inputs: {\n        model_id: 1,\n        prompt: 'Tell me a joke',\n      },\n    });\n    const result = await processing;\n    expect(result).toStrictEqual({\n      m1_res: `Hi, I am an AI model, my name is AI_MODEL_1, temperature is 0.5, system prompt is \"I'm Model 1.\", prompt is \"Tell me a joke\"`,\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/engine/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  EngineServices,\n  IEngine,\n  IExecutor,\n  INode,\n  WorkflowOutputs,\n  IContext,\n  InvokeParams,\n  ITask,\n  FlowGramNode,\n  IValidation,\n} from '@flowgram.ai/runtime-interface';\n\nimport { compareNodeGroups } from '@infra/utils';\nimport { WorkflowRuntimeTask } from '../task';\nimport { WorkflowRuntimeContext } from '../context';\nimport { WorkflowRuntimeContainer } from '../container';\n\nexport class WorkflowRuntimeEngine implements IEngine {\n  private readonly validation: IValidation;\n\n  private readonly executor: IExecutor;\n\n  constructor(service: EngineServices) {\n    this.validation = service.Validation;\n    this.executor = service.Executor;\n  }\n\n  public invoke(params: InvokeParams): ITask {\n    const context = WorkflowRuntimeContext.create();\n    context.init(params);\n    const valid = this.validate(params, context);\n    if (!valid) {\n      return WorkflowRuntimeTask.create({\n        processing: Promise.resolve({}),\n        context,\n      });\n    }\n    const processing = this.process(context);\n    processing.then(() => {\n      context.dispose();\n    });\n    return WorkflowRuntimeTask.create({\n      processing,\n      context,\n    });\n  }\n\n  public async executeNode(params: { context: IContext; node: INode }) {\n    const { node, context } = params;\n    if (!this.canExecuteNode({ node, context })) {\n      return;\n    }\n    context.statusCenter.nodeStatus(node.id).process();\n    const snapshot = context.snapshotCenter.create({\n      nodeID: node.id,\n      data: node.data,\n    });\n    let nextNodes: INode[] = [];\n    try {\n      const inputs = context.state.getNodeInputs(node);\n      snapshot.update({\n        inputs,\n      });\n      const result = await this.executor.execute({\n        node,\n        inputs,\n        runtime: context,\n        container: WorkflowRuntimeContainer.instance,\n        snapshot,\n      });\n      if (context.statusCenter.workflow.terminated) {\n        return;\n      }\n      const { outputs, branch } = result;\n      snapshot.update({ outputs, branch });\n      context.state.setNodeOutputs({ node, outputs });\n      context.state.addExecutedNode(node);\n      context.statusCenter.nodeStatus(node.id).success();\n      nextNodes = this.getNextNodes({ node, branch, context });\n    } catch (e) {\n      const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred';\n      snapshot.update({ error: errorMessage });\n      context.messageCenter.error({\n        nodeID: node.id,\n        message: errorMessage,\n      });\n      context.statusCenter.nodeStatus(node.id).fail();\n      console.error(e);\n      throw e;\n    }\n    await this.executeNext({ node, nextNodes, context });\n  }\n\n  private async process(context: IContext): Promise<WorkflowOutputs> {\n    const startNode = context.document.start;\n    context.statusCenter.workflow.process();\n    try {\n      await this.executeNode({ node: startNode, context });\n      const outputs = context.ioCenter.outputs;\n      context.statusCenter.workflow.success();\n      return outputs;\n    } catch (e) {\n      context.statusCenter.workflow.fail();\n      return {};\n    }\n  }\n\n  private validate(params: InvokeParams, context: IContext): boolean {\n    const { valid, errors } = this.validation.invoke(params);\n    if (valid) {\n      return true;\n    }\n    errors?.forEach((message) => {\n      context.messageCenter.error({\n        message,\n      });\n    });\n    context.statusCenter.workflow.fail();\n    return false;\n  }\n\n  private canExecuteNode(params: { context: IContext; node: INode }) {\n    const { node, context } = params;\n    const prevNodes = node.prev;\n    if (prevNodes.length === 0) {\n      return true;\n    }\n    return prevNodes.every((prevNode) => context.state.isExecutedNode(prevNode));\n  }\n\n  private getNextNodes(params: { context: IContext; node: INode; branch?: string }) {\n    const { node, branch, context } = params;\n    const allNextNodes = node.next;\n    if (!branch) {\n      return allNextNodes;\n    }\n    const targetPort = node.ports.outputs.find((port) => port.id === branch);\n    if (!targetPort) {\n      throw new Error(`Branch \"${branch}\" not found`);\n    }\n    const nextNodeIDs: Set<string> = new Set(targetPort.edges.map((edge) => edge.to.id));\n    const nextNodes = allNextNodes.filter((nextNode) => nextNodeIDs.has(nextNode.id));\n    const skipNodes = allNextNodes.filter((nextNode) => !nextNodeIDs.has(nextNode.id));\n    const nextGroups = nextNodes.map((nextNode) => [nextNode, ...nextNode.successors]);\n    const skipGroups = skipNodes.map((skipNode) => [skipNode, ...skipNode.successors]);\n    const { uniqueToB: skippedNodes } = compareNodeGroups(nextGroups, skipGroups);\n    skippedNodes.forEach((node) => {\n      context.state.addExecutedNode(node);\n    });\n    return nextNodes;\n  }\n\n  private async executeNext(params: { context: IContext; node: INode; nextNodes: INode[] }) {\n    const { context, node, nextNodes } = params;\n    const terminatingNodeTypes = [\n      FlowGramNode.End,\n      FlowGramNode.BlockEnd,\n      FlowGramNode.Break,\n      FlowGramNode.Continue,\n    ];\n    if (terminatingNodeTypes.includes(node.type)) {\n      return;\n    }\n    if (nextNodes.length === 0) {\n      throw new Error(`Node \"${node.id}\" has no next nodes`); // inside loop node may have no next nodes\n    }\n    await Promise.all(\n      nextNodes.map((nextNode) =>\n        this.executeNode({\n          node: nextNode,\n          context,\n        })\n      )\n    );\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/executor/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramNode } from '@flowgram.ai/runtime-interface';\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  IExecutor,\n  INodeExecutor,\n  INodeExecutorFactory,\n} from '@flowgram.ai/runtime-interface';\n\nexport class WorkflowRuntimeExecutor implements IExecutor {\n  private nodeExecutors: Map<FlowGramNode, INodeExecutor> = new Map();\n\n  constructor(nodeExecutors: INodeExecutorFactory[]) {\n    // register node executors\n    nodeExecutors.forEach((executor) => {\n      this.register(new executor());\n    });\n  }\n\n  public register(executor: INodeExecutor): void {\n    this.nodeExecutors.set(executor.type, executor);\n  }\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const nodeType = context.node.type;\n    const nodeExecutor = this.nodeExecutors.get(nodeType);\n    if (!nodeExecutor) {\n      throw new Error(`No executor found for node type ${nodeType}`);\n    }\n    const output = await nodeExecutor.execute(context);\n    return output;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/io-center/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IIOCenter, IOData, WorkflowInputs, WorkflowOutputs } from '@flowgram.ai/runtime-interface';\n\nexport class WorkflowRuntimeIOCenter implements IIOCenter {\n  private _inputs: WorkflowInputs;\n\n  private _outputs: WorkflowOutputs;\n\n  public init(inputs: WorkflowInputs): void {\n    this.setInputs(inputs);\n  }\n\n  public dispose(): void {}\n\n  public get inputs(): WorkflowInputs {\n    return this._inputs ?? {};\n  }\n\n  public get outputs(): WorkflowOutputs {\n    return this._outputs ?? {};\n  }\n\n  public setInputs(inputs: WorkflowInputs): void {\n    this._inputs = inputs;\n  }\n\n  public setOutputs(outputs: WorkflowOutputs): void {\n    this._outputs = outputs;\n  }\n\n  public export(): IOData {\n    return {\n      inputs: this._inputs,\n      outputs: this._outputs,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/message/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeMessageCenter } from './message-center';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/message/message-center/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { WorkflowMessageType, MessageData } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeMessageCenter } from './index';\n\ndescribe('WorkflowRuntimeMessageCenter', () => {\n  let messageCenter: WorkflowRuntimeMessageCenter;\n  const mockMessageData: MessageData = {\n    nodeID: 'test-node-1',\n    message: 'Test message',\n    timestamp: Date.now(),\n  };\n\n  beforeEach(() => {\n    messageCenter = new WorkflowRuntimeMessageCenter();\n    messageCenter.init();\n  });\n\n  describe('init', () => {\n    it('should initialize with empty messages object', () => {\n      const messages = messageCenter.export();\n      expect(messages).toEqual({\n        [WorkflowMessageType.Log]: [],\n        [WorkflowMessageType.Info]: [],\n        [WorkflowMessageType.Debug]: [],\n        [WorkflowMessageType.Error]: [],\n        [WorkflowMessageType.Warn]: [],\n      });\n    });\n\n    it('should clear existing messages when called', () => {\n      messageCenter.log(mockMessageData);\n      const messagesAfterLog = messageCenter.export();\n      expect(messagesAfterLog[WorkflowMessageType.Log]).toHaveLength(1);\n\n      messageCenter.init();\n      const messagesAfterInit = messageCenter.export();\n      expect(messagesAfterInit).toEqual({\n        [WorkflowMessageType.Log]: [],\n        [WorkflowMessageType.Info]: [],\n        [WorkflowMessageType.Debug]: [],\n        [WorkflowMessageType.Error]: [],\n        [WorkflowMessageType.Warn]: [],\n      });\n    });\n  });\n\n  describe('dispose', () => {\n    it('should not throw error when called', () => {\n      expect(() => messageCenter.dispose()).not.toThrow();\n    });\n  });\n\n  describe('log', () => {\n    it('should create and store log message', () => {\n      const message = messageCenter.log(mockMessageData);\n\n      expect(message.type).toBe(WorkflowMessageType.Log);\n      expect(message.nodeID).toBe(mockMessageData.nodeID);\n      expect(message.message).toBe(mockMessageData.message);\n      expect(message.timestamp).toBe(mockMessageData.timestamp);\n      expect(message.id).toBeDefined();\n      expect(typeof message.id).toBe('string');\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Log]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Log][0]).toBe(message);\n    });\n\n    it('should handle message without nodeID', () => {\n      const dataWithoutNodeID = {\n        message: 'Test message without nodeID',\n        timestamp: Date.now(),\n      };\n\n      const message = messageCenter.log(dataWithoutNodeID);\n      expect(message.nodeID).toBeUndefined();\n      expect(message.message).toBe(dataWithoutNodeID.message);\n    });\n  });\n\n  describe('info', () => {\n    it('should create and store info message', () => {\n      const message = messageCenter.info(mockMessageData);\n\n      expect(message.type).toBe(WorkflowMessageType.Info);\n      expect(message.nodeID).toBe(mockMessageData.nodeID);\n      expect(message.message).toBe(mockMessageData.message);\n      expect(message.timestamp).toBe(mockMessageData.timestamp);\n      expect(message.id).toBeDefined();\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Info]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Info][0]).toBe(message);\n    });\n  });\n\n  describe('debug', () => {\n    it('should create and store debug message', () => {\n      const message = messageCenter.debug(mockMessageData);\n\n      expect(message.type).toBe(WorkflowMessageType.Debug);\n      expect(message.nodeID).toBe(mockMessageData.nodeID);\n      expect(message.message).toBe(mockMessageData.message);\n      expect(message.timestamp).toBe(mockMessageData.timestamp);\n      expect(message.id).toBeDefined();\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Debug]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Debug][0]).toBe(message);\n    });\n  });\n\n  describe('error', () => {\n    it('should create and store error message', () => {\n      const message = messageCenter.error(mockMessageData);\n\n      expect(message.type).toBe(WorkflowMessageType.Error);\n      expect(message.nodeID).toBe(mockMessageData.nodeID);\n      expect(message.message).toBe(mockMessageData.message);\n      expect(message.timestamp).toBe(mockMessageData.timestamp);\n      expect(message.id).toBeDefined();\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Error]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Error][0]).toBe(message);\n    });\n  });\n\n  describe('warn', () => {\n    it('should create and store warning message', () => {\n      const message = messageCenter.warn(mockMessageData);\n\n      expect(message.type).toBe(WorkflowMessageType.Warn);\n      expect(message.nodeID).toBe(mockMessageData.nodeID);\n      expect(message.message).toBe(mockMessageData.message);\n      expect(message.timestamp).toBe(mockMessageData.timestamp);\n      expect(message.id).toBeDefined();\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Warn]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Warn][0]).toBe(message);\n    });\n  });\n\n  describe('export', () => {\n    beforeEach(() => {\n      // Add different types of messages\n      messageCenter.log({ message: 'Log message', timestamp: 1 });\n      messageCenter.info({ message: 'Info message', timestamp: 2 });\n      messageCenter.debug({ message: 'Debug message', timestamp: 3 });\n      messageCenter.error({ message: 'Error message', timestamp: 4 });\n      messageCenter.warn({ message: 'Warning message', timestamp: 5 });\n    });\n\n    it('should return all messages grouped by type', () => {\n      const messages = messageCenter.export();\n\n      expect(messages[WorkflowMessageType.Log]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Info]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Debug]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Error]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Warn]).toHaveLength(1);\n\n      // Verify that a copy is returned, not the original array\n      messages[WorkflowMessageType.Log].pop();\n      const newMessages = messageCenter.export();\n      expect(newMessages[WorkflowMessageType.Log]).toHaveLength(1);\n    });\n\n    it('should return correct log messages', () => {\n      const messages = messageCenter.export();\n      const logMessages = messages[WorkflowMessageType.Log];\n      expect(logMessages).toHaveLength(1);\n      expect(logMessages[0].type).toBe(WorkflowMessageType.Log);\n      expect(logMessages[0].message).toBe('Log message');\n    });\n\n    it('should return correct info messages', () => {\n      const messages = messageCenter.export();\n      const infoMessages = messages[WorkflowMessageType.Info];\n      expect(infoMessages).toHaveLength(1);\n      expect(infoMessages[0].type).toBe(WorkflowMessageType.Info);\n      expect(infoMessages[0].message).toBe('Info message');\n    });\n\n    it('should return correct debug messages', () => {\n      const messages = messageCenter.export();\n      const debugMessages = messages[WorkflowMessageType.Debug];\n      expect(debugMessages).toHaveLength(1);\n      expect(debugMessages[0].type).toBe(WorkflowMessageType.Debug);\n      expect(debugMessages[0].message).toBe('Debug message');\n    });\n\n    it('should return correct error messages', () => {\n      const messages = messageCenter.export();\n      const errorMessages = messages[WorkflowMessageType.Error];\n      expect(errorMessages).toHaveLength(1);\n      expect(errorMessages[0].type).toBe(WorkflowMessageType.Error);\n      expect(errorMessages[0].message).toBe('Error message');\n    });\n\n    it('should return correct warning messages', () => {\n      const messages = messageCenter.export();\n      const warnMessages = messages[WorkflowMessageType.Warn];\n      expect(warnMessages).toHaveLength(1);\n      expect(warnMessages[0].type).toBe(WorkflowMessageType.Warn);\n      expect(warnMessages[0].message).toBe('Warning message');\n    });\n\n    it('should return empty arrays when no messages exist', () => {\n      messageCenter.init(); // Clear all messages\n\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Log]).toEqual([]);\n      expect(messages[WorkflowMessageType.Info]).toEqual([]);\n      expect(messages[WorkflowMessageType.Debug]).toEqual([]);\n      expect(messages[WorkflowMessageType.Error]).toEqual([]);\n      expect(messages[WorkflowMessageType.Warn]).toEqual([]);\n    });\n\n    it('should maintain message order within each type', () => {\n      // Add multiple messages of the same type\n      messageCenter.log({ message: 'Log message 2', timestamp: 6 });\n      messageCenter.log({ message: 'Log message 3', timestamp: 7 });\n\n      const messages = messageCenter.export();\n      const logMessages = messages[WorkflowMessageType.Log];\n\n      expect(logMessages).toHaveLength(3);\n      expect(logMessages[0].message).toBe('Log message');\n      expect(logMessages[1].message).toBe('Log message 2');\n      expect(logMessages[2].message).toBe('Log message 3');\n    });\n  });\n\n  describe('message uniqueness', () => {\n    it('should generate unique IDs for each message', () => {\n      const message1 = messageCenter.log(mockMessageData);\n      const message2 = messageCenter.log(mockMessageData);\n      const message3 = messageCenter.info(mockMessageData);\n\n      expect(message1.id).not.toBe(message2.id);\n      expect(message1.id).not.toBe(message3.id);\n      expect(message2.id).not.toBe(message3.id);\n    });\n  });\n\n  describe('integration tests', () => {\n    it('should handle multiple operations correctly', () => {\n      // Add various types of messages\n      const logMsg = messageCenter.log({ message: 'Log 1', timestamp: 1 });\n      const infoMsg = messageCenter.info({ message: 'Info 1', timestamp: 2 });\n      const errorMsg = messageCenter.error({ message: 'Error 1', timestamp: 3 });\n\n      expect(logMsg.type).toBe(WorkflowMessageType.Log);\n      expect(infoMsg.type).toBe(WorkflowMessageType.Info);\n      expect(errorMsg.type).toBe(WorkflowMessageType.Error);\n\n      // Verify messages are grouped by type\n      const messages = messageCenter.export();\n      expect(messages[WorkflowMessageType.Log]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Info]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Error]).toHaveLength(1);\n      expect(messages[WorkflowMessageType.Debug]).toHaveLength(0);\n      expect(messages[WorkflowMessageType.Warn]).toHaveLength(0);\n\n      // Verify message content\n      expect(messages[WorkflowMessageType.Log][0]).toBe(logMsg);\n      expect(messages[WorkflowMessageType.Info][0]).toBe(infoMsg);\n      expect(messages[WorkflowMessageType.Error][0]).toBe(errorMsg);\n\n      // Reinitialize\n      messageCenter.init();\n      const emptyMessages = messageCenter.export();\n      expect(emptyMessages[WorkflowMessageType.Log]).toHaveLength(0);\n      expect(emptyMessages[WorkflowMessageType.Info]).toHaveLength(0);\n      expect(emptyMessages[WorkflowMessageType.Error]).toHaveLength(0);\n      expect(emptyMessages[WorkflowMessageType.Debug]).toHaveLength(0);\n      expect(emptyMessages[WorkflowMessageType.Warn]).toHaveLength(0);\n\n      // Add new message\n      const newMsg = messageCenter.debug({ message: 'Debug after init', timestamp: 4 });\n      const newMessages = messageCenter.export();\n      expect(newMessages[WorkflowMessageType.Debug]).toHaveLength(1);\n      expect(newMessages[WorkflowMessageType.Debug][0]).toBe(newMsg);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/message/message-center/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  IMessage,\n  IMessageCenter,\n  MessageData,\n  WorkflowMessages,\n  WorkflowMessageType,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeMessage } from '../message-value-object';\n\nexport class WorkflowRuntimeMessageCenter implements IMessageCenter {\n  private messages: WorkflowMessages;\n\n  public init(): void {\n    this.messages = {\n      [WorkflowMessageType.Log]: [],\n      [WorkflowMessageType.Info]: [],\n      [WorkflowMessageType.Debug]: [],\n      [WorkflowMessageType.Error]: [],\n      [WorkflowMessageType.Warn]: [],\n    };\n  }\n\n  public dispose(): void {}\n\n  public log(data: MessageData): IMessage {\n    const message = WorkflowRuntimeMessage.create({\n      type: WorkflowMessageType.Log,\n      ...data,\n    });\n    this.messages[WorkflowMessageType.Log].push(message);\n    return message;\n  }\n\n  public info(data: MessageData): IMessage {\n    const message = WorkflowRuntimeMessage.create({\n      type: WorkflowMessageType.Info,\n      ...data,\n    });\n    this.messages[WorkflowMessageType.Info].push(message);\n    return message;\n  }\n\n  public debug(data: MessageData): IMessage {\n    const message = WorkflowRuntimeMessage.create({\n      type: WorkflowMessageType.Debug,\n      ...data,\n    });\n    this.messages[WorkflowMessageType.Debug].push(message);\n    return message;\n  }\n\n  public error(data: MessageData): IMessage {\n    const message = WorkflowRuntimeMessage.create({\n      type: WorkflowMessageType.Error,\n      ...data,\n    });\n    this.messages[WorkflowMessageType.Error].push(message);\n    return message;\n  }\n\n  public warn(data: MessageData): IMessage {\n    const message = WorkflowRuntimeMessage.create({\n      type: WorkflowMessageType.Warn,\n      ...data,\n    });\n    this.messages[WorkflowMessageType.Warn].push(message);\n    return message;\n  }\n\n  public export(): WorkflowMessages {\n    return {\n      [WorkflowMessageType.Log]: this.messages[WorkflowMessageType.Log].slice(),\n      [WorkflowMessageType.Info]: this.messages[WorkflowMessageType.Info].slice(),\n      [WorkflowMessageType.Debug]: this.messages[WorkflowMessageType.Debug].slice(),\n      [WorkflowMessageType.Error]: this.messages[WorkflowMessageType.Error].slice(),\n      [WorkflowMessageType.Warn]: this.messages[WorkflowMessageType.Warn].slice(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/message/message-value-object/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMessage, MessageData, WorkflowMessageType } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport namespace WorkflowRuntimeMessage {\n  export const create = (\n    params: MessageData & {\n      type: WorkflowMessageType;\n    }\n  ): IMessage => {\n    const message = {\n      id: uuid(),\n      ...params,\n    };\n    if (!params.timestamp) {\n      message.timestamp = Date.now();\n    }\n    return message as IMessage;\n  };\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/report/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeReporter } from './reporter';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/report/report-value-object/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IReport, VOData } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport namespace WorkflowRuntimeReport {\n  export const create = (params: VOData<IReport>): IReport => ({\n    id: uuid(),\n    ...params,\n  });\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/report/reporter/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ISnapshotCenter,\n  IReporter,\n  IStatusCenter,\n  IIOCenter,\n  IReport,\n  NodeReport,\n  WorkflowReports,\n  IMessageCenter,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeReport } from '../report-value-object';\n\nexport class WorkflowRuntimeReporter implements IReporter {\n  constructor(\n    public readonly ioCenter: IIOCenter,\n    public readonly snapshotCenter: ISnapshotCenter,\n    public readonly statusCenter: IStatusCenter,\n    public readonly messageCenter: IMessageCenter\n  ) {}\n\n  public init(): void {}\n\n  public dispose(): void {}\n\n  public export(): IReport {\n    const report = WorkflowRuntimeReport.create({\n      inputs: this.ioCenter.inputs,\n      outputs: this.ioCenter.outputs,\n      workflowStatus: this.statusCenter.workflow.export(),\n      reports: this.nodeReports(),\n      messages: this.messageCenter.export(),\n    });\n    return report;\n  }\n\n  private nodeReports(): WorkflowReports {\n    const reports: WorkflowReports = {};\n    const statuses = this.statusCenter.exportNodeStatus();\n    const snapshots = this.snapshotCenter.export();\n    Object.keys(statuses).forEach((nodeID) => {\n      const status = statuses[nodeID];\n      const nodeSnapshots = snapshots[nodeID] || [];\n      const nodeReport: NodeReport = {\n        id: nodeID,\n        ...status,\n        snapshots: nodeSnapshots,\n      };\n      reports[nodeID] = nodeReport;\n    });\n    return reports;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/snapshot/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeSnapshotCenter } from './snapshot-center';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/snapshot/snapshot-center/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Snapshot, ISnapshotCenter, SnapshotData, ISnapshot } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\nimport { WorkflowRuntimeSnapshot } from '../snapshot-entity';\n\nexport class WorkflowRuntimeSnapshotCenter implements ISnapshotCenter {\n  public readonly id: string;\n\n  private snapshots: ISnapshot[];\n\n  constructor() {\n    this.id = uuid();\n  }\n\n  public create(snapshotData: Partial<SnapshotData>): ISnapshot {\n    const snapshot = WorkflowRuntimeSnapshot.create(snapshotData);\n    this.snapshots.push(snapshot);\n    return snapshot;\n  }\n\n  public init(): void {\n    this.snapshots = [];\n  }\n\n  public dispose(): void {\n    // because the data is not persisted, do not clear the execution result\n  }\n\n  public exportAll(): Snapshot[] {\n    return this.snapshots.slice().map((snapshot) => snapshot.export());\n  }\n\n  public export(): Record<string, Snapshot[]> {\n    const result: Record<string, Snapshot[]> = {};\n    this.exportAll().forEach((snapshot) => {\n      if (result[snapshot.nodeID]) {\n        result[snapshot.nodeID].push(snapshot);\n      } else {\n        result[snapshot.nodeID] = [snapshot];\n      }\n    });\n    return result;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/snapshot/snapshot-entity/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ISnapshot, Snapshot, SnapshotData } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport class WorkflowRuntimeSnapshot implements ISnapshot {\n  public readonly id: string;\n\n  public readonly data: Partial<SnapshotData>;\n\n  public constructor(data: Partial<SnapshotData>) {\n    this.id = uuid();\n    this.data = data;\n  }\n\n  public update(data: Partial<SnapshotData>): void {\n    Object.assign(this.data, data);\n  }\n\n  public validate(): boolean {\n    const required = ['nodeID', 'inputs', 'outputs', 'data'] as (keyof SnapshotData)[];\n    return required.every((key) => this.data[key] !== undefined);\n  }\n\n  public export(): Snapshot {\n    const snapshot: Snapshot = {\n      id: this.id,\n      ...this.data,\n    } as Snapshot;\n    return snapshot;\n  }\n\n  public static create(params: Partial<SnapshotData>): ISnapshot {\n    return new WorkflowRuntimeSnapshot(params);\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/state/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport {\n  IState,\n  IFlowValue,\n  IFlowRefValue,\n  IVariableParseResult,\n  INode,\n  WorkflowInputs,\n  WorkflowOutputs,\n  IVariableStore,\n  WorkflowVariableType,\n  IFlowTemplateValue,\n  IJsonSchema,\n  WorkflowSchema,\n} from '@flowgram.ai/runtime-interface';\n\nimport { uuid, WorkflowRuntimeType } from '@infra/utils';\n\nexport class WorkflowRuntimeState implements IState {\n  public readonly id: string;\n\n  private executedNodes: Set<string>;\n\n  constructor(public readonly variableStore: IVariableStore) {\n    this.id = uuid();\n  }\n\n  public init(schema?: WorkflowSchema): void {\n    this.setGlobalVariable(schema?.globalVariable);\n    this.executedNodes = new Set();\n  }\n\n  public dispose(): void {\n    this.executedNodes.clear();\n  }\n\n  public getNodeInputs(node: INode): WorkflowInputs {\n    const inputsDeclare = node.declare.inputs;\n    const inputsValues = node.declare.inputsValues;\n    return this.parseInputs({\n      values: inputsValues,\n      declare: inputsDeclare,\n    });\n  }\n\n  public setNodeOutputs(params: { node: INode; outputs: WorkflowOutputs }): void {\n    const { node, outputs } = params;\n    const outputsDeclare = node.declare.outputs as IJsonSchema<'object'>;\n    if (outputsDeclare?.type !== 'object' || !outputsDeclare.properties) {\n      return;\n    }\n    Object.entries(outputsDeclare.properties).forEach(([key, typeInfo]) => {\n      if (!key || !typeInfo) {\n        return;\n      }\n      const type = typeInfo.type as WorkflowVariableType;\n      const itemsType = typeInfo.items?.type as WorkflowVariableType;\n      const defaultValue = this.parseJSONContent(typeInfo.default, type);\n      const value = outputs[key] ?? defaultValue;\n      // create variable\n      this.variableStore.setVariable({\n        nodeID: node.id,\n        key,\n        value,\n        type,\n        itemsType,\n      });\n    });\n  }\n\n  public parseInputs(params: {\n    values?: Record<string, IFlowValue>;\n    declare?: IJsonSchema;\n  }): WorkflowInputs {\n    const { values, declare } = params;\n    if (!declare || !values) {\n      return {};\n    }\n    return Object.entries(values).reduce((prev, [key, flowValue]) => {\n      const typeInfo = declare.properties?.[key];\n      if (!typeInfo) {\n        return prev;\n      }\n      const declareType = typeInfo.type as WorkflowVariableType;\n      // get value\n      const result = this.parseFlowValue({ flowValue, declareType });\n      if (!result) {\n        return prev;\n      }\n      const { value, type } = result;\n      if (!WorkflowRuntimeType.isTypeEqual(type, declareType)) {\n        return prev;\n      }\n      prev[key] = value;\n      return prev;\n    }, {} as WorkflowInputs);\n  }\n\n  public parseRef<T = unknown>(ref: IFlowRefValue): IVariableParseResult<T> | null {\n    if (ref?.type !== 'ref') {\n      throw new Error(`Invalid ref value: ${ref}`);\n    }\n    if (!ref.content || ref.content.length < 2) {\n      return null;\n    }\n    const [nodeID, variableKey, ...variablePath] = ref.content;\n    const result = this.variableStore.getValue<T>({\n      nodeID,\n      variableKey,\n      variablePath,\n    });\n    if (!result) {\n      return null;\n    }\n    return result;\n  }\n\n  public parseTemplate(template: IFlowTemplateValue): IVariableParseResult<string> | null {\n    if (template?.type !== 'template') {\n      throw new Error(`Invalid template value: ${template}`);\n    }\n    if (!template.content) {\n      return null;\n    }\n    const parsedValue = template.content.replace(\n      /\\{\\{([^\\}]+)\\}\\}/g,\n      (match: string, pattern: string): string => {\n        // 将路径分割成数组，如 'start_0.work.role' => ['start_0', 'work', 'role']\n        const ref = pattern.trim().split('.');\n\n        const variable = this.parseRef<string>({\n          type: 'ref',\n          content: ref,\n        });\n\n        if (!variable) {\n          return '';\n        }\n\n        return variable.value;\n      }\n    );\n    return {\n      type: WorkflowVariableType.String,\n      value: parsedValue,\n    };\n  }\n\n  public parseFlowValue<T = unknown>(params: {\n    flowValue: IFlowValue;\n    declareType: WorkflowVariableType;\n  }): IVariableParseResult<T> | null {\n    const { flowValue, declareType } = params;\n    if (!flowValue?.type) {\n      throw new Error(`Invalid flow value type: ${(flowValue as any).type}`);\n    }\n    // constant\n    if (flowValue.type === 'constant') {\n      const value = this.parseJSONContent<T>(flowValue.content, declareType);\n      const type = declareType ?? WorkflowRuntimeType.getWorkflowType(value);\n      if (isNil(value) || !type) {\n        return null;\n      }\n      return {\n        value,\n        type,\n      };\n    }\n    // ref\n    if (flowValue.type === 'ref') {\n      return this.parseRef<T>(flowValue);\n    }\n    // template\n    if (flowValue.type === 'template') {\n      return this.parseTemplate(flowValue) as IVariableParseResult<T> | null;\n    }\n    // unknown type\n    throw new Error(`Unknown flow value type: ${(flowValue as any).type}`);\n  }\n\n  public isExecutedNode(node: INode): boolean {\n    return this.executedNodes.has(node.id);\n  }\n\n  public addExecutedNode(node: INode): void {\n    this.executedNodes.add(node.id);\n  }\n\n  private parseJSONContent<T = unknown>(\n    jsonContent: string | unknown,\n    declareType: WorkflowVariableType\n  ): T {\n    const JSONTypes = [\n      WorkflowVariableType.Object,\n      WorkflowVariableType.Array,\n      WorkflowVariableType.Map,\n    ];\n    if (declareType && JSONTypes.includes(declareType) && typeof jsonContent === 'string') {\n      try {\n        return JSON.parse(jsonContent) as T;\n      } catch (e) {\n        return jsonContent as T;\n      }\n    }\n    return jsonContent as T;\n  }\n\n  private setGlobalVariable(globalVariableDeclare: IJsonSchema | undefined): void {\n    if (globalVariableDeclare?.type !== 'object' || !globalVariableDeclare.properties) {\n      return;\n    }\n    Object.entries(globalVariableDeclare.properties).forEach(([key, typeInfo]) => {\n      if (!key || !typeInfo) {\n        return;\n      }\n      const type = typeInfo.type as WorkflowVariableType;\n      const itemsType = typeInfo.items?.type as WorkflowVariableType;\n      const defaultValue = this.parseJSONContent(typeInfo.default, type);\n      // create variable\n      this.variableStore.setVariable({\n        nodeID: 'global',\n        key,\n        value: defaultValue,\n        type,\n        itemsType,\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/status/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeStatusCenter } from './status-center';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/status/status-center/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IStatus, IStatusCenter, StatusData, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeStatus } from '../status-entity';\n\nexport class WorkflowRuntimeStatusCenter implements IStatusCenter {\n  private _workflowStatus: IStatus;\n\n  private _nodeStatus: Map<string, IStatus>;\n\n  public startTime: number;\n\n  public endTime?: number;\n\n  public init(): void {\n    this._workflowStatus = WorkflowRuntimeStatus.create();\n    this._nodeStatus = new Map();\n  }\n\n  public dispose(): void {\n    // because the data is not persisted, do not clear the execution result\n  }\n\n  public get workflow(): IStatus {\n    return this._workflowStatus;\n  }\n\n  public get workflowStatus(): IStatus {\n    return this._workflowStatus;\n  }\n\n  public nodeStatus(nodeID: string): IStatus {\n    if (!this._nodeStatus.has(nodeID)) {\n      this._nodeStatus.set(nodeID, WorkflowRuntimeStatus.create());\n    }\n    const status = this._nodeStatus.get(nodeID)!;\n    return status;\n  }\n\n  public getStatusNodeIDs(status: WorkflowStatus): string[] {\n    return Array.from(this._nodeStatus.entries())\n      .filter(([, nodeStatus]) => nodeStatus.status === status)\n      .map(([nodeID]) => nodeID);\n  }\n\n  public exportNodeStatus(): Record<string, StatusData> {\n    return Object.fromEntries(\n      Array.from(this._nodeStatus.entries()).map(([nodeID, status]) => [nodeID, status.export()])\n    );\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/status/status-entity/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IStatus, StatusData, WorkflowStatus } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport class WorkflowRuntimeStatus implements IStatus {\n  public readonly id: string;\n\n  private _status: WorkflowStatus;\n\n  private _startTime: number;\n\n  private _endTime?: number;\n\n  constructor() {\n    this.id = uuid();\n    this._status = WorkflowStatus.Pending;\n  }\n\n  public get status(): WorkflowStatus {\n    return this._status;\n  }\n\n  public get terminated(): boolean {\n    return [WorkflowStatus.Succeeded, WorkflowStatus.Failed, WorkflowStatus.Cancelled].includes(\n      this.status\n    );\n  }\n\n  public get startTime(): number {\n    return this._startTime;\n  }\n\n  public get endTime(): number | undefined {\n    return this._endTime;\n  }\n\n  public get timeCost(): number {\n    if (!this.startTime) {\n      return 0;\n    }\n    if (this.endTime) {\n      return this.endTime - this.startTime;\n    }\n    return Date.now() - this.startTime;\n  }\n\n  public process(): void {\n    this._status = WorkflowStatus.Processing;\n    this._startTime = Date.now();\n    this._endTime = undefined;\n  }\n\n  public success(): void {\n    if (this.terminated) {\n      return;\n    }\n    this._status = WorkflowStatus.Succeeded;\n    this._endTime = Date.now();\n  }\n\n  public fail(): void {\n    if (this.terminated) {\n      return;\n    }\n    this._status = WorkflowStatus.Failed;\n    this._endTime = Date.now();\n  }\n\n  public cancel(): void {\n    if (this.terminated) {\n      return;\n    }\n    this._status = WorkflowStatus.Cancelled;\n    this._endTime = Date.now();\n  }\n\n  public export(): StatusData {\n    return {\n      status: this.status,\n      terminated: this.terminated,\n      startTime: this.startTime,\n      endTime: this.endTime,\n      timeCost: this.timeCost,\n    };\n  }\n\n  public static create(): WorkflowRuntimeStatus {\n    const status = new WorkflowRuntimeStatus();\n    return status;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/task/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  IContext,\n  ITask,\n  TaskParams,\n  WorkflowOutputs,\n  WorkflowStatus,\n} from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport class WorkflowRuntimeTask implements ITask {\n  public readonly id: string;\n\n  public readonly processing: Promise<WorkflowOutputs>;\n\n  public readonly context: IContext;\n\n  constructor(params: TaskParams) {\n    this.id = uuid();\n    this.context = params.context;\n    this.processing = params.processing;\n  }\n\n  public cancel(): void {\n    this.context.statusCenter.workflow.cancel();\n    const cancelNodeIDs = this.context.statusCenter.getStatusNodeIDs(WorkflowStatus.Processing);\n    cancelNodeIDs.forEach((nodeID) => {\n      this.context.statusCenter.nodeStatus(nodeID).cancel();\n    });\n  }\n\n  public static create(params: TaskParams): WorkflowRuntimeTask {\n    return new WorkflowRuntimeTask(params);\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { WorkflowSchema, FlowGramNode } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeValidation } from './index';\n\ndescribe('WorkflowRuntimeValidation Integration', () => {\n  let validation: WorkflowRuntimeValidation;\n\n  beforeEach(() => {\n    validation = new WorkflowRuntimeValidation();\n  });\n\n  const createMockNode = (id: string, type: string = 'test') => ({\n    id,\n    type,\n    meta: { position: { x: 0, y: 0 } },\n    data: {},\n  });\n\n  const createMockEdge = (sourceNodeID: string, targetNodeID: string) => ({\n    sourceNodeID,\n    targetNodeID,\n  });\n\n  const createMockStartNode = (id: string) => ({\n    id,\n    type: FlowGramNode.Start,\n    meta: { position: { x: 0, y: 0 } },\n    data: {\n      outputs: {\n        type: 'object',\n        properties: {},\n      },\n    },\n  });\n\n  const createMockEndNode = (id: string) => ({\n    id,\n    type: FlowGramNode.End,\n    meta: { position: { x: 0, y: 0 } },\n    data: {},\n  });\n\n  it('should pass validation for valid acyclic workflow', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockStartNode('start'),\n        createMockNode('middle'),\n        { id: 'end', type: FlowGramNode.End, meta: { position: { x: 0, y: 0 } }, data: {} },\n      ],\n      edges: [createMockEdge('start', 'middle'), createMockEdge('middle', 'end')],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(true);\n  });\n\n  it('should fail validation for workflow with cycles', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockStartNode('start'),\n        createMockNode('A'),\n        createMockNode('B'),\n        { id: 'end', type: FlowGramNode.End, meta: { position: { x: 0, y: 0 } }, data: {} },\n      ],\n      edges: [\n        createMockEdge('start', 'A'),\n        createMockEdge('A', 'B'),\n        createMockEdge('B', 'A'), // Creates a cycle\n        createMockEdge('B', 'end'),\n      ],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema contains a cycle, which is not allowed');\n  });\n\n  it('should fail validation for workflow with non-existent edge targets', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockStartNode('start'),\n        { id: 'end', type: FlowGramNode.End, meta: { position: { x: 0, y: 0 } }, data: {} },\n      ],\n      edges: [\n        createMockEdge('start', 'nonexistent'), // Non-existent target\n      ],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema edge target node \"nonexistent\" not exist');\n  });\n\n  it('should fail validation for workflow without start node', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('middle'), createMockEndNode('end')],\n      edges: [createMockEdge('middle', 'end')],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema must have a start node');\n  });\n\n  it('should fail validation for workflow without end node', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockStartNode('start'), createMockNode('middle')],\n      edges: [createMockEdge('start', 'middle')],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema must have an end node');\n  });\n\n  it('should fail validation for workflow with multiple start nodes', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockStartNode('start1'),\n        createMockStartNode('start2'),\n        createMockEndNode('end'),\n      ],\n      edges: [createMockEdge('start1', 'end'), createMockEdge('start2', 'end')],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema must have only one start node');\n  });\n\n  it('should handle complex workflow with nested blocks and cycles', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockStartNode('start'),\n        {\n          id: 'container',\n          type: 'container',\n          meta: { position: { x: 0, y: 0 } },\n          data: {},\n          blocks: [createMockNode('block1'), createMockNode('block2')],\n          edges: [\n            createMockEdge('block1', 'block2'),\n            createMockEdge('block2', 'block1'), // Cycle in nested blocks\n          ],\n        },\n        createMockEndNode('end'),\n      ],\n      edges: [createMockEdge('start', 'container'), createMockEdge('container', 'end')],\n    };\n\n    const result = validation.invoke({ schema, inputs: {} });\n    expect(result.valid).toBe(false);\n    expect(result.errors).toContain('Workflow schema contains a cycle, which is not allowed');\n  });\n\n  // Schema format validation tests\n  describe('Schema Format Validation', () => {\n    it('should fail validation for invalid schema structure', () => {\n      const invalidSchema = null as any;\n      const result = validation.invoke({ schema: invalidSchema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must be a valid object');\n    });\n\n    it('should fail validation for schema without nodes array', () => {\n      const invalidSchema = { edges: [] } as any;\n      const result = validation.invoke({ schema: invalidSchema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must have a valid nodes array');\n    });\n\n    it('should fail validation for schema without edges array', () => {\n      const invalidSchema = { nodes: [] } as any;\n      const result = validation.invoke({ schema: invalidSchema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must have a valid edges array');\n    });\n\n    it('should fail validation for node without required fields', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          {\n            // Missing id field\n            type: 'test',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n          } as any,\n        ],\n        edges: [],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('nodes[0].id must be a non-empty string');\n    });\n\n    it('should fail validation for edge without required fields', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockStartNode('start'), createMockEndNode('end')],\n        edges: [\n          {\n            // Missing targetNodeID\n            sourceNodeID: 'start',\n          } as any,\n        ],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('edges[0].targetNodeID must be a non-empty string');\n    });\n  });\n\n  // Input validation tests\n  describe('Input Validation', () => {\n    const createSchemaWithInputs = () => ({\n      nodes: [\n        {\n          id: 'start',\n          type: FlowGramNode.Start,\n          meta: { position: { x: 0, y: 0 } },\n          data: {\n            outputs: {\n              type: 'object',\n              properties: {\n                name: { type: 'string' },\n                age: { type: 'number' },\n                active: { type: 'boolean' },\n              },\n              required: ['name', 'age'],\n            },\n          },\n        },\n        createMockEndNode('end'),\n      ],\n      edges: [createMockEdge('start', 'end')],\n    });\n\n    it('should pass validation with valid inputs', () => {\n      const schema = createSchemaWithInputs();\n      const inputs = {\n        name: 'John Doe',\n        age: 30,\n        active: true,\n      };\n      const result = validation.invoke({ schema, inputs });\n      expect(result.valid).toBe(true);\n    });\n\n    it('should fail validation with missing required inputs', () => {\n      const schema = createSchemaWithInputs();\n      const inputs = {\n        name: 'John Doe',\n        // Missing required 'age' field\n      };\n      const result = validation.invoke({ schema, inputs });\n      expect(result.valid).toBe(false);\n      expect(result.errors?.[0]).toContain('JSON Schema validation failed');\n    });\n\n    it('should fail validation with wrong input types', () => {\n      const schema = createSchemaWithInputs();\n      const inputs = {\n        name: 'John Doe',\n        age: 'thirty', // Should be number\n        active: true,\n      };\n      const result = validation.invoke({ schema, inputs });\n      expect(result.valid).toBe(false);\n      expect(result.errors?.[0]).toContain('JSON Schema validation failed');\n    });\n  });\n\n  // Edge cases and boundary conditions\n  describe('Edge Cases', () => {\n    it('should handle empty workflow', () => {\n      const schema: WorkflowSchema = {\n        nodes: [],\n        edges: [],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toBeDefined();\n      expect(result.errors!.length).toBeGreaterThan(0);\n      // Empty workflow should trigger start/end node validation errors\n      const errorMessages = result.errors!.join(' ');\n      expect(errorMessages).toContain('start node');\n    });\n\n    it('should handle workflow with only start node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockStartNode('start')],\n        edges: [],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must have an end node');\n    });\n\n    it('should handle workflow with disconnected nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockStartNode('start'), createMockNode('isolated'), createMockEndNode('end')],\n        edges: [createMockEdge('start', 'end')], // 'isolated' node is not connected\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(true); // This should pass as disconnected nodes are allowed\n    });\n\n    it('should handle workflow with self-referencing edge', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockStartNode('start'), createMockNode('self'), createMockEndNode('end')],\n        edges: [\n          createMockEdge('start', 'self'),\n          createMockEdge('self', 'self'), // Self-referencing edge\n          createMockEdge('self', 'end'),\n        ],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema contains a cycle, which is not allowed');\n    });\n\n    it('should handle workflow with multiple end nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockStartNode('start'), createMockEndNode('end1'), createMockEndNode('end2')],\n        edges: [createMockEdge('start', 'end1'), createMockEdge('start', 'end2')],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must have only one end node');\n    });\n  });\n\n  // Multiple error scenarios\n  describe('Multiple Error Scenarios', () => {\n    it('should collect multiple validation errors', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockStartNode('start1'),\n          createMockStartNode('start2'), // Multiple start nodes\n          createMockNode('A'),\n          createMockNode('B'),\n        ],\n        edges: [\n          createMockEdge('start1', 'A'),\n          createMockEdge('A', 'B'),\n          createMockEdge('B', 'A'), // Cycle\n          createMockEdge('A', 'nonexistent'), // Non-existent target\n        ],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toBeDefined();\n      expect(result.errors!.length).toBeGreaterThan(1);\n      // Check that multiple errors are collected\n      expect(result.errors!.some((error) => error.includes('cycle'))).toBe(true);\n      expect(result.errors!.some((error) => error.includes('target node'))).toBe(true);\n    });\n\n    it('should handle schema format errors before other validations', () => {\n      const invalidSchema = {\n        nodes: 'invalid', // Should be array\n        edges: [],\n      } as any;\n      const result = validation.invoke({ schema: invalidSchema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema must have a valid nodes array');\n    });\n  });\n\n  // Complex nested scenarios\n  describe('Complex Nested Scenarios', () => {\n    it('should validate deeply nested blocks', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockStartNode('start'),\n          {\n            id: 'container1',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              {\n                id: 'block-start',\n                type: FlowGramNode.BlockStart,\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n              },\n              {\n                id: 'container2',\n                type: 'container',\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n                blocks: [\n                  {\n                    id: 'nested-block-start',\n                    type: FlowGramNode.BlockStart,\n                    meta: { position: { x: 0, y: 0 } },\n                    data: {},\n                  },\n                  createMockNode('deep1'),\n                  createMockNode('deep2'),\n                  {\n                    id: 'nested-block-end',\n                    type: FlowGramNode.BlockEnd,\n                    meta: { position: { x: 0, y: 0 } },\n                    data: {},\n                  },\n                ],\n                edges: [\n                  createMockEdge('nested-block-start', 'deep1'),\n                  createMockEdge('deep1', 'deep2'),\n                  createMockEdge('deep2', 'nested-block-end'),\n                ],\n              },\n              {\n                id: 'block-end',\n                type: FlowGramNode.BlockEnd,\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n              },\n            ],\n            edges: [\n              createMockEdge('block-start', 'container2'),\n              createMockEdge('container2', 'block-end'),\n            ],\n          },\n          createMockEndNode('end'),\n        ],\n        edges: [createMockEdge('start', 'container1'), createMockEdge('container1', 'end')],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(true);\n    });\n\n    it('should detect cycles in deeply nested blocks', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockStartNode('start'),\n          {\n            id: 'container1',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              {\n                id: 'container2',\n                type: 'container',\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n                blocks: [createMockNode('deep1'), createMockNode('deep2')],\n                edges: [\n                  createMockEdge('deep1', 'deep2'),\n                  createMockEdge('deep2', 'deep1'), // Cycle in deep nested block\n                ],\n              },\n            ],\n            edges: [],\n          },\n          createMockEndNode('end'),\n        ],\n        edges: [createMockEdge('start', 'container1'), createMockEdge('container1', 'end')],\n      };\n      const result = validation.invoke({ schema, inputs: {} });\n      expect(result.valid).toBe(false);\n      expect(result.errors).toContain('Workflow schema contains a cycle, which is not allowed');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowSchema,\n  IValidation,\n  ValidationResult,\n  InvokeParams,\n  IJsonSchema,\n  FlowGramNode,\n} from '@flowgram.ai/runtime-interface';\n\nimport { JSONSchemaValidator } from '@infra/index';\nimport { cycleDetection, edgeSourceTargetExist, startEndNode, schemaFormat } from './validators';\n\nexport class WorkflowRuntimeValidation implements IValidation {\n  public invoke(params: InvokeParams): ValidationResult {\n    const { schema, inputs } = params;\n    const schemaValidationResult = this.schema(schema);\n    if (!schemaValidationResult.valid) {\n      return schemaValidationResult;\n    }\n    const inputsValidationResult = this.inputs(this.getWorkflowInputsDeclare(schema), inputs);\n    if (!inputsValidationResult.valid) {\n      return inputsValidationResult;\n    }\n    return {\n      valid: true,\n    };\n  }\n\n  private schema(schema: WorkflowSchema): ValidationResult {\n    const errors: string[] = [];\n\n    // Run all validations concurrently and collect errors\n    const validations = [\n      () => schemaFormat(schema),\n      () => cycleDetection(schema),\n      () => edgeSourceTargetExist(schema),\n      () => startEndNode(schema),\n    ];\n\n    // Execute all validations and collect any errors\n    validations.forEach((validation) => {\n      try {\n        validation();\n      } catch (error) {\n        errors.push(error instanceof Error ? error.message : String(error));\n      }\n    });\n\n    return {\n      valid: errors.length === 0,\n      errors: errors.length > 0 ? errors : undefined,\n    };\n  }\n\n  private inputs(inputsSchema: IJsonSchema, inputs: Record<string, unknown>): ValidationResult {\n    const { result, errorMessage } = JSONSchemaValidator({\n      schema: inputsSchema,\n      value: inputs,\n    });\n    if (!result) {\n      const error = `JSON Schema validation failed: ${errorMessage}`;\n      return {\n        valid: false,\n        errors: [error],\n      };\n    }\n    return {\n      valid: true,\n    };\n  }\n\n  private getWorkflowInputsDeclare(schema: WorkflowSchema): IJsonSchema {\n    const startNode = schema.nodes.find((node) => node.type === FlowGramNode.Start)!;\n    if (!startNode) {\n      throw new Error('Workflow schema must have a start node');\n    }\n    return startNode.data.outputs;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/cycle-detection.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nimport { cycleDetection } from './cycle-detection';\n\ndescribe('cycleDetection', () => {\n  const createMockNode = (id: string, type: string = 'test') => ({\n    id,\n    type,\n    meta: { position: { x: 0, y: 0 } },\n    data: {},\n  });\n\n  const createMockEdge = (sourceNodeID: string, targetNodeID: string) => ({\n    sourceNodeID,\n    targetNodeID,\n  });\n\n  it('should not throw error for acyclic graph', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C')],\n      edges: [createMockEdge('A', 'B'), createMockEdge('B', 'C')],\n    };\n\n    expect(() => cycleDetection(schema)).not.toThrow();\n  });\n\n  it('should throw error for simple cycle', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C')],\n      edges: [\n        createMockEdge('A', 'B'),\n        createMockEdge('B', 'C'),\n        createMockEdge('C', 'A'), // Creates a cycle\n      ],\n    };\n\n    expect(() => cycleDetection(schema)).toThrow(\n      'Workflow schema contains a cycle, which is not allowed'\n    );\n  });\n\n  it('should throw error for self-loop', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B')],\n      edges: [\n        createMockEdge('A', 'B'),\n        createMockEdge('B', 'B'), // Self-loop\n      ],\n    };\n\n    expect(() => cycleDetection(schema)).toThrow(\n      'Workflow schema contains a cycle, which is not allowed'\n    );\n  });\n\n  it('should handle disconnected components without cycles', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C'), createMockNode('D')],\n      edges: [createMockEdge('A', 'B'), createMockEdge('C', 'D')],\n    };\n\n    expect(() => cycleDetection(schema)).not.toThrow();\n  });\n\n  it('should detect cycle in disconnected components', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C'), createMockNode('D')],\n      edges: [\n        createMockEdge('A', 'B'),\n        createMockEdge('C', 'D'),\n        createMockEdge('D', 'C'), // Creates a cycle in second component\n      ],\n    };\n\n    expect(() => cycleDetection(schema)).toThrow(\n      'Workflow schema contains a cycle, which is not allowed'\n    );\n  });\n\n  it('should handle empty schema', () => {\n    const schema: WorkflowSchema = {\n      nodes: [],\n      edges: [],\n    };\n\n    expect(() => cycleDetection(schema)).not.toThrow();\n  });\n\n  it('should handle schema with nodes but no edges', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C')],\n      edges: [],\n    };\n\n    expect(() => cycleDetection(schema)).not.toThrow();\n  });\n\n  it('should detect cycles in nested blocks', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('A'),\n        {\n          id: 'B',\n          type: 'container',\n          meta: { position: { x: 0, y: 0 } },\n          data: {},\n          blocks: [createMockNode('B1'), createMockNode('B2')],\n          edges: [\n            createMockEdge('B1', 'B2'),\n            createMockEdge('B2', 'B1'), // Creates a cycle in nested blocks\n          ],\n        },\n      ],\n      edges: [createMockEdge('A', 'B')],\n    };\n\n    expect(() => cycleDetection(schema)).toThrow(\n      'Workflow schema contains a cycle, which is not allowed'\n    );\n  });\n\n  it('should handle nested blocks without cycles', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('A'),\n        {\n          id: 'B',\n          type: 'container',\n          meta: { position: { x: 0, y: 0 } },\n          data: {},\n          blocks: [createMockNode('B1'), createMockNode('B2'), createMockNode('B3')],\n          edges: [createMockEdge('B1', 'B2'), createMockEdge('B2', 'B3')],\n        },\n      ],\n      edges: [createMockEdge('A', 'B')],\n    };\n\n    expect(() => cycleDetection(schema)).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/cycle-detection.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const cycleDetection = (schema: WorkflowSchema) => {\n  const { nodes, edges } = schema;\n\n  // Build adjacency list for the graph\n  const adjacencyList = new Map<string, string[]>();\n  const nodeIds = new Set(nodes.map((node) => node.id));\n\n  // Initialize adjacency list\n  nodeIds.forEach((nodeId) => {\n    adjacencyList.set(nodeId, []);\n  });\n\n  // Populate adjacency list with edges\n  edges.forEach((edge) => {\n    const sourceList = adjacencyList.get(edge.sourceNodeID);\n    if (sourceList) {\n      sourceList.push(edge.targetNodeID);\n    }\n  });\n\n  enum NodeStatus {\n    Unvisited,\n    Visiting,\n    Visited,\n  }\n\n  const nodeStatusMap = new Map<string, NodeStatus>();\n\n  // Initialize all nodes as WHITE\n  nodeIds.forEach((nodeId) => {\n    nodeStatusMap.set(nodeId, NodeStatus.Unvisited);\n  });\n\n  const detectCycleFromNode = (nodeId: string): boolean => {\n    nodeStatusMap.set(nodeId, NodeStatus.Visiting);\n\n    const neighbors = adjacencyList.get(nodeId) || [];\n    for (const neighbor of neighbors) {\n      const neighborColor = nodeStatusMap.get(neighbor);\n\n      if (neighborColor === NodeStatus.Visiting) {\n        // Back edge found - cycle detected\n        return true;\n      }\n\n      if (neighborColor === NodeStatus.Unvisited && detectCycleFromNode(neighbor)) {\n        return true;\n      }\n    }\n\n    nodeStatusMap.set(nodeId, NodeStatus.Visited);\n    return false;\n  };\n\n  // Check for cycles starting from each unvisited node\n  for (const nodeId of nodeIds) {\n    if (nodeStatusMap.get(nodeId) === NodeStatus.Unvisited) {\n      if (detectCycleFromNode(nodeId)) {\n        throw new Error('Workflow schema contains a cycle, which is not allowed');\n      }\n    }\n  }\n\n  // Recursively check cycles in nested blocks\n  nodes.forEach((node) => {\n    if (node.blocks) {\n      cycleDetection({\n        nodes: node.blocks,\n        edges: node.edges ?? [],\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/edge-source-target-exist.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nimport { edgeSourceTargetExist } from './edge-source-target-exist';\n\ndescribe('edgeSourceTargetExist', () => {\n  const createMockNode = (id: string, type: string = 'test') => ({\n    id,\n    type,\n    meta: { position: { x: 0, y: 0 } },\n    data: {},\n  });\n\n  const createMockEdge = (sourceNodeID: string, targetNodeID: string) => ({\n    sourceNodeID,\n    targetNodeID,\n  });\n\n  it('should not throw error when all edge nodes exist', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B'), createMockNode('C')],\n      edges: [createMockEdge('A', 'B'), createMockEdge('B', 'C')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).not.toThrow();\n  });\n\n  it('should not throw error for empty edges', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B')],\n      edges: [],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).not.toThrow();\n  });\n\n  it('should throw error when source node does not exist', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B')],\n      edges: [createMockEdge('C', 'A')], // 'C' does not exist\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).toThrow(\n      'Workflow schema edge source node \"C\" not exist'\n    );\n  });\n\n  it('should throw error when target node does not exist', () => {\n    const schema: WorkflowSchema = {\n      nodes: [createMockNode('A'), createMockNode('B')],\n      edges: [createMockEdge('A', 'C')], // 'C' does not exist\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).toThrow(\n      'Workflow schema edge target node \"C\" not exist'\n    );\n  });\n\n  it('should validate edges in nested blocks', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('root'),\n        {\n          ...createMockNode('parent', 'container'),\n          blocks: [createMockNode('child1'), createMockNode('child2')],\n          edges: [createMockEdge('child1', 'child2')],\n        },\n      ],\n      edges: [createMockEdge('root', 'parent')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).not.toThrow();\n  });\n\n  it('should throw error when nested edge source node does not exist', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('root'),\n        {\n          ...createMockNode('parent', 'container'),\n          blocks: [createMockNode('child1'), createMockNode('child2')],\n          edges: [createMockEdge('nonexistent', 'child2')], // 'nonexistent' does not exist in blocks\n        },\n      ],\n      edges: [createMockEdge('root', 'parent')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).toThrow(\n      'Workflow schema edge source node \"nonexistent\" not exist'\n    );\n  });\n\n  it('should throw error when nested edge target node does not exist', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('root'),\n        {\n          ...createMockNode('parent', 'container'),\n          blocks: [createMockNode('child1'), createMockNode('child2')],\n          edges: [createMockEdge('child1', 'nonexistent')], // 'nonexistent' does not exist in blocks\n        },\n      ],\n      edges: [createMockEdge('root', 'parent')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).toThrow(\n      'Workflow schema edge target node \"nonexistent\" not exist'\n    );\n  });\n\n  it('should handle deeply nested structures', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('root'),\n        {\n          ...createMockNode('level1', 'container'),\n          blocks: [\n            createMockNode('child1'),\n            {\n              ...createMockNode('level2', 'container'),\n              blocks: [createMockNode('grandchild1'), createMockNode('grandchild2')],\n              edges: [createMockEdge('grandchild1', 'grandchild2')],\n            },\n          ],\n          edges: [createMockEdge('child1', 'level2')],\n        },\n      ],\n      edges: [createMockEdge('root', 'level1')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).not.toThrow();\n  });\n\n  it('should handle nodes without blocks or edges', () => {\n    const schema: WorkflowSchema = {\n      nodes: [\n        createMockNode('A'),\n        createMockNode('B'),\n        {\n          ...createMockNode('C', 'container'),\n          // No blocks or edges defined\n        },\n      ],\n      edges: [createMockEdge('A', 'B'), createMockEdge('B', 'C')],\n    };\n\n    expect(() => edgeSourceTargetExist(schema)).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/edge-source-target-exist.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nexport const edgeSourceTargetExist = (schema: WorkflowSchema) => {\n  const { nodes, edges } = schema;\n  const nodeSet = new Set(nodes.map((node) => node.id));\n  edges.forEach((edge) => {\n    if (!nodeSet.has(edge.sourceNodeID)) {\n      throw new Error(`Workflow schema edge source node \"${edge.sourceNodeID}\" not exist`);\n    }\n    if (!nodeSet.has(edge.targetNodeID)) {\n      throw new Error(`Workflow schema edge target node \"${edge.targetNodeID}\" not exist`);\n    }\n  });\n  nodes.forEach((node) => {\n    if (node.blocks) {\n      edgeSourceTargetExist({\n        nodes: node.blocks,\n        edges: node.edges ?? [],\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { cycleDetection } from './cycle-detection';\nexport { startEndNode } from './start-end-node';\nexport { edgeSourceTargetExist } from './edge-source-target-exist';\nexport { schemaFormat } from './schema-format';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/schema-format.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\nimport { schemaFormat } from './schema-format';\n\ndescribe('schemaFormat', () => {\n  const validSchema: WorkflowSchema = {\n    nodes: [\n      {\n        id: 'start_1',\n        type: 'start',\n        meta: {\n          position: { x: 0, y: 0 },\n        },\n        data: {\n          title: 'Start Node',\n        },\n      },\n      {\n        id: 'end_1',\n        type: 'end',\n        meta: {\n          position: { x: 100, y: 100 },\n        },\n        data: {\n          title: 'End Node',\n        },\n      },\n    ],\n    edges: [\n      {\n        sourceNodeID: 'start_1',\n        targetNodeID: 'end_1',\n      },\n    ],\n  };\n\n  describe('valid schemas', () => {\n    it('should pass validation for a valid basic schema', () => {\n      expect(() => schemaFormat(validSchema)).not.toThrow();\n    });\n\n    it('should pass validation for schema with optional fields', () => {\n      const schemaWithOptionals: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            type: 'custom',\n            meta: { position: { x: 0, y: 0 } },\n            data: {\n              title: 'Custom Node',\n              inputs: {\n                type: 'object',\n                properties: {\n                  input1: { type: 'string' },\n                },\n              },\n              outputs: {\n                type: 'object',\n                properties: {\n                  output1: { type: 'string' },\n                },\n              },\n              inputsValues: {\n                input1: { value: 'test', type: 'string' },\n              },\n            },\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(schemaWithOptionals)).not.toThrow();\n    });\n\n    it('should pass validation for schema with nested blocks', () => {\n      const schemaWithBlocks: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'parent_node',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: { title: 'Parent Node' },\n            blocks: [\n              {\n                id: 'child_node',\n                type: 'child',\n                meta: { position: { x: 10, y: 10 } },\n                data: { title: 'Child Node' },\n              },\n            ],\n            edges: [],\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(schemaWithBlocks)).not.toThrow();\n    });\n\n    it('should pass validation for edges with optional port IDs', () => {\n      const schemaWithPorts: WorkflowSchema = {\n        nodes: validSchema.nodes,\n        edges: [\n          {\n            sourceNodeID: 'start_1',\n            targetNodeID: 'end_1',\n            sourcePortID: 'output_port',\n            targetPortID: 'input_port',\n          },\n        ],\n      };\n\n      expect(() => schemaFormat(schemaWithPorts)).not.toThrow();\n    });\n  });\n\n  describe('invalid schemas', () => {\n    it('should throw error for null schema', () => {\n      expect(() => schemaFormat(null as any)).toThrow('Workflow schema must be a valid object');\n    });\n\n    it('should throw error for undefined schema', () => {\n      expect(() => schemaFormat(undefined as any)).toThrow(\n        'Workflow schema must be a valid object'\n      );\n    });\n\n    it('should throw error for non-object schema', () => {\n      expect(() => schemaFormat('invalid' as any)).toThrow(\n        'Workflow schema must be a valid object'\n      );\n    });\n\n    it('should throw error for missing nodes array', () => {\n      const invalidSchema = { edges: [] } as any;\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'Workflow schema must have a valid nodes array'\n      );\n    });\n\n    it('should throw error for non-array nodes', () => {\n      const invalidSchema = { nodes: 'invalid', edges: [] } as any;\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'Workflow schema must have a valid nodes array'\n      );\n    });\n\n    it('should throw error for missing edges array', () => {\n      const invalidSchema = { nodes: [] } as any;\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'Workflow schema must have a valid edges array'\n      );\n    });\n\n    it('should throw error for non-array edges', () => {\n      const invalidSchema = { nodes: [], edges: 'invalid' } as any;\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'Workflow schema must have a valid edges array'\n      );\n    });\n  });\n\n  describe('invalid nodes', () => {\n    it('should throw error for node without id', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            type: 'start',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n          } as any,\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow('nodes[0].id must be a non-empty string');\n    });\n\n    it('should throw error for node with empty id', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: '',\n            type: 'start',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow('nodes[0].id must be a non-empty string');\n    });\n\n    it('should throw error for node without type', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n          } as any,\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow('nodes[0].type must be a non-empty string');\n    });\n\n    it('should throw error for node without meta', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            type: 'start',\n            data: {},\n          } as any,\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow('nodes[0].meta must be a valid object');\n    });\n\n    it('should throw error for node without data', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            type: 'start',\n            meta: { position: { x: 0, y: 0 } },\n          } as any,\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow('nodes[0].data must be a valid object');\n    });\n\n    it('should throw error for invalid blocks field', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: 'invalid' as any,\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'nodes[0].blocks must be an array if present'\n      );\n    });\n\n    it('should throw error for invalid data.inputs field', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'node_1',\n            type: 'start',\n            meta: { position: { x: 0, y: 0 } },\n            data: {\n              inputs: 'invalid',\n            } as any,\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'nodes[0].data.inputs must be a valid object if present'\n      );\n    });\n  });\n\n  describe('invalid edges', () => {\n    it('should throw error for edge without sourceNodeID', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: validSchema.nodes,\n        edges: [\n          {\n            targetNodeID: 'end_1',\n          } as any,\n        ],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'edges[0].sourceNodeID must be a non-empty string'\n      );\n    });\n\n    it('should throw error for edge with empty sourceNodeID', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: validSchema.nodes,\n        edges: [\n          {\n            sourceNodeID: '',\n            targetNodeID: 'end_1',\n          },\n        ],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'edges[0].sourceNodeID must be a non-empty string'\n      );\n    });\n\n    it('should throw error for edge without targetNodeID', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: validSchema.nodes,\n        edges: [\n          {\n            sourceNodeID: 'start_1',\n          } as any,\n        ],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'edges[0].targetNodeID must be a non-empty string'\n      );\n    });\n\n    it('should throw error for invalid sourcePortID type', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: validSchema.nodes,\n        edges: [\n          {\n            sourceNodeID: 'start_1',\n            targetNodeID: 'end_1',\n            sourcePortID: 123 as any,\n          },\n        ],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'edges[0].sourcePortID must be a string if present'\n      );\n    });\n  });\n\n  describe('nested validation', () => {\n    it('should validate nested blocks recursively', () => {\n      const invalidNestedSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'parent_node',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: { title: 'Parent Node' },\n            blocks: [\n              {\n                id: '', // Invalid empty id in nested block\n                type: 'child',\n                meta: { position: { x: 10, y: 10 } },\n                data: { title: 'Child Node' },\n              },\n            ],\n            edges: [],\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidNestedSchema)).toThrow(\n        'nodes[0].id must be a non-empty string'\n      );\n    });\n\n    it('should throw error for invalid blocks structure', () => {\n      const invalidSchema: WorkflowSchema = {\n        nodes: [\n          {\n            id: 'parent_node',\n            type: 'container',\n            meta: { position: { x: 0, y: 0 } },\n            data: { title: 'Parent Node' },\n            blocks: 'not an array' as any,\n          },\n        ],\n        edges: [],\n      };\n\n      expect(() => schemaFormat(invalidSchema)).toThrow(\n        'nodes[0].blocks must be an array if present'\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/schema-format.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema } from '@flowgram.ai/runtime-interface';\n\n/**\n * Validates the basic format and structure of a workflow schema\n * Ensures all required fields are present and have correct types\n */\nexport const schemaFormat = (schema: WorkflowSchema): void => {\n  // Check if schema is a valid object\n  if (!schema || typeof schema !== 'object') {\n    throw new Error('Workflow schema must be a valid object');\n  }\n\n  // Check if nodes array exists and is valid\n  if (!Array.isArray(schema.nodes)) {\n    throw new Error('Workflow schema must have a valid nodes array');\n  }\n\n  // Check if edges array exists and is valid\n  if (!Array.isArray(schema.edges)) {\n    throw new Error('Workflow schema must have a valid edges array');\n  }\n\n  // Validate each node structure\n  schema.nodes.forEach((node, index) => {\n    validateNodeFormat(node, `nodes[${index}]`);\n  });\n\n  // Validate each edge structure\n  schema.edges.forEach((edge, index) => {\n    validateEdgeFormat(edge, `edges[${index}]`);\n  });\n\n  // Recursively validate nested blocks\n  schema.nodes.forEach((node, nodeIndex) => {\n    if (node.blocks) {\n      if (!Array.isArray(node.blocks)) {\n        throw new Error(`Node nodes[${nodeIndex}].blocks must be an array`);\n      }\n\n      const nestedSchema = {\n        nodes: node.blocks,\n        edges: node.edges || [],\n      };\n\n      schemaFormat(nestedSchema);\n    }\n  });\n};\n\n/**\n * Validates the format of a single node\n */\nconst validateNodeFormat = (node: any, path: string): void => {\n  if (!node || typeof node !== 'object') {\n    throw new Error(`${path} must be a valid object`);\n  }\n\n  // Check required fields\n  if (typeof node.id !== 'string' || !node.id.trim()) {\n    throw new Error(`${path}.id must be a non-empty string`);\n  }\n\n  if (typeof node.type !== 'string' || !node.type.trim()) {\n    throw new Error(`${path}.type must be a non-empty string`);\n  }\n\n  if (!node.meta || typeof node.meta !== 'object') {\n    throw new Error(`${path}.meta must be a valid object`);\n  }\n\n  if (!node.data || typeof node.data !== 'object') {\n    throw new Error(`${path}.data must be a valid object`);\n  }\n\n  // Validate optional fields if present\n  if (node.blocks !== undefined && !Array.isArray(node.blocks)) {\n    throw new Error(`${path}.blocks must be an array if present`);\n  }\n\n  if (node.edges !== undefined && !Array.isArray(node.edges)) {\n    throw new Error(`${path}.edges must be an array if present`);\n  }\n\n  // Validate data.inputs and data.outputs if present\n  if (\n    node.data.inputs !== undefined &&\n    (typeof node.data.inputs !== 'object' || node.data.inputs === null)\n  ) {\n    throw new Error(`${path}.data.inputs must be a valid object if present`);\n  }\n\n  if (\n    node.data.outputs !== undefined &&\n    (typeof node.data.outputs !== 'object' || node.data.outputs === null)\n  ) {\n    throw new Error(`${path}.data.outputs must be a valid object if present`);\n  }\n\n  if (\n    node.data.inputsValues !== undefined &&\n    (typeof node.data.inputsValues !== 'object' || node.data.inputsValues === null)\n  ) {\n    throw new Error(`${path}.data.inputsValues must be a valid object if present`);\n  }\n\n  if (node.data.title !== undefined && typeof node.data.title !== 'string') {\n    throw new Error(`${path}.data.title must be a string if present`);\n  }\n};\n\n/**\n * Validates the format of a single edge\n */\nconst validateEdgeFormat = (edge: any, path: string): void => {\n  if (!edge || typeof edge !== 'object') {\n    throw new Error(`${path} must be a valid object`);\n  }\n\n  // Check required fields\n  if (typeof edge.sourceNodeID !== 'string' || !edge.sourceNodeID.trim()) {\n    throw new Error(`${path}.sourceNodeID must be a non-empty string`);\n  }\n\n  if (typeof edge.targetNodeID !== 'string' || !edge.targetNodeID.trim()) {\n    throw new Error(`${path}.targetNodeID must be a non-empty string`);\n  }\n\n  // Validate optional fields if present\n  if (edge.sourcePortID !== undefined && typeof edge.sourcePortID !== 'string') {\n    throw new Error(`${path}.sourcePortID must be a string if present`);\n  }\n\n  if (edge.targetPortID !== undefined && typeof edge.targetPortID !== 'string') {\n    throw new Error(`${path}.targetPortID must be a string if present`);\n  }\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/start-end-node.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { WorkflowSchema, FlowGramNode } from '@flowgram.ai/runtime-interface';\n\nimport { startEndNode } from './start-end-node';\n\ndescribe('startEndNode', () => {\n  const createMockNode = (id: string, type: string) => ({\n    id,\n    type,\n    meta: { position: { x: 0, y: 0 } },\n    data: {},\n  });\n\n  describe('valid scenarios', () => {\n    it('should not throw error when schema has exactly one start and one end node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          createMockNode('middle1', 'custom'),\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).not.toThrow();\n    });\n\n    it('should not throw error when schema has start, end nodes and nested blocks with block-start/block-end', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              createMockNode('custom1', 'custom'),\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).not.toThrow();\n    });\n  });\n\n  describe('missing start and end nodes', () => {\n    it('should throw error when schema has no start and no end nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockNode('middle1', 'custom')],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow schema must have a start node and an end node'\n      );\n    });\n  });\n\n  describe('missing start node', () => {\n    it('should throw error when schema has no start node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockNode('middle1', 'custom'), createMockNode('end1', FlowGramNode.End)],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow('Workflow schema must have a start node');\n    });\n  });\n\n  describe('missing end node', () => {\n    it('should throw error when schema has no end node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [createMockNode('start1', FlowGramNode.Start), createMockNode('middle1', 'custom')],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow('Workflow schema must have an end node');\n    });\n  });\n\n  describe('multiple start nodes', () => {\n    it('should throw error when schema has multiple start nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          createMockNode('start2', FlowGramNode.Start),\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow('Workflow schema must have only one start node');\n    });\n  });\n\n  describe('multiple end nodes', () => {\n    it('should throw error when schema has multiple end nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          createMockNode('end1', FlowGramNode.End),\n          createMockNode('end2', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow('Workflow schema must have only one end node');\n    });\n  });\n\n  describe('nested block validation', () => {\n    it('should throw error when nested block has no block-start node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('custom1', 'custom'),\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have a block-start node'\n      );\n    });\n\n    it('should throw error when nested block has no block-end node', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              createMockNode('custom1', 'custom'),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have an block-end node'\n      );\n    });\n\n    it('should throw error when nested block has multiple block-start nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              createMockNode('block-start2', FlowGramNode.BlockStart),\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have only one block-start node'\n      );\n    });\n\n    it('should throw error when nested block has multiple block-end nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n              createMockNode('block-end2', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have only one block-end node'\n      );\n    });\n\n    it('should throw error when nested block has no block-start and no block-end nodes', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [createMockNode('custom1', 'custom')],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have a block-start node and a block-end node'\n      );\n    });\n  });\n\n  describe('deeply nested blocks', () => {\n    it('should validate deeply nested blocks recursively', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              {\n                id: 'nested-loop',\n                type: FlowGramNode.Loop,\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n                blocks: [\n                  createMockNode('nested-block-start', FlowGramNode.BlockStart),\n                  createMockNode('nested-custom', 'custom'),\n                  createMockNode('nested-block-end', FlowGramNode.BlockEnd),\n                ],\n                edges: [],\n              },\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).not.toThrow();\n    });\n\n    it('should throw error for invalid deeply nested blocks', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [\n              createMockNode('block-start1', FlowGramNode.BlockStart),\n              {\n                id: 'nested-loop',\n                type: FlowGramNode.Loop,\n                meta: { position: { x: 0, y: 0 } },\n                data: {},\n                blocks: [\n                  // Missing nested block-start node\n                  createMockNode('nested-custom', 'custom'),\n                  createMockNode('nested-block-end', FlowGramNode.BlockEnd),\n                ],\n                edges: [],\n              },\n              createMockNode('block-end1', FlowGramNode.BlockEnd),\n            ],\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have a block-start node'\n      );\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle empty nodes array', () => {\n      const schema: WorkflowSchema = {\n        nodes: [],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow schema must have a start node and an end node'\n      );\n    });\n\n    it('should handle nodes without blocks property', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'custom1',\n            type: 'custom',\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            // No blocks property\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).not.toThrow();\n    });\n\n    it('should handle nodes with empty blocks array', () => {\n      const schema: WorkflowSchema = {\n        nodes: [\n          createMockNode('start1', FlowGramNode.Start),\n          {\n            id: 'loop1',\n            type: FlowGramNode.Loop,\n            meta: { position: { x: 0, y: 0 } },\n            data: {},\n            blocks: [], // Empty blocks\n            edges: [],\n          },\n          createMockNode('end1', FlowGramNode.End),\n        ],\n        edges: [],\n      };\n\n      expect(() => startEndNode(schema)).toThrow(\n        'Workflow block schema must have a block-start node and a block-end node'\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/validation/validators/start-end-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowSchema, FlowGramNode } from '@flowgram.ai/runtime-interface';\n\nconst blockStartEndNode = (schema: WorkflowSchema) => {\n  // Optimize performance by using single traversal instead of two separate filter operations\n  const { blockStartNodes, blockEndNodes } = schema.nodes.reduce(\n    (acc, node) => {\n      if (node.type === FlowGramNode.BlockStart) {\n        acc.blockStartNodes.push(node);\n      } else if (node.type === FlowGramNode.BlockEnd) {\n        acc.blockEndNodes.push(node);\n      }\n      return acc;\n    },\n    { blockStartNodes: [] as typeof schema.nodes, blockEndNodes: [] as typeof schema.nodes }\n  );\n  if (!blockStartNodes.length && !blockEndNodes.length) {\n    throw new Error('Workflow block schema must have a block-start node and a block-end node');\n  }\n  if (!blockStartNodes.length) {\n    throw new Error('Workflow block schema must have a block-start node');\n  }\n  if (!blockEndNodes.length) {\n    throw new Error('Workflow block schema must have an block-end node');\n  }\n  if (blockStartNodes.length > 1) {\n    throw new Error('Workflow block schema must have only one block-start node');\n  }\n  if (blockEndNodes.length > 1) {\n    throw new Error('Workflow block schema must have only one block-end node');\n  }\n  schema.nodes.forEach((node) => {\n    if (node.blocks) {\n      blockStartEndNode({\n        nodes: node.blocks,\n        edges: node.edges ?? [],\n      });\n    }\n  });\n};\n\nexport const startEndNode = (schema: WorkflowSchema) => {\n  // Optimize performance by using single traversal instead of two separate filter operations\n  const { startNodes, endNodes } = schema.nodes.reduce(\n    (acc, node) => {\n      if (node.type === FlowGramNode.Start) {\n        acc.startNodes.push(node);\n      } else if (node.type === FlowGramNode.End) {\n        acc.endNodes.push(node);\n      }\n      return acc;\n    },\n    { startNodes: [] as typeof schema.nodes, endNodes: [] as typeof schema.nodes }\n  );\n  if (!startNodes.length && !endNodes.length) {\n    throw new Error('Workflow schema must have a start node and an end node');\n  }\n  if (!startNodes.length) {\n    throw new Error('Workflow schema must have a start node');\n  }\n  if (!endNodes.length) {\n    throw new Error('Workflow schema must have an end node');\n  }\n  if (startNodes.length > 1) {\n    throw new Error('Workflow schema must have only one start node');\n  }\n  if (endNodes.length > 1) {\n    throw new Error('Workflow schema must have only one end node');\n  }\n  schema.nodes.forEach((node) => {\n    if (node.blocks) {\n      blockStartEndNode({\n        nodes: node.blocks,\n        edges: node.edges ?? [],\n      });\n    }\n  });\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/variable/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { WorkflowRuntimeVariableStore } from './variable-store';\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/variable/variable-store/index.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { IVariableStore, WorkflowVariableType } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeVariableStore } from './index';\n\ndescribe('WorkflowRuntimeVariableStore', () => {\n  let variableStore: IVariableStore;\n\n  beforeEach(() => {\n    variableStore = new WorkflowRuntimeVariableStore();\n    variableStore.init();\n  });\n\n  it('should create a store with unique id', () => {\n    const store1 = new WorkflowRuntimeVariableStore();\n    const store2 = new WorkflowRuntimeVariableStore();\n    expect(store1.id).toBeTruthy();\n    expect(store2.id).toBeTruthy();\n    expect(store1.id).not.toBe(store2.id);\n  });\n\n  describe('set', () => {\n    it('should set variable', () => {\n      const value = { foo: 'bar' };\n      variableStore.setVariable({\n        nodeID: 'node1',\n        key: 'var1',\n        value,\n        type: WorkflowVariableType.Object,\n      });\n\n      expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual(value);\n    });\n\n    it('should update existing variable', () => {\n      variableStore.setVariable({\n        nodeID: 'node1',\n        key: 'var1',\n        value: { foo: 'bar' },\n        type: WorkflowVariableType.Object,\n      });\n\n      variableStore.setVariable({\n        nodeID: 'node1',\n        key: 'var1',\n        value: { baz: 'qux' },\n        type: WorkflowVariableType.Object,\n      });\n\n      expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({ baz: 'qux' });\n    });\n  });\n\n  describe('setValue', () => {\n    it('should set value without path', () => {\n      const value = { foo: 'bar' };\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        value,\n      });\n\n      expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual(value);\n    });\n\n    it('should set value with path', () => {\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        variablePath: ['foo', 'bar'],\n        value: 'baz',\n      });\n\n      expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({\n        foo: { bar: 'baz' },\n      });\n    });\n\n    it('should update existing value', () => {\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        value: { foo: 'bar' },\n      });\n\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        value: { baz: 'qux' },\n      });\n\n      expect(variableStore.store.get('node1')?.get('var1')?.value).toEqual({ baz: 'qux' });\n    });\n  });\n\n  describe('get', () => {\n    beforeEach(() => {\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        value: { foo: { bar: 'baz' } },\n      });\n    });\n\n    it('should get value without path', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n      });\n\n      expect(result?.value).toEqual({ foo: { bar: 'baz' } });\n    });\n\n    it('should get value with path', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        variablePath: ['foo', 'bar'],\n      });\n\n      expect(result?.value).toBe('baz');\n    });\n\n    it('should get value with empty path', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        variablePath: [],\n      });\n\n      expect(result?.value).toStrictEqual({ foo: { bar: 'baz' } });\n    });\n\n    it('should get value with undefined path', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n      });\n\n      expect(result?.value).toStrictEqual({ foo: { bar: 'baz' } });\n    });\n\n    it('should return undefined for non-existent node', () => {\n      const result = variableStore.getValue({\n        nodeID: 'non-existent',\n        variableKey: 'var1',\n      });\n\n      expect(result?.value).toBeUndefined();\n    });\n\n    it('should return undefined for non-existent variable', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'non-existent',\n      });\n\n      expect(result?.value).toBeUndefined();\n    });\n\n    it('should return undefined for non-existent path', () => {\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        variablePath: ['non', 'existent'],\n      });\n\n      expect(result?.value).toBeUndefined();\n    });\n\n    it('should get number value', () => {\n      variableStore.setVariable({\n        nodeID: 'start_0',\n        key: 'llm_settings',\n        value: { temperature: 0.5 },\n        type: WorkflowVariableType.Object,\n      });\n\n      const result = variableStore.getValue({\n        nodeID: 'start_0',\n        variableKey: 'llm_settings',\n        variablePath: ['temperature'],\n      });\n\n      expect(result?.value).toBe(0.5);\n    });\n\n    it('should return 0', () => {\n      variableStore.setValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n        value: 0,\n      });\n      const result = variableStore.getValue({\n        nodeID: 'node1',\n        variableKey: 'var1',\n      });\n      expect(result?.value).toBe(0);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/variable/variable-store/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get, set } from 'lodash-es';\nimport {\n  WorkflowVariableType,\n  IVariableStore,\n  IVariable,\n  IVariableParseResult,\n} from '@flowgram.ai/runtime-interface';\n\nimport { uuid, WorkflowRuntimeType } from '@infra/utils';\nimport { WorkflowRuntimeVariable } from '../variable-value-object';\n\nexport class WorkflowRuntimeVariableStore implements IVariableStore {\n  public readonly id: string;\n\n  private parent?: WorkflowRuntimeVariableStore;\n\n  constructor() {\n    this.id = uuid();\n  }\n\n  public store: Map<string, Map<string, IVariable>>;\n\n  public init(): void {\n    this.store = new Map();\n  }\n\n  public dispose(): void {\n    this.store.clear();\n  }\n\n  public setParent(parent: IVariableStore): void {\n    this.parent = parent as WorkflowRuntimeVariableStore;\n  }\n\n  public globalGet(nodeID: string): Map<string, IVariable> | undefined {\n    const store = this.store.get(nodeID);\n    if (!store && this.parent) {\n      return this.parent.globalGet(nodeID);\n    }\n    return store;\n  }\n\n  public setVariable(params: {\n    nodeID: string;\n    key: string;\n    value: Object;\n    type: WorkflowVariableType;\n    itemsType?: WorkflowVariableType;\n  }): void {\n    const { nodeID, key, value, type, itemsType } = params;\n    if (!this.store.has(nodeID)) {\n      // create node store\n      this.store.set(nodeID, new Map());\n    }\n    const nodeStore = this.store.get(nodeID)!;\n    // create variable store\n    const variable = WorkflowRuntimeVariable.create({\n      nodeID,\n      key,\n      value,\n      type, // TODO check type\n      itemsType, // TODO check is array\n    });\n    nodeStore.set(key, variable);\n  }\n\n  public setValue(params: {\n    nodeID: string;\n    variableKey: string;\n    variablePath?: string[];\n    value: Object;\n  }): void {\n    const { nodeID, variableKey, variablePath, value } = params;\n    if (!this.store.has(nodeID)) {\n      // create node store\n      this.store.set(nodeID, new Map());\n    }\n    const nodeStore = this.store.get(nodeID)!;\n    if (!nodeStore.has(variableKey)) {\n      // create variable store\n      const variable = WorkflowRuntimeVariable.create({\n        nodeID,\n        key: variableKey,\n        value: {},\n        type: WorkflowVariableType.Object,\n      });\n      nodeStore.set(variableKey, variable);\n    }\n    const variable = nodeStore.get(variableKey)!;\n    if (!variablePath) {\n      variable.value = value;\n      return;\n    }\n    set(variable.value, variablePath, value);\n  }\n\n  public getValue<T = unknown>(params: {\n    nodeID: string;\n    variableKey: string;\n    variablePath?: string[];\n  }): IVariableParseResult<T> | null {\n    const { nodeID, variableKey, variablePath } = params;\n    const variable = this.globalGet(nodeID)?.get(variableKey);\n    if (!variable) {\n      return null;\n    }\n    if (!variablePath || variablePath.length === 0) {\n      return {\n        value: variable.value as T,\n        type: variable.type,\n        itemsType: variable.itemsType,\n      };\n    }\n    const value = get(variable.value, variablePath) as T;\n    const type = WorkflowRuntimeType.getWorkflowType(value);\n    if (!type) {\n      return null;\n    }\n    if (type === WorkflowVariableType.Array && Array.isArray(value)) {\n      const itemsType = WorkflowRuntimeType.getWorkflowType(value[0]);\n      if (!itemsType) {\n        return null;\n      }\n      return {\n        value,\n        type,\n        itemsType,\n      };\n    }\n    return {\n      value,\n      type,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/domain/variable/variable-value-object/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IVariable, VOData } from '@flowgram.ai/runtime-interface';\n\nimport { uuid } from '@infra/utils';\n\nexport namespace WorkflowRuntimeVariable {\n  export const create = (params: VOData<IVariable>): IVariable => ({\n    id: uuid(),\n    ...params,\n  });\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './api';\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './utils';\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/compare-node-groups.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { INode } from '@flowgram.ai/runtime-interface';\n\nimport { compareNodeGroups } from './compare-node-groups';\n\n// Helper function to create mock nodes\nfunction createMockNode(id: string): INode {\n  return {\n    id,\n    type: 'basic' as any,\n    name: `Node ${id}`,\n    position: { x: 0, y: 0 },\n    declare: {},\n    data: {},\n    ports: { inputs: [], outputs: [] },\n    edges: { inputs: [], outputs: [] },\n    parent: null,\n    children: [],\n    prev: [],\n    next: [],\n    successors: [],\n    predecessors: [],\n    isBranch: false,\n  };\n}\n\ndescribe('compareNodeGroups', () => {\n  it('should correctly identify common, unique to A, and unique to B nodes', () => {\n    // Create test nodes\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n    const nodeC = createMockNode('C');\n    const nodeD = createMockNode('D');\n    const nodeE = createMockNode('E');\n    const nodeF = createMockNode('F');\n    const nodeG = createMockNode('G');\n    const nodeH = createMockNode('H');\n    const nodeI = createMockNode('I');\n    const nodeJ = createMockNode('J');\n\n    // Set up groups according to user example\n    const groupA = [\n      [nodeA, nodeD, nodeE, nodeF, nodeJ],\n      [nodeB, nodeC, nodeD, nodeE, nodeF, nodeJ],\n    ];\n    const groupB = [[nodeG, nodeH, nodeI, nodeD, nodeE, nodeF, nodeJ]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    // Verify common nodes: D,E,F,J\n    expect(result.common).toHaveLength(4);\n    expect(result.common.map((n) => n.id)).toEqual(expect.arrayContaining(['D', 'E', 'F', 'J']));\n\n    // Verify nodes unique to group A: A,B,C\n    expect(result.uniqueToA).toHaveLength(3);\n    expect(result.uniqueToA.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'B', 'C']));\n\n    // Verify nodes unique to group B: G,H,I\n    expect(result.uniqueToB).toHaveLength(3);\n    expect(result.uniqueToB.map((n) => n.id)).toEqual(expect.arrayContaining(['G', 'H', 'I']));\n  });\n\n  it('should handle empty groups', () => {\n    const result = compareNodeGroups([], []);\n\n    expect(result.common).toHaveLength(0);\n    expect(result.uniqueToA).toHaveLength(0);\n    expect(result.uniqueToB).toHaveLength(0);\n  });\n\n  it('should handle one empty group', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n\n    const groupA = [[nodeA, nodeB]];\n    const groupB: INode[][] = [];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(0);\n    expect(result.uniqueToA).toHaveLength(2);\n    expect(result.uniqueToA.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'B']));\n    expect(result.uniqueToB).toHaveLength(0);\n  });\n\n  it('should handle duplicate nodes within the same group', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n\n    const groupA = [\n      [nodeA, nodeA, nodeB], // nodeA duplicated\n      [nodeA, nodeB], // nodeA and nodeB duplicated\n    ];\n    const groupB = [[nodeA]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    // Should deduplicate, nodeA is common, nodeB is unique to A\n    expect(result.common).toHaveLength(1);\n    expect(result.common[0].id).toBe('A');\n    expect(result.uniqueToA).toHaveLength(1);\n    expect(result.uniqueToA[0].id).toBe('B');\n    expect(result.uniqueToB).toHaveLength(0);\n  });\n\n  it('should handle all nodes being common', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n    const nodeC = createMockNode('C');\n\n    const groupA = [[nodeA, nodeB, nodeC]];\n    const groupB = [[nodeA, nodeB, nodeC]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(3);\n    expect(result.common.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'B', 'C']));\n    expect(result.uniqueToA).toHaveLength(0);\n    expect(result.uniqueToB).toHaveLength(0);\n  });\n\n  it('should handle no common nodes', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n    const nodeC = createMockNode('C');\n    const nodeD = createMockNode('D');\n\n    const groupA = [[nodeA, nodeB]];\n    const groupB = [[nodeC, nodeD]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(0);\n    expect(result.uniqueToA).toHaveLength(2);\n    expect(result.uniqueToA.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'B']));\n    expect(result.uniqueToB).toHaveLength(2);\n    expect(result.uniqueToB.map((n) => n.id)).toEqual(expect.arrayContaining(['C', 'D']));\n  });\n\n  it('should handle single node in each group', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n\n    const groupA = [[nodeA]];\n    const groupB = [[nodeB]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(0);\n    expect(result.uniqueToA).toHaveLength(1);\n    expect(result.uniqueToA[0].id).toBe('A');\n    expect(result.uniqueToB).toHaveLength(1);\n    expect(result.uniqueToB[0].id).toBe('B');\n  });\n\n  it('should handle multiple sub-arrays with mixed scenarios', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n    const nodeC = createMockNode('C');\n    const nodeD = createMockNode('D');\n    const nodeE = createMockNode('E');\n\n    const groupA = [\n      [nodeA, nodeB],\n      [nodeC, nodeD],\n    ];\n    const groupB = [[nodeB, nodeC], [nodeE]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(2);\n    expect(result.common.map((n) => n.id)).toEqual(expect.arrayContaining(['B', 'C']));\n    expect(result.uniqueToA).toHaveLength(2);\n    expect(result.uniqueToA.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'D']));\n    expect(result.uniqueToB).toHaveLength(1);\n    expect(result.uniqueToB[0].id).toBe('E');\n  });\n\n  it('should preserve node object references in results', () => {\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n\n    const groupA = [[nodeA]];\n    const groupB = [[nodeA, nodeB]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    // Verify that original node object references are returned\n    expect(result.common[0]).toBe(nodeA);\n    expect(result.uniqueToB[0]).toBe(nodeB);\n  });\n\n  it('should handle large number of nodes efficiently', () => {\n    // Create large number of nodes to test performance\n    const nodes = Array.from({ length: 100 }, (_, i) => createMockNode(`node${i}`));\n\n    const groupA = [nodes.slice(0, 60)];\n    const groupB = [nodes.slice(40, 100)];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    // Verify result correctness\n    expect(result.common).toHaveLength(20); // nodes 40-59 (20 nodes)\n    expect(result.uniqueToA).toHaveLength(40); // nodes 0-39 (40 nodes)\n    expect(result.uniqueToB).toHaveLength(40); // nodes 60-99 (40 nodes)\n  });\n\n  it('should handle edge case with same node instance in both groups', () => {\n    const sharedNode = createMockNode('shared');\n    const nodeA = createMockNode('A');\n    const nodeB = createMockNode('B');\n\n    const groupA = [[sharedNode, nodeA]];\n    const groupB = [[sharedNode, nodeB]];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    expect(result.common).toHaveLength(1);\n    expect(result.common[0]).toBe(sharedNode);\n    expect(result.uniqueToA).toHaveLength(1);\n    expect(result.uniqueToA[0]).toBe(nodeA);\n    expect(result.uniqueToB).toHaveLength(1);\n    expect(result.uniqueToB[0]).toBe(nodeB);\n  });\n\n  it('should handle complex nested scenarios', () => {\n    const nodes = Array.from({ length: 10 }, (_, i) => createMockNode(String.fromCharCode(65 + i))); // A-J\n\n    const groupA = [\n      [nodes[0], nodes[1], nodes[2]], // A,B,C\n      [nodes[3], nodes[4]], // D,E\n      [nodes[5], nodes[6], nodes[7]], // F,G,H\n    ];\n    const groupB = [\n      [nodes[2], nodes[3], nodes[4]], // C,D,E\n      [nodes[7], nodes[8], nodes[9]], // H,I,J\n    ];\n\n    const result = compareNodeGroups(groupA, groupB);\n\n    // Common nodes: C,D,E,H\n    expect(result.common).toHaveLength(4);\n    expect(result.common.map((n) => n.id)).toEqual(expect.arrayContaining(['C', 'D', 'E', 'H']));\n\n    // Unique to group A: A,B,F,G\n    expect(result.uniqueToA).toHaveLength(4);\n    expect(result.uniqueToA.map((n) => n.id)).toEqual(expect.arrayContaining(['A', 'B', 'F', 'G']));\n\n    // Unique to group B: I,J\n    expect(result.uniqueToB).toHaveLength(2);\n    expect(result.uniqueToB.map((n) => n.id)).toEqual(expect.arrayContaining(['I', 'J']));\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/compare-node-groups.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { INode } from '@flowgram.ai/runtime-interface';\n\n/**\n * Interface for node comparison results\n */\nexport interface NodeComparisonResult {\n  /** Nodes common to both groups A and B */\n  common: INode[];\n  /** Nodes unique to group A */\n  uniqueToA: INode[];\n  /** Nodes unique to group B */\n  uniqueToB: INode[];\n}\n\n/**\n * Compare two groups of node arrays to find common nodes and nodes unique to each group\n *\n * @param groupA Array of nodes in group A\n * @param groupB Array of nodes in group B\n * @returns Node comparison result\n *\n * @example\n * ```typescript\n * const groupA = [\n *   [node1, node4, node5, node6, node10],\n *   [node2, node3, node4, node5, node6, node10]\n * ];\n * const groupB = [\n *   [node7, node8, node9, node4, node5, node6, node10]\n * ];\n *\n * const result = compareNodeGroups(groupA, groupB);\n * -> result.common: [node4, node5, node6, node10]\n * -> result.uniqueToA: [node1, node2, node3]\n * -> result.uniqueToB: [node7, node8, node9]\n * ```\n */\nexport function compareNodeGroups(groupA: INode[][], groupB: INode[][]): NodeComparisonResult {\n  // Flatten and deduplicate all nodes in group A\n  const flatA = groupA.flat();\n  const setA = new Map<string, INode>();\n  flatA.forEach((node) => {\n    setA.set(node.id, node);\n  });\n\n  // Flatten and deduplicate all nodes in group B\n  const flatB = groupB.flat();\n  const setB = new Map<string, INode>();\n  flatB.forEach((node) => {\n    setB.set(node.id, node);\n  });\n\n  // Find common nodes\n  const common: INode[] = [];\n  const uniqueToA: INode[] = [];\n  const uniqueToB: INode[] = [];\n\n  // Iterate through group A nodes to find common nodes and nodes unique to A\n  setA.forEach((node, id) => {\n    if (setB.has(id)) {\n      common.push(node);\n    } else {\n      uniqueToA.push(node);\n    }\n  });\n\n  // Iterate through group B nodes to find nodes unique to B\n  setB.forEach((node, id) => {\n    if (!setA.has(id)) {\n      uniqueToB.push(node);\n    }\n  });\n\n  return {\n    common,\n    uniqueToA,\n    uniqueToB,\n  };\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/delay.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { delay } from './delay';\nexport { uuid } from './uuid';\nexport { WorkflowRuntimeType } from './runtime-type';\nexport { traverseNodes } from './traverse-nodes';\nexport { compareNodeGroups } from './compare-node-groups';\nexport { JSONSchemaValidator } from './json-schema-validator';\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { IJsonSchema } from '@flowgram.ai/runtime-interface';\n\nimport { JSONSchemaValidator } from './json-schema-validator';\n\ndescribe('JSONSchemaValidator', () => {\n  const testSchema: IJsonSchema = {\n    type: 'object',\n    properties: {\n      AA: {\n        type: 'string',\n        extra: { index: 0 },\n      },\n      BB: {\n        type: 'integer',\n        extra: { index: 1 },\n      },\n      CC: {\n        type: 'object',\n        extra: { index: 2 },\n        properties: {\n          CA: {\n            type: 'string',\n            extra: { index: 0 },\n          },\n          CB: {\n            type: 'integer',\n            extra: { index: 1 },\n          },\n        },\n        required: ['CA', 'CB'],\n      },\n      DD: {\n        type: 'array',\n        extra: { index: 3 },\n        items: {\n          type: 'object',\n          properties: {\n            DA: {\n              type: 'string',\n              extra: { index: 1 },\n            },\n            DB: {\n              type: 'object',\n              extra: { index: 2 },\n              properties: {\n                DBA: {\n                  type: 'string',\n                  extra: { index: 1 },\n                },\n              },\n              required: ['DBA'],\n            },\n          },\n          required: [],\n        },\n      },\n    },\n    required: ['AA'],\n  };\n\n  it('should validate valid input successfully', () => {\n    const validValue = {\n      AA: 'test string',\n      BB: 42,\n      CC: {\n        CA: 'nested string',\n        CB: 123,\n      },\n      DD: [\n        {\n          DA: 'array item string',\n          DB: {\n            DBA: 'deep nested string',\n          },\n        },\n      ],\n    };\n\n    const result = JSONSchemaValidator({\n      schema: testSchema,\n      value: validValue,\n    });\n\n    expect(result.result).toBe(true);\n    expect(result.errorMessage).toBeUndefined();\n  });\n\n  it('should fail when required property is missing', () => {\n    const invalidValue = {\n      BB: 42,\n      // Missing required AA\n    };\n\n    const result = JSONSchemaValidator({\n      schema: testSchema,\n      value: invalidValue,\n    });\n\n    expect(result.result).toBe(false);\n    expect(result.errorMessage).toContain('Missing required property \"AA\"');\n  });\n\n  it('should fail when property type is wrong', () => {\n    const invalidValue = {\n      AA: 123, // Should be string, not number\n    };\n\n    const result = JSONSchemaValidator({\n      schema: testSchema,\n      value: invalidValue,\n    });\n\n    expect(result.result).toBe(false);\n    expect(result.errorMessage).toContain('Expected string at AA, but got: number');\n  });\n\n  it('should fail when nested required property is missing', () => {\n    const invalidValue = {\n      AA: 'test string',\n      CC: {\n        CA: 'nested string',\n        // Missing required CB\n      },\n    };\n\n    const result = JSONSchemaValidator({\n      schema: testSchema,\n      value: invalidValue,\n    });\n\n    expect(result.result).toBe(false);\n    expect(result.errorMessage).toContain('Missing required property \"CB\"');\n  });\n\n  it('should validate array items correctly', () => {\n    const invalidValue = {\n      AA: 'test string',\n      DD: [\n        {\n          DA: 'array item string',\n          DB: {\n            // Missing required DBA\n          },\n        },\n      ],\n    };\n\n    const result = JSONSchemaValidator({\n      schema: testSchema,\n      value: invalidValue,\n    });\n\n    expect(result.result).toBe(false);\n    expect(result.errorMessage).toContain('Missing required property \"DBA\"');\n  });\n\n  it('should handle enum validation', () => {\n    const enumSchema: IJsonSchema = {\n      type: 'string',\n      enum: ['option1', 'option2', 'option3'],\n    };\n\n    const validResult = JSONSchemaValidator({\n      schema: enumSchema,\n      value: 'option1',\n    });\n    expect(validResult.result).toBe(true);\n\n    const invalidResult = JSONSchemaValidator({\n      schema: enumSchema,\n      value: 'invalid_option',\n    });\n    expect(invalidResult.result).toBe(false);\n    expect(invalidResult.errorMessage).toContain('must be one of: option1, option2, option3');\n  });\n\n  it('should handle different basic types', () => {\n    const typeTests = [\n      { type: 'boolean', validValue: true, invalidValue: 'not boolean' },\n      { type: 'integer', validValue: 42, invalidValue: 3.14 },\n      { type: 'number', validValue: 3.14, invalidValue: 'not number' },\n      { type: 'array', validValue: [1, 2, 3], invalidValue: 'not array' },\n    ];\n\n    typeTests.forEach(({ type, validValue, invalidValue }) => {\n      const schema: IJsonSchema = { type: type };\n\n      const validResult = JSONSchemaValidator({ schema, value: validValue });\n      expect(validResult.result).toBe(true);\n\n      const invalidResult = JSONSchemaValidator({ schema, value: invalidValue });\n      expect(invalidResult.result).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/json-schema-validator.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { IJsonSchema } from '@flowgram.ai/runtime-interface';\n\n// Define validation result type\ntype ValidationResult = {\n  result: boolean;\n  errorMessage?: string;\n};\n\n// Define JSON Schema validator parameters type\ntype JSONSchemaValidatorParams = {\n  schema: IJsonSchema;\n  value: unknown;\n};\n\nconst ROOT_PATH = 'root';\n\nexport const isRootPath = (path: string) => path === ROOT_PATH;\n\n// Recursively validate value against JSON Schema\nconst validateValue = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {\n  // Handle $ref references (temporarily skip as no reference resolution mechanism is provided)\n  if (schema.$ref) {\n    return { result: true }; // Temporarily skip reference validation\n  }\n\n  // Check enum values\n  if (schema.enum && schema.enum.length > 0) {\n    if (!schema.enum.includes(value as string | number)) {\n      return {\n        result: false,\n        errorMessage: `Value at ${path} must be one of: ${schema.enum.join(\n          ', '\n        )}, but got: ${JSON.stringify(value)}`,\n      };\n    }\n  }\n\n  // Validate based on type\n  switch (schema.type) {\n    case 'boolean':\n      return validateBoolean(value, path);\n\n    case 'string':\n      return validateString(value, path);\n\n    case 'integer':\n      return validateInteger(value, path);\n\n    case 'number':\n      return validateNumber(value, path);\n\n    case 'object':\n      return validateObject(value, schema, path);\n\n    case 'array':\n      return validateArray(value, schema, path);\n\n    case 'map':\n      return validateMap(value, schema, path);\n\n    default:\n      return {\n        result: false,\n        errorMessage: `Unknown type \"${schema.type}\" at ${path}`,\n      };\n  }\n};\n\n// Validate boolean value\nconst validateBoolean = (value: unknown, path: string): ValidationResult => {\n  if (typeof value !== 'boolean') {\n    return {\n      result: false,\n      errorMessage: `Expected boolean at ${path}, but got: ${typeof value}`,\n    };\n  }\n  return { result: true };\n};\n\n// Validate string value\nconst validateString = (value: unknown, path: string): ValidationResult => {\n  if (typeof value !== 'string') {\n    return {\n      result: false,\n      errorMessage: `Expected string at ${path}, but got: ${typeof value}`,\n    };\n  }\n  return { result: true };\n};\n\n// Validate integer value\nconst validateInteger = (value: unknown, path: string): ValidationResult => {\n  if (!Number.isInteger(value)) {\n    return {\n      result: false,\n      errorMessage: `Expected integer at ${path}, but got: ${JSON.stringify(value)}`,\n    };\n  }\n  return { result: true };\n};\n\n// Validate number value\nconst validateNumber = (value: unknown, path: string): ValidationResult => {\n  if (typeof value !== 'number' || isNaN(value)) {\n    return {\n      result: false,\n      errorMessage: `Expected number at ${path}, but got: ${JSON.stringify(value)}`,\n    };\n  }\n  return { result: true };\n};\n\n// Validate object value\nconst validateObject = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {\n  if (value === null || value === undefined) {\n    return {\n      result: false,\n      errorMessage: `Expected object at ${path}, but got: ${value}`,\n    };\n  }\n\n  if (typeof value !== 'object' || Array.isArray(value)) {\n    return {\n      result: false,\n      errorMessage: `Expected object at ${path}, but got: ${\n        Array.isArray(value) ? 'array' : typeof value\n      }`,\n    };\n  }\n\n  const objectValue = value as Record<string, unknown>;\n\n  // Check required properties\n  if (schema.required && schema.required.length > 0) {\n    for (const requiredProperty of schema.required) {\n      if (!(requiredProperty in objectValue)) {\n        return {\n          result: false,\n          errorMessage: `Missing required property \"${requiredProperty}\" at ${path}`,\n        };\n      }\n    }\n  }\n\n  // Check is field required\n  if (schema.properties) {\n    for (const [propertyName] of Object.entries(schema.properties)) {\n      const isRequired = schema.required?.includes(propertyName) ?? false;\n      if (isRequired && !(propertyName in objectValue)) {\n        return {\n          result: false,\n          errorMessage: `Missing required property \"${propertyName}\" at ${path}`,\n        };\n      }\n    }\n  }\n\n  // Validate properties\n  if (schema.properties) {\n    for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {\n      if (propertyName in objectValue) {\n        const propertyPath = isRootPath(path) ? propertyName : `${path}.${propertyName}`;\n        const propertyResult = validateValue(\n          objectValue[propertyName],\n          propertySchema,\n          propertyPath\n        );\n        if (!propertyResult.result) {\n          return propertyResult;\n        }\n      }\n    }\n  }\n\n  // Validate additional properties\n  if (schema.additionalProperties) {\n    const definedProperties = new Set(Object.keys(schema.properties || {}));\n    for (const [propertyName, propertyValue] of Object.entries(objectValue)) {\n      if (!definedProperties.has(propertyName)) {\n        const propertyPath = isRootPath(path) ? propertyName : `${path}.${propertyName}`;\n        const propertyResult = validateValue(\n          propertyValue,\n          schema.additionalProperties,\n          propertyPath\n        );\n        if (!propertyResult.result) {\n          return propertyResult;\n        }\n      }\n    }\n  }\n\n  return { result: true };\n};\n\n// Validate array value\nconst validateArray = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {\n  if (!Array.isArray(value)) {\n    return {\n      result: false,\n      errorMessage: `Expected array at ${path}, but got: ${typeof value}`,\n    };\n  }\n\n  // Validate array items\n  if (schema.items) {\n    for (const [index, item] of value.entries()) {\n      const itemPath = `${path}[${index}]`;\n      const itemResult = validateValue(item, schema.items, itemPath);\n      if (!itemResult.result) {\n        return itemResult;\n      }\n    }\n  }\n\n  return { result: true };\n};\n\n// Validate map value (similar to object, but all values must conform to the same schema)\nconst validateMap = (value: unknown, schema: IJsonSchema, path: string): ValidationResult => {\n  if (value === null || value === undefined) {\n    return {\n      result: false,\n      errorMessage: `Expected map at ${path}, but got: ${value}`,\n    };\n  }\n\n  if (typeof value !== 'object' || Array.isArray(value)) {\n    return {\n      result: false,\n      errorMessage: `Expected map at ${path}, but got: ${\n        Array.isArray(value) ? 'array' : typeof value\n      }`,\n    };\n  }\n\n  const mapValue = value as Record<string, unknown>;\n\n  // If additionalProperties exists, validate all values\n  if (schema.additionalProperties) {\n    for (const [key, mapItemValue] of Object.entries(mapValue)) {\n      const keyPath = isRootPath(path) ? key : `${path}.${key}`;\n      const keyResult = validateValue(mapItemValue, schema.additionalProperties, keyPath);\n      if (!keyResult.result) {\n        return keyResult;\n      }\n    }\n  }\n\n  return { result: true };\n};\n\n// Main JSON Schema validator function\nexport const JSONSchemaValidator = (params: JSONSchemaValidatorParams): ValidationResult => {\n  const { schema, value } = params;\n\n  try {\n    const validationResult = validateValue(value, schema, ROOT_PATH);\n    return validationResult;\n  } catch (error) {\n    return {\n      result: false,\n      errorMessage: `Validation error: ${error instanceof Error ? error.message : String(error)}`,\n    };\n  }\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/runtime-type.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, it } from 'vitest';\nimport { WorkflowVariableType } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeType } from './runtime-type';\n\ndescribe('WorkflowRuntimeType', () => {\n  describe('getWorkflowType', () => {\n    describe('null and undefined values', () => {\n      it('should return Null for null value', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(null);\n        expect(result).toBe(WorkflowVariableType.Null);\n      });\n\n      it('should return Null for undefined value', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(undefined);\n        expect(result).toBe(WorkflowVariableType.Null);\n      });\n\n      it('should return Null when no parameter is passed', () => {\n        const result = WorkflowRuntimeType.getWorkflowType();\n        expect(result).toBe(WorkflowVariableType.Null);\n      });\n    });\n\n    describe('string values', () => {\n      it('should return String for string value', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('hello');\n        expect(result).toBe(WorkflowVariableType.String);\n      });\n\n      it('should return String for empty string', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('');\n        expect(result).toBe(WorkflowVariableType.String);\n      });\n\n      it('should return String for string with spaces', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('  hello world  ');\n        expect(result).toBe(WorkflowVariableType.String);\n      });\n    });\n\n    describe('datetime values', () => {\n      it('should return DateTime for valid ISO 8601 string with milliseconds and Z', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49.000Z');\n        expect(result).toBe(WorkflowVariableType.DateTime);\n      });\n\n      it('should return DateTime for valid ISO 8601 string without milliseconds', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49Z');\n        expect(result).toBe(WorkflowVariableType.DateTime);\n      });\n\n      it('should return DateTime for valid ISO 8601 string without Z suffix', () => {\n        const result = WorkflowRuntimeType.getWorkflowType('2025-09-11T12:05:49.000');\n        expect(result).toBe(WorkflowVariableType.DateTime);\n      });\n\n      it('should return String for invalid datetime strings', () => {\n        const invalidDateTimes = [\n          '2025-13-11T12:05:49.000Z', // Invalid month\n          '2025-09-32T12:05:49.000Z', // Invalid day\n          '2025-09-11T25:05:49.000Z', // Invalid hour\n          '2025-09-11T12:65:49.000Z', // Invalid minute\n          '2025-09-11T12:05:65.000Z', // Invalid second\n          '2025-09-11 12:05:49.000Z', // Missing T separator\n          'not-a-date', // Completely invalid\n          '2025-09-11', // Date only\n          '12:05:49', // Time only\n        ];\n\n        invalidDateTimes.forEach((invalidDateTime) => {\n          const result = WorkflowRuntimeType.getWorkflowType(invalidDateTime);\n          expect(result).toBe(WorkflowVariableType.String);\n        });\n      });\n    });\n\n    describe('boolean values', () => {\n      it('should return Boolean for true', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(true);\n        expect(result).toBe(WorkflowVariableType.Boolean);\n      });\n\n      it('should return Boolean for false', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(false);\n        expect(result).toBe(WorkflowVariableType.Boolean);\n      });\n    });\n\n    describe('number values', () => {\n      it('should return Integer for positive integer', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(42);\n        expect(result).toBe(WorkflowVariableType.Integer);\n      });\n\n      it('should return Integer for negative integer', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(-42);\n        expect(result).toBe(WorkflowVariableType.Integer);\n      });\n\n      it('should return Integer for zero', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(0);\n        expect(result).toBe(WorkflowVariableType.Integer);\n      });\n\n      it('should return Number for positive float', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(3.14);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Number for negative float', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(-3.14);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Number for very small decimal', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(0.001);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Number for Infinity', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(Infinity);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Number for -Infinity', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(-Infinity);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Number for NaN', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(NaN);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n    });\n\n    describe('array values', () => {\n      it('should return Array for empty array', () => {\n        const result = WorkflowRuntimeType.getWorkflowType([]);\n        expect(result).toBe(WorkflowVariableType.Array);\n      });\n\n      it('should return Array for array with numbers', () => {\n        const result = WorkflowRuntimeType.getWorkflowType([1, 2, 3]);\n        expect(result).toBe(WorkflowVariableType.Array);\n      });\n\n      it('should return Array for array with strings', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(['a', 'b', 'c']);\n        expect(result).toBe(WorkflowVariableType.Array);\n      });\n\n      it('should return Array for mixed type array', () => {\n        const result = WorkflowRuntimeType.getWorkflowType([1, 'hello', true, null]);\n        expect(result).toBe(WorkflowVariableType.Array);\n      });\n\n      it('should return Array for nested arrays', () => {\n        const result = WorkflowRuntimeType.getWorkflowType([\n          [1, 2],\n          [3, 4],\n        ]);\n        expect(result).toBe(WorkflowVariableType.Array);\n      });\n    });\n\n    describe('object values', () => {\n      it('should return Object for empty object', () => {\n        const result = WorkflowRuntimeType.getWorkflowType({});\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n\n      it('should return Object for simple object', () => {\n        const result = WorkflowRuntimeType.getWorkflowType({ name: 'John', age: 30 });\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n\n      it('should return Object for nested object', () => {\n        const result = WorkflowRuntimeType.getWorkflowType({\n          user: { name: 'John', profile: { age: 30 } },\n        });\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n\n      it('should return Object for Date object', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(new Date());\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n\n      it('should return Object for RegExp object', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(/test/);\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n    });\n\n    describe('unsupported types', () => {\n      it('should return null for function', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(() => {});\n        expect(result).toBe(null);\n      });\n\n      it('should return null for symbol', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(Symbol('test'));\n        expect(result).toBe(null);\n      });\n\n      it('should return null for bigint', () => {\n        const result = WorkflowRuntimeType.getWorkflowType(BigInt(123));\n        expect(result).toBe(null);\n      });\n    });\n  });\n\n  describe('isMatchWorkflowType', () => {\n    describe('matching types', () => {\n      it('should return true for matching string type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          'hello',\n          WorkflowVariableType.String\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching boolean type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(true, WorkflowVariableType.Boolean);\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching integer type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(42, WorkflowVariableType.Integer);\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching number type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(3.14, WorkflowVariableType.Number);\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching array type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          [1, 2, 3],\n          WorkflowVariableType.Array\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching object type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          { name: 'John' },\n          WorkflowVariableType.Object\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for matching null type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(null, WorkflowVariableType.Null);\n        expect(result).toBe(true);\n      });\n    });\n\n    describe('non-matching types', () => {\n      it('should return false for string vs number type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          'hello',\n          WorkflowVariableType.Number\n        );\n        expect(result).toBe(false);\n      });\n\n      it('should return false for number vs string type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(42, WorkflowVariableType.String);\n        expect(result).toBe(false);\n      });\n\n      it('should return false for array vs object type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          [1, 2, 3],\n          WorkflowVariableType.Object\n        );\n        expect(result).toBe(false);\n      });\n\n      it('should return false for object vs array type', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          { name: 'John' },\n          WorkflowVariableType.Array\n        );\n        expect(result).toBe(false);\n      });\n    });\n\n    describe('unsupported values', () => {\n      it('should return false for function', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(() => {},\n        WorkflowVariableType.Object);\n        expect(result).toBe(false);\n      });\n\n      it('should return false for symbol', () => {\n        const result = WorkflowRuntimeType.isMatchWorkflowType(\n          Symbol('test'),\n          WorkflowVariableType.String\n        );\n        expect(result).toBe(false);\n      });\n    });\n  });\n\n  describe('isTypeEqual', () => {\n    describe('exact type matches', () => {\n      it('should return true for same string types', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.String,\n          WorkflowVariableType.String\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for same boolean types', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Boolean,\n          WorkflowVariableType.Boolean\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for same array types', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Array,\n          WorkflowVariableType.Array\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for same object types', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Object,\n          WorkflowVariableType.Object\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for same null types', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Null,\n          WorkflowVariableType.Null\n        );\n        expect(result).toBe(true);\n      });\n    });\n\n    describe('number and integer equivalence', () => {\n      it('should return true for Number and Integer', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Number,\n          WorkflowVariableType.Integer\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for Integer and Number', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Integer,\n          WorkflowVariableType.Number\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for Number and Number', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Number,\n          WorkflowVariableType.Number\n        );\n        expect(result).toBe(true);\n      });\n\n      it('should return true for Integer and Integer', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Integer,\n          WorkflowVariableType.Integer\n        );\n        expect(result).toBe(true);\n      });\n    });\n\n    describe('different type mismatches', () => {\n      it('should return false for String and Boolean', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.String,\n          WorkflowVariableType.Boolean\n        );\n        expect(result).toBe(false);\n      });\n\n      it('should return false for Array and Object', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Array,\n          WorkflowVariableType.Object\n        );\n        expect(result).toBe(false);\n      });\n\n      it('should return false for String and Number', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.String,\n          WorkflowVariableType.Number\n        );\n        expect(result).toBe(false);\n      });\n\n      it('should return false for Boolean and Null', () => {\n        const result = WorkflowRuntimeType.isTypeEqual(\n          WorkflowVariableType.Boolean,\n          WorkflowVariableType.Null\n        );\n        expect(result).toBe(false);\n      });\n    });\n  });\n\n  describe('getArrayItemsType', () => {\n    describe('uniform type arrays', () => {\n      it('should return String for all string types', () => {\n        const types = [\n          WorkflowVariableType.String,\n          WorkflowVariableType.String,\n          WorkflowVariableType.String,\n        ];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.String);\n      });\n\n      it('should return Number for all number types', () => {\n        const types = [WorkflowVariableType.Number, WorkflowVariableType.Number];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.Number);\n      });\n\n      it('should return Boolean for all boolean types', () => {\n        const types = [WorkflowVariableType.Boolean];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.Boolean);\n      });\n\n      it('should return Object for all object types', () => {\n        const types = [\n          WorkflowVariableType.Object,\n          WorkflowVariableType.Object,\n          WorkflowVariableType.Object,\n          WorkflowVariableType.Object,\n        ];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.Object);\n      });\n    });\n\n    describe('mixed type arrays', () => {\n      it('should throw error for String and Number mix', () => {\n        const types = [WorkflowVariableType.String, WorkflowVariableType.Number];\n        expect(() => WorkflowRuntimeType.getArrayItemsType(types)).toThrow(\n          'Array items type must be same, expect string, but got number'\n        );\n      });\n\n      it('should throw error for Boolean and String mix', () => {\n        const types = [WorkflowVariableType.Boolean, WorkflowVariableType.String];\n        expect(() => WorkflowRuntimeType.getArrayItemsType(types)).toThrow(\n          'Array items type must be same, expect boolean, but got string'\n        );\n      });\n\n      it('should throw error for Object and Array mix', () => {\n        const types = [WorkflowVariableType.Object, WorkflowVariableType.Array];\n        expect(() => WorkflowRuntimeType.getArrayItemsType(types)).toThrow(\n          'Array items type must be same, expect object, but got array'\n        );\n      });\n\n      it('should throw error for multiple different types', () => {\n        const types = [\n          WorkflowVariableType.String,\n          WorkflowVariableType.Number,\n          WorkflowVariableType.Boolean,\n        ];\n        expect(() => WorkflowRuntimeType.getArrayItemsType(types)).toThrow(\n          'Array items type must be same, expect string, but got number'\n        );\n      });\n    });\n\n    describe('edge cases', () => {\n      it('should handle single item array', () => {\n        const types = [WorkflowVariableType.Integer];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.Integer);\n      });\n\n      it('should handle Null types', () => {\n        const types = [WorkflowVariableType.Null, WorkflowVariableType.Null];\n        const result = WorkflowRuntimeType.getArrayItemsType(types);\n        expect(result).toBe(WorkflowVariableType.Null);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/runtime-type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowVariableType } from '@flowgram.ai/runtime-interface';\n\nexport namespace WorkflowRuntimeType {\n  export const getWorkflowType = (value?: unknown): WorkflowVariableType | null => {\n    // 处理 null 和 undefined 的情况\n    if (value === null || value === undefined) {\n      return WorkflowVariableType.Null;\n    }\n\n    // 处理基本类型\n    if (typeof value === 'string') {\n      // Check if string is a valid ISO 8601 datetime format\n      const iso8601Regex = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?$/;\n      if (iso8601Regex.test(value)) {\n        const date = new Date(value);\n        // Validate that the date is actually valid\n        if (!isNaN(date.getTime())) {\n          return WorkflowVariableType.DateTime;\n        }\n      }\n      return WorkflowVariableType.String;\n    }\n\n    if (typeof value === 'boolean') {\n      return WorkflowVariableType.Boolean;\n    }\n\n    if (typeof value === 'number') {\n      if (Number.isInteger(value)) {\n        return WorkflowVariableType.Integer;\n      }\n      return WorkflowVariableType.Number;\n    }\n\n    // 处理数组\n    if (Array.isArray(value)) {\n      return WorkflowVariableType.Array;\n    }\n\n    // 处理普通对象\n    if (typeof value === 'object') {\n      return WorkflowVariableType.Object;\n    }\n\n    return null;\n  };\n\n  export const isMatchWorkflowType = (value: unknown, type: WorkflowVariableType): boolean => {\n    const workflowType = getWorkflowType(value);\n    if (!workflowType) {\n      return false;\n    }\n    return workflowType === type;\n  };\n\n  export const isTypeEqual = (\n    typeA: WorkflowVariableType,\n    typeB: WorkflowVariableType\n  ): boolean => {\n    // 处理 Number 和 Integer 等价的情况\n    if (\n      (typeA === WorkflowVariableType.Number && typeB === WorkflowVariableType.Integer) ||\n      (typeA === WorkflowVariableType.Integer && typeB === WorkflowVariableType.Number)\n    ) {\n      return true;\n    }\n    return typeA === typeB;\n  };\n\n  export const getArrayItemsType = (types: WorkflowVariableType[]): WorkflowVariableType => {\n    const expectedType = types[0];\n    types.forEach((type) => {\n      if (type !== expectedType) {\n        throw new Error(`Array items type must be same, expect ${expectedType}, but got ${type}`);\n      }\n    });\n    return expectedType;\n  };\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/traverse-nodes.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { beforeEach, describe, expect, it } from 'vitest';\nimport { FlowGramNode, CreateNodeParams, INode, IPort } from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeNode } from '@workflow/document/entity';\nimport { traverseNodes } from './traverse-nodes';\n\ndescribe('traverseNodes', () => {\n  let node: WorkflowRuntimeNode;\n  let mockParams: CreateNodeParams;\n\n  beforeEach(() => {\n    mockParams = {\n      id: 'test-node',\n      type: FlowGramNode.Start,\n      name: 'Test Node',\n      position: { x: 0, y: 0 },\n      variable: {},\n      data: { testData: 'data' },\n    };\n    node = new WorkflowRuntimeNode(mockParams);\n  });\n\n  describe('basic functionality', () => {\n    it('should return empty array when no connected nodes', () => {\n      const result = traverseNodes(node, () => []);\n      expect(result).toEqual([]);\n    });\n\n    it('should return direct connected nodes', () => {\n      const connectedNode1 = new WorkflowRuntimeNode({ ...mockParams, id: 'connected-1' });\n      const connectedNode2 = new WorkflowRuntimeNode({ ...mockParams, id: 'connected-2' });\n\n      const result = traverseNodes(node, () => [connectedNode1, connectedNode2]);\n\n      expect(result).toHaveLength(2);\n      expect(result).toContain(connectedNode1);\n      expect(result).toContain(connectedNode2);\n    });\n\n    it('should traverse recursively through connected nodes', () => {\n      const node1 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-1' });\n      const node2 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-2' });\n      const node3 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-3' });\n\n      // Setup edges: node -> node1 -> node2 -> node3\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: node1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: node1,\n        to: node2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: node2,\n        to: node3,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      node1.addOutputEdge(edge2);\n      node2.addOutputEdge(edge3);\n\n      const result = traverseNodes(node, (n) => n.next);\n\n      expect(result).toHaveLength(3);\n      expect(result).toContain(node1);\n      expect(result).toContain(node2);\n      expect(result).toContain(node3);\n    });\n  });\n\n  describe('circular reference handling', () => {\n    it('should handle circular references without infinite loop', () => {\n      const node1 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-1' });\n      const node2 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-2' });\n\n      // Create a cycle: node -> node1 -> node2 -> node\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: node1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: node1,\n        to: node2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: node2,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      node1.addOutputEdge(edge2);\n      node2.addOutputEdge(edge3);\n\n      const result = traverseNodes(node, (n) => n.next);\n\n      // Should visit each node only once, avoiding infinite loop\n      expect(result).toHaveLength(3);\n      expect(result).toContain(node1);\n      expect(result).toContain(node2);\n      expect(result).toContain(node); // node will be visited when traversing from node2\n    });\n\n    it('should not revisit already visited nodes', () => {\n      const node1 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-1' });\n      const node2 = new WorkflowRuntimeNode({ ...mockParams, id: 'node-2' });\n      const sharedNode = new WorkflowRuntimeNode({ ...mockParams, id: 'shared-node' });\n\n      // Create diamond pattern: node -> [node1, node2] -> sharedNode\n      const edge1 = {\n        id: 'edge-1',\n        from: node,\n        to: node1,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: node,\n        to: node2,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge3 = {\n        id: 'edge-3',\n        from: node1,\n        to: sharedNode,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge4 = {\n        id: 'edge-4',\n        from: node2,\n        to: sharedNode,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge1);\n      node.addOutputEdge(edge2);\n      node1.addOutputEdge(edge3);\n      node2.addOutputEdge(edge4);\n\n      const result = traverseNodes(node, (n) => n.next);\n\n      // sharedNode should only appear once in the result\n      expect(result).toHaveLength(3);\n      expect(result).toContain(node1);\n      expect(result).toContain(node2);\n      expect(result).toContain(sharedNode);\n\n      // Verify sharedNode appears only once\n      const sharedNodeCount = result.filter((n) => n.id === 'shared-node').length;\n      expect(sharedNodeCount).toBe(1);\n    });\n  });\n\n  describe('different connection types', () => {\n    it('should work with predecessor connections', () => {\n      const predecessor1 = new WorkflowRuntimeNode({ ...mockParams, id: 'pred-1' });\n      const predecessor2 = new WorkflowRuntimeNode({ ...mockParams, id: 'pred-2' });\n\n      const edge1 = {\n        id: 'edge-1',\n        from: predecessor1,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n      const edge2 = {\n        id: 'edge-2',\n        from: predecessor2,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addInputEdge(edge1);\n      node.addInputEdge(edge2);\n\n      const result = traverseNodes(node, (n) => n.prev);\n\n      expect(result).toHaveLength(2);\n      expect(result).toContain(predecessor1);\n      expect(result).toContain(predecessor2);\n    });\n\n    it('should work with custom connection function', () => {\n      const customNode1 = new WorkflowRuntimeNode({ ...mockParams, id: 'custom-1' });\n      const customNode2 = new WorkflowRuntimeNode({ ...mockParams, id: 'custom-2' });\n\n      // Custom function that returns specific nodes\n      const customConnector = (n: INode) => {\n        if (n.id === 'test-node') {\n          return [customNode1];\n        }\n        if (n.id === 'custom-1') {\n          return [customNode2];\n        }\n        return [];\n      };\n\n      const result = traverseNodes(node, customConnector);\n\n      expect(result).toHaveLength(2);\n      expect(result).toContain(customNode1);\n      expect(result).toContain(customNode2);\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle empty connections gracefully', () => {\n      const result = traverseNodes(node, () => []);\n      expect(result).toEqual([]);\n    });\n\n    it('should handle null/undefined connections gracefully', () => {\n      const result = traverseNodes(node, () => []);\n      expect(result).toEqual([]);\n    });\n\n    it('should handle single node with self-reference', () => {\n      const edge = {\n        id: 'self-edge',\n        from: node,\n        to: node,\n        fromPort: {} as IPort,\n        toPort: {} as IPort,\n      };\n\n      node.addOutputEdge(edge);\n\n      const result = traverseNodes(node, (n) => n.next);\n\n      // Should include the node itself when it's connected to itself\n      expect(result).toHaveLength(1);\n      expect(result).toContain(node);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/traverse-nodes.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { INode } from '@flowgram.ai/runtime-interface';\n\n/**\n * Generic function to traverse a node graph\n * @param startNode The starting node\n * @param getConnectedNodes Function to get connected nodes\n * @returns Array of all traversed nodes\n */\nexport function traverseNodes(\n  startNode: INode,\n  getConnectedNodes: (node: INode) => INode[]\n): INode[] {\n  const visited = new Set<string>();\n  const result: INode[] = [];\n\n  const traverse = (node: INode) => {\n    for (const connectedNode of getConnectedNodes(node)) {\n      if (!visited.has(connectedNode.id)) {\n        visited.add(connectedNode.id);\n        result.push(connectedNode);\n        traverse(connectedNode);\n      }\n    }\n  };\n\n  traverse(startNode);\n  return result;\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/infrastructure/utils/uuid.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { v4 } from 'uuid';\n\nexport const uuid = v4;\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/break/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport class BreakExecutor implements INodeExecutor {\n  public type = FlowGramNode.Break;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    context.runtime.cache.set('loop-break', true);\n    return {\n      outputs: {},\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/code/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten';\nimport {\n  CodeNodeSchema,\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport interface CodeExecutorInputs {\n  params: Record<string, any>;\n  script: {\n    language: 'javascript';\n    content: string;\n  };\n}\n\nexport class CodeExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.Code;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = this.parseInputs(context);\n    if (inputs.script.language === 'javascript') {\n      return this.javascript(inputs);\n    }\n    throw new Error(`Unsupported code language \"${inputs.script.language}\"`);\n  }\n\n  private parseInputs(context: ExecutionContext): CodeExecutorInputs {\n    const codeNode = context.node as INode<CodeNodeSchema['data']>;\n    const params = context.inputs;\n    const { language, content } = codeNode.data.script;\n    if (!content) {\n      throw new Error('Code content is required');\n    }\n    return {\n      params,\n      script: {\n        language,\n        content,\n      },\n    };\n  }\n\n  private async javascript(inputs: CodeExecutorInputs): Promise<ExecutionResult> {\n    const { params = {}, script } = inputs;\n\n    // Serialize before allocating WASM resources – fails fast on circular references.\n    const serializedParams = JSON.stringify(params);\n\n    const QuickJS = await getQuickJS();\n\n    // Each execution gets an isolated context; no host globals are exposed by default.\n    const context = QuickJS.newContext();\n    try {\n      // Apply resource limits on the underlying runtime.\n      const runtime = context.runtime;\n      runtime.setMemoryLimit(32 * 1024 * 1024); // 32 MB\n      runtime.setMaxStackSize(512 * 1024); // 512 KB\n      // Interrupt execution if it runs longer than 1 minute.\n      runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + 60_000));\n\n      // Wrap user code: define main, inject params, call main, return result.\n      const wrappedCode = `\n'use strict';\n\n${script.content}\n\nif (typeof main !== 'function') {\n  throw new Error('main function is required in the script');\n}\n\nconst __params__ = ${serializedParams};\nmain({ params: __params__ });\n`;\n\n      const evalResult = context.evalCode(wrappedCode);\n      const resultHandle = context.unwrapResult(evalResult);\n\n      let rawResult: unknown;\n\n      try {\n        const promiseState = context.getPromiseState(resultHandle);\n        if (promiseState.type === 'fulfilled') {\n          rawResult = context.dump(promiseState.value);\n          promiseState.value.dispose();\n        } else if (promiseState.type === 'rejected') {\n          const errMsg = context.dump(promiseState.error);\n          promiseState.error.dispose();\n          throw new Error(typeof errMsg === 'string' ? errMsg : JSON.stringify(errMsg));\n        } else {\n          // Pending promise: resolve asynchronously via the QuickJS event loop.\n          const resolvedResult = await context.resolvePromise(resultHandle);\n          const resolvedHandle = context.unwrapResult(resolvedResult);\n          rawResult = context.dump(resolvedHandle);\n          resolvedHandle.dispose();\n        }\n      } finally {\n        resultHandle.dispose();\n      }\n\n      // Ensure result is a plain object.\n      const outputs =\n        rawResult && typeof rawResult === 'object' && !Array.isArray(rawResult)\n          ? (rawResult as Record<string, unknown>)\n          : { result: rawResult };\n\n      return { outputs };\n    } catch (error: any) {\n      throw new Error(`Code execution failed: ${error.message}`);\n    } finally {\n      // Always release WASM memory for this execution context.\n      context.dispose();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/array.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionArrayHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as object;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/boolean.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionBooleanHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as boolean;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.EQ) {\n    const rightValue = condition.rightValue as boolean;\n    return leftValue === rightValue;\n  }\n  if (operator === ConditionOperator.NEQ) {\n    const rightValue = condition.rightValue as boolean;\n    return leftValue !== rightValue;\n  }\n  if (operator === ConditionOperator.IS_TRUE) {\n    return leftValue === true;\n  }\n  if (operator === ConditionOperator.IS_FALSE) {\n    return leftValue === false;\n  }\n  if (operator === ConditionOperator.IN) {\n    const rightValue = condition.rightValue as boolean[];\n    return rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.NIN) {\n    const rightValue = condition.rightValue as boolean[];\n    return !rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/datetime.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\n// Convert ISO string to Date object for comparison\nconst parseDateTime = (value: string | Date): Date => {\n  if (value instanceof Date) {\n    return value;\n  }\n  return new Date(value);\n};\n\nexport const conditionDateTimeHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as string;\n\n  // Handle empty checks first\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n\n  // For comparison operations, parse both datetime values\n  const leftTime = parseDateTime(leftValue).getTime();\n  const rightValue = condition.rightValue as string;\n  const rightTime = parseDateTime(rightValue).getTime();\n\n  if (operator === ConditionOperator.EQ) {\n    return leftTime === rightTime;\n  }\n  if (operator === ConditionOperator.NEQ) {\n    return leftTime !== rightTime;\n  }\n  if (operator === ConditionOperator.GT) {\n    return leftTime > rightTime;\n  }\n  if (operator === ConditionOperator.GTE) {\n    return leftTime >= rightTime;\n  }\n  if (operator === ConditionOperator.LT) {\n    return leftTime < rightTime;\n  }\n  if (operator === ConditionOperator.LTE) {\n    return leftTime <= rightTime;\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowVariableType } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandlers } from '../type';\nimport { conditionStringHandler } from './string';\nimport { conditionObjectHandler } from './object';\nimport { conditionNumberHandler } from './number';\nimport { conditionNullHandler } from './null';\nimport { conditionMapHandler } from './map';\nimport { conditionDateTimeHandler } from './datetime';\nimport { conditionBooleanHandler } from './boolean';\nimport { conditionArrayHandler } from './array';\n\nexport const conditionHandlers: ConditionHandlers = {\n  [WorkflowVariableType.String]: conditionStringHandler,\n  [WorkflowVariableType.Number]: conditionNumberHandler,\n  [WorkflowVariableType.Integer]: conditionNumberHandler,\n  [WorkflowVariableType.Boolean]: conditionBooleanHandler,\n  [WorkflowVariableType.Object]: conditionObjectHandler,\n  [WorkflowVariableType.Map]: conditionMapHandler,\n  [WorkflowVariableType.Array]: conditionArrayHandler,\n  [WorkflowVariableType.DateTime]: conditionDateTimeHandler,\n  [WorkflowVariableType.Null]: conditionNullHandler,\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/map.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionMapHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as object;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/null.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionNullHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as unknown | null;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.EQ) {\n    return isNil(leftValue) && isNil(condition.rightValue);\n  }\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/number.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionNumberHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as number;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.EQ) {\n    const rightValue = condition.rightValue as number;\n    return leftValue === rightValue;\n  }\n  if (operator === ConditionOperator.NEQ) {\n    const rightValue = condition.rightValue as number;\n    return leftValue !== rightValue;\n  }\n  if (operator === ConditionOperator.GT) {\n    const rightValue = condition.rightValue as number;\n    return leftValue > rightValue;\n  }\n  if (operator === ConditionOperator.GTE) {\n    const rightValue = condition.rightValue as number;\n    return leftValue >= rightValue;\n  }\n  if (operator === ConditionOperator.LT) {\n    const rightValue = condition.rightValue as number;\n    return leftValue < rightValue;\n  }\n  if (operator === ConditionOperator.LTE) {\n    const rightValue = condition.rightValue as number;\n    return leftValue <= rightValue;\n  }\n  if (operator === ConditionOperator.IN) {\n    const rightValue = condition.rightValue as number[];\n    return rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.NIN) {\n    const rightValue = condition.rightValue as number[];\n    return !rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/object.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionObjectHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as object;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/handlers/string.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ConditionOperator } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionHandler } from '../type';\n\nexport const conditionStringHandler: ConditionHandler = (condition) => {\n  const { operator } = condition;\n  const leftValue = condition.leftValue as string;\n  // Switch case share scope, so we need to use if else here\n  if (operator === ConditionOperator.EQ) {\n    const rightValue = condition.rightValue as string;\n    return leftValue === rightValue;\n  }\n  if (operator === ConditionOperator.NEQ) {\n    const rightValue = condition.rightValue as string;\n    return leftValue !== rightValue;\n  }\n  if (operator === ConditionOperator.CONTAINS) {\n    const rightValue = condition.rightValue as string;\n    return leftValue.includes(rightValue);\n  }\n  if (operator === ConditionOperator.NOT_CONTAINS) {\n    const rightValue = condition.rightValue as string;\n    return !leftValue.includes(rightValue);\n  }\n  if (operator === ConditionOperator.IN) {\n    const rightValue = condition.rightValue as string[];\n    return rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.NIN) {\n    const rightValue = condition.rightValue as string[];\n    return !rightValue.includes(leftValue);\n  }\n  if (operator === ConditionOperator.IS_EMPTY) {\n    return isNil(leftValue);\n  }\n  if (operator === ConditionOperator.IS_NOT_EMPTY) {\n    return !isNil(leftValue);\n  }\n  return false;\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport {\n  ConditionItem,\n  ConditionOperator,\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n  WorkflowVariableType,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeType } from '@infra/index';\nimport { ConditionValue, Conditions } from './type';\nimport { conditionRules } from './rules';\nimport { conditionHandlers } from './handlers';\n\nexport class ConditionExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.Condition;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const conditions: Conditions = context.node.data?.conditions;\n    if (!conditions) {\n      return {\n        outputs: {},\n      };\n    }\n    const parsedConditions = conditions\n      .map((item) => this.parseCondition(item, context))\n      .filter((item) => this.checkCondition(item));\n    const activatedCondition = parsedConditions.find((item) => this.handleCondition(item));\n    if (!activatedCondition) {\n      return {\n        outputs: {},\n        branch: 'else',\n      };\n    }\n    return {\n      outputs: {},\n      branch: activatedCondition.key,\n    };\n  }\n\n  private parseCondition(item: ConditionItem, context: ExecutionContext): ConditionValue {\n    const { key, value } = item;\n    const { left, operator, right } = value;\n    const parsedLeft = context.runtime.state.parseRef(left);\n    const leftValue = parsedLeft?.value ?? null;\n    const leftType = parsedLeft?.type ?? WorkflowVariableType.Null;\n    const expectedRightType = this.getRuleType({ leftType, operator });\n    const parsedRight = Boolean(right)\n      ? context.runtime.state.parseFlowValue({\n          flowValue: right,\n          declareType: expectedRightType,\n        })\n      : null;\n    const rightValue = parsedRight?.value ?? null;\n    const rightType = parsedRight?.type ?? WorkflowVariableType.Null;\n    return {\n      key,\n      leftValue,\n      leftType,\n      rightValue,\n      rightType,\n      operator,\n    };\n  }\n\n  private checkCondition(condition: ConditionValue): boolean {\n    const rule = conditionRules[condition.leftType];\n    if (isNil(rule)) {\n      throw new Error(`Condition left type \"${condition.leftType}\" is not supported`);\n    }\n    const ruleType = rule[condition.operator];\n    if (isNil(ruleType)) {\n      throw new Error(\n        `Condition left type \"${condition.leftType}\" has no operator \"${condition.operator}\"`\n      );\n    }\n    if (!WorkflowRuntimeType.isTypeEqual(ruleType, condition.rightType)) {\n      return false;\n    }\n    return true;\n  }\n\n  private handleCondition(condition: ConditionValue): boolean {\n    const handler = conditionHandlers[condition.leftType];\n    if (!handler) {\n      throw new Error(`Condition left type ${condition.leftType} is not supported`);\n    }\n    const isActive = handler(condition);\n    return isActive;\n  }\n\n  private getRuleType(params: {\n    leftType: WorkflowVariableType;\n    operator: ConditionOperator;\n  }): WorkflowVariableType {\n    const { leftType, operator } = params;\n    const rule = conditionRules[leftType];\n    if (isNil(rule)) {\n      return WorkflowVariableType.Null;\n    }\n    const ruleType = rule[operator];\n    if (isNil(ruleType)) {\n      return WorkflowVariableType.Null;\n    }\n    return ruleType;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/rules.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ConditionOperator, WorkflowVariableType } from '@flowgram.ai/runtime-interface';\n\nimport { ConditionRules } from './type';\n\nexport const conditionRules: ConditionRules = {\n  [WorkflowVariableType.String]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.String,\n    [ConditionOperator.NEQ]: WorkflowVariableType.String,\n    [ConditionOperator.CONTAINS]: WorkflowVariableType.String,\n    [ConditionOperator.NOT_CONTAINS]: WorkflowVariableType.String,\n    [ConditionOperator.IN]: WorkflowVariableType.Array,\n    [ConditionOperator.NIN]: WorkflowVariableType.Array,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.String,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.String,\n  },\n  [WorkflowVariableType.Number]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.Number,\n    [ConditionOperator.NEQ]: WorkflowVariableType.Number,\n    [ConditionOperator.GT]: WorkflowVariableType.Number,\n    [ConditionOperator.GTE]: WorkflowVariableType.Number,\n    [ConditionOperator.LT]: WorkflowVariableType.Number,\n    [ConditionOperator.LTE]: WorkflowVariableType.Number,\n    [ConditionOperator.IN]: WorkflowVariableType.Array,\n    [ConditionOperator.NIN]: WorkflowVariableType.Array,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Integer]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.Integer,\n    [ConditionOperator.NEQ]: WorkflowVariableType.Integer,\n    [ConditionOperator.GT]: WorkflowVariableType.Integer,\n    [ConditionOperator.GTE]: WorkflowVariableType.Integer,\n    [ConditionOperator.LT]: WorkflowVariableType.Integer,\n    [ConditionOperator.LTE]: WorkflowVariableType.Integer,\n    [ConditionOperator.IN]: WorkflowVariableType.Array,\n    [ConditionOperator.NIN]: WorkflowVariableType.Array,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Boolean]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.Boolean,\n    [ConditionOperator.NEQ]: WorkflowVariableType.Boolean,\n    [ConditionOperator.IS_TRUE]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_FALSE]: WorkflowVariableType.Null,\n    [ConditionOperator.IN]: WorkflowVariableType.Array,\n    [ConditionOperator.NIN]: WorkflowVariableType.Array,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Object]: {\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Map]: {\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.DateTime]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.DateTime,\n    [ConditionOperator.NEQ]: WorkflowVariableType.DateTime,\n    [ConditionOperator.GT]: WorkflowVariableType.DateTime,\n    [ConditionOperator.GTE]: WorkflowVariableType.DateTime,\n    [ConditionOperator.LT]: WorkflowVariableType.DateTime,\n    [ConditionOperator.LTE]: WorkflowVariableType.DateTime,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Array]: {\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n  [WorkflowVariableType.Null]: {\n    [ConditionOperator.EQ]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_EMPTY]: WorkflowVariableType.Null,\n    [ConditionOperator.IS_NOT_EMPTY]: WorkflowVariableType.Null,\n  },\n};\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/condition/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  WorkflowVariableType,\n  ConditionOperator,\n  ConditionItem,\n} from '@flowgram.ai/runtime-interface';\n\nexport type Conditions = ConditionItem[];\n\nexport type ConditionRule = Partial<Record<ConditionOperator, WorkflowVariableType | null>>;\n\nexport type ConditionRules = Record<WorkflowVariableType, ConditionRule>;\n\nexport interface ConditionValue {\n  key: string;\n  leftValue: unknown | null;\n  rightValue: unknown | null;\n  leftType: WorkflowVariableType;\n  rightType: WorkflowVariableType;\n  operator: ConditionOperator;\n}\n\nexport type ConditionHandler = (condition: ConditionValue) => boolean;\n\nexport type ConditionHandlers = Record<WorkflowVariableType, ConditionHandler>;\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/continue/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport class ContinueExecutor implements INodeExecutor {\n  public type = FlowGramNode.Continue;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    context.runtime.cache.set('loop-continue', true);\n    return {\n      outputs: {},\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/empty/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport class BlockStartExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.BlockStart;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    return {\n      outputs: {},\n    };\n  }\n}\n\nexport class BlockEndExecutor implements INodeExecutor {\n  public type = FlowGramNode.BlockEnd;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    return {\n      outputs: {},\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/end/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport class EndExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.End;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    context.runtime.ioCenter.setOutputs(context.inputs);\n    return {\n      outputs: context.inputs,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/http/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  HTTPBodyType,\n  HTTPMethod,\n  HTTPNodeSchema,\n  INode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport interface HTTPExecutorInputs {\n  method: HTTPMethod;\n  url: string;\n  headers: Record<string, string>;\n  params: Record<string, string>;\n  bodyType: HTTPBodyType;\n  body: string;\n  retryTimes: number;\n  timeout: number;\n}\n\nexport class HTTPExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.HTTP;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = this.parseInputs(context);\n    const response = await this.request(inputs);\n\n    const responseHeaders: Record<string, string> = {};\n    response.headers.forEach((value, key) => {\n      responseHeaders[key] = value;\n    });\n\n    const responseBody = await response.text();\n\n    return {\n      outputs: {\n        headers: responseHeaders,\n        statusCode: response.status,\n        body: responseBody,\n      },\n    };\n  }\n\n  private async request(inputs: HTTPExecutorInputs): Promise<Response> {\n    const { method, url, headers, params, bodyType, body, retryTimes, timeout } = inputs;\n\n    // Build URL with query parameters\n    const urlWithParams = this.buildUrlWithParams(url, params);\n\n    // Prepare request options\n    const requestOptions: RequestInit = {\n      method,\n      headers: this.prepareHeaders(headers, bodyType),\n      signal: AbortSignal.timeout(timeout),\n    };\n\n    // Add body if method supports it\n    if (method !== 'GET' && method !== 'HEAD' && body) {\n      requestOptions.body = this.prepareBody(body, bodyType);\n    }\n\n    // Implement retry logic\n    let lastError: Error | null = null;\n    for (let attempt = 0; attempt <= retryTimes; attempt++) {\n      try {\n        const response = await fetch(urlWithParams, requestOptions);\n        return response;\n      } catch (error) {\n        lastError = error as Error;\n        if (attempt < retryTimes) {\n          // Wait before retry (exponential backoff)\n          await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1000));\n        }\n      }\n    }\n\n    throw lastError || new Error('HTTP request failed after all retry attempts');\n  }\n\n  private parseInputs(context: ExecutionContext): HTTPExecutorInputs {\n    const httpNode = context.node as INode<HTTPNodeSchema['data']>;\n    const method = httpNode.data.api.method;\n    const urlVariable = context.runtime.state.parseTemplate(httpNode.data.api.url);\n    if (!urlVariable) {\n      throw new Error('HTTP url is required');\n    }\n    const url = urlVariable.value;\n    const headers = context.runtime.state.parseInputs({\n      values: httpNode.data.headersValues,\n      declare: httpNode.data.headers,\n    });\n    const params = context.runtime.state.parseInputs({\n      values: httpNode.data.paramsValues,\n      declare: httpNode.data.params,\n    });\n    const body = this.parseBody(context);\n    const retryTimes = httpNode.data.timeout.retryTimes;\n    const timeout = httpNode.data.timeout.timeout;\n    const inputs = {\n      method,\n      url,\n      headers,\n      params,\n      bodyType: body.bodyType,\n      body: body.body,\n      retryTimes,\n      timeout,\n    };\n    context.snapshot.update({\n      inputs: JSON.parse(JSON.stringify(inputs)),\n    });\n    return inputs;\n  }\n\n  private parseBody(context: ExecutionContext): {\n    bodyType: HTTPBodyType;\n    body: string;\n  } {\n    const httpNode = context.node as INode<HTTPNodeSchema['data']>;\n    const bodyType = httpNode.data.body.bodyType;\n    if (bodyType === HTTPBodyType.None) {\n      return {\n        bodyType,\n        body: '',\n      };\n    }\n    if (bodyType === HTTPBodyType.JSON) {\n      if (!httpNode.data.body.json) {\n        throw new Error('HTTP json body is required');\n      }\n      const jsonVariable = context.runtime.state.parseTemplate(httpNode.data.body.json);\n      if (!jsonVariable) {\n        throw new Error('HTTP json body is required');\n      }\n      return {\n        bodyType,\n        body: jsonVariable.value,\n      };\n    }\n    if (bodyType === HTTPBodyType.FormData) {\n      if (!httpNode.data.body.formData || !httpNode.data.body.formDataValues) {\n        throw new Error('HTTP form-data body is required');\n      }\n\n      const formData = context.runtime.state.parseInputs({\n        values: httpNode.data.body.formDataValues,\n        declare: httpNode.data.body.formData,\n      });\n      return {\n        bodyType,\n        body: JSON.stringify(formData),\n      };\n    }\n    if (bodyType === HTTPBodyType.RawText) {\n      if (!httpNode.data.body.json) {\n        throw new Error('HTTP json body is required');\n      }\n      const jsonVariable = context.runtime.state.parseTemplate(httpNode.data.body.json);\n      if (!jsonVariable) {\n        throw new Error('HTTP json body is required');\n      }\n      return {\n        bodyType,\n        body: jsonVariable.value,\n      };\n    }\n    if (bodyType === HTTPBodyType.Binary) {\n      if (!httpNode.data.body.binary) {\n        throw new Error('HTTP binary body is required');\n      }\n      const binaryVariable = context.runtime.state.parseTemplate(httpNode.data.body.binary);\n      if (!binaryVariable) {\n        throw new Error('HTTP binary body is required');\n      }\n      return {\n        bodyType,\n        body: binaryVariable.value,\n      };\n    }\n    if (bodyType === HTTPBodyType.XWwwFormUrlencoded) {\n      if (!httpNode.data.body.xWwwFormUrlencoded || !httpNode.data.body.xWwwFormUrlencodedValues) {\n        throw new Error('HTTP x-www-form-urlencoded body is required');\n      }\n      const xWwwFormUrlencoded = context.runtime.state.parseInputs({\n        values: httpNode.data.body.xWwwFormUrlencodedValues,\n        declare: httpNode.data.body.xWwwFormUrlencoded,\n      });\n      return {\n        bodyType,\n        body: JSON.stringify(xWwwFormUrlencoded),\n      };\n    }\n    throw new Error(`HTTP invalid body type \"${bodyType}\"`);\n  }\n\n  private buildUrlWithParams(url: string, params: Record<string, string>): string {\n    const urlObj = new URL(url);\n    Object.entries(params).forEach(([key, value]) => {\n      if (value !== undefined && value !== null && value !== '') {\n        urlObj.searchParams.set(key, value);\n      }\n    });\n    return urlObj.toString();\n  }\n\n  private prepareHeaders(\n    headers: Record<string, string>,\n    bodyType: HTTPBodyType\n  ): Record<string, string> {\n    const preparedHeaders = { ...headers };\n\n    // Set Content-Type based on body type if not already set\n    if (!preparedHeaders['Content-Type'] && !preparedHeaders['content-type']) {\n      switch (bodyType) {\n        case HTTPBodyType.JSON:\n          preparedHeaders['Content-Type'] = 'application/json';\n          break;\n        case HTTPBodyType.FormData:\n          // Don't set Content-Type for FormData, let browser set it with boundary\n          break;\n        case HTTPBodyType.XWwwFormUrlencoded:\n          preparedHeaders['Content-Type'] = 'application/x-www-form-urlencoded';\n          break;\n        case HTTPBodyType.RawText:\n          preparedHeaders['Content-Type'] = 'text/plain';\n          break;\n        case HTTPBodyType.Binary:\n          preparedHeaders['Content-Type'] = 'application/octet-stream';\n          break;\n      }\n    }\n\n    return preparedHeaders;\n  }\n\n  private prepareBody(body: string, bodyType: HTTPBodyType): string | FormData {\n    switch (bodyType) {\n      case HTTPBodyType.JSON:\n        return body;\n      case HTTPBodyType.FormData:\n        const formData = new FormData();\n        try {\n          const data = JSON.parse(body);\n          Object.entries(data).forEach(([key, value]) => {\n            formData.append(key, String(value));\n          });\n        } catch (error) {\n          throw new Error('Invalid FormData body format');\n        }\n        return formData;\n      case HTTPBodyType.XWwwFormUrlencoded:\n        try {\n          const data = JSON.parse(body);\n          const params = new URLSearchParams();\n          Object.entries(data).forEach(([key, value]) => {\n            params.append(key, String(value));\n          });\n          return params.toString();\n        } catch (error) {\n          throw new Error('Invalid x-www-form-urlencoded body format');\n        }\n      case HTTPBodyType.RawText:\n      case HTTPBodyType.Binary:\n      default:\n        return body;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { INodeExecutorFactory } from '@flowgram.ai/runtime-interface';\n\nimport { StartExecutor } from './start';\nimport { LoopExecutor } from './loop';\nimport { LLMExecutor } from './llm';\nimport { HTTPExecutor } from './http';\nimport { EndExecutor } from './end';\nimport { BlockEndExecutor, BlockStartExecutor } from './empty';\nimport { ContinueExecutor } from './continue';\nimport { ConditionExecutor } from './condition';\nimport { CodeExecutor } from './code';\nimport { BreakExecutor } from './break';\n\nexport const WorkflowRuntimeNodeExecutors: INodeExecutorFactory[] = [\n  StartExecutor,\n  EndExecutor,\n  LLMExecutor,\n  ConditionExecutor,\n  LoopExecutor,\n  BlockStartExecutor,\n  BlockEndExecutor,\n  HTTPExecutor,\n  CodeExecutor,\n  BreakExecutor,\n  ContinueExecutor,\n];\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/llm/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport { ChatOpenAI } from '@langchain/openai';\nimport { SystemMessage, HumanMessage, BaseMessageLike } from '@langchain/core/messages';\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport interface LLMExecutorInputs {\n  modelName: string;\n  apiKey: string;\n  apiHost: string;\n  temperature: number;\n  systemPrompt?: string;\n  prompt: string;\n}\n\nexport class LLMExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.LLM;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const inputs = context.inputs as LLMExecutorInputs;\n    this.checkInputs(inputs);\n\n    const { modelName, temperature, apiKey, apiHost, systemPrompt, prompt } = inputs;\n\n    const model = new ChatOpenAI({\n      modelName,\n      temperature,\n      apiKey,\n      configuration: {\n        baseURL: apiHost,\n      },\n      maxRetries: 3,\n    });\n\n    const messages: BaseMessageLike[] = [];\n\n    if (systemPrompt) {\n      messages.push(new SystemMessage(systemPrompt));\n    }\n    messages.push(new HumanMessage(prompt));\n\n    let apiMessage;\n    try {\n      apiMessage = await model.invoke(messages);\n    } catch (error) {\n      // 调用 LLM API 失败\n      const errorMessage = (error as Error)?.message;\n      if (errorMessage === 'Connection error.') {\n        throw new Error(`Network error: unreachable api \"${apiHost}\"`);\n      }\n      throw error;\n    }\n\n    const result = apiMessage.content;\n    return {\n      outputs: {\n        result,\n      },\n    };\n  }\n\n  protected checkInputs(inputs: LLMExecutorInputs) {\n    const { modelName, temperature, apiKey, apiHost, prompt } = inputs;\n    const missingInputs = [];\n\n    if (!modelName) missingInputs.push('modelName');\n    if (isNil(temperature)) missingInputs.push('temperature');\n    if (!apiKey) missingInputs.push('apiKey');\n    if (!apiHost) missingInputs.push('apiHost');\n    if (!prompt) missingInputs.push('prompt');\n\n    if (missingInputs.length > 0) {\n      throw new Error(`LLM node missing required inputs: \"${missingInputs.join('\", \"')}\"`);\n    }\n\n    this.checkApiHost(apiHost);\n  }\n\n  private checkApiHost(apiHost: string): void {\n    if (!apiHost || typeof apiHost !== 'string') {\n      throw new Error(`Invalid API host format - ${apiHost}`);\n    }\n\n    const url = new URL(apiHost);\n    if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n      throw new Error(`Invalid API host protocol - ${url.protocol}`);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/loop/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { isNil } from 'lodash-es';\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  IContext,\n  IEngine,\n  IFlowRefValue,\n  INode,\n  INodeExecutor,\n  IVariableParseResult,\n  LoopNodeSchema,\n  WorkflowVariableType,\n} from '@flowgram.ai/runtime-interface';\n\nimport { WorkflowRuntimeType } from '@infra/index';\n\ntype LoopArray = Array<any>;\ntype LoopBlockVariables = Record<string, IVariableParseResult>;\ntype LoopOutputs = Record<string, any>;\n\nexport interface LoopExecutorInputs {\n  loopFor: LoopArray;\n}\n\nexport class LoopExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.Loop;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    const loopNodeID = context.node.id;\n\n    const engine = context.container.get<IEngine>(IEngine);\n    const { value: loopArray, itemsType } = this.getLoopArrayVariable(context);\n    const subNodes = context.node.children;\n    const blockStartNode = subNodes.find((node) => node.type === FlowGramNode.BlockStart);\n\n    if (!blockStartNode) {\n      throw new Error('Loop block start node not found');\n    }\n\n    const blockOutputs: LoopOutputs[] = [];\n\n    // not use Array method to make error stack more concise, and better performance\n    for (let index = 0; index < loopArray.length; index++) {\n      const loopItem = loopArray[index];\n      const subContext = context.runtime.sub();\n      subContext.variableStore.setVariable({\n        nodeID: `${loopNodeID}_locals`,\n        key: 'item',\n        type: itemsType,\n        value: loopItem,\n      });\n      subContext.variableStore.setVariable({\n        nodeID: `${loopNodeID}_locals`,\n        key: 'index',\n        type: WorkflowVariableType.Number,\n        value: index,\n      });\n      try {\n        await engine.executeNode({\n          context: subContext,\n          node: blockStartNode,\n        });\n      } catch (e) {\n        throw new Error(`Loop block execute error`);\n      }\n      if (this.isBreak(subContext)) {\n        break;\n      }\n      if (this.isContinue(subContext)) {\n        continue;\n      }\n      const blockOutput = this.getBlockOutput(context, subContext);\n      blockOutputs.push(blockOutput);\n    }\n\n    this.setLoopNodeOutputs(context, blockOutputs);\n    const outputs = this.combineBlockOutputs(context, blockOutputs);\n\n    return {\n      outputs,\n    };\n  }\n\n  private getLoopArrayVariable(\n    executionContext: ExecutionContext\n  ): IVariableParseResult<LoopArray> & {\n    itemsType: WorkflowVariableType;\n  } {\n    const loopNodeData = executionContext.node.data as LoopNodeSchema['data'];\n    const LoopArrayVariable = executionContext.runtime.state.parseRef<LoopArray>(\n      loopNodeData.loopFor\n    );\n    this.checkLoopArray(LoopArrayVariable);\n    return LoopArrayVariable as IVariableParseResult<LoopArray> & {\n      itemsType: WorkflowVariableType;\n    };\n  }\n\n  private checkLoopArray(LoopArrayVariable: IVariableParseResult<LoopArray> | null): void {\n    const loopArray = LoopArrayVariable?.value;\n    if (!loopArray || isNil(loopArray) || !Array.isArray(loopArray)) {\n      throw new Error('Loop \"loopFor\" is required');\n    }\n    const loopArrayType = LoopArrayVariable.type;\n    if (loopArrayType !== WorkflowVariableType.Array) {\n      throw new Error('Loop \"loopFor\" must be an array');\n    }\n    const loopArrayItemType = LoopArrayVariable.itemsType;\n    if (isNil(loopArrayItemType)) {\n      throw new Error('Loop \"loopFor.items\" must be array items');\n    }\n  }\n\n  private getBlockOutput(\n    executionContext: ExecutionContext,\n    subContext: IContext\n  ): LoopBlockVariables {\n    const loopOutputsDeclare = this.getLoopOutputsDeclare(executionContext);\n    const blockOutput = Object.entries(loopOutputsDeclare).reduce(\n      (acc, [outputName, outputRef]) => {\n        const outputVariable = subContext.state.parseRef(outputRef);\n        if (!outputVariable) {\n          return acc;\n        }\n        return {\n          ...acc,\n          [outputName]: outputVariable,\n        };\n      },\n      {} as LoopBlockVariables\n    );\n    return blockOutput;\n  }\n\n  private setLoopNodeOutputs(\n    executionContext: ExecutionContext,\n    blockOutputs: LoopBlockVariables[]\n  ) {\n    const loopNode = executionContext.node as INode<LoopNodeSchema['data']>;\n    const loopOutputsDeclare = this.getLoopOutputsDeclare(executionContext);\n    const loopOutputNames = Object.keys(loopOutputsDeclare);\n    loopOutputNames.forEach((outputName) => {\n      const outputVariables = blockOutputs.map((blockOutput) => blockOutput[outputName]);\n      const outputTypes = outputVariables.map((fieldVariable) => fieldVariable.type);\n      const itemsType = WorkflowRuntimeType.getArrayItemsType(outputTypes);\n      const value = outputVariables.map((fieldVariable) => fieldVariable.value);\n      executionContext.runtime.variableStore.setVariable({\n        nodeID: loopNode.id,\n        key: outputName,\n        type: WorkflowVariableType.Array,\n        itemsType,\n        value,\n      });\n    });\n  }\n\n  private combineBlockOutputs(\n    executionContext: ExecutionContext,\n    blockOutputs: LoopBlockVariables[]\n  ): LoopOutputs {\n    const loopOutputsDeclare = this.getLoopOutputsDeclare(executionContext);\n    const loopOutputNames = Object.keys(loopOutputsDeclare);\n    const loopOutput = loopOutputNames.reduce(\n      (outputs, outputName) => ({\n        ...outputs,\n        [outputName]: blockOutputs.map((blockOutput) => blockOutput[outputName].value),\n      }),\n      {} as LoopOutputs\n    );\n    return loopOutput;\n  }\n\n  private getLoopOutputsDeclare(executionContext: ExecutionContext): Record<string, IFlowRefValue> {\n    const loopNodeData = executionContext.node.data as LoopNodeSchema['data'];\n    const loopOutputsDeclare = loopNodeData.loopOutputs ?? {};\n    return loopOutputsDeclare;\n  }\n\n  private isBreak(subContext: IContext): boolean {\n    return subContext.cache.get('loop-break') === true;\n  }\n\n  private isContinue(subContext: IContext): boolean {\n    return subContext.cache.get('loop-continue') === true;\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/src/nodes/start/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  ExecutionContext,\n  ExecutionResult,\n  FlowGramNode,\n  INodeExecutor,\n} from '@flowgram.ai/runtime-interface';\n\nexport class StartExecutor implements INodeExecutor {\n  public readonly type = FlowGramNode.Start;\n\n  public async execute(context: ExecutionContext): Promise<ExecutionResult> {\n    return {\n      outputs: context.runtime.ioCenter.inputs,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/runtime/js-core/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@application/*\": [\n        \"application/*\"\n      ],\n      \"@workflow/*\": [\n        \"domain/*\"\n      ],\n      \"@infra/*\": [\n        \"infrastructure/*\"\n      ],\n      \"@nodes/*\": [\n        \"nodes/*\"\n      ],\n    }\n  },\n  \"include\": [\n    \"./src\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/runtime/js-core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { config } from \"dotenv\";\nimport path from 'path';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  resolve: {\n    alias: [\n      {find: \"@application\", replacement: path.resolve(__dirname, './src/application') },\n      {find: \"@workflow\", replacement: path.resolve(__dirname, './src/domain') },\n      {find: \"@infra\", replacement: path.resolve(__dirname, './src/infrastructure') },\n      {find: \"@nodes\", replacement: path.resolve(__dirname, './src/nodes') },\n    ],\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'node',\n    testTimeout: 15000,\n    setupFiles: [path.resolve(__dirname, './src/domain/__tests__/setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n    env: {\n      ...config({ path: path.resolve(__dirname, './.env/.env.test') }).parsed\n    }\n  },\n});\n"
  },
  {
    "path": "packages/runtime/nodejs/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.*\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n!.yarn/releases\n!.yarn/versions\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# env files (can opt-in for committing if needed)\n.env*\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n"
  },
  {
    "path": "packages/runtime/nodejs/README.md",
    "content": "# FlowGram Runtime NodeJS\n\n## Starting the server\n```bash\npnpm dev\n```\n\n## Building the server\n```bash\npnpm build\n```\n\n## Running the server\n```bash\nnode dist/index.js\n```\n"
  },
  {
    "path": "packages/runtime/nodejs/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { defineFlatConfig } from '@flowgram.ai/eslint-config';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nexport default defineFlatConfig({\n  parser: '@typescript-eslint/parser',\n  preset: 'node',\n  packageRoot: __dirname,\n  ignore: ['.eslintrc.cjs'],\n  parserOptions: {\n    requireConfigFile: false,\n    ecmaVersion: 2017,\n    sourceType: 'module',\n    ecmaFeatures: {\n      modules: true,\n    },\n  },\n  rules: {\n    'no-console': 'off',\n  },\n  plugins: ['json', '@typescript-eslint'],\n  settings: {\n    react: {\n      version: '18',\n    },\n  },\n});\n"
  },
  {
    "path": "packages/runtime/nodejs/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/runtime-nodejs\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/index.js\"\n  },\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"dev\": \"tsx watch src\",\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format esm --sourcemap --out-dir dist\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"lint\": \"eslint --cache src\",\n    \"lint-fix\": \"eslint --fix src\",\n    \"type-check\": \"tsc\",\n    \"start\": \"node dist/index.js\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/runtime-js\": \"workspace:*\",\n    \"@flowgram.ai/runtime-interface\": \"workspace:*\",\n    \"@fastify/cors\": \"^8.2.1\",\n    \"@fastify/swagger\": \"^8.5.1\",\n    \"@fastify/swagger-ui\": \"4.1.0\",\n    \"@fastify/websocket\": \"^10.0.1\",\n    \"@trpc/server\": \"^10.27.1\",\n    \"trpc-openapi\": \"^1.2.0\",\n    \"fastify\": \"^4.17.0\",\n    \"zod\": \"^3.24.4\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@types/cors\": \"^2.8.13\",\n    \"dotenv\": \"~16.5.0\",\n    \"@types/node\": \"^18\",\n    \"eslint\": \"^9.0.0\",\n    \"typescript\": \"^5.8.3\",\n    \"tsup\": \"^8.0.1\",\n    \"tsx\": \"~4.19.4\",\n    \"eslint-plugin-json\": \"^4.0.1\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.0.0\",\n    \"@typescript-eslint/parser\": \"^8.0.0\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/runtime/nodejs/src/api/create-api.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport z from 'zod';\nimport { WorkflowRuntimeAPIs } from '@flowgram.ai/runtime-js';\nimport { FlowGramAPIMethod, FlowGramAPIName, FlowGramAPIs } from '@flowgram.ai/runtime-interface';\n\nimport { APIHandler } from './type';\nimport { publicProcedure } from './trpc';\n\nexport const createAPI = (apiName: FlowGramAPIName): APIHandler => {\n  const define = FlowGramAPIs[apiName];\n  const caller = WorkflowRuntimeAPIs[apiName];\n  if (define.method === FlowGramAPIMethod.GET) {\n    const procedure = publicProcedure\n      .meta({\n        openapi: {\n          method: define.method,\n          path: define.path,\n          summary: define.name,\n          tags: [define.module],\n        },\n      })\n      .input(define.schema.input)\n      .output(z.union([define.schema.output, z.undefined()]))\n      .query(async (opts) => {\n        const input = opts.input;\n        try {\n          const output = await caller(input);\n          return output;\n        } catch {\n          return undefined;\n        }\n      });\n\n    return {\n      define,\n      procedure: procedure as any,\n    };\n  }\n\n  const procedure = publicProcedure\n    .meta({\n      openapi: {\n        method: define.method,\n        path: define.path,\n        summary: define.name,\n        tags: [define.module],\n      },\n    })\n    .input(define.schema.input)\n    .output(z.union([define.schema.output, z.undefined()]))\n    .mutation(async (opts) => {\n      const input = opts.input;\n      try {\n        const output = await caller(input);\n        return output;\n      } catch {\n        return undefined;\n      }\n    });\n  return {\n    define,\n    procedure: procedure as any,\n  };\n};\n"
  },
  {
    "path": "packages/runtime/nodejs/src/api/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowGramAPINames } from '@flowgram.ai/runtime-interface';\n\nimport { APIRouter } from './type';\nimport { router } from './trpc';\nimport { createAPI } from './create-api';\n\nconst APIS = FlowGramAPINames.map((apiName) => createAPI(apiName));\n\nexport const routers = APIS.reduce((acc, api) => {\n  acc[api.define.path] = api.procedure;\n  return acc;\n}, {} as APIRouter);\n\nexport const appRouter = router(routers);\n\nexport type AppRouter = typeof appRouter;\n"
  },
  {
    "path": "packages/runtime/nodejs/src/api/trpc.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { OpenApiMeta } from 'trpc-openapi';\nimport { initTRPC } from '@trpc/server';\n\nimport type { Context } from '../server/context';\n\nconst t = initTRPC\n  .context<Context>()\n  .meta<OpenApiMeta>()\n  .create({\n    errorFormatter({ shape }) {\n      return shape;\n    },\n  });\n\nexport const router = t.router;\nexport const publicProcedure = t.procedure;\n"
  },
  {
    "path": "packages/runtime/nodejs/src/api/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { BuildProcedure } from '@trpc/server';\nimport { FlowGramAPIDefine } from '@flowgram.ai/runtime-interface';\n\nexport interface APIHandler {\n  define: FlowGramAPIDefine;\n  procedure: BuildProcedure<any, any, any>;\n}\n\nexport type APIRouter = Record<FlowGramAPIDefine['path'], APIHandler['procedure']>;\n"
  },
  {
    "path": "packages/runtime/nodejs/src/config/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { ServerParams } from '@server/type';\n\nexport const ServerConfig: ServerParams = {\n  name: 'flowgram-runtime',\n  title: 'FlowGram Runtime',\n  description: 'FlowGram Runtime Demo',\n  runtime: 'nodejs',\n  version: '0.0.1',\n  dev: false,\n  port: 4000,\n  basePath: '/api',\n  docsPath: '/docs',\n};\n"
  },
  {
    "path": "packages/runtime/nodejs/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { createServer } from '@server/index';\n\nasync function main() {\n  const server = await createServer();\n  server.start();\n}\n\nmain();\n"
  },
  {
    "path": "packages/runtime/nodejs/src/server/context.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';\n\nexport function createContext(ctx: CreateFastifyContextOptions) {\n  const { req, res } = ctx;\n  return { req, res };\n}\n\nexport type Context = Awaited<ReturnType<typeof createContext>>;\n"
  },
  {
    "path": "packages/runtime/nodejs/src/server/docs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { generateOpenApiDocument } from 'trpc-openapi';\n\nimport { ServerConfig } from '@config/index';\nimport { appRouter } from '@api/index';\n\n// Generate OpenAPI schema document\nexport const serverDocument = generateOpenApiDocument(appRouter, {\n  title: ServerConfig.title,\n  description: ServerConfig.description,\n  version: ServerConfig.version,\n  baseUrl: `http://localhost:${ServerConfig.port}${ServerConfig.basePath}`,\n  docsUrl: 'https://flowgram.ai',\n  tags: ['Task'],\n});\n"
  },
  {
    "path": "packages/runtime/nodejs/src/server/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { fastifyTRPCOpenApiPlugin } from 'trpc-openapi';\nimport fastify from 'fastify';\nimport { fastifyTRPCPlugin } from '@trpc/server/adapters/fastify';\nimport { ServerInfoDefine, type ServerInfoOutput } from '@flowgram.ai/runtime-interface';\nimport ws from '@fastify/websocket';\nimport fastifySwaggerUI from '@fastify/swagger-ui';\nimport fastifySwagger from '@fastify/swagger';\nimport cors from '@fastify/cors';\n\nimport { ServerConfig } from '@config/index';\nimport { appRouter } from '@api/index';\nimport { serverDocument } from './docs';\nimport { createContext } from './context';\n\nexport async function createServer() {\n  const server = fastify({ logger: ServerConfig.dev });\n\n  await server.register(cors);\n  await server.register(ws);\n  await server.register(fastifyTRPCPlugin, {\n    prefix: '/trpc',\n    useWss: false,\n    trpcOptions: { router: appRouter, createContext },\n  });\n  await server.register(fastifyTRPCOpenApiPlugin, {\n    basePath: ServerConfig.basePath,\n    router: appRouter,\n    createContext,\n  } as any);\n\n  await server.register(fastifySwagger, {\n    mode: 'static',\n    specification: { document: serverDocument },\n    uiConfig: { displayOperationId: true },\n    exposeRoute: true,\n  } as any);\n\n  await server.register(fastifySwaggerUI, {\n    routePrefix: ServerConfig.docsPath,\n    uiConfig: {\n      docExpansion: 'full',\n      deepLinking: false,\n    },\n    uiHooks: {\n      onRequest: function (request, reply, next) {\n        next();\n      },\n      preHandler: function (request, reply, next) {\n        next();\n      },\n    },\n    staticCSP: true,\n    transformStaticCSP: (header) => header,\n    transformSpecification: (swaggerObject, request, reply) => swaggerObject,\n    transformSpecificationClone: true,\n  });\n\n  server.get(ServerInfoDefine.path, async (): Promise<ServerInfoOutput> => {\n    const serverTime = new Date();\n    const output: ServerInfoOutput = {\n      name: ServerConfig.name,\n      title: ServerConfig.title,\n      description: ServerConfig.description,\n      runtime: ServerConfig.runtime,\n      version: ServerConfig.version,\n      time: serverTime.toISOString(),\n    };\n    return output;\n  });\n\n  const stop = async () => {\n    await server.close();\n  };\n  const start = async () => {\n    try {\n      const address = await server.listen({ port: ServerConfig.port });\n      await server.ready();\n      server.swagger();\n      console.log(\n        `> Listen Port: ${ServerConfig.port}\\n> Server Address: ${address}\\n> API Docs: http://localhost:4000/docs`\n      );\n    } catch (err) {\n      server.log.error(err);\n      process.exit(1);\n    }\n  };\n\n  return { server, start, stop };\n}\n"
  },
  {
    "path": "packages/runtime/nodejs/src/server/type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ServerInfoOutput } from '@flowgram.ai/runtime-interface';\n\nexport interface ServerParams extends Omit<ServerInfoOutput, 'time'> {\n  dev: boolean;\n  port: number;\n  basePath: string;\n  docsPath: string;\n}\n"
  },
  {
    "path": "packages/runtime/nodejs/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"plugins\": [],\n    \"baseUrl\": \"src\",\n    \"paths\": {\n      \"@api/*\": [\n        \"api/*\"\n      ],\n      \"@application/*\": [\n        \"application/*\"\n      ],\n      \"@server/*\": [\n        \"server/*\"\n      ],\n      \"@config/*\": [\n        \"config/*\"\n      ],\n      \"@workflow/*\": [\n        \"workflow/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \"src/workflow/executor/condition/constant.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/runtime/nodejs/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { config } from \"dotenv\";\nimport path from 'path';\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  resolve: {\n    alias: [\n      {find: \"@api\", replacement: path.resolve(__dirname, './src/api') },\n      {find: \"@application\", replacement: path.resolve(__dirname, './src/application') },\n      {find: \"@server\", replacement: path.resolve(__dirname, './src/server') },\n      {find: \"@config\", replacement: path.resolve(__dirname, './src/config') },\n      {find: \"@workflow\", replacement: path.resolve(__dirname, './src/workflow') },\n    ],\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    testTimeout: 15000,\n    setupFiles: [path.resolve(__dirname, './src/workflow/__tests__/setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n    env: {\n      ...config({ path: path.resolve(__dirname, './.env/.env.test') }).parsed\n    }\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  ignorePatterns: ['**/tests__'],\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/json-schema\",\n  \"version\": \"0.1.8\",\n  \"description\": \"json schema type manager\",\n  \"keywords\": [\n    \"flow\",\n    \"variable\",\n    \"scope\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"exit 0\",\n    \"test:cov\": \"exit 0\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/variable-core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/base/base-type-manager.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { BaseTypeRegistry, TypeRegistryCreator } from './types';\n\n@injectable()\nexport abstract class BaseTypeManager<\n  Schema,\n  Registry extends BaseTypeRegistry,\n  Manager extends BaseTypeManager<Schema, Registry, Manager>\n> {\n  protected typeRegistryMap: Map<string, Registry> = new Map();\n\n  protected onTypeRegistryChangeEmitter = new Emitter<Registry[]>();\n\n  public onTypeRegistryChange = this.onTypeRegistryChangeEmitter.event;\n\n  /**\n   * 获取 typeSchema 对应的 type\n   * 不能直接访问 type 的原因的是\n   * Schema 中 type 可能为空，需要获取 $ref 中的 type\n   */\n  protected abstract getTypeNameFromSchema(typeSchema: Schema): string;\n\n  getTypeByName(typeName: string): Registry | undefined {\n    return this.typeRegistryMap.get(typeName);\n  }\n\n  getTypeBySchema(type: Schema): Registry | undefined {\n    const key = this.getTypeNameFromSchema(type);\n    return this.typeRegistryMap.get(key);\n  }\n\n  getDefaultTypeRegistry(): Registry | undefined {\n    return this.typeRegistryMap.get('default');\n  }\n\n  /**\n   * 注册 TypeRegistry\n   */\n  register(\n    originRegistry: Partial<Registry> | TypeRegistryCreator<Schema, Registry, Manager>\n  ): void {\n    const registry =\n      typeof originRegistry === 'function'\n        ? originRegistry({ typeManager: this as unknown as Manager })\n        : originRegistry;\n\n    const type = registry.type;\n    if (!type) {\n      return;\n    }\n\n    let originTypeRegistry = this.getTypeByName(type);\n\n    let extendTypeRegistry = registry.extend\n      ? this.getTypeByName(registry.extend)\n      : this.getDefaultTypeRegistry();\n\n    if (originTypeRegistry) {\n      // 如果是重复注册的类型，进行 merge\n      this.typeRegistryMap.set(type, {\n        ...extendTypeRegistry,\n        ...originTypeRegistry,\n        ...registry,\n      });\n    } else {\n      this.typeRegistryMap.set(type, {\n        ...extendTypeRegistry,\n        ...registry,\n      } as Registry);\n    }\n  }\n\n  triggerChanges() {\n    this.onTypeRegistryChangeEmitter.fire(Array.from(this.typeRegistryMap.values()));\n  }\n\n  /**\n   * 获取全量的 TypeRegistries\n   */\n  public getAllTypeRegistries(): Registry[] {\n    return Array.from(this.typeRegistryMap.values());\n  }\n\n  unregister(type: string): void {\n    this.typeRegistryMap.delete(type);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/base/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { BaseTypeManager } from './base-type-manager';\nexport { BaseTypeRegistry, TypeRegistryCreator } from './types';\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/base/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type BaseTypeManager } from './base-type-manager';\n\n/**\n * Base information for TypeRegistry\n */\nexport interface BaseTypeRegistry {\n  /**\n   * type reference\n   */\n  type: string;\n  /**\n   * The inherited type. If there is an inheritance, the definition of this type will use the inherited type definition,\n   * and the new definition will override the inherited definition.\n   */\n  extend?: string;\n}\n\n/**\n * TypeRegistryCreator\n */\nexport type TypeRegistryCreator<\n  Schema,\n  Registry extends BaseTypeRegistry,\n  Manager extends BaseTypeManager<Schema, Registry, Manager>\n> = (ctx: { typeManager: Manager }) => Partial<Registry>;\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/container-module.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { JsonSchemaTypeManager } from './json-schema';\nimport { BaseTypeManager } from './base';\n\nexport const TypeManager = Symbol('TypeManager');\n\nexport const jsonSchemaContainerModule = new ContainerModule((bind) => {\n  bind(BaseTypeManager).to(JsonSchemaTypeManager).inSingletonScope();\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/context.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React, { createContext, useContext, useMemo } from 'react';\n\nimport { usePlaygroundContainer } from '@flowgram.ai/core';\n\nimport {\n  IJsonSchema,\n  jsonSchemaTypeManager,\n  JsonSchemaTypeManager,\n  JsonSchemaTypeRegistry,\n} from './json-schema';\nimport { BaseTypeManager, TypeRegistryCreator } from './base';\n\n// use global default\nconst TypePresetContext = createContext<JsonSchemaTypeManager | null>(null);\n\nexport const useTypeManager = () => {\n  const typeManagerFromContext = useContext(TypePresetContext);\n  const container = usePlaygroundContainer();\n\n  if (typeManagerFromContext) {\n    return typeManagerFromContext;\n  }\n\n  if (container?.isBound?.(BaseTypeManager)) {\n    return container.get(BaseTypeManager);\n  }\n\n  // Global Singleton\n  return jsonSchemaTypeManager;\n};\n\nexport const TypePresetProvider = <\n  Registry extends JsonSchemaTypeRegistry = JsonSchemaTypeRegistry\n>({\n  children,\n  types,\n}: React.PropsWithChildren<{\n  types: (\n    | Partial<Registry>\n    | TypeRegistryCreator<IJsonSchema, Registry, JsonSchemaTypeManager<IJsonSchema, Registry>>\n  )[];\n}>) => {\n  const schemaManager = useMemo(() => {\n    const typeManager = new JsonSchemaTypeManager<IJsonSchema, Registry>();\n\n    types.forEach((_type) => typeManager.register(_type));\n\n    return typeManager;\n  }, [...types]);\n\n  return (\n    <TypePresetContext.Provider value={schemaManager as unknown as JsonSchemaTypeManager}>\n      {children}\n    </TypePresetContext.Provider>\n  );\n};\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './base';\nexport * from './json-schema';\n\nexport { useTypeManager, TypePresetProvider } from './context';\nexport { jsonSchemaContainerModule } from './container-module';\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { JsonSchemaTypeManager } from './json-schema-type-manager';\n\nexport {\n  JsonSchemaBasicType,\n  IJsonSchema,\n  IBasicJsonSchema,\n  JsonSchemaTypeRegistry,\n  JsonSchemaTypeRegistryCreator,\n} from './types';\nexport { JsonSchemaUtils } from './utils';\nexport { JsonSchemaTypeManager } from './json-schema-type-manager';\n\n// Global JsonSchemaTypeManager\nexport const jsonSchemaTypeManager = new JsonSchemaTypeManager();\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/json-schema-type-manager.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\nimport React from 'react';\n\nimport { injectable } from 'inversify';\n\nimport { IJsonSchema, JsonSchemaTypeRegistry, JsonSchemaTypeRegistryCreator } from './types';\nimport { defaultTypeDefinitionRegistry } from './type-definition/default';\nimport { dateTimeRegistryCreator } from './type-definition/date-time';\nimport { BaseTypeManager } from '../base';\nimport {\n  arrayRegistryCreator,\n  booleanRegistryCreator,\n  integerRegistryCreator,\n  mapRegistryCreator,\n  numberRegistryCreator,\n  objectRegistryCreator,\n  stringRegistryCreator,\n  unknownRegistryCreator,\n} from './type-definition';\n\n@injectable()\nexport class JsonSchemaTypeManager<\n  Schema extends Partial<IJsonSchema> = IJsonSchema,\n  Registry extends JsonSchemaTypeRegistry<Schema> = JsonSchemaTypeRegistry<Schema>\n> extends BaseTypeManager<Schema, Registry, JsonSchemaTypeManager<Schema, Registry>> {\n  /**\n   * get type name\n   * @param typeSchema\n   * @returns\n   */\n  protected getTypeNameFromSchema(typeSchema: Schema): string {\n    if (!typeSchema) {\n      return 'unknown';\n    }\n    if (typeSchema.enum) {\n      return 'enum';\n    }\n    if (typeSchema.format && typeSchema.type === 'string') {\n      return typeSchema.format;\n    }\n\n    return typeSchema.type || typeSchema.$ref || 'unknown';\n  }\n\n  constructor() {\n    super();\n\n    const registries = [\n      defaultTypeDefinitionRegistry,\n      stringRegistryCreator,\n      integerRegistryCreator,\n      numberRegistryCreator,\n      booleanRegistryCreator,\n      objectRegistryCreator,\n      arrayRegistryCreator,\n      unknownRegistryCreator,\n      mapRegistryCreator,\n      dateTimeRegistryCreator,\n    ];\n\n    registries.forEach((registry) => {\n      this.register(\n        registry as unknown as JsonSchemaTypeRegistryCreator<\n          Schema,\n          Registry,\n          JsonSchemaTypeManager<Schema, Registry>\n        >\n      );\n    });\n  }\n\n  /**\n   * Get TypeRegistries based on the current parentType\n   */\n  public getTypeRegistriesWithParentType = (parentType = ''): Registry[] =>\n    this.getAllTypeRegistries()\n      .filter((v) => v.type !== 'default')\n      .filter((v) => !v.parentType || v.parentType.includes(parentType));\n\n  /**\n   * Get the deepest child field of a field\n   * Array<Array<String>> -> String\n   */\n  getTypeSchemaDeepChildField = (type: Schema) => {\n    let registry = this.getTypeBySchema(type);\n\n    let childType = type;\n\n    while (registry?.getItemType && registry.getItemType(childType)) {\n      childType = registry.getItemType(childType)!;\n      registry = this.getTypeBySchema(childType);\n    }\n\n    return childType;\n  };\n\n  /**\n   * Get the plain text display string of the type schema, for example:\n   * Array<Array<String>>, Map<String, Number>\n   */\n  public getComplexText = (type: Schema): string => {\n    const registry = this.getTypeBySchema(type);\n\n    if (registry?.customComplexText) {\n      return registry.customComplexText(type);\n    }\n\n    if (registry?.container && type.items) {\n      return `${registry.label}<${this.getComplexText(type.items as Schema)}>`;\n    } else if (registry?.container && type.additionalProperties) {\n      return `${registry.label}<String, ${this.getComplexText(\n        type.additionalProperties as Schema\n      )}>`;\n    } else {\n      return registry?.label || type.type || 'unknown';\n    }\n  };\n\n  public getDisplayIcon = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.getDisplayIcon?.(type) || registry?.icon || <></>;\n  };\n\n  public getTypeSchemaProperties = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.getTypeSchemaProperties(type);\n  };\n\n  public getPropertiesParent = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.getPropertiesParent(type);\n  };\n\n  public getJsonPaths = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.getJsonPaths(type);\n  };\n\n  public canAddField = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.canAddField(type);\n  };\n\n  public getDefaultValue = (type: Schema) => {\n    const registry = this.getTypeBySchema(type);\n    return registry?.getDefaultValue();\n  };\n}\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/array.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const arrayRegistryCreator: JsonSchemaTypeRegistryCreator = ({ typeManager }) => {\n  const icon = (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M5.23759 1.00342H2.00391V14.997H5.23759V13.6251H3.35127V2.37534H5.23759V1.00342Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.7624 1.00342H13.9961V14.997H10.7624V13.6251H12.6487V2.37534H10.7624V1.00342Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n\n  const getDisplayIcon = (type: IJsonSchema): JSX.Element => {\n    const item = type.items;\n    const config = typeManager.getTypeBySchema(item!);\n\n    switch (config?.type) {\n      case 'object':\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM3.82031 5.9091C3.82031 5.18535 4.40703 4.59863 5.13078 4.59863C5.85453 4.59863 6.44124 5.18535 6.44124 5.9091C6.44124 6.56485 5.9596 7.1081 5.33078 7.2044V8.70018H5.32877C5.32982 8.75093 5.33078 8.80912 5.33078 8.87034V9.72111C5.33078 10.0195 5.57268 10.2614 5.87109 10.2614H6.24124C6.55613 10.2614 6.8114 10.5167 6.8114 10.8316C6.8114 11.1465 6.55613 11.4017 6.24124 11.4017H5.87109C4.94291 11.4017 4.19047 10.6493 4.19047 9.72111V6.82186C3.96158 6.58607 3.82031 6.26397 3.82031 5.9091ZM7.33679 5.9091C7.33679 5.59421 7.59205 5.33894 7.90694 5.33894H11.6085C11.9234 5.33894 12.1786 5.59421 12.1786 5.9091C12.1786 6.22399 11.9234 6.47925 11.6085 6.47925H7.90694C7.59205 6.47925 7.33679 6.22399 7.33679 5.9091ZM7.33679 9.86846C7.33679 9.55357 7.59205 9.2983 7.90694 9.2983H11.6085C11.9234 9.2983 12.1786 9.55357 12.1786 9.86846C12.1786 10.1833 11.9234 10.4386 11.6085 10.4386H7.90694C7.59205 10.4386 7.33679 10.1833 7.33679 9.86846Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n      case 'string': {\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM5.23701 4.07158C5.50364 3.3161 6.56205 3.28894 6.86709 4.02974L10 11.6383C10.1329 11.9609 9.979 12.3302 9.65631 12.4631C9.33363 12.596 8.96434 12.4421 8.83147 12.1194L7.8021 9.61951H4.61903L3.7474 12.0891C3.63126 12.4182 3.27034 12.5908 2.94127 12.4747C2.6122 12.3585 2.43958 11.9976 2.55573 11.6685L5.23701 4.07158ZM6.08814 5.45704L5.06505 8.35579H7.28174L6.08814 5.45704ZM8.81938 6.07534C8.81938 5.75166 9.08177 5.48926 9.40545 5.48926H12.8941C13.2178 5.48926 13.4802 5.75166 13.4802 6.07534C13.4802 6.39902 13.2178 6.66142 12.8941 6.66142H9.40545C9.08177 6.66142 8.81938 6.39902 8.81938 6.07534ZM10.2668 9.69181C10.2668 9.36812 10.5292 9.10573 10.8529 9.10573H12.8941C13.2178 9.10573 13.4802 9.36812 13.4802 9.69181C13.4802 10.0155 13.2178 10.2779 12.8941 10.2779H10.8529C10.5292 10.2779 10.2668 10.0155 10.2668 9.69181Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n      }\n      case 'enum': {\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M3.6139 1.58105H0V14.4186H3.6139V13.1264H1.36702V2.87326H3.6139V1.58105ZM3.41656 13.3266V13.3264H1.16987V13.3266H3.41656ZM0.197537 14.2186H0.197344V1.78105H3.41656V1.78125H0.197537V14.2186ZM16 1.58105H12.3861V2.87326H14.633V13.1264H12.3861V14.4186H16V1.58105ZM12.5834 1.78105V2.67326H12.5836V1.78125H15.8027V1.78105H12.5834ZM12.5836 14.2186V13.3266H14.8305V2.67345H14.8303V13.3264H12.5834V14.2186H12.5836Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              d=\"M6.01442 7.34421C5.89401 7.46462 5.89401 7.65985 6.01442 7.78026L7.78218 9.54802C7.9026 9.66844 8.09782 9.66844 8.21823 9.54802L9.986 7.78026C10.1064 7.65985 10.1064 7.46462 9.986 7.34421L9.69137 7.04958C9.57096 6.92917 9.37573 6.92917 9.25532 7.04958L8.00021 8.3047L6.74509 7.04958C6.62468 6.92917 6.42946 6.92917 6.30904 7.04958L6.01442 7.34421ZM3.31699 7.99984C3.31699 10.5864 5.41379 12.6832 8.00033 12.6832C10.5869 12.6832 12.6837 10.5864 12.6837 7.99984C12.6837 5.4133 10.5869 3.3165 8.00033 3.3165C5.41379 3.3165 3.31699 5.4133 3.31699 7.99984ZM11.6503 7.99984C11.6503 10.0157 10.0162 11.6498 8.00033 11.6498C5.98449 11.6498 4.35033 10.0157 4.35033 7.99984C4.35033 5.984 5.98449 4.34984 8.00033 4.34984C10.0162 4.34984 11.6503 5.984 11.6503 7.99984Z\"\n              fill=\"currentColor\"\n              stroke=\"currentColor\"\n              strokeWidth=\"0.2\"\n            />\n          </svg>\n        );\n      }\n\n      case 'integer': {\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM10.3614 5.22374C10.7161 4.90585 11.1581 4.75 11.6762 4.75C12.2173 4.75 12.6723 4.91467 13.0281 5.25207L13.0291 5.253C13.3852 5.59688 13.561 6.03946 13.561 6.56767C13.561 6.89 13.4945 7.17448 13.3539 7.41445C13.2572 7.57972 13.1279 7.71948 12.9685 7.83428C13.1575 7.95643 13.3099 8.11182 13.4225 8.30109C13.5793 8.5644 13.6531 8.88311 13.6531 9.24936C13.6531 9.83787 13.4612 10.3151 13.0656 10.6612C12.6982 10.9795 12.2305 11.1341 11.6762 11.1341C11.1356 11.1341 10.6805 10.9925 10.324 10.6977C9.92124 10.3691 9.71723 9.90026 9.69942 9.31256L9.69473 9.15802H10.846L10.8539 9.2997C10.8689 9.5698 10.9591 9.75553 11.1096 9.87941L11.1106 9.88027C11.2519 9.99882 11.4365 10.0631 11.6762 10.0631C11.9765 10.0631 12.1743 9.98692 12.2984 9.86071C12.4229 9.73404 12.4984 9.53136 12.4984 9.22422C12.4984 8.92116 12.4215 8.72127 12.2939 8.59581C12.1658 8.46989 11.961 8.39373 11.6511 8.39373H11.3586V7.34788H11.6511C11.9297 7.34788 12.111 7.27834 12.2238 7.16555C12.3366 7.05276 12.4062 6.87138 12.4062 6.59281C12.4062 6.30696 12.3378 6.12041 12.2277 6.00501C12.1188 5.89092 11.9446 5.82098 11.6762 5.82098C11.4248 5.82098 11.2539 5.88537 11.1407 5.99325C11.0268 6.10185 10.9497 6.27522 10.9291 6.5375L10.9183 6.67577H9.76788L9.77492 6.51904C9.79886 5.98644 9.99237 5.54989 10.3614 5.22374ZM5.91032 5.26037C6.26612 4.92297 6.72112 4.7583 7.26219 4.7583C7.80751 4.7583 8.26297 4.91938 8.61401 5.25194L8.61501 5.25289C8.96719 5.59272 9.13852 6.04185 9.13852 6.58435C9.13852 6.84997 9.08709 7.09565 8.9817 7.31883L8.98114 7.31999C8.89563 7.49712 8.74775 7.71415 8.54418 7.96862L8.54322 7.96981L6.87446 10.0127H9.13852V11.0753H5.36909V10.1089L7.69946 7.27679C7.89456 7.04062 7.98374 6.80773 7.98374 6.57597C7.98374 6.29602 7.91626 6.11385 7.8078 6.00122C7.70036 5.88964 7.52811 5.8209 7.26219 5.8209C7.04017 5.8209 6.87439 5.88173 6.75075 5.99193C6.61227 6.11766 6.53226 6.30918 6.53226 6.59273V6.74273H5.37747V6.59273C5.37747 6.05443 5.55248 5.60586 5.90934 5.2613L5.91032 5.26037ZM3.50907 4.80865H4.56964V11.0754H3.41486V6.2201L2.25 7.24249V5.89561L3.50907 4.80865Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n      }\n\n      case 'number': {\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M3.6139 1.58154H0V14.4191H3.6139V13.1269H1.36702V2.87375H3.6139V1.58154ZM3.41656 13.3271V13.3269H1.17155V13.3271H3.41656ZM0.199219 14.2191H0.197344V1.78154H3.41656V1.78174H0.199219V14.2191ZM16 1.58154H12.3861V2.87375H14.633V13.1269H12.3861V14.4191H16V1.58154ZM12.5834 1.78154V2.67375H12.5853V1.78174H15.8027V1.78154H12.5834ZM12.5853 14.2191V13.3271H14.8322V2.67394H14.8303V13.3269H12.5834V14.2191H12.5853ZM6.86771 4.5C5.87019 4.5 5.00104 5.30767 5.00104 6.31667V9.63333C5.00104 10.6423 5.87019 11.45 6.86771 11.45C7.86523 11.45 8.73438 10.6423 8.73438 9.63333V6.31667C8.73438 5.30767 7.86523 4.5 6.86771 4.5ZM11.1177 4.5C10.1202 4.5 9.25104 5.30767 9.25104 6.31667V9.63333C9.25104 10.6423 10.1202 11.45 11.1177 11.45C12.1152 11.45 12.9844 10.6423 12.9844 9.63333V6.31667C12.9844 5.30767 12.1152 4.5 11.1177 4.5ZM6.13438 6.31667C6.13438 5.9503 6.47884 5.63333 6.86771 5.63333C7.25657 5.63333 7.60104 5.9503 7.60104 6.31667V9.63333C7.60104 9.9997 7.25657 10.3167 6.86771 10.3167C6.47884 10.3167 6.13438 9.9997 6.13438 9.63333V6.31667ZM10.3844 6.31667C10.3844 5.9503 10.7288 5.63333 11.1177 5.63333C11.5066 5.63333 11.851 5.9503 11.851 6.31667V9.63333C11.851 9.9997 11.5066 10.3167 11.1177 10.3167C10.7288 10.3167 10.3844 9.9997 10.3844 9.63333V6.31667ZM3.75938 9.85C3.33135 9.85 2.98438 10.197 2.98438 10.625C2.98438 11.053 3.33135 11.4 3.75938 11.4C4.1874 11.4 4.53438 11.053 4.53438 10.625C4.53438 10.197 4.1874 9.85 3.75938 9.85Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n      }\n\n      case 'boolean':\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M0 1.58105H3.6139V2.87326H1.36702V13.1264H3.6139V14.4186H0V1.58105ZM3.41656 13.3264V13.3266H1.17155V13.3264H3.41656ZM0.197344 14.2186H0.199219V1.78125H3.41656V1.78105H0.197344V14.2186ZM12.3861 1.58105H16V14.4186H12.3861V13.1264H14.633V2.87326H12.3861V1.58105ZM12.5834 2.67326V1.78105H15.8027V1.78125H12.5853V2.67326H12.5834ZM12.5853 13.3266V14.2186H12.5834V13.3264H14.8303V2.67345H14.8322V13.3266H12.5853ZM2.75 7.99993C2.75 6.14518 4.25358 4.6416 6.10833 4.6416H9.775C11.6298 4.6416 13.1333 6.14518 13.1333 7.99993C13.1333 9.85469 11.6298 11.3583 9.775 11.3583H6.10833C4.25358 11.3583 2.75 9.85469 2.75 7.99993ZM6.10833 5.85827C4.92552 5.85827 3.96667 6.81713 3.96667 7.99993C3.96667 9.18274 4.92552 10.1416 6.10833 10.1416H9.775C10.9578 10.1416 11.9167 9.18274 11.9167 7.99993C11.9167 6.81713 10.9578 5.85827 9.775 5.85827H6.10833ZM8.25 7.99993C8.25 7.1577 8.93277 6.47493 9.775 6.47493C10.6172 6.47493 11.3 7.1577 11.3 7.99993C11.3 8.84217 10.6172 9.52493 9.775 9.52493C8.93277 9.52493 8.25 8.84217 8.25 7.99993ZM9.775 7.6916C9.60471 7.6916 9.46667 7.82965 9.46667 7.99993C9.46667 8.17022 9.60471 8.30827 9.775 8.30827C9.94529 8.30827 10.0833 8.17022 10.0833 7.99993C10.0833 7.82965 9.94529 7.6916 9.775 7.6916Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n\n      case 'stream':\n      case 'map': {\n        return (\n          <svg\n            width=\"16\"\n            height=\"16\"\n            viewBox=\"0 0 16 16\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M3.6139 1.58105H0V14.4186H3.6139V13.1264H1.36702V2.87326H3.6139V1.58105ZM3.41656 13.3266V13.3264H1.16987V13.3266H3.41656ZM0.197537 14.2186H0.197344V1.78105H3.41656V1.78125H0.197537V14.2186ZM16 1.58105H12.3861V2.87326H14.633V13.1264H12.3861V14.4186H16V1.58105ZM12.5834 1.78105V2.67326H12.5836V1.78125H15.8027V1.78105H12.5834ZM12.5836 14.2186V13.3266H14.8305V2.67345H14.8303V13.3264H12.5834V14.2186H12.5836Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M5.02962 3.6001H10.7696C11.0983 3.6001 11.4119 3.73574 11.6424 3.97635C11.8719 4.21592 11.9994 4.53988 11.9994 4.87541V10.0898C12.0056 10.2146 11.9675 10.3376 11.8918 10.4372L11.8662 10.4709L11.8287 10.4905C11.6508 10.5836 11.5323 10.6684 11.4581 10.7562C11.3893 10.8377 11.3538 10.9274 11.3538 11.0488C11.3538 11.2093 11.3914 11.285 11.4495 11.3472C11.5199 11.4227 11.6265 11.4867 11.8093 11.5964C11.8188 11.6022 11.8286 11.608 11.8385 11.614L11.8704 11.6331L11.8927 11.6629C11.9509 11.7407 11.9869 11.8329 11.997 11.9295C12.007 12.0258 11.9909 12.123 11.9503 12.2109C11.9101 12.2996 11.8454 12.375 11.7639 12.4282C11.682 12.4817 11.5865 12.5106 11.4887 12.5114L11.487 12.5114H5.02962C4.70105 12.5114 4.38727 12.3758 4.1573 12.1349C3.92697 11.8925 3.79871 11.5702 3.79981 11.2358V4.87541C3.79981 4.54075 3.92717 4.21692 4.15725 3.97736C4.26942 3.85898 4.40438 3.76453 4.55403 3.69969C4.7039 3.63476 4.86629 3.60087 5.02962 3.6001ZM5.03192 4.65263C5.00441 4.65342 4.97731 4.65952 4.95211 4.6706C4.92641 4.6819 4.90318 4.69816 4.88376 4.71845C4.86435 4.73874 4.84912 4.76266 4.83897 4.78884C4.82881 4.81502 4.82391 4.84295 4.82456 4.87103L4.82467 4.87541L4.82461 9.64304C4.94924 9.6064 5.07861 9.5878 5.2092 9.58824L10.9746 9.58755V4.87065C10.9752 4.8426 10.9703 4.81472 10.9602 4.78858C10.95 4.76244 10.9348 4.73857 10.9153 4.71832C10.8959 4.69807 10.8727 4.68184 10.847 4.67056C10.8219 4.65951 10.7948 4.65342 10.7673 4.65263H10.7696V4.46303L10.7657 4.65259L10.7673 4.65263H5.03192ZM5.2088 10.6401C5.00466 10.6401 4.82461 10.8155 4.82461 11.0498C4.82461 11.2833 5.00452 11.4589 5.2088 11.4589H10.4698C10.4687 11.456 10.4676 11.4531 10.4665 11.4502C10.4145 11.3112 10.4004 11.1647 10.4 11.0496C10.4 10.9312 10.4101 10.7838 10.4699 10.6401L5.2088 10.6401Z\"\n              fill=\"currentColor\"\n            />\n            <path\n              fillRule=\"evenodd\"\n              clipRule=\"evenodd\"\n              d=\"M5.71924 4.53617C5.71924 4.25475 5.94738 4.02661 6.2288 4.02661C6.51022 4.02661 6.73835 4.25475 6.73835 4.53617V9.65545C6.73835 9.93688 6.51022 10.165 6.2288 10.165C5.94737 10.165 5.71924 9.93688 5.71924 9.65545V4.53617Z\"\n              fill=\"currentColor\"\n            />\n          </svg>\n        );\n      }\n\n      case 'array': {\n        return getDisplayIcon(item!);\n      }\n      default:\n        return icon;\n    }\n  };\n  return {\n    type: 'array',\n\n    label: 'Array',\n\n    icon,\n\n    container: true,\n\n    getJsonPaths: (type: IJsonSchema) => {\n      const itemDefinition = type.items && typeManager.getTypeBySchema(type.items);\n      const childrenPath = itemDefinition?.getJsonPaths\n        ? itemDefinition.getJsonPaths(type.items!)\n        : [];\n\n      return ['items', ...childrenPath];\n    },\n\n    getDefaultValue: () => [],\n\n    getSupportedItemTypes: (): Array<{ type: string; disabled?: string }> =>\n      typeManager.getTypeRegistriesWithParentType('array'),\n\n    getTypeSchemaProperties: (type: IJsonSchema): Record<string, IJsonSchema> => {\n      const itemDefinition = type.items && typeManager.getTypeBySchema(type.items);\n      return (itemDefinition && itemDefinition.getTypeSchemaProperties?.(type.items!)) || {};\n    },\n\n    canAddField: (type: IJsonSchema) => {\n      if (!type.items) {\n        return false;\n      }\n\n      const childConfig = typeManager.getTypeBySchema(type.items);\n\n      return childConfig?.canAddField?.(type.items) || false;\n    },\n\n    getItemType: (type) => type.items,\n\n    getStringValueByTypeSchema: (type: IJsonSchema): string => {\n      if (!type.items) {\n        return type.type || '';\n      }\n\n      const childConfig = typeManager.getTypeBySchema(type.items);\n\n      return [type.type, childConfig?.getStringValueByTypeSchema?.(type.items)].join('-');\n    },\n\n    getTypeSchemaByStringValue: (optionValue: string): IJsonSchema => {\n      if (!optionValue) {\n        return { type: 'array' };\n      }\n\n      const [root, ...rest] = optionValue.split('-');\n\n      const rootType = typeManager.getTypeByName(root);\n\n      if (!rootType) {\n        return { type: 'array' };\n      }\n\n      let itemType;\n      if (rootType.getTypeSchemaByStringValue) {\n        itemType = rootType.getTypeSchemaByStringValue(rest.join('-'))!;\n      } else {\n        itemType = rootType?.getDefaultSchema();\n      }\n\n      return {\n        type: 'array',\n        items: itemType,\n      };\n    },\n\n    getDefaultSchema: (): IJsonSchema => ({\n      type: 'array',\n      items: { type: 'string' },\n    }),\n\n    getPropertiesParent: (type: IJsonSchema) => {\n      const itemDef = type.items && typeManager.getTypeBySchema(type.items);\n\n      return itemDef && itemDef.getPropertiesParent\n        ? itemDef.getPropertiesParent(type.items!)\n        : type;\n    },\n    getDisplayIcon,\n  };\n};\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/boolean.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const booleanRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'boolean',\n\n  label: 'Boolean',\n\n  icon: (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M10.668 4.66683H5.33463C3.49369 4.66683 2.0013 6.15921 2.0013 8.00016C2.0013 9.84111 3.49369 11.3335 5.33463 11.3335H10.668C12.5089 11.3335 14.0013 9.84111 14.0013 8.00016C14.0013 6.15921 12.5089 4.66683 10.668 4.66683ZM5.33463 3.3335C2.75731 3.3335 0.667969 5.42283 0.667969 8.00016C0.667969 10.5775 2.75731 12.6668 5.33463 12.6668H10.668C13.2453 12.6668 15.3346 10.5775 15.3346 8.00016C15.3346 5.42283 13.2453 3.3335 10.668 3.3335H5.33463Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M8.66797 8.00016C8.66797 6.89559 9.5634 6.00016 10.668 6.00016C11.7725 6.00016 12.668 6.89559 12.668 8.00016C12.668 9.10473 11.7725 10.0002 10.668 10.0002C9.5634 10.0002 8.66797 9.10473 8.66797 8.00016ZM10.668 7.3335C10.2998 7.3335 10.0013 7.63197 10.0013 8.00016C10.0013 8.36835 10.2998 8.66683 10.668 8.66683C11.0362 8.66683 11.3346 8.36835 11.3346 8.00016C11.3346 7.63197 11.0362 7.3335 10.668 7.3335Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n\n  getDefaultSchema: () => ({ type: 'boolean' }),\n\n  getValueText: (value?: unknown) => (value === undefined ? '' : value ? 'True' : 'False'),\n\n  getDefaultValue: () => false,\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/date-time.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const dateTimeRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'date-time',\n\n  label: 'DateTime',\n\n  icon: (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      focusable=\"false\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M2 5v14a3 3 0 0 0 3 3h7.1a7.02 7.02 0 0 1-1.43-2H6a2 2 0 0 1-2-2V8a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2.67c.75.36 1.43.85 2 1.43V5a3 3 0 0 0-3-3H5a3 3 0 0 0-3 3Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path d=\"M16 10h1c-.54 0-1.06.06-1.57.18A1 1 0 0 1 16 10Z\" fill=\"currentColor\"></path>\n      <path\n        d=\"M13.5 10.94a1 1 0 0 0-1-.94h-1a1 1 0 0 0-1 1v1a1 1 0 0 0 .77.97 7.03 7.03 0 0 1 2.23-2.03Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M7 10a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1H7Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M6 16a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-1Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M22 17a5 5 0 1 1-10 0 5 5 0 0 1 10 0Zm-4-2a1 1 0 1 0-2 0v2c0 .27.1.52.3.7l1.5 1.5a1 1 0 0 0 1.4-1.4L18 16.58V15Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  ),\n\n  // TODO date-time compat format\n  // https://json-schema.org/understanding-json-schema/reference/type#built-in-formats\n  getDefaultSchema: () => ({\n    type: 'date-time',\n  }),\n\n  getValueText: (value?: unknown) => (value ? `${value}` : ''),\n\n  getDefaultValue: () => '',\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/default.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const defaultTypeDefinitionRegistry: JsonSchemaTypeRegistryCreator = ({ typeManager }) => ({\n  type: 'default',\n\n  label: '',\n\n  icon: <></>,\n\n  container: false,\n\n  getJsonPaths: () => [],\n\n  getDisplayIcon: (schema): JSX.Element => typeManager.getTypeBySchema(schema)?.icon || <></>,\n\n  getPropertiesParent: () => undefined,\n\n  canAddField: () => false,\n\n  getDefaultValue: () => undefined,\n\n  getValueText: () => '',\n\n  getStringValueByTypeSchema: (type: IJsonSchema): string | undefined => type.type,\n\n  getTypeSchemaProperties: () => undefined,\n\n  getDisplayLabel: (schema) => (\n    <div\n      style={{\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        gap: 8,\n      }}\n    >\n      {typeManager.getTypeBySchema(schema)?.label}\n      <span\n        style={{\n          fontSize: 12,\n          whiteSpace: 'nowrap',\n          overflow: 'hidden',\n          textOverflow: 'ellipsis',\n        }}\n      >\n        {typeManager.getComplexText(schema)}\n      </span>\n    </div>\n  ),\n\n  getDisplayText: (type: IJsonSchema) => typeManager.getComplexText(type),\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { stringRegistryCreator } from './string';\nexport { objectRegistryCreator } from './object';\nexport { numberRegistryCreator } from './number';\nexport { booleanRegistryCreator } from './boolean';\nexport { arrayRegistryCreator } from './array';\nexport { integerRegistryCreator } from './integer';\nexport { unknownRegistryCreator } from './unknown';\nexport { mapRegistryCreator } from './map';\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/integer.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const integerRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'integer',\n  label: 'Integer',\n\n  icon: (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M15.132 11.4601C15.644 11.0121 15.9 10.3921 15.9 9.60007C15.9 8.60807 15.5 7.93607 14.7 7.58407C15.412 7.23207 15.768 6.62407 15.768 5.76007C15.768 5.05607 15.536 4.48007 15.072 4.03207C14.608 3.59207 14.012 3.37207 13.284 3.37207C12.588 3.37207 12.008 3.58007 11.544 3.99607C11.064 4.42007 10.808 4.98807 10.776 5.70007H12C12.064 4.88407 12.492 4.47607 13.284 4.47607C14.124 4.47607 14.544 4.91607 14.544 5.79607C14.544 6.66007 14.112 7.09207 13.248 7.09207H13.044V8.16007H13.248C14.2 8.16007 14.676 8.62807 14.676 9.56407C14.676 10.5081 14.212 10.9801 13.284 10.9801C12.9 10.9801 12.584 10.8761 12.336 10.6681C12.064 10.4441 11.916 10.1161 11.892 9.68407H10.668C10.692 10.4761 10.964 11.0841 11.484 11.5081C11.948 11.8921 12.548 12.0841 13.284 12.0841C14.036 12.0841 14.652 11.8761 15.132 11.4601Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M4.46875 12.0003V10.9083L7.75675 6.91228C8.06075 6.54428 8.21275 6.16428 8.21275 5.77228C8.21275 4.90828 7.79675 4.47628 6.96475 4.47628C6.60475 4.47628 6.31275 4.57628 6.08875 4.77628C5.83275 5.00828 5.70475 5.34828 5.70475 5.79628H4.48075C4.48075 5.07628 4.71275 4.49228 5.17675 4.04428C5.64075 3.60428 6.23675 3.38428 6.96475 3.38428C7.70075 3.38428 8.29675 3.60028 8.75275 4.03228C9.20875 4.47228 9.43675 5.05628 9.43675 5.78428C9.43675 6.13628 9.36875 6.45628 9.23275 6.74428C9.12075 6.97628 8.92075 7.27228 8.63275 7.63228L5.95675 10.9083H9.43675V12.0003H4.46875Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M1.668 12.0001V4.78805L0 6.25205V4.89605L1.668 3.45605H2.892V12.0001H1.668Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n\n  getDefaultSchema: (): IJsonSchema => ({ type: 'integer' }),\n\n  getValueText: (value: unknown) => (value !== undefined ? `${value}` : ''),\n\n  getDefaultValue: () => 0,\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/map.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const mapRegistryCreator: JsonSchemaTypeRegistryCreator = ({ typeManager }) => {\n  const icon = (\n    <svg\n      viewBox=\"0 0 1024 1024\"\n      version=\"1.1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"16\"\n      height=\"16\"\n    >\n      <path\n        d=\"M877.860571 938.642286h-645.851428c-27.574857 0-54.052571-11.337143-73.508572-31.744a110.957714 110.957714 0 0 1-30.500571-76.8V193.828571c0-28.745143 10.971429-56.32 30.500571-76.726857a101.888 101.888 0 0 1 73.508572-31.817143h574.171428c27.501714 0 53.979429 11.337143 73.508572 31.744 19.529143 20.333714 30.500571 48.054857 30.500571 76.8v522.020572a34.157714 34.157714 0 0 1-6.948571 22.820571c-37.156571 19.382857-57.636571 39.350857-57.636572 72.630857 0 39.716571 19.894857 50.029714 57.636572 72.777143a34.816 34.816 0 0 1-8.045714 49.298286 32.256 32.256 0 0 1-17.334858 5.193143z m-32.256-254.537143V193.828571a40.228571 40.228571 0 0 0-39.497142-41.179428H232.009143a40.301714 40.301714 0 0 0-39.497143 41.252571V699.245714c17.773714-9.874286 37.449143-14.994286 57.417143-14.921143h595.675428v-0.073142z m-595.675428 187.245714h566.198857c-22.893714-11.190857-27.940571-39.497143-28.013714-59.977143 0-20.260571 3.218286-43.885714 28.013714-59.904h-566.125714c-31.670857 0-57.417143 26.843429-57.417143 59.977143 0 33.060571 25.746286 59.904 57.344 59.904z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        d=\"M320 128m32.036571 0l-0.073142 0q32.036571 0 32.036571 32.036571l0 511.926858q0 32.036571-32.036571 32.036571l0.073142 0q-32.036571 0-32.036571-32.036571l0-511.926858q0-32.036571 32.036571-32.036571Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  );\n\n  return {\n    type: 'map',\n\n    label: 'Map',\n\n    icon,\n\n    container: true,\n\n    getJsonPaths: (type: IJsonSchema) => {\n      const itemDefinition =\n        type.additionalProperties && typeManager.getTypeBySchema(type.additionalProperties);\n      const childrenPath = itemDefinition?.getJsonPaths\n        ? itemDefinition.getJsonPaths(type.additionalProperties!)\n        : [];\n\n      return ['additionalProperties', ...childrenPath];\n    },\n\n    getDefaultValue: () => [],\n\n    getSupportedItemTypes: (): Array<{ type: string; disabled?: string }> =>\n      typeManager.getTypeRegistriesWithParentType('map'),\n\n    getTypeSchemaProperties: (type: IJsonSchema): Record<string, IJsonSchema> => {\n      const itemDefinition =\n        type.additionalProperties && typeManager.getTypeBySchema(type.additionalProperties);\n      return (\n        (itemDefinition && itemDefinition.getTypeSchemaProperties?.(type.additionalProperties!)) ||\n        {}\n      );\n    },\n\n    canAddField: (type: IJsonSchema) => {\n      if (!type.additionalProperties) {\n        return false;\n      }\n\n      const childConfig = typeManager.getTypeBySchema(type.additionalProperties);\n\n      return childConfig?.canAddField?.(type.additionalProperties) || false;\n    },\n\n    getItemType: (type) => type.additionalProperties,\n\n    getStringValueByTypeSchema: (type: IJsonSchema): string => {\n      if (!type.additionalProperties) {\n        return type.type || '';\n      }\n\n      const childConfig = typeManager.getTypeBySchema(type.additionalProperties);\n\n      return [type.type, childConfig?.getStringValueByTypeSchema?.(type.additionalProperties)].join(\n        '-'\n      );\n    },\n\n    getTypeSchemaByStringValue: (optionValue: string): IJsonSchema => {\n      if (!optionValue) {\n        return { type: 'map' };\n      }\n\n      const [root, ...rest] = optionValue.split('-');\n\n      const rooType = typeManager.getTypeByName(root);\n\n      if (!rooType) {\n        return { type: 'map' };\n      }\n\n      let itemType;\n      if (rooType.getTypeSchemaByStringValue) {\n        itemType = rooType.getTypeSchemaByStringValue(rest.join('-'))!;\n      } else {\n        itemType = rooType?.getDefaultSchema();\n      }\n\n      return {\n        type: 'map',\n        additionalProperties: itemType,\n      };\n    },\n\n    getDefaultSchema: (): IJsonSchema => ({\n      type: 'map',\n      additionalProperties: { type: 'string' },\n    }),\n\n    getPropertiesParent: (type: IJsonSchema) => {\n      const itemDef =\n        type.additionalProperties && typeManager.getTypeBySchema(type.additionalProperties);\n\n      return itemDef && itemDef.getPropertiesParent\n        ? itemDef.getPropertiesParent(type.additionalProperties!)\n        : type;\n    },\n  };\n};\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/number.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const numberRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'number',\n\n  label: 'Number',\n\n  extend: 'integer',\n\n  icon: (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M3.44151 5.3068C3.44151 3.83404 4.71542 2.64014 6.18818 2.64014C7.66094 2.64014 8.93484 3.83404 8.93484 5.3068V10.6135C8.93484 12.0862 7.66094 13.2801 6.18818 13.2801C4.71542 13.2801 3.44151 12.0862 3.44151 10.6135V5.3068ZM7.60151 5.3068C7.60151 4.57042 6.92456 3.97347 6.18818 3.97347C5.4518 3.97347 4.77484 4.57042 4.77484 5.3068V10.6135C4.77484 11.3498 5.4518 11.9468 6.18818 11.9468C6.92456 11.9468 7.60151 11.3498 7.60151 10.6135V5.3068Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M12.9882 2.64014C11.5154 2.64014 10.2415 3.83404 10.2415 5.3068V10.6135C10.2415 12.0862 11.5154 13.2801 12.9882 13.2801C14.4609 13.2801 15.7348 12.0862 15.7348 10.6135V5.3068C15.7348 3.83404 14.4609 2.64014 12.9882 2.64014ZM14.4015 10.6135C14.4015 11.3498 13.7246 11.9468 12.9882 11.9468C12.2518 11.9468 11.5748 11.3498 11.5748 10.6135V5.3068C11.5748 4.57042 12.2518 3.97347 12.9882 3.97347C13.7246 3.97347 14.4015 4.57042 14.4015 5.3068V10.6135Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M1.21484 13.2001C1.76713 13.2001 2.21484 12.7524 2.21484 12.2001C2.21484 11.6479 1.76713 11.2001 1.21484 11.2001C0.662559 11.2001 0.214844 11.6479 0.214844 12.2001C0.214844 12.7524 0.662559 13.2001 1.21484 13.2001Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n  getValueText: (value?: unknown) => (value ? `${value}` : ''),\n\n  getDefaultSchema: (): IJsonSchema => ({ type: 'number' }),\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/object.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const objectRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'object',\n  label: 'Object',\n\n  icon: (\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M5.33893 1.5835C5.66613 1.5835 5.93137 1.88142 5.93137 2.20862C5.93137 2.53582 5.66613 2.76838 5.33893 2.76838H4.9099C4.34717 2.76838 4.08062 3.07557 4.08062 3.71921V6.58633C4.08062 7.30996 3.80723 7.84734 3.26798 8.19105C3.11426 8.28902 3.10884 8.55273 3.26068 8.65359C3.80476 9.01503 4.08062 9.53994 4.08062 10.2434V13.1251C4.08062 13.7395 4.34717 14.0613 4.9099 14.0613H5.33893C5.66613 14.0613 5.93137 14.3435 5.93137 14.6707C5.93137 14.9979 5.66613 15.2462 5.33893 15.2462H4.64335C3.99177 15.2462 3.48828 15.0268 3.13287 14.6172C2.80708 14.2369 2.64419 13.7103 2.64419 13.0666V10.3165C2.64419 9.8923 2.55534 9.58511 2.37764 9.39494C2.26816 9.27135 1.80618 9.17938 1.38154 9.11602C1.02726 9.06315 0.759057 8.76744 0.765747 8.4093C0.772379 8.0543 1.03439 7.7566 1.38545 7.70346C1.80778 7.63952 2.26906 7.54968 2.37764 7.43477C2.55534 7.22997 2.64419 6.92278 2.64419 6.51319V3.77772C2.64419 3.11945 2.80708 2.59284 3.13287 2.21251C3.48828 1.78829 3.99177 1.5835 4.64335 1.5835H5.33893Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.962 15.2463C10.6348 15.2463 10.3696 14.9483 10.3696 14.6211C10.3696 14.2939 10.6348 14.0614 10.962 14.0614H11.391C11.9538 14.0614 12.2203 13.7542 12.2203 13.1105V10.2434C12.2203 9.51979 12.4937 8.98241 13.033 8.6387C13.1867 8.54073 13.1921 8.27703 13.0403 8.17616C12.4962 7.81472 12.2203 7.28982 12.2203 6.58638V3.70463C12.2203 3.09024 11.9538 2.76842 11.391 2.76842L10.962 2.76842C10.6348 2.76842 10.3696 2.48627 10.3696 2.15907C10.3696 1.83188 10.6348 1.58354 10.962 1.58354L11.6576 1.58354C12.3092 1.58354 12.8127 1.80296 13.1681 2.21255C13.4939 2.59289 13.6568 3.1195 13.6568 3.76314V6.51324C13.6568 6.93745 13.7456 7.24464 13.9233 7.43481C14.03 7.5553 14.4328 7.64858 14.8186 7.71393C15.1718 7.77376 15.4401 8.06977 15.4334 8.42791C15.4268 8.78291 15.1646 9.08018 14.814 9.13633C14.4306 9.19774 14.0291 9.28303 13.9233 9.39499C13.7456 9.59978 13.6568 9.90697 13.6568 10.3166V13.052C13.6568 13.7103 13.4939 14.2369 13.1681 14.6172C12.8127 15.0415 12.3092 15.2463 11.6576 15.2463H10.962Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n\n  getTypeSchemaProperties: (type: IJsonSchema): Record<string, IJsonSchema> =>\n    type.properties || {},\n\n  getPropertiesParent: (type) => type,\n\n  canAddField: () => true,\n\n  getDefaultSchema: (): IJsonSchema => ({\n    type: 'object',\n    required: [],\n    properties: {},\n  }),\n\n  getIJsonSchemaPropertiesParent: (type: IJsonSchema) => type,\n\n  getValueText: () => '',\n\n  getJsonPaths: () => ['properties'],\n\n  getDefaultValue: () => ({}),\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/string.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const stringRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'string',\n\n  label: 'String',\n\n  icon: (\n    <svg\n      width=\"1em\"\n      height=\"1em\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        d=\"M9.3342 3.33321C8.96601 3.33321 8.66753 3.63169 8.66753 3.99988C8.66753 4.36807 8.96601 4.66655 9.3342 4.66655H14.6675C15.0357 4.66655 15.3342 4.36807 15.3342 3.99988C15.3342 3.63169 15.0357 3.33321 14.6675 3.33321H9.3342Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M10.0009 7.99988C10.0009 7.63169 10.2993 7.33321 10.6675 7.33321H14.6675C15.0357 7.33321 15.3342 7.63169 15.3342 7.99988C15.3342 8.36807 15.0357 8.66655 14.6675 8.66655H10.6675C10.2993 8.66655 10.0009 8.36807 10.0009 7.99988Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M12.0009 11.3332C11.6327 11.3332 11.3342 11.6317 11.3342 11.9999C11.3342 12.3681 11.6327 12.6665 12.0009 12.6665H14.6675C15.0357 12.6665 15.3342 12.3681 15.3342 11.9999C15.3342 11.6317 15.0357 11.3332 14.6675 11.3332H12.0009Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M9.86659 14.1482L8.23444 10.1844H3.18136C3.13868 10.1844 3.09685 10.1808 3.05616 10.1738L1.66589 14.1129C1.53049 14.4965 1.10971 14.6978 0.726058 14.5624C0.342408 14.427 0.141166 14.0062 0.276572 13.6225L4.37566 2.00848C4.71323 1.05202 6.05321 1.01763 6.4394 1.95552L11.2289 13.5872C11.3838 13.9634 11.2044 14.394 10.8282 14.5489C10.452 14.7038 10.0215 14.5244 9.86659 14.1482ZM5.44412 3.40791L3.57241 8.71109H7.62778L5.44412 3.40791Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  ),\n\n  getDefaultSchema: () => ({\n    type: 'string',\n  }),\n\n  getValueText: (value?: unknown) => (value ? `${value}` : ''),\n\n  getDefaultValue: () => '',\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/type-definition/unknown.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport React from 'react';\n\nimport { IJsonSchema, JsonSchemaTypeRegistryCreator } from '../types';\n\nexport const unknownRegistryCreator: JsonSchemaTypeRegistryCreator = () => ({\n  type: 'unknown',\n  label: 'Unknown',\n  parentType: [],\n\n  icon: (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      focusable=\"false\"\n      aria-hidden=\"true\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M4.22183 4.22182C6.21136 2.23232 8.96273 1 12 1C15.0373 1 17.7886 2.23231 19.7782 4.22182L19.0711 4.92893L19.7782 4.22183C21.7677 6.21136 23 8.96273 23 12C23 15.0373 21.7677 17.7886 19.7782 19.7782C17.7886 21.7677 15.0373 23 12 23C8.96273 23 6.21136 21.7677 4.22183 19.7782L4.92893 19.0711L4.22182 19.7782C2.23231 17.7886 1 15.0373 1 12C1 8.96273 2.23232 6.21136 4.22182 4.22183L4.22183 4.22182ZM12 3C9.51447 3 7.26584 4.00626 5.63603 5.63604C4.00625 7.26585 3 9.51447 3 12C3 14.4855 4.00627 16.7342 5.63604 18.3639C7.26584 19.9937 9.51447 21 12 21C14.4855 21 16.7342 19.9937 18.3639 18.3639C19.9937 16.7342 21 14.4855 21 12C21 9.51447 19.9937 7.26584 18.3639 5.63604C16.7342 4.00627 14.4855 3 12 3Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M8 9.31245C8 7.10331 9.79086 5.31245 12 5.31245C14.2091 5.31245 16 7.10331 16 9.31245C16 11.1763 14.7252 12.7424 13 13.1864V14.3124C13 14.8647 12.5523 15.3124 12 15.3124C11.4477 15.3124 11 14.8647 11 14.3124V12.3124C11 11.7602 11.4477 11.3124 12 11.3124C13.1046 11.3124 14 10.417 14 9.31245C14 8.20788 13.1046 7.31245 12 7.31245C10.8954 7.31245 10 8.20788 10 9.31245C10 9.86473 9.55228 10.3124 9 10.3124C8.44772 10.3124 8 9.86473 8 9.31245Z\"\n        fill=\"currentColor\"\n      ></path>\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 18.8125C12.6904 18.8125 13.25 18.2528 13.25 17.5625C13.25 16.8721 12.6904 16.3125 12 16.3125C11.3097 16.3125 10.75 16.8721 10.75 17.5625C10.75 18.2528 11.3097 18.8125 12 18.8125Z\"\n        fill=\"currentColor\"\n      ></path>\n    </svg>\n  ),\n\n  getDefaultSchema: (): IJsonSchema => ({ type: 'unknown' } as unknown as IJsonSchema),\n\n  getDefaultValue: () => undefined,\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { BaseTypeRegistry, TypeRegistryCreator } from '../base';\nimport { type JsonSchemaTypeManager } from './json-schema-type-manager';\n\nexport type JsonSchemaBasicType =\n  | 'boolean'\n  | 'string'\n  | 'integer'\n  | 'number'\n  | 'object'\n  | 'array'\n  | 'map';\n\nexport interface IJsonSchema<T = string> {\n  type?: T;\n  /**\n   * The format of string\n   * https://json-schema.org/understanding-json-schema/reference/type#format\n   */\n  format?: string;\n  default?: any;\n  title?: string;\n  description?: string;\n  enum?: (string | number)[];\n  properties?: Record<string, IJsonSchema<T>>;\n  additionalProperties?: IJsonSchema<T>;\n  items?: IJsonSchema<T>;\n  required?: string[];\n  $ref?: string;\n  extra?: {\n    index?: number;\n    // Used in BaseType.isEqualWithJSONSchema, the type comparison will be weak\n    weak?: boolean;\n    // Set the render component\n    formComponent?: string;\n    [key: string]: any;\n  };\n}\n\nexport type IBasicJsonSchema = IJsonSchema<JsonSchemaBasicType>;\n\n/**\n * TypeRegistry based on IJsonSchema\n */\nexport interface JsonSchemaTypeRegistry<Schema extends Partial<IJsonSchema> = IJsonSchema>\n  extends BaseTypeRegistry {\n  /**\n   * The icon of this type\n   */\n  icon: React.JSX.Element;\n  /**\n   * The display text of this type, not including the icon\n   */\n  label: string;\n  /**\n   * Whether it is a container type\n   */\n  container: boolean;\n  /**\n   * Supported parent types. Some types can only appear as subtypes in type selection, but not as basic types.\n   */\n  parentType?: string[];\n\n  /*\n   * Get supported sub-types\n   */\n  getSupportedItemTypes?: (ctx: {\n    level: number;\n    parentTypes?: string[];\n  }) => Array<{ type: string; disabled?: string }>;\n\n  /**\n   * Get the display label\n   */\n  getDisplayLabel: (typeSchema: Schema) => React.JSX.Element;\n\n  /**\n   * Get the display text\n   */\n  getDisplayText: (typeSchema: Schema) => string | undefined;\n\n  /**\n   * Get the sub-type\n   */\n  getItemType?: (typeSchema: Schema) => Schema | undefined;\n\n  /**\n   * Generate default Schema\n   */\n  getDefaultSchema: () => Schema;\n\n  /**\n   * onInit initialization logic, which is called at the appropriate time externally to register data into the type system\n   */\n  onInit?: () => void;\n\n  /**\n   * Whether to allow adding fields, such as object\n   */\n  canAddField: (typeSchema: Schema) => boolean;\n\n  /**\n   * Get the string value, for example\n   *  { type: \"array\", items: { type: \"string\" } }\n   *  The value is \"array-string\"\n   *\n   * The use case is that in some UI components, a string value needs to be generated\n   */\n  getStringValueByTypeSchema?: (optionValue: Schema) => string | undefined;\n\n  /**\n   * Restore the string value to typeSchema\n   * \"array-string\" is restored to\n   *  { type: \"array\", items: { type: \"string\" } }\n   */\n  getTypeSchemaByStringValue?: (type: string) => Schema;\n\n  /**\n   * Get the display icon, and the composite icon of the array is also processed\n   */\n  getDisplayIcon: (typeSchema: Schema) => JSX.Element;\n\n  /**\n   * Get sub-properties\n   */\n  getTypeSchemaProperties: (typeSchema: Schema) => Record<string, Schema> | undefined;\n\n  /**\n   * Get the default value\n   */\n  getDefaultValue: () => unknown;\n\n  /**\n   * Get the display text based on the value\n   */\n  getValueText: (value?: unknown) => string;\n  /**\n   * Get the json path of a certain type in the flow schema\n   * For example\n   * { type: \"object\", properties: { name: { type: \"string\" } } }\n   * -> ['properties', 'name']\n   * { type: \"array\", items: { type: \"string\" } }\n   * ->['items']\n   */\n  getJsonPaths: (typeSchema: Schema) => string[];\n\n  /**\n   * Get the parent node of the sub-field\n   * object is itself\n   * array<object> is items\n   * map<object> is additionalProperties\n   */\n  getPropertiesParent: (typeSchema: Schema) => Schema | undefined;\n  /**\n   * The complexText of a custom type\n   * For example, Array<string>, can be modified\n   */\n  customComplexText?: (typeSchema: Schema) => string;\n}\n\nexport type JsonSchemaTypeRegistryCreator<\n  Schema extends Partial<IJsonSchema> = IJsonSchema,\n  Registry extends JsonSchemaTypeRegistry<Schema> = JsonSchemaTypeRegistry<Schema>,\n  Manager extends JsonSchemaTypeManager<Schema, Registry> = JsonSchemaTypeManager<Schema, Registry>\n> = TypeRegistryCreator<Schema, Registry, Manager>;\n"
  },
  {
    "path": "packages/variable-engine/json-schema/src/json-schema/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { get } from 'lodash-es';\nimport {\n  ASTFactory,\n  ASTKind,\n  ASTMatch,\n  ASTNode,\n  ASTNodeJSON,\n  BaseType,\n} from '@flowgram.ai/variable-core';\n\nimport { IJsonSchema } from './types';\n\nexport namespace JsonSchemaUtils {\n  /**\n   * Converts a JSON schema to an Abstract Syntax Tree (AST) representation.\n   * This function recursively processes the JSON schema and creates corresponding AST nodes.\n   *\n   * For more information on JSON Schema, refer to the official documentation:\n   * https://json-schema.org/\n   *\n   * @param jsonSchema - The JSON schema to convert.\n   * @returns An AST node representing the JSON schema, or undefined if the schema type is not recognized.\n   */\n  export function schemaToAST(jsonSchema: IJsonSchema): ASTNodeJSON | undefined {\n    const { type, extra, required } = jsonSchema || {};\n    const { weak = false } = extra || {};\n\n    if (!type) {\n      return undefined;\n    }\n\n    switch (type) {\n      case 'object':\n        if (weak) {\n          return { kind: ASTKind.Object, weak: true };\n        }\n        return ASTFactory.createObject({\n          properties: Object.entries(jsonSchema.properties || {})\n            /**\n             * Sorts the properties of a JSON schema based on the 'extra.index' field.\n             * If the 'extra.index' field is not present, the property will be treated as having an index of 0.\n             */\n            .sort((a, b) => (get(a?.[1], 'extra.index') || 0) - (get(b?.[1], 'extra.index') || 0))\n            .map(([key, _property]) => ({\n              key,\n              type: schemaToAST(_property),\n              meta: {\n                title: _property.title,\n                description: _property.description,\n                required: !!required?.includes(key),\n                default: _property.default,\n              },\n            })),\n        });\n      case 'array':\n        if (weak) {\n          return { kind: ASTKind.Array, weak: true };\n        }\n        return ASTFactory.createArray({\n          items: schemaToAST(jsonSchema.items!),\n        });\n      case 'map':\n        if (weak) {\n          return { kind: ASTKind.Map, weak: true };\n        }\n        return ASTFactory.createMap({\n          valueType: schemaToAST(jsonSchema.additionalProperties!),\n        });\n      case 'string':\n        return ASTFactory.createString({ format: jsonSchema.format });\n      case 'number':\n        return ASTFactory.createNumber();\n      case 'boolean':\n        return ASTFactory.createBoolean();\n      case 'integer':\n        return ASTFactory.createInteger();\n\n      default:\n        // If the type is not recognized, return CustomType\n        return ASTFactory.createCustomType({ typeName: type });\n    }\n  }\n\n  /**\n   * Convert AST To JSON Schema\n   * @param typeAST\n   * @returns\n   */\n  export function astToSchema(\n    typeAST?: ASTNode,\n    options?: {\n      drilldown?: boolean;\n      drilldownObject?: boolean;\n      drilldownMap?: boolean;\n      drilldownArray?: boolean;\n    }\n  ): IJsonSchema | undefined {\n    const {\n      drilldown = true,\n      drilldownMap = drilldown,\n      drilldownObject = drilldown,\n      drilldownArray = drilldown,\n    } = options || {};\n\n    if (!typeAST) {\n      return undefined;\n    }\n\n    if (ASTMatch.isString(typeAST)) {\n      return {\n        type: 'string',\n        format: typeAST.format,\n      };\n    }\n\n    if (ASTMatch.isBoolean(typeAST)) {\n      return {\n        type: 'boolean',\n      };\n    }\n\n    if (ASTMatch.isNumber(typeAST)) {\n      return {\n        type: 'number',\n      };\n    }\n\n    if (ASTMatch.isInteger(typeAST)) {\n      return {\n        type: 'integer',\n      };\n    }\n\n    if (ASTMatch.isObject(typeAST)) {\n      return {\n        type: 'object',\n        required: typeAST.properties\n          .filter((property) => property.meta?.required)\n          .map((property) => property.key),\n        properties: drilldownObject\n          ? Object.fromEntries(\n              typeAST.properties.map((property) => {\n                const schema = astToSchema(property.type);\n\n                if (property.meta?.title && schema) {\n                  schema.title = property.meta.title;\n                }\n                if (property.meta?.description && schema) {\n                  schema.description = property.meta.description;\n                }\n                if (property.meta?.default && schema) {\n                  schema.default = property.meta.default;\n                }\n\n                return [property.key, schema!];\n              })\n            )\n          : {},\n      };\n    }\n\n    if (ASTMatch.isArray(typeAST)) {\n      return {\n        type: 'array',\n        items: drilldownArray ? astToSchema(typeAST.items) : undefined,\n      };\n    }\n\n    if (ASTMatch.isMap(typeAST)) {\n      return {\n        type: 'map',\n        items: drilldownMap ? astToSchema(typeAST.valueType) : undefined,\n      };\n    }\n\n    if (ASTMatch.isCustomType(typeAST)) {\n      return {\n        type: typeAST.typeName,\n      };\n    }\n\n    console.warn('JsonSchemaUtils.astToSchema: AST must extends BaseType', typeAST);\n\n    return undefined;\n  }\n\n  /**\n   * Check if the AST type is match the JSON Schema\n   * @param typeAST\n   * @param schema\n   * @returns\n   */\n  export function isASTMatchSchema(\n    typeAST: BaseType,\n    schema: IJsonSchema | IJsonSchema[]\n  ): boolean {\n    if (Array.isArray(schema)) {\n      return typeAST.isTypeEqual(\n        ASTFactory.createUnion({\n          types: schema.map((_schema) => schemaToAST(_schema)!).filter(Boolean),\n        })\n      );\n    }\n\n    return typeAST.isTypeEqual(schemaToAST(schema));\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/json-schema/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n}\n"
  },
  {
    "path": "packages/variable-engine/json-schema/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__/**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/json-schema/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__mocks__/container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container, ContainerModule, interfaces } from 'inversify';\nimport { ScopeChain, VariableContainerModule } from '../src';\nimport { MockScopeChain } from './mock-chain';\nimport { createPlaygroundContainer } from '@flowgram.ai/core';\n\nexport function getContainer(customModule?: interfaces.ContainerModuleCallBack): Container {\n  const container = createPlaygroundContainer() as Container;\n  container.load(VariableContainerModule);\n  container.bind(ScopeChain).to(MockScopeChain).inSingletonScope();\n\n  if(customModule) {\n    container.load(new ContainerModule(customModule))\n  }\n\n  return container;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__mocks__/mock-chain.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ScopeChain, Scope } from '../src';\n\n/**\n * 规则：\n * - Global 覆盖所有 Scope，所有 Scope 依赖 Global\n * - 存在环路：cycle1 -> cycle2 -> cycle3 -> cycle1\n */\n\nexport class MockScopeChain extends ScopeChain {\n  getDeps(scope: Scope): Scope[] {\n    const res: Scope[] = [];\n    if (scope.id === 'global') {\n      return [];\n    }\n\n    const global = this.variableEngine.getScopeById('global');\n    if (global) {\n      res.push(global);\n    }\n\n    // 模拟循环依赖场景\n    if (String(scope.id).startsWith('cycle')) {\n      return this.variableEngine\n        .getAllScopes()\n        .filter((_scope) => String(_scope.id).startsWith('cycle') && _scope.id !== scope.id);\n    }\n\n    return res;\n  }\n\n  getCovers(scope: Scope): Scope[] {\n    if (scope.id === 'global') {\n      return this.variableEngine.getAllScopes().filter(_scope => _scope.id !== 'global');\n    }\n\n    // 模拟循环依赖场景\n    if (String(scope.id).startsWith('cycle')) {\n      return this.variableEngine\n        .getAllScopes()\n        .filter((_scope) => String(_scope.id).startsWith('cycle') && _scope.id !== scope.id);\n    }\n\n    return [];\n  }\n\n  sortAll(): Scope[] {\n    return this.variableEngine.getAllScopes();\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__mocks__/variables.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind, ObjectJSON, VariableDeclarationListJSON } from '../src';\n\nexport const simpleVariableList = {\n  kind: ASTKind.VariableDeclarationList,\n  declarations: [\n    {\n      type: ASTKind.String,\n      key: 'string',\n    },\n    {\n      type: ASTKind.Boolean,\n      key: 'boolean',\n    },\n    {\n      // VariableDeclarationList 的 declarations 中可以不用声明 Kind\n      // kind: ASTKind.VariableDeclaration,\n      type: ASTKind.Number,\n      key: 'number',\n    },\n    {\n      kind: ASTKind.VariableDeclaration,\n      type: ASTKind.Integer,\n      key: 'integer',\n    },\n    {\n      kind: ASTKind.VariableDeclaration,\n      type: {\n        kind: ASTKind.Object,\n        properties: [\n          {\n            key: 'key1',\n            type: ASTKind.String,\n            // Object 的 properties 中可以不用声明 Kind\n            kind: ASTKind.Property,\n          },\n          {\n            key: 'key2',\n            type: {\n              kind: ASTKind.Object,\n              properties: [\n                {\n                  key: 'key1',\n                  type: ASTKind.Number,\n                },\n              ],\n            } as ObjectJSON,\n          },\n          {\n            key: 'key3',\n            type: {\n              kind: ASTKind.Array,\n              itemType: ASTKind.String,\n            },\n          },\n          {\n            key: 'key4',\n            type: {\n              kind: ASTKind.Array,\n              items: {\n                kind: ASTKind.Object,\n                properties: [\n                  {\n                    key: 'key1',\n                    type: ASTKind.Boolean,\n                  },\n                ],\n              } as ObjectJSON,\n            },\n          },\n        ],\n      } as ObjectJSON,\n      key: 'object',\n    },\n    {\n      kind: ASTKind.VariableDeclaration,\n      type: { kind: ASTKind.Map, valueType: ASTKind.Number },\n      key: 'map',\n    },\n  ],\n} as VariableDeclarationListJSON;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/__snapshots__/key-path-expression-v2.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`test Key Path Expression V2 > init const test_key_path = source.a 1`] = `\n{\n  \"initializer\": {\n    \"keyPath\": [\n      \"source\",\n      \"a\",\n    ],\n    \"kind\": \"KeyPathExpression\",\n  },\n  \"key\": \"test_key_path\",\n  \"kind\": \"VariableDeclaration\",\n  \"order\": 0,\n  \"type\": {\n    \"kind\": \"String\",\n  },\n}\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 1`] = `\n[\n  1,\n  {\n    \"kind\": \"Boolean\",\n  },\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 2`] = `\n[\n  2,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 3`] = `\n[\n  3,\n  undefined,\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 4`] = `\n[\n  4,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 5`] = `\n[\n  5,\n  undefined,\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 6`] = `\n[\n  6,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 7`] = `\n{\n  \"initializer\": {\n    \"enumerateFor\": {\n      \"keyPath\": [\n        \"source\",\n        \"b\",\n      ],\n      \"kind\": \"KeyPathExpression\",\n    },\n    \"kind\": \"EnumerateExpression\",\n  },\n  \"key\": \"test_key_path\",\n  \"kind\": \"VariableDeclaration\",\n  \"order\": 0,\n  \"type\": {\n    \"kind\": \"Number\",\n  },\n}\n`;\n\nexports[`test Key Path Expression V2 > subscribe variable changes by expression 8`] = `\n[\n  7,\n  undefined,\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/__snapshots__/variable-declaration.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`test Basic Variable Declaration > test globalVariableTable variables 1`] = `\n[\n  \"string\",\n  \"boolean\",\n  \"number\",\n  \"integer\",\n  \"object\",\n  \"map\",\n]\n`;\n\nexports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 1`] = `\n[\n  \"object\",\n  \"new_integer\",\n]\n`;\n\nexports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 2`] = `[]`;\n\nexports[`test Basic Variable Declaration > test simple variable declarations 1`] = `\n{\n  \"declarations\": [\n    {\n      \"key\": \"string\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 0,\n      \"type\": {\n        \"kind\": \"String\",\n      },\n    },\n    {\n      \"key\": \"boolean\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 1,\n      \"type\": {\n        \"kind\": \"Boolean\",\n      },\n    },\n    {\n      \"key\": \"number\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 2,\n      \"type\": {\n        \"kind\": \"Number\",\n      },\n    },\n    {\n      \"key\": \"integer\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 3,\n      \"type\": {\n        \"kind\": \"Integer\",\n      },\n    },\n    {\n      \"key\": \"object\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 4,\n      \"type\": {\n        \"kind\": \"Object\",\n        \"properties\": [\n          {\n            \"key\": \"key1\",\n            \"kind\": \"Property\",\n            \"type\": {\n              \"kind\": \"String\",\n            },\n          },\n          {\n            \"key\": \"key2\",\n            \"kind\": \"Property\",\n            \"type\": {\n              \"kind\": \"Object\",\n              \"properties\": [\n                {\n                  \"key\": \"key1\",\n                  \"kind\": \"Property\",\n                  \"type\": {\n                    \"kind\": \"Number\",\n                  },\n                },\n              ],\n            },\n          },\n          {\n            \"key\": \"key3\",\n            \"kind\": \"Property\",\n            \"type\": {\n              \"kind\": \"Array\",\n            },\n          },\n          {\n            \"key\": \"key4\",\n            \"kind\": \"Property\",\n            \"type\": {\n              \"items\": {\n                \"kind\": \"Object\",\n                \"properties\": [\n                  {\n                    \"key\": \"key1\",\n                    \"kind\": \"Property\",\n                    \"type\": {\n                      \"kind\": \"Boolean\",\n                    },\n                  },\n                ],\n              },\n              \"kind\": \"Array\",\n            },\n          },\n        ],\n      },\n    },\n    {\n      \"key\": \"map\",\n      \"kind\": \"VariableDeclaration\",\n      \"order\": 5,\n      \"type\": {\n        \"keyType\": {\n          \"kind\": \"String\",\n        },\n        \"kind\": \"Map\",\n        \"valueType\": {\n          \"kind\": \"Number\",\n        },\n      },\n    },\n  ],\n  \"kind\": \"VariableDeclarationList\",\n}\n`;\n\nexports[`test Basic Variable Declaration > variable declaration subscribe 1`] = `\n{\n  \"kind\": \"Number\",\n}\n`;\n\nexports[`test Basic Variable Declaration > variable declaration subscribe 2`] = `\n{\n  \"kind\": \"Object\",\n  \"properties\": [\n    {\n      \"key\": \"key1\",\n      \"kind\": \"Property\",\n      \"type\": {\n        \"kind\": \"String\",\n      },\n    },\n  ],\n}\n`;\n\nexports[`test Basic Variable Declaration > variable declaration subscribe 3`] = `\n{\n  \"kind\": \"Object\",\n  \"properties\": [\n    {\n      \"key\": \"key1\",\n      \"kind\": \"Property\",\n      \"type\": {\n        \"kind\": \"Number\",\n      },\n    },\n  ],\n}\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/__snapshots__/variable-with-initializer.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`test Variable With Initializer > init const test_key_path = source.a 1`] = `\n{\n  \"initializer\": {\n    \"keyPath\": [\n      \"source\",\n      \"a\",\n    ],\n    \"kind\": \"KeyPathExpression\",\n  },\n  \"key\": \"test_key_path\",\n  \"kind\": \"VariableDeclaration\",\n  \"order\": 0,\n  \"type\": {\n    \"kind\": \"String\",\n  },\n}\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 1`] = `\n[\n  1,\n  {\n    \"kind\": \"Boolean\",\n  },\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 2`] = `\n[\n  2,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 3`] = `\n[\n  3,\n  undefined,\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 4`] = `\n[\n  4,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 5`] = `\n[\n  5,\n  undefined,\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 6`] = `\n[\n  6,\n  {\n    \"kind\": \"Number\",\n  },\n]\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 7`] = `\n{\n  \"initializer\": {\n    \"enumerateFor\": {\n      \"keyPath\": [\n        \"source\",\n        \"b\",\n      ],\n      \"kind\": \"KeyPathExpression\",\n    },\n    \"kind\": \"EnumerateExpression\",\n  },\n  \"key\": \"test_key_path\",\n  \"kind\": \"VariableDeclaration\",\n  \"order\": 0,\n  \"type\": {\n    \"kind\": \"Number\",\n  },\n}\n`;\n\nexports[`test Variable With Initializer > subscribe variable changes by expression 8`] = `\n[\n  7,\n  undefined,\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/ast-decorators.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { VariableEngine } from '../../src/variable-engine';\nimport { ASTNode, postConstructAST } from '../../src/ast';\nimport { getContainer } from '../../__mocks__/container';\n\ndescribe('test ast decorators', () => {\n  const container = getContainer();\n\n  const variableEngine: VariableEngine = container.get(VariableEngine);\n\n  const testScope = variableEngine.createScope('test');\n\n  test('@postConstructAST()', () => {\n    let postConstructCalled = false;\n\n    class PostConstructTest extends ASTNode {\n      static kind = 'PostConstructTest';\n\n      testFlag = false;\n\n      @postConstructAST()\n      init() {\n        postConstructCalled = true;\n        this.testFlag = true;\n      }\n\n      fromJSON(json: any): void {\n        // do nothing\n      }\n\n      toJSON() {\n        return {\n          testFlag: this.testFlag,\n        };\n      }\n    }\n\n    variableEngine.astRegisters.registerAST(PostConstructTest);\n    const ast: PostConstructTest = testScope.ast.set('test', {\n      kind: 'PostConstructTest',\n    });\n    const ast2: PostConstructTest = testScope.ast.set('test', {\n      kind: 'PostConstructTest',\n    });\n\n    expect(postConstructCalled).toBeTruthy();\n    expect(ast.testFlag).toBeTruthy();\n    expect(ast2.testFlag).toBeTruthy();\n  });\n\n  test('@postConstructAST() Annotate Only One time', () => {\n    expect(() => {\n      class PostConstructTest2 extends ASTNode {\n        static kind = 'PostConstructTest2';\n\n        testFlag1 = false;\n\n        testFlag2 = false;\n\n        @postConstructAST()\n        init() {\n          this.testFlag1 = true;\n        }\n\n        @postConstructAST()\n        init2() {\n          this.testFlag2 = true;\n        }\n\n        fromJSON(json: any): void {\n          // do nothing\n        }\n\n        toJSON() {\n          return {\n            testFlag1: this.testFlag1,\n            testFlag2: this.testFlag2,\n          };\n        }\n      }\n      variableEngine.astRegisters.registerAST(PostConstructTest2);\n    }).toThrowError();\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/key-path-expression-v2.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport { getParentFields } from '../../src/ast/utils/variable-field';\nimport { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';\nimport { getContainer } from '../../__mocks__/container';\n\nconst {\n  createVariableDeclaration,\n  createObject,\n  createProperty,\n  createString,\n  createNumber,\n  createKeyPathExpression,\n  createArray,\n  createEnumerateExpression,\n} = ASTFactory;\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\n/**\n * 测试通过表达式生成的变量\n */\ndescribe('test Key Path Expression V2', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const globalScope = variableEngine.createScope('global');\n  const testScope = variableEngine.createScope('test');\n\n  const globalTestVariable = globalScope.ast.set<VariableDeclaration>(\n    'test',\n    createVariableDeclaration({\n      key: 'source',\n      type: createObject({\n        properties: [\n          createProperty({\n            key: 'a',\n            type: createString(),\n          }),\n          createProperty({\n            key: 'b',\n            type: createNumber(),\n          }),\n        ],\n      }),\n    })\n  );\n\n  test('init const test_key_path = source.a', () => {\n    const variableByKeyPath = testScope.ast.set<VariableDeclaration>(\n      'variableByKeyPath',\n      createVariableDeclaration({\n        key: 'test_key_path',\n        initializer: createKeyPathExpression({\n          keyPath: ['source', 'a'],\n        }),\n      })\n    )!;\n\n    expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);\n    expect(variableByKeyPath.initializer?.refs.map((_ref) => _ref?.key)).toEqual(['a']);\n    // variableByKeyPath 的类型重新建立了实例，其父节点为当前节点\n    expect(getParentFields(variableByKeyPath.type).map((_field) => _field?.key)).toEqual([\n      'test_key_path',\n    ]);\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });\n    expect(variableByKeyPath.toJSON()).toMatchSnapshot();\n  });\n\n  test('subscribe variable changes by expression', () => {\n    // const variableByKeyPath = source.a;\n    const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;\n\n    let typeChangeTimes = 0;\n    variableByKeyPath.onTypeChange((type) => {\n      typeChangeTimes++;\n      expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();\n    });\n\n    // source.a 发生变化时，则响应更新变量\n    // source.a -> boolean\n    globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });\n    expect(typeChangeTimes).toBe(1);\n\n    // 改成引用 source.b，响应更新变量\n    // const variableByKeyPath = source.b;\n    variableByKeyPath.updateInitializer({\n      kind: ASTKind.KeyPathExpression,\n      keyPath: ['source', 'b'],\n    });\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(2);\n\n    // source.b 被删除，变量类型变为空\n    globalTestVariable.type.fromJSON({\n      properties: [\n        {\n          key: 'a',\n          type: ASTKind.String,\n        },\n      ],\n    });\n\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(typeChangeTimes).toBe(3);\n\n    // source.b 加回来，变量类型变回来且触发表达式更新\n    globalTestVariable.type.fromJSON(\n      createObject({\n        properties: [\n          createProperty({ key: 'a', type: createString() }),\n          createProperty({ key: 'b', type: createNumber() }),\n        ],\n      })\n    );\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(4);\n\n    // 改成 EnumerateExpression\n    variableByKeyPath.updateInitializer(\n      createEnumerateExpression({\n        enumerateFor: createKeyPathExpression({\n          keyPath: ['source', 'b'],\n        }),\n      })\n    );\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(variableByKeyPath.initializer?.refs).toEqual([]);\n    variableByKeyPath.initializer?.refreshRefs();\n    expect(variableByKeyPath.initializer?.refs).toEqual([]);\n    expect(typeChangeTimes).toBe(5);\n\n    // 数组下钻，类型也会更新\n    globalTestVariable.type.fromJSON(\n      createObject({\n        properties: [\n          createProperty({\n            key: 'b',\n            type: createArray({\n              items: createNumber(),\n            }),\n          }),\n        ],\n      })\n    );\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(6);\n    expect(variableByKeyPath.toJSON()).toMatchSnapshot();\n\n    // 原作用域删除，显示为空类型\n    globalScope.dispose();\n    expect(variableByKeyPath.scope.depScopes.map((_scope) => _scope.id)).toEqual([]);\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(typeChangeTimes).toBe(7);\n  });\n\n  const cycle1 = variableEngine.createScope('cycle1');\n  const cycle2 = variableEngine.createScope('cycle2');\n  const cycle3 = variableEngine.createScope('cycle3');\n\n  test('cycle scope with normal refs', () => {\n    const cycle1Var = cycle1.ast.set<VariableDeclaration>(\n      'var',\n      createVariableDeclaration({\n        key: 'cycle1_var',\n        type: createObject({\n          properties: [\n            createProperty({\n              key: 'a',\n              type: createArray({\n                items: createString(),\n              }),\n            }),\n          ],\n        }),\n      })\n    );\n\n    const cycle2Var = cycle2.ast.set<VariableDeclaration>(\n      'var',\n      createVariableDeclaration({\n        key: 'cycle2_var',\n        initializer: createEnumerateExpression({\n          enumerateFor: createKeyPathExpression({\n            keyPath: ['cycle1_var', 'a'],\n          }),\n        }),\n      })\n    );\n\n    const cycle3Var = cycle3.ast.set<VariableDeclaration>(\n      'var',\n      createVariableDeclaration({\n        key: 'cycle3_var',\n        initializer: createKeyPathExpression({\n          keyPath: ['cycle2_var'],\n        }),\n      })\n    );\n\n    expect(cycle1Var.type.toJSON()).toEqual({\n      kind: ASTKind.Object,\n      properties: [\n        {\n          kind: ASTKind.Property,\n          key: 'a',\n          type: {\n            kind: ASTKind.Array,\n            items: { kind: ASTKind.String },\n          },\n        },\n      ],\n    });\n    expect(cycle2Var.type.toJSON()).toEqual({\n      kind: ASTKind.String,\n    });\n    expect(cycle3Var.type.toJSON()).toEqual({\n      kind: ASTKind.String,\n    });\n  });\n\n  test('cycle scope with child ref', () => {\n    const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;\n    const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;\n    const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;\n\n    cycle1Var.fromJSON({\n      type: createObject({\n        properties: [\n          createProperty({\n            key: 'a',\n            type: createArray({\n              items: createString(),\n            }),\n          }),\n          createProperty({\n            key: 'b',\n            initializer: createKeyPathExpression({\n              keyPath: ['cycle3_var'],\n            }),\n          }),\n        ],\n      }),\n    });\n\n    expect(cycle1Var.type.toJSON()).toEqual({\n      kind: ASTKind.Object,\n      properties: [\n        {\n          kind: ASTKind.Property,\n          key: 'a',\n          type: {\n            kind: ASTKind.Array,\n            items: { kind: ASTKind.String },\n          },\n        },\n        {\n          kind: ASTKind.Property,\n          key: 'b',\n          initializer: {\n            kind: ASTKind.KeyPathExpression,\n            keyPath: ['cycle3_var'],\n          },\n          type: {\n            kind: ASTKind.String,\n          },\n        },\n      ],\n    });\n    expect(cycle2Var.type.toJSON()).toEqual({\n      kind: ASTKind.String,\n    });\n    expect(cycle3Var.type.toJSON()).toEqual({\n      kind: ASTKind.String,\n    });\n  });\n\n  test('cycle scope with cycle ref', () => {\n    const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;\n    const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;\n    const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;\n\n    cycle1Var.fromJSON({\n      type: createObject({\n        properties: [\n          createProperty({\n            key: 'a',\n            initializer: createKeyPathExpression({\n              keyPath: ['cycle3_var'],\n            }),\n          }),\n          createProperty({\n            key: 'b',\n            initializer: createKeyPathExpression({\n              keyPath: ['cycle3_var'],\n            }),\n          }),\n        ],\n      }),\n    });\n\n    expect(cycle1Var.type.toJSON()).toEqual({\n      kind: ASTKind.Object,\n      properties: [\n        {\n          kind: ASTKind.Property,\n          key: 'a',\n          initializer: {\n            kind: ASTKind.KeyPathExpression,\n            keyPath: ['cycle3_var'],\n          },\n          // 发生循环引用时，type 清空\n        },\n        {\n          kind: ASTKind.Property,\n          key: 'b',\n          initializer: {\n            kind: ASTKind.KeyPathExpression,\n            keyPath: ['cycle3_var'],\n          },\n          // 发生循环引用时，type 清空\n        },\n      ],\n    });\n    expect(cycle2Var.type?.toJSON()).toBeUndefined();\n    expect(cycle3Var.type?.toJSON()).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/variable-declaration.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport {\n  ASTKind,\n  ASTMatch,\n  ObjectType,\n  NumberType,\n  VariableEngine,\n  VariableDeclaration,\n} from '../../src';\nimport { simpleVariableList } from '../../__mocks__/variables';\nimport { getContainer } from '../../__mocks__/container';\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\n/**\n * 测试基本的变量声明场景\n */\ndescribe('test Basic Variable Declaration', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const globalVariableTable = variableEngine.globalVariableTable;\n  const testScope = variableEngine.createScope('test');\n\n  test('test simple variable declarations', () => {\n    const simpleCase = testScope.ast.set('simple case', simpleVariableList);\n    expect(simpleCase?.toJSON()).toMatchSnapshot();\n  });\n\n  test('test globalVariableTable variables', () => {\n    expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();\n  });\n\n  test('test get variable by key path', () => {\n    const undefinedCases = [\n      ['object', 'key2', 'unExisted'],\n      ['unExisted'],\n      ['string', 'drilldownString'],\n      ['object', 'key1', 'drilldownString'],\n      ['object', 'key4', 'unExistedKeyInArray', 'key1'],\n    ];\n\n    const availableCases: [string[], string][] = [\n      [['object', 'key2', 'key1'], ASTKind.Number],\n      [['object', 'key4', '0', 'key1'], ASTKind.Boolean],\n    ];\n\n    availableCases.forEach(([_case, _resType]) => {\n      expect(globalVariableTable.getByKeyPath(_case)?.type.kind).toEqual(_resType);\n    });\n\n    undefinedCases.forEach((_case) => {\n      expect(globalVariableTable.getByKeyPath(_case)).toBeUndefined();\n    });\n  });\n\n  test('test remove variable, update variable, add variable by from a new json', () => {\n    const previousVariable1 = globalVariableTable.getByKeyPath(['object', 'key2', 'key1'])!;\n    const previousVariable2 = globalVariableTable.getByKeyPath(['integer'])!;\n    const previousVariable3 = globalVariableTable.getByKeyPath(['object', 'key4'])!;\n\n    testScope.ast.set('simple case', {\n      kind: ASTKind.VariableDeclarationList,\n      declarations: [\n        {\n          kind: ASTKind.VariableDeclaration,\n          type: ASTKind.Integer,\n          key: 'new_integer',\n        },\n        {\n          kind: ASTKind.VariableDeclaration,\n          key: 'object',\n          type: {\n            kind: ASTKind.Object,\n            properties: [\n              {\n                key: 'key2',\n                type: {\n                  kind: ASTKind.Object,\n                },\n              },\n              {\n                key: 'key4',\n                type: ASTKind.String,\n              },\n              {\n                key: 'key5',\n                type: ASTKind.Array,\n              },\n            ],\n          },\n        },\n      ],\n    });\n    expect(previousVariable1.disposed).toBe(true);\n    expect(previousVariable2.disposed).toBe(true);\n    expect(previousVariable3.disposed).toBe(false);\n    expect(previousVariable3.version).toBe(1); // 更新次数为一次\n    expect(testScope.ast.version).toBe(2); // 调用了两次 fromJSON，因此更新了两次\n    expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();\n    expect(globalVariableTable.version).toBe(2 + 1); // 调用了两次 fromJSON + Object 变量的下钻发生变化，因此 version 是 2 + 1\n    expect(testScope.available.variables.map((_v) => _v.key)).toMatchSnapshot();\n  });\n\n  test('remove variables', () => {\n    let isOutputChanged = false;\n    let isAnyVariableChanged = false;\n    testScope.output.onDataChange(() => {\n      isOutputChanged = true;\n    });\n    testScope.output.onAnyVariableChange(() => {\n      isAnyVariableChanged = true;\n    });\n\n    // 删除所有变量\n    testScope.ast.set('simple case', {\n      kind: ASTKind.VariableDeclarationList,\n      declarations: [],\n    });\n\n    expect(isOutputChanged).toBeTruthy();\n    expect(isAnyVariableChanged).toBeFalsy();\n  });\n\n  test('variable declaration subscribe', () => {\n    const declaration: VariableDeclaration = testScope.ast.set('subscribeTest', {\n      kind: ASTKind.VariableDeclaration,\n      type: ASTKind.String,\n      ui: { label: 'test Label' },\n    })!;\n\n    let declarationChangeTimes = 0;\n    let typeChangeTimes = 0;\n    let outputChangeTimes = 0;\n    let anyVariableChangeTimes = 0;\n\n    testScope.output.onDataChange(() => {\n      outputChangeTimes++;\n    });\n    testScope.output.onAnyVariableChange(() => {\n      anyVariableChangeTimes++;\n    });\n    declaration.subscribe(() => {\n      declarationChangeTimes++;\n    });\n    declaration.onTypeChange((type) => {\n      expect(type?.toJSON()).toMatchSnapshot();\n      typeChangeTimes++;\n    });\n\n    declaration.fromJSON({\n      type: ASTKind.String,\n      meta: { label: 'test New Label' },\n    });\n    expect(declarationChangeTimes).toBe(1);\n    expect(anyVariableChangeTimes).toBe(1);\n    expect(typeChangeTimes).toBe(0);\n    expect(outputChangeTimes).toBe(0);\n    expect(declaration.meta.label).toEqual('test New Label');\n\n    declaration.fromJSON({\n      type: ASTKind.Number,\n      meta: { label: 'test Label' },\n    });\n    expect(ASTMatch.is(declaration.type, NumberType)).toBeTruthy();\n    expect(declarationChangeTimes).toBe(2);\n    expect(anyVariableChangeTimes).toBe(2);\n    expect(typeChangeTimes).toBe(1);\n    expect(outputChangeTimes).toBe(0);\n    expect(declaration.meta.label).toEqual('test Label');\n\n    declaration.fromJSON({\n      type: {\n        kind: ASTKind.Object,\n        properties: [\n          {\n            key: 'key1',\n            type: ASTKind.String,\n          },\n        ],\n      },\n      meta: { label: 'test Label' },\n    });\n    expect(ASTMatch.is(declaration.type, ObjectType)).toBeTruthy();\n    expect(declarationChangeTimes).toBe(3);\n    expect(anyVariableChangeTimes).toBe(3);\n    expect(typeChangeTimes).toBe(2);\n    expect(outputChangeTimes).toBe(0);\n\n    // 变量下钻字段变换类型，变量也会更新\n    const key1Variable = (declaration.type as ObjectType).getByKeyPath(['key1']);\n    key1Variable?.updateType(ASTKind.Number);\n    expect(declarationChangeTimes).toBe(4);\n    expect(anyVariableChangeTimes).toBe(4);\n    expect(typeChangeTimes).toBe(3);\n    expect(outputChangeTimes).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/variable-match.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport {\n  ASTMatch,\n  ObjectType,\n  NumberType,\n  VariableEngine,\n  StringType,\n  VariableDeclarationList,\n  BooleanType,\n  IntegerType,\n  MapType,\n  ArrayType,\n} from '../../src';\nimport { simpleVariableList } from '../../__mocks__/variables';\nimport { getContainer } from '../../__mocks__/container';\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\n/**\n * 测试基本的变量声明场景\n */\ndescribe('test Basic Variable Declaration', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const testScope = variableEngine.createScope('test');\n\n  test('test simple variable match', () => {\n    const simpleCase = testScope.ast.set('simple case', simpleVariableList);\n\n    if (!ASTMatch.isVariableDeclarationList(simpleCase)) {\n      throw new Error('simpleCase is not a VariableDeclarationList');\n    }\n    expect(ASTMatch.isVariableDeclarationList(simpleCase)).toBeTruthy();\n    expect(ASTMatch.is(simpleCase, VariableDeclarationList)).toBeTruthy();\n\n    const stringDeclaration = simpleCase.declarations[0];\n    const booleanDeclaration = simpleCase.declarations[1];\n    const numberDeclaration = simpleCase.declarations[2];\n    const integerDeclaration = simpleCase.declarations[3];\n    const objectDeclaration = simpleCase.declarations[4];\n    const mapDeclaration = simpleCase.declarations[5];\n    const arrayProperty = testScope.output.globalVariableTable.getByKeyPath(['object', 'key4']);\n\n    expect(ASTMatch.isString(stringDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(stringDeclaration.type, StringType)).toBeTruthy();\n\n    expect(ASTMatch.isBoolean(booleanDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(booleanDeclaration.type, BooleanType)).toBeTruthy();\n\n    expect(ASTMatch.isNumber(numberDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(numberDeclaration.type, NumberType)).toBeTruthy();\n\n    expect(ASTMatch.isInteger(integerDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(integerDeclaration.type, IntegerType)).toBeTruthy();\n\n    expect(ASTMatch.isObject(objectDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(objectDeclaration.type, ObjectType)).toBeTruthy();\n\n    expect(ASTMatch.isMap(mapDeclaration.type)).toBeTruthy();\n    expect(ASTMatch.is(mapDeclaration.type, MapType)).toBeTruthy();\n\n    if (!ASTMatch.isProperty(arrayProperty)) {\n      throw new Error('arrayProperty is not a Property');\n    }\n    expect(ASTMatch.isArray(arrayProperty.type)).toBeTruthy();\n    expect(ASTMatch.is(arrayProperty.type, ArrayType)).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/variable-throw-errors.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { VariableEngine } from '../../src/variable-engine';\nimport { ASTFactory } from '../../src/ast';\nimport { simpleVariableList } from '../../__mocks__/variables';\nimport { getContainer } from '../../__mocks__/container';\n\ndescribe('Test Variable ThrowError', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const testScope = variableEngine.createScope('test');\n  testScope.ast.set('simple case', simpleVariableList);\n\n  test('throw error when call getByKeyPath in base type', () => {\n    const stringVariable = testScope.output.getVariableByKey('string');\n    expect(() => stringVariable?.type.getByKeyPath(['throw'])).toThrowError();\n  });\n\n  test('not throw error when variable is disposed for twice', () => {\n    const stringVariable = testScope.output.getVariableByKey('string');\n\n    expect(stringVariable?.disposed).toBeFalsy();\n\n    // 删除所有变量\n    testScope.ast.set(\n      'simple case',\n      ASTFactory.createVariableDeclarationList({ declarations: [] }),\n    );\n\n    expect(stringVariable?.disposed).toBeTruthy();\n    stringVariable?.dispose();\n    expect(stringVariable?.disposed).toBeTruthy();\n  });\n\n  test('not throw error when scope is disposed and fire its events', () => {\n    const toDisposeScope = variableEngine.createScope('toDisposeTest');\n\n    let eventCalledTimes = 0;\n    toDisposeScope.event.on('test', () => eventCalledTimes++);\n\n    toDisposeScope.refreshDeps();\n    toDisposeScope.event.dispatch({ type: 'test' });\n    expect(toDisposeScope.disposed).toBeFalsy();\n    expect(eventCalledTimes).toBe(1);\n\n    toDisposeScope.dispose();\n    expect(toDisposeScope.disposed).toBeTruthy();\n    toDisposeScope.event.dispatch({ type: 'test' });\n    toDisposeScope.refreshDeps();\n    expect(eventCalledTimes).toBe(1);\n  });\n\n  test('not throw error when ast is disposed and fire its events', () => {\n    const toDisposeAST = testScope.ast.set(\n      'dispose case',\n      ASTFactory.createVariableDeclaration({\n        key: 'toDispose',\n        type: ASTFactory.createString(),\n      }),\n    );\n\n    let changeTimes = 0;\n    toDisposeAST.subscribe(() => changeTimes++);\n\n    toDisposeAST.fromJSON({\n      key: 'toDispose',\n      type: ASTFactory.createNumber(),\n    });\n    expect(changeTimes).toBe(1);\n    expect(toDisposeAST.disposed).toBeFalsy();\n\n    testScope.ast.remove('dispose case');\n    expect(toDisposeAST.disposed).toBeTruthy();\n    toDisposeAST.fireChange();\n    expect(changeTimes).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/variable-type-equal.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\n\nimport { VariableEngine } from '../../src/variable-engine';\nimport { ASTFactory, ASTKind, CustomType, VariableDeclaration } from '../../src/ast';\nimport { getContainer } from '../../__mocks__/container';\n\nconst {\n  createObject,\n  createNumber,\n  createInteger,\n  createBoolean,\n  createString,\n  createArray,\n  createCustomType,\n  createMap,\n  createProperty,\n  createUnion,\n  create,\n} = ASTFactory;\n\ndescribe('Test Variable Type Equal', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const testScope = variableEngine.createScope('test');\n\n  const testObject1 = createObject({\n    properties: [\n      createProperty({ key: 'a', type: createString() }),\n      createProperty({ key: 'b', type: createNumber() }),\n      createProperty({ key: 'c', type: createBoolean() }),\n      createProperty({ key: 'd', type: createObject({}) }),\n      createProperty({ key: 'e', type: createArray({}) }),\n      createProperty({ key: 'f', type: createMap({}) }),\n      createProperty({ key: 'g', type: createCustomType({ typeName: 'Custom' }) }),\n    ],\n  });\n\n  const testObject2 = createObject({\n    properties: [\n      createProperty({ key: 'a', type: createString() }),\n      createProperty({ key: 'b', type: createInteger() }),\n      createProperty({ key: 'c', type: createBoolean() }),\n    ],\n  });\n\n  const testObject3 = createObject({\n    properties: [\n      createProperty({ key: 'a', type: createString() }),\n      createProperty({ key: 'b', type: testObject2 }),\n      createProperty({\n        key: 'c',\n        type: createArray({\n          items: testObject2,\n        }),\n      }),\n      createProperty({\n        key: 'd',\n        type: createArray({ items: createString() }),\n      }),\n      createProperty({\n        key: 'e',\n        type: createMap({\n          valueType: createNumber(),\n        }),\n      }),\n      createProperty({\n        key: 'f',\n        type: createMap({\n          valueType: createString(),\n        }),\n      }),\n    ],\n  });\n\n  const variable1: VariableDeclaration = testScope.ast.set('variable1', {\n    kind: ASTKind.VariableDeclaration,\n    key: 'variable1',\n    type: testObject1,\n  });\n\n  const variable2: VariableDeclaration = testScope.ast.set('variable2', {\n    kind: ASTKind.VariableDeclaration,\n    key: 'variable2',\n    type: testObject2,\n  });\n\n  const variable3: VariableDeclaration = testScope.ast.set('variable3', {\n    kind: ASTKind.VariableDeclaration,\n    key: 'variable3',\n    type: testObject3,\n  });\n\n  test('Test Simple Equal', () => {\n    expect(variable1.type.isTypeEqual(testObject1)).toBeTruthy();\n    expect(variable2.type.isTypeEqual(testObject2)).toBeTruthy();\n    expect(variable3.type.isTypeEqual(testObject3)).toBeTruthy();\n\n    expect(variable1.type.isTypeEqual(testObject2)).toBeFalsy();\n    expect(variable2.type.isTypeEqual(testObject3)).toBeFalsy();\n  });\n\n  test('Test Union Type Equal', () => {\n    expect(\n      variable1.type.isTypeEqual(\n        createUnion({\n          types: [testObject1, testObject2, testObject3],\n        })\n      )\n    ).toBeTruthy();\n\n    expect(\n      variable2.type.isTypeEqual(\n        createUnion({\n          types: [testObject1, testObject2, testObject3],\n        })\n      )\n    ).toBeTruthy();\n\n    expect(\n      variable3.type.isTypeEqual(\n        createUnion({\n          types: [testObject1, testObject2, testObject3],\n        })\n      )\n    ).toBeTruthy();\n\n    expect(variable1.type.isTypeEqual({ kind: ASTKind.Union })).toBeFalsy();\n  });\n\n  test('Test Union Type Equal In Child Properties', () => {\n    const UnionBasicTypes = createUnion({\n      types: [ASTKind.String, ASTKind.Number, ASTKind.Boolean],\n    });\n\n    expect(\n      variable2.type.isTypeEqual({\n        kind: ASTKind.Object,\n        properties: [\n          {\n            key: 'a',\n            type: UnionBasicTypes,\n          },\n          {\n            key: 'b',\n            type: UnionBasicTypes,\n          },\n          {\n            key: 'c',\n            type: UnionBasicTypes,\n          },\n        ],\n      })\n    );\n  });\n\n  test('Test Weak Compare', () => {\n    expect(variable1.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();\n    expect(variable2.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();\n    expect(variable3.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();\n\n    expect(\n      variable3.getByKeyPath(['c'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })\n    ).toBeTruthy();\n    expect(\n      variable3.getByKeyPath(['d'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })\n    ).toBeTruthy();\n    expect(\n      variable3.getByKeyPath(['e'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })\n    ).toBeTruthy();\n    expect(\n      variable3.getByKeyPath(['f'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })\n    ).toBeTruthy();\n  });\n\n  test('CustomType Equal', () => {\n    const customType1 = createCustomType({ typeName: 'Custom' });\n    const customType2 = createCustomType({ typeName: 'Custom2' });\n    const customType3 = create(CustomType, { typeName: 'Custom' });\n    const unionCustomTypes = createUnion({\n      types: [customType1, customType2],\n    });\n\n    const customTypeProperty = variable1.getByKeyPath(['g'])?.type;\n\n    expect(customTypeProperty?.isTypeEqual(customType1)).toBeTruthy();\n    expect(customTypeProperty?.isTypeEqual(customType2)).toBeFalsy();\n    expect(customTypeProperty?.isTypeEqual(customType3)).toBeTruthy();\n    expect(customTypeProperty?.isTypeEqual(unionCustomTypes)).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/ast/variable-with-initializer.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';\nimport { getContainer } from '../../__mocks__/container';\n\nconst {\n  createVariableDeclaration,\n  createObject,\n  createProperty,\n  createString,\n  createNumber,\n  createKeyPathExpression,\n  createArray,\n  createEnumerateExpression,\n} = ASTFactory;\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\n/**\n * 测试通过表达式生成的变量\n */\ndescribe('test Variable With Initializer', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const globalScope = variableEngine.createScope('global');\n  const testScope = variableEngine.createScope('test');\n\n  const globalTestVariable = globalScope.ast.set<VariableDeclaration>(\n    'test',\n    createVariableDeclaration({\n      key: 'source',\n      type: createObject({\n        properties: [\n          createProperty({\n            key: 'a',\n            type: createString(),\n          }),\n          createProperty({\n            key: 'b',\n            type: createNumber(),\n          }),\n        ],\n      }),\n    }),\n  );\n\n  test('test depScopes', () => {\n    expect(testScope.depScopes.map(_scope => _scope.id)).toEqual(['global']);\n  });\n\n  test('init const test_key_path = source.a', () => {\n    const variableByKeyPath = testScope.ast.set<VariableDeclaration>(\n      'variableByKeyPath',\n      createVariableDeclaration({\n        key: 'test_key_path',\n        initializer: createKeyPathExpression({\n          keyPath: ['source', 'a'],\n        }),\n      }),\n    )!;\n\n    expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);\n    expect(variableByKeyPath.initializer?.refs.map(_ref => _ref?.key)).toEqual(['a']);\n    expect(variableByKeyPath.initializer?.parentFields.map(_field => _field?.key)).toEqual([\n      'test_key_path',\n    ]);\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });\n    expect(variableByKeyPath.toJSON()).toMatchSnapshot();\n  });\n\n  test('subscribe variable changes by expression', () => {\n    // const variableByKeyPath = source.a;\n    const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;\n\n    let typeChangeTimes = 0;\n    variableByKeyPath.onTypeChange(type => {\n      typeChangeTimes++;\n      expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();\n    });\n\n    // source.a 发生变化时，则响应更新变量\n    // source.a -> boolean\n    globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });\n    expect(typeChangeTimes).toBe(1);\n\n    // 改成引用 source.b，响应更新变量\n    // const variableByKeyPath = source.b;\n    variableByKeyPath.updateInitializer({\n      kind: ASTKind.KeyPathExpression,\n      keyPath: ['source', 'b'],\n    });\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(2);\n\n    // source.b 被删除，变量类型变为空\n    globalTestVariable.type.fromJSON({\n      properties: [\n        {\n          key: 'a',\n          type: ASTKind.String,\n        },\n      ],\n    });\n\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(typeChangeTimes).toBe(3);\n\n    // source.b 加回来，变量类型变回来且触发表达式更新\n    globalTestVariable.type.fromJSON(\n      createObject({\n        properties: [\n          createProperty({ key: 'a', type: createString() }),\n          createProperty({ key: 'b', type: createNumber() }),\n        ],\n      }),\n    );\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(4);\n\n    // 改成 EnumerateExpression\n    variableByKeyPath.updateInitializer(\n      createEnumerateExpression({\n        enumerateFor: createKeyPathExpression({\n          keyPath: ['source', 'b'],\n        }),\n      }),\n    );\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(variableByKeyPath.initializer?.refs).toEqual([]);\n    variableByKeyPath.initializer?.refreshRefs();\n    expect(variableByKeyPath.initializer?.refs).toEqual([]);\n    expect(typeChangeTimes).toBe(5);\n\n    // 数组下钻，类型也会更新\n    globalTestVariable.type.fromJSON(\n      createObject({\n        properties: [\n          createProperty({\n            key: 'b',\n            type: createArray({\n              items: createNumber(),\n            }),\n          }),\n        ],\n      }),\n    );\n    expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });\n    expect(typeChangeTimes).toBe(6);\n    expect(variableByKeyPath.toJSON()).toMatchSnapshot();\n\n    // 原作用域删除，显示为空类型\n    globalScope.dispose();\n    expect(variableByKeyPath.scope.depScopes.map(_scope => _scope.id)).toEqual([]);\n    expect(variableByKeyPath.type).toBeUndefined();\n    expect(typeChangeTimes).toBe(7);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/case-run-down/blockwise-python-expression.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\nimport { isEqual } from 'lodash-es';\nimport { Container, injectable } from 'inversify';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { CreateASTParams } from '../../src/ast/types';\nimport {\n  ASTFactory,\n  BaseExpression,\n  BaseType,\n  VariableDeclaration,\n  VariableEngine,\n  injectToAST,\n} from '../../src';\nimport { getContainer } from '../../__mocks__/container';\n\ninterface PyExpressionJSON {\n  content: string;\n  uri: string;\n}\n\nconst delay = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout));\n\ndescribe('Case Run Down: Python Expression In Blockwise', () => {\n  @injectable()\n  class PythonService {\n    // 模拟 Blockwise 表达式后端存储推导的信息\n    uriToType: Record<string, any> = {\n      'blockwise://expression/a': { kind: 'String' },\n      'blockwise://expression/b': { kind: 'Number' },\n    };\n\n    // 模拟 Blockwise 单表达式推导函数\n    async infer(json: PyExpressionJSON) {\n      return Promise.resolve(this.uriToType[json.uri]);\n    }\n\n    // 模拟表达式后端重新推导表达式\n    batchInferEmitter = new Emitter<void>();\n\n    onBatchInfer = this.batchInferEmitter.event;\n  }\n\n  class PythonExpression extends BaseExpression<PyExpressionJSON> {\n    @injectToAST(PythonService) declare service: PythonService;\n\n    static kind: string = 'BlockwisePythonExpression';\n\n    _uri?: string;\n\n    _content?: string;\n\n    // Blockwise 通过 schema 变更时输出的方式来进行类型推导\n    getRefFields() {\n      return [];\n    }\n\n    returnType: BaseType<any> | undefined;\n\n    _prevType: any;\n\n    // Blockwise 中 表达式通过 uri 来进行索引定位\n    fromJSON(json: PyExpressionJSON): void {\n      if (json.uri !== this._uri || json.content !== this._content) {\n        this.service.infer(json).then((res) => {\n          this._prevType = res;\n          this.updateChildNodeByKey('returnType', res);\n        });\n        this._uri = json.uri;\n        this._content = json.content;\n      }\n    }\n\n    toJSON(): PyExpressionJSON {\n      return {\n        content: this._content!,\n        uri: this._uri!,\n      };\n    }\n\n    constructor(params: CreateASTParams) {\n      super(params);\n\n      this.toDispose.push(\n        // 监听后端触发批量校验\n        this.service.onBatchInfer(() => {\n          const nextType = this.service.uriToType[this._uri!];\n\n          if (!isEqual(nextType, this._prevType)) {\n            this.updateChildNodeByKey('returnType', nextType);\n            this._prevType = nextType;\n          }\n        })\n      );\n    }\n  }\n\n  const createBlockwisePythonExpression = (json: PyExpressionJSON) => ({\n    kind: 'BlockwisePythonExpression',\n    ...json,\n  });\n\n  const container: Container = getContainer((bind) => {\n    bind(PythonService).toSelf().inSingletonScope();\n  });\n\n  const variableEngine: VariableEngine = container.get(VariableEngine);\n  const pythonService: PythonService = container.get(PythonService);\n\n  variableEngine.astRegisters.registerAST(PythonExpression, () => ({\n    pythonService,\n  }));\n  const testScope = variableEngine.createScope('test');\n\n  test('1. Infer When Expression Changed', async () => {\n    const variable1: VariableDeclaration = testScope.ast.set(\n      'variable1',\n      ASTFactory.createVariableDeclaration({\n        key: 'variable1',\n        initializer: createBlockwisePythonExpression({\n          uri: 'blockwise://expression/a',\n          content: 'a + b',\n        }),\n      })\n    );\n\n    await delay(0);\n    expect(variable1.type?.kind).toEqual('String');\n    expect(variable1.version).toEqual(1);\n\n    // 更新表达式\n    pythonService.uriToType['blockwise://expression/a'] = { kind: 'Number' };\n    variable1.fromJSON({\n      initializer: createBlockwisePythonExpression({\n        uri: 'blockwise://expression/a',\n        content: 'a + b + c',\n      }),\n    });\n\n    await delay(0);\n    expect(variable1.type?.kind).toEqual('Number');\n    expect(variable1.version).toEqual(2);\n  });\n\n  test('2. Infer When Global Update Triggered', async () => {\n    const variable2: VariableDeclaration = testScope.ast.set(\n      'variable2',\n      ASTFactory.createVariableDeclaration({\n        key: 'variable2',\n        initializer: createBlockwisePythonExpression({\n          uri: 'blockwise://expression/b',\n          content: 'a + b',\n        }),\n      })\n    );\n    await delay(0);\n    expect(variable2.type?.kind).toEqual('Number');\n    expect(variable2.version).toEqual(1);\n\n    // 1. 表达式类型没有变化\n    pythonService.uriToType['blockwise://expression/b'] = { kind: 'Number' };\n    pythonService.batchInferEmitter.fire();\n    await delay(0);\n    expect(variable2.type?.kind).toEqual('Number');\n    expect(variable2.version).toEqual(1);\n\n    // 2. 表达式类型发生了变化\n    pythonService.uriToType['blockwise://expression/b'] = { kind: 'Boolean' };\n    pythonService.batchInferEmitter.fire();\n    await delay(0);\n    expect(variable2.type?.kind).toEqual('Boolean');\n    expect(variable2.version).toEqual(2);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/case-run-down/variable-rename-listener.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, beforeEach, expect } from 'vitest';\nimport { cloneDeep } from 'lodash-es';\n\nimport { ASTNode, ASTNodeFlags, VariableEngine, VariableFieldKeyRenameService } from '../../src';\nimport { simpleVariableList } from '../../__mocks__/variables';\nimport { getContainer } from '../../__mocks__/container';\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\nfunction getFieldKeys(node: ASTNode): string[] {\n  let _curr = node?.parent;\n  const parentKeys: string[] = [];\n\n  while (_curr) {\n    if (_curr.flags & ASTNodeFlags.VariableField) {\n      parentKeys.unshift(_curr.key);\n    }\n    _curr = _curr.parent;\n  }\n\n  return [...parentKeys, node.key];\n}\n\n/**\n * 测试变量 Key Rename 的场景\n */\ndescribe('test Listen Variable Key Rename', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const renameService = container.get(VariableFieldKeyRenameService);\n\n  const testScope = variableEngine.createScope('test');\n\n  let renameInfo: { before: string[]; after: string[] } | null = null;\n  let disposeList: string[][] = [];\n\n  renameService.onRename((_rename) => {\n    renameInfo = {\n      before: getFieldKeys(_rename.before),\n      after: getFieldKeys(_rename.after),\n    };\n  });\n\n  renameService.onDisposeInList((_field) => {\n    disposeList.push(getFieldKeys(_field));\n  });\n\n  beforeEach(() => {\n    testScope.ast.set('test', simpleVariableList);\n    renameInfo = null;\n    disposeList = [];\n  });\n\n  const produceNextJSON = (producer: (json: any) => void) => {\n    const next = cloneDeep(simpleVariableList);\n    producer(next);\n    return next;\n  };\n\n  test.each([\n    // 更改一个字段\n    [\n      produceNextJSON((json) => {\n        json.declarations[0].key = 'string1111';\n      }),\n      ['string'],\n      ['string1111'],\n      [],\n    ],\n    // 更改一个下钻字段\n    [\n      produceNextJSON((json) => {\n        json.declarations[4].type.properties[0].key = 'changedKey';\n      }),\n      ['object', 'key1'],\n      ['object', 'changedKey'],\n      [],\n    ],\n    // 更改字段和他的类型\n    [\n      produceNextJSON((json) => {\n        json.declarations[0].key = 'string1111';\n        json.declarations[0].type = 'Number';\n      }),\n      null,\n      null,\n      [['string']],\n    ],\n    // 更改多个下钻字段\n    [\n      produceNextJSON((json) => {\n        json.declarations[4].type.properties[0].key = 'changedKey';\n        json.declarations[4].type.properties[1].key = 'changedKey222';\n      }),\n      null,\n      null,\n      [\n        ['object', 'key1'],\n        ['object', 'key2'],\n      ],\n    ],\n    // 更改多个字段\n    [\n      produceNextJSON((json) => {\n        json.declarations[0].key = 'string1111';\n        json.declarations[1].key = 'boolean1111';\n      }),\n      null,\n      null,\n      [['string'], ['boolean']],\n    ],\n    // 删除变量\n    [\n      produceNextJSON((json) => {\n        json.declarations = json.declarations.slice(1);\n      }),\n      null,\n      null,\n      [['string']],\n    ],\n    // 添加变量\n    [\n      produceNextJSON((json) => {\n        json.declarations.push({ kind: 'String', key: 'newKey' });\n      }),\n      null,\n      null,\n      [],\n    ],\n  ])('test variable change', (_json, _before, _after, _disposeList) => {\n    testScope.ast.set('test', _json);\n    expect(renameInfo?.before || null).toEqual(_before);\n    expect(renameInfo?.after || null).toEqual(_after);\n    expect(disposeList).toEqual(_disposeList);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/__tests__/scope/variable-engine.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { vi, describe, test, expect } from 'vitest';\n\nimport { VariableEngine } from '../../src';\nimport { simpleVariableList } from '../../__mocks__/variables';\nimport { getContainer } from '../../__mocks__/container';\n\nvi.mock('nanoid', () => {\n  let mockId = 0;\n  return {\n    nanoid: () => 'mocked-id-' + mockId++,\n  };\n});\n\n/**\n * 测试基本的变量声明场景\n */\ndescribe('test Variable Engine', () => {\n  const container = getContainer();\n  const variableEngine = container.get(VariableEngine);\n  const globalVariableTable = variableEngine.globalVariableTable;\n  const testScope = variableEngine.createScope('test');\n  const simpleCase = testScope.ast.set('simple case', simpleVariableList);\n\n  test('test variable Engine Dispose', () => {\n    // unbind All will trigger @preDestroy\n    container.unbindAll();\n\n    expect(variableEngine.chain.disposed).toBeTruthy();\n    expect(testScope.disposed).toBeTruthy();\n    expect(simpleCase.disposed).toBeTruthy();\n    expect(globalVariableTable.variableKeys.length).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  ignorePatterns: ['**/tests__'],\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/variable-core\",\n  \"version\": \"0.1.8\",\n  \"description\": \"variable engine based on scope\",\n  \"keywords\": [\n    \"flow\",\n    \"variable\",\n    \"scope\",\n    \"engine\"\n  ],\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/utils\": \"workspace:*\",\n    \"fast-equals\": \"^2.0.0\",\n    \"inversify\": \"^6.0.1\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nanoid\": \"^5.0.9\",\n    \"reflect-metadata\": \"~0.2.2\",\n    \"rxjs\": \"^7.8.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/react\": \"^18\",\n    \"@types/react-dom\": \"^18\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16.8\",\n    \"react-dom\": \">=16.8\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/ast-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  BehaviorSubject,\n  animationFrameScheduler,\n  debounceTime,\n  distinctUntilChanged,\n  map,\n  skip,\n  tap,\n} from 'rxjs';\nimport { nanoid } from 'nanoid';\nimport { isNil, omitBy } from 'lodash-es';\nimport { shallowEqual } from 'fast-equals';\nimport { Disposable, DisposableCollection } from '@flowgram.ai/utils';\n\nimport { subsToDisposable } from '../utils/toDisposable';\nimport { updateChildNodeHelper } from './utils/helpers';\nimport { type Scope } from '../scope';\nimport {\n  type ASTNodeJSON,\n  type ObserverOrNext,\n  type ASTKindType,\n  type CreateASTParams,\n  type Identifier,\n  SubscribeConfig,\n  GlobalEventActionType,\n  DisposeASTAction,\n  UpdateASTAction,\n} from './types';\nimport { ASTNodeFlags } from './flags';\n\nexport interface ASTNodeRegistry<JSON extends ASTNodeJSON = any> {\n  kind: string;\n  new (params: CreateASTParams, injectOpts: any): ASTNode<JSON>;\n}\n\n/**\n * An `ASTNode` represents a fundamental unit of variable information within the system's Abstract Syntax Tree.\n * It can model various constructs, for example:\n * - **Declarations**: `const a = 1`\n * - **Expressions**: `a.b.c`\n * - **Types**: `number`, `string`, `boolean`\n *\n * Here is some characteristic of ASTNode:\n * - **Tree-like Structure**: ASTNodes can be nested to form a tree, representing complex variable structures.\n * - **Extendable**: New features can be added by extending the base ASTNode class.\n * - **Reactive**: Changes in an ASTNode's value trigger events, enabling reactive programming patterns.\n * - **Serializable**: ASTNodes can be converted to and from a JSON format (ASTNodeJSON) for storage or transmission.\n */\nexport abstract class ASTNode<JSON extends ASTNodeJSON = any> implements Disposable {\n  /**\n   * @deprecated\n   * Get the injected options for the ASTNode.\n   *\n   * Please use `@injectToAst(XXXService) declare xxxService: XXXService` to achieve external dependency injection.\n   */\n  public readonly opts?: any;\n\n  /**\n   * The unique identifier of the ASTNode, which is **immutable**.\n   * - Immutable: Once assigned, the key cannot be changed.\n   * - Automatically generated if not specified, and cannot be changed as well.\n   * - If a new key needs to be generated, the current ASTNode should be destroyed and a new ASTNode should be generated.\n   */\n  public readonly key: Identifier;\n\n  /**\n   * The kind of the ASTNode.\n   */\n  static readonly kind: ASTKindType;\n\n  /**\n   * Node flags, used to record some flag information.\n   */\n  public readonly flags: number = ASTNodeFlags.None;\n\n  /**\n   * The scope in which the ASTNode is located.\n   */\n  public readonly scope: Scope;\n\n  /**\n   * The parent ASTNode.\n   */\n  public readonly parent: ASTNode | undefined;\n\n  /**\n   * The version number of the ASTNode, which increments by 1 each time `fireChange` is called.\n   */\n  protected _version: number = 0;\n\n  /**\n   * Update lock.\n   * - When set to `true`, `fireChange` will not trigger any events.\n   * - This is useful when multiple updates are needed, and you want to avoid multiple triggers.\n   */\n  public changeLocked = false;\n\n  /**\n   * Parameters related to batch updates.\n   */\n  private _batch: {\n    batching: boolean;\n    hasChangesInBatch: boolean;\n  } = {\n    batching: false,\n    hasChangesInBatch: false,\n  };\n\n  /**\n   * AST node change Observable events, implemented based on RxJS.\n   * - Emits the current ASTNode value upon subscription.\n   * - Emits a new value whenever `fireChange` is called.\n   */\n  public readonly value$: BehaviorSubject<ASTNode> = new BehaviorSubject<ASTNode>(this as ASTNode);\n\n  /**\n   * Child ASTNodes.\n   */\n  protected _children = new Set<ASTNode>();\n\n  /**\n   * List of disposal handlers for the ASTNode.\n   */\n  public readonly toDispose: DisposableCollection = new DisposableCollection(\n    Disposable.create(() => {\n      // When a child element is deleted, the parent element triggers an update.\n      this.parent?.fireChange();\n      this.children.forEach((child) => child.dispose());\n    })\n  );\n\n  /**\n   * Callback triggered upon disposal.\n   */\n  onDispose = this.toDispose.onDispose;\n\n  /**\n   * Constructor.\n   * @param createParams Necessary parameters for creating an ASTNode.\n   * @param injectOptions Dependency injection for various modules.\n   */\n  constructor({ key, parent, scope }: CreateASTParams, opts?: any) {\n    this.scope = scope;\n    this.parent = parent;\n    this.opts = opts;\n\n    // Initialize the key value. If a key is passed in, use it; otherwise, generate a random one using nanoid.\n    this.key = key || nanoid();\n\n    // All `fireChange` calls within the subsequent `fromJSON` will be merged into one.\n    this.fromJSON = this.withBatchUpdate(this.fromJSON.bind(this));\n\n    // Add the kind field to the JSON output.\n    const rawToJSON = this.toJSON?.bind(this);\n    this.toJSON = () =>\n      omitBy(\n        {\n          // always include kind\n          kind: this.kind,\n          ...(rawToJSON?.() || {}),\n        },\n        // remove undefined fields\n        isNil\n      ) as JSON;\n  }\n\n  /**\n   * The type of the ASTNode.\n   */\n  get kind(): string {\n    if (!(this.constructor as any).kind) {\n      throw new Error(`ASTNode Registry need a kind: ${this.constructor.name}`);\n    }\n    return (this.constructor as any).kind;\n  }\n\n  /**\n   * Parses AST JSON data.\n   * @param json AST JSON data.\n   */\n  abstract fromJSON(json: JSON): void;\n\n  /**\n   * Gets all child ASTNodes of the current ASTNode.\n   */\n  get children(): ASTNode[] {\n    return Array.from(this._children);\n  }\n\n  /**\n   * Serializes the current ASTNode to ASTNodeJSON.\n   * @returns\n   */\n  abstract toJSON(): JSON;\n\n  /**\n   * Creates a child ASTNode.\n   * @param json The AST JSON of the child ASTNode.\n   * @returns\n   */\n  protected createChildNode<ChildNode extends ASTNode = ASTNode>(json: ASTNodeJSON): ChildNode {\n    const astRegisters = this.scope.variableEngine.astRegisters;\n\n    const child = astRegisters.createAST(json, {\n      parent: this,\n      scope: this.scope,\n    }) as ChildNode;\n\n    // Add to the _children set.\n    this._children.add(child);\n    child.toDispose.push(\n      Disposable.create(() => {\n        this._children.delete(child);\n      })\n    );\n\n    return child;\n  }\n\n  /**\n   * Updates a child ASTNode, quickly implementing the consumption logic for child ASTNode updates.\n   * @param keyInThis The specified key on the current object.\n   */\n  protected updateChildNodeByKey(keyInThis: keyof this, nextJSON?: ASTNodeJSON) {\n    this.withBatchUpdate(updateChildNodeHelper).call(this, {\n      getChildNode: () => this[keyInThis] as ASTNode,\n      updateChildNode: (_node) => ((this as any)[keyInThis] = _node),\n      removeChildNode: () => ((this as any)[keyInThis] = undefined),\n      nextJSON,\n    });\n  }\n\n  /**\n   * Batch updates the ASTNode, merging all `fireChange` calls within the batch function into one.\n   * @param updater The batch function.\n   * @returns\n   */\n  protected withBatchUpdate<ParamTypes extends any[], ReturnType>(\n    updater: (...args: ParamTypes) => ReturnType\n  ) {\n    return (...args: ParamTypes) => {\n      // Nested batchUpdate can only take effect once.\n      if (this._batch.batching) {\n        return updater.call(this, ...args);\n      }\n\n      this._batch.hasChangesInBatch = false;\n\n      this._batch.batching = true;\n      const res = updater.call(this, ...args);\n      this._batch.batching = false;\n\n      if (this._batch.hasChangesInBatch) {\n        this.fireChange();\n      }\n      this._batch.hasChangesInBatch = false;\n\n      return res;\n    };\n  }\n\n  /**\n   * Triggers an update for the current node.\n   */\n  fireChange(): void {\n    if (this.changeLocked || this.disposed) {\n      return;\n    }\n\n    if (this._batch.batching) {\n      this._batch.hasChangesInBatch = true;\n      return;\n    }\n\n    this._version++;\n    this.value$.next(this);\n    this.dispatchGlobalEvent<UpdateASTAction>({ type: 'UpdateAST' });\n    this.parent?.fireChange();\n  }\n\n  /**\n   * The version value of the ASTNode.\n   * - You can used to check whether ASTNode are updated.\n   */\n  get version(): number {\n    return this._version;\n  }\n\n  /**\n   * The unique hash value of the ASTNode.\n   * - It will update when the ASTNode is updated.\n   * - You can used to check two ASTNode are equal.\n   */\n  get hash(): string {\n    return `${this._version}${this.kind}${this.key}`;\n  }\n\n  /**\n   * Listens for changes to the ASTNode.\n   * @param observer The listener callback.\n   * @param selector Listens for specified data.\n   * @returns\n   */\n  subscribe<Data = this>(\n    observer: ObserverOrNext<Data>,\n    { selector, debounceAnimation, triggerOnInit }: SubscribeConfig<this, Data> = {}\n  ): Disposable {\n    return subsToDisposable(\n      this.value$\n        .pipe(\n          map(() => (selector ? selector(this) : (this as any))),\n          distinctUntilChanged(\n            (a, b) => shallowEqual(a, b),\n            (value) => {\n              if (value instanceof ASTNode) {\n                // If the value is an ASTNode, compare its hash.\n                return value.hash;\n              }\n              return value;\n            }\n          ),\n          // By default, skip the first trigger of BehaviorSubject.\n          triggerOnInit ? tap(() => null) : skip(1),\n          // All updates within each animationFrame are merged into one.\n          debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null)\n        )\n        .subscribe(observer)\n    );\n  }\n\n  /**\n   * Dispatches a global event for the current ASTNode.\n   * @param event The global event.\n   */\n  dispatchGlobalEvent<ActionType extends GlobalEventActionType = GlobalEventActionType>(\n    event: Omit<ActionType, 'ast'>\n  ) {\n    this.scope.event.dispatch({\n      ...event,\n      ast: this,\n    });\n  }\n\n  /**\n   * Disposes the ASTNode.\n   */\n  dispose(): void {\n    // Prevent multiple disposals.\n    if (this.toDispose.disposed) {\n      return;\n    }\n\n    this.toDispose.dispose();\n    this.dispatchGlobalEvent<DisposeASTAction>({ type: 'DisposeAST' });\n\n    // When the complete event is emitted, ensure that the current ASTNode is in a disposed state.\n    this.value$.complete();\n    this.value$.unsubscribe();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  /**\n   * Extended information of the ASTNode.\n   */\n  [key: string]: unknown;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/ast-registers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { omit } from 'lodash-es';\nimport { injectable } from 'inversify';\n\nimport { POST_CONSTRUCT_AST_SYMBOL } from './utils/inversify';\nimport { ASTKindType, ASTNodeJSON, CreateASTParams, NewASTAction } from './types';\nimport { ArrayType } from './type/array';\nimport {\n  BooleanType,\n  CustomType,\n  IntegerType,\n  MapType,\n  NumberType,\n  ObjectType,\n  StringType,\n} from './type';\nimport { EnumerateExpression, KeyPathExpression, WrapArrayExpression } from './expression';\nimport { Property, VariableDeclaration, VariableDeclarationList } from './declaration';\nimport { DataNode, MapNode } from './common';\nimport { ASTNode, ASTNodeRegistry } from './ast-node';\n\ntype DataInjector = () => Record<string, any>;\n\n/**\n * Register the AST node to the engine.\n */\n@injectable()\nexport class ASTRegisters {\n  /**\n   * @deprecated Please use `@injectToAst(XXXService) declare xxxService: XXXService` to achieve external dependency injection.\n   */\n  protected injectors: Map<ASTKindType, DataInjector> = new Map();\n\n  protected astMap: Map<ASTKindType, ASTNodeRegistry> = new Map();\n\n  /**\n   * Core AST node registration.\n   */\n  constructor() {\n    this.registerAST(StringType);\n    this.registerAST(NumberType);\n    this.registerAST(BooleanType);\n    this.registerAST(IntegerType);\n    this.registerAST(ObjectType);\n    this.registerAST(ArrayType);\n    this.registerAST(MapType);\n    this.registerAST(CustomType);\n    this.registerAST(Property);\n    this.registerAST(VariableDeclaration);\n    this.registerAST(VariableDeclarationList);\n    this.registerAST(KeyPathExpression);\n\n    this.registerAST(EnumerateExpression);\n    this.registerAST(WrapArrayExpression);\n    this.registerAST(MapNode);\n    this.registerAST(DataNode);\n  }\n\n  /**\n   * Creates an AST node.\n   * @param param Creation parameters.\n   * @returns\n   */\n  createAST<ReturnNode extends ASTNode = ASTNode>(\n    json: ASTNodeJSON,\n    { parent, scope }: CreateASTParams\n  ): ReturnNode {\n    const Registry = this.astMap.get(json.kind!);\n\n    if (!Registry) {\n      throw Error(`ASTKind: ${String(json.kind)} can not find its ASTNode Registry`);\n    }\n\n    const injector = this.injectors.get(json.kind!);\n\n    const node = new Registry(\n      {\n        key: json.key,\n        scope,\n        parent,\n      },\n      injector?.() || {}\n    ) as ReturnNode;\n\n    // Do not trigger fireChange during initial creation.\n    node.changeLocked = true;\n    node.fromJSON(omit(json, ['key', 'kind']));\n    node.changeLocked = false;\n\n    node.dispatchGlobalEvent<NewASTAction>({ type: 'NewAST' });\n\n    if (Reflect.hasMetadata(POST_CONSTRUCT_AST_SYMBOL, node)) {\n      const postConstructKey = Reflect.getMetadata(POST_CONSTRUCT_AST_SYMBOL, node);\n      (node[postConstructKey] as () => void)?.();\n    }\n\n    return node;\n  }\n\n  /**\n   * Gets the node Registry by AST node type.\n   * @param kind\n   * @returns\n   */\n  getASTRegistryByKind(kind: ASTKindType) {\n    return this.astMap.get(kind);\n  }\n\n  /**\n   * Registers an AST node.\n   * @param ASTNode\n   */\n  registerAST(\n    ASTNode: ASTNodeRegistry,\n    /**\n     * @deprecated Please use `@injectToAst(XXXService) declare xxxService: XXXService` to achieve external dependency injection.\n     */\n    injector?: DataInjector\n  ) {\n    this.astMap.set(ASTNode.kind, ASTNode);\n    if (injector) {\n      this.injectors.set(ASTNode.kind, injector);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/common/data-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { shallowEqual } from 'fast-equals';\n\nimport { ASTKind, ASTNodeJSON } from '../types';\nimport { ASTNode } from '../ast-node';\n\n/**\n * Represents a general data node with no child nodes.\n */\nexport class DataNode<Data = any> extends ASTNode {\n  static kind: string = ASTKind.DataNode;\n\n  protected _data: Data;\n\n  /**\n   * The data of the node.\n   */\n  get data(): Data {\n    return this._data;\n  }\n\n  /**\n   * Deserializes the `DataNodeJSON` to the `DataNode`.\n   * @param json The `DataNodeJSON` to deserialize.\n   */\n  fromJSON(json: Data): void {\n    const { kind, ...restData } = json as ASTNodeJSON;\n\n    if (!shallowEqual(restData, this._data)) {\n      this._data = restData as unknown as Data;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * Serialize the `DataNode` to `DataNodeJSON`.\n   * @returns The JSON representation of `DataNode`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.DataNode,\n      ...this._data,\n    };\n  }\n\n  /**\n   * Partially update the data of the node.\n   * @param nextData The data to be updated.\n   */\n  partialUpdate(nextData: Data) {\n    if (!shallowEqual(nextData, this._data)) {\n      this._data = {\n        ...this._data,\n        ...nextData,\n      };\n      this.fireChange();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/common/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { DataNode } from './data-node';\nexport { ListNode, type ListNodeJSON } from './list-node';\nexport { MapNode, type MapNodeJSON } from './map-node';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/common/list-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind, ASTNodeJSON } from '../types';\nimport { ASTNode } from '../ast-node';\n\n/**\n * ASTNodeJSON representation of `ListNode`\n */\nexport interface ListNodeJSON {\n  /**\n   * The list of nodes.\n   */\n  list: ASTNodeJSON[];\n}\n\n/**\n * Represents a list of nodes.\n */\nexport class ListNode extends ASTNode<ListNodeJSON> {\n  static kind: string = ASTKind.ListNode;\n\n  protected _list: ASTNode[];\n\n  /**\n   * The list of nodes.\n   */\n  get list(): ASTNode[] {\n    return this._list;\n  }\n\n  /**\n   * Deserializes the `ListNodeJSON` to the `ListNode`.\n   * @param json The `ListNodeJSON` to deserialize.\n   */\n  fromJSON({ list }: ListNodeJSON): void {\n    // Children that exceed the length need to be destroyed.\n    this._list.slice(list.length).forEach((_item) => {\n      _item.dispose();\n      this.fireChange();\n    });\n\n    // Processing of remaining children.\n    this._list = list.map((_item, idx) => {\n      const prevItem = this._list[idx];\n\n      if (prevItem.kind !== _item.kind) {\n        prevItem.dispose();\n        this.fireChange();\n        return this.createChildNode(_item);\n      }\n\n      prevItem.fromJSON(_item);\n      return prevItem;\n    });\n  }\n\n  /**\n   * Serialize the `ListNode` to `ListNodeJSON`.\n   * @returns The JSON representation of `ListNode`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.ListNode,\n      list: this._list.map((item) => item.toJSON()),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/common/map-node.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { updateChildNodeHelper } from '../utils/helpers';\nimport { ASTKind, ASTNodeJSON } from '../types';\nimport { ASTNode } from '../ast-node';\n\n/**\n * ASTNodeJSON representation of `MapNode`\n */\nexport interface MapNodeJSON {\n  /**\n   * The map of nodes.\n   */\n  map: [string, ASTNodeJSON][];\n}\n\n/**\n * Represents a map of nodes.\n */\nexport class MapNode extends ASTNode<MapNodeJSON> {\n  static kind: string = ASTKind.MapNode;\n\n  protected map: Map<string, ASTNode> = new Map<string, ASTNode>();\n\n  /**\n   * Deserializes the `MapNodeJSON` to the `MapNode`.\n   * @param json The `MapNodeJSON` to deserialize.\n   */\n  fromJSON({ map }: MapNodeJSON): void {\n    const removedKeys = new Set(this.map.keys());\n\n    for (const [key, item] of map || []) {\n      removedKeys.delete(key);\n      this.set(key, item);\n    }\n\n    for (const removeKey of Array.from(removedKeys)) {\n      this.remove(removeKey);\n    }\n  }\n\n  /**\n   * Serialize the `MapNode` to `MapNodeJSON`.\n   * @returns The JSON representation of `MapNode`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.MapNode,\n      map: Array.from(this.map.entries()),\n    };\n  }\n\n  /**\n   * Set a node in the map.\n   * @param key The key of the node.\n   * @param nextJSON The JSON representation of the node.\n   * @returns The node instance.\n   */\n  set<Node extends ASTNode = ASTNode>(key: string, nextJSON: ASTNodeJSON): Node {\n    return this.withBatchUpdate(updateChildNodeHelper).call(this, {\n      getChildNode: () => this.get(key),\n      removeChildNode: () => this.map.delete(key),\n      updateChildNode: (nextNode) => this.map.set(key, nextNode),\n      nextJSON,\n    }) as Node;\n  }\n\n  /**\n   * Remove a node from the map.\n   * @param key The key of the node.\n   */\n  remove(key: string) {\n    this.get(key)?.dispose();\n    this.map.delete(key);\n    this.fireChange();\n  }\n\n  /**\n   * Get a node from the map.\n   * @param key The key of the node.\n   * @returns The node instance if found, otherwise `undefined`.\n   */\n  get<Node extends ASTNode = ASTNode>(key: string): Node | undefined {\n    return this.map.get(key) as Node | undefined;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/declaration/base-variable-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { shallowEqual } from 'fast-equals';\n\nimport { getParentFields } from '../utils/variable-field';\nimport { ASTNodeJSON, ASTNodeJSONOrKind, Identifier } from '../types';\nimport { type BaseType } from '../type';\nimport { ASTNodeFlags } from '../flags';\nimport { type BaseExpression } from '../expression';\nimport { ASTNode } from '../ast-node';\n\n/**\n * ASTNodeJSON representation of `BaseVariableField`\n */\nexport interface BaseVariableFieldJSON<VariableMeta = any> extends ASTNodeJSON {\n  /**\n   * key of the variable field\n   * - For `VariableDeclaration`, the key should be global unique.\n   * - For `Property`, the key is the property name.\n   */\n  key: Identifier;\n  /**\n   * type of the variable field, similar to js code:\n   * `const v: string`\n   */\n  type?: ASTNodeJSONOrKind;\n  /**\n   * initializer of the variable field, similar to js code:\n   * `const v = 'hello'`\n   *\n   * with initializer, the type of field will be inferred from the initializer.\n   */\n  initializer?: ASTNodeJSON;\n  /**\n   * meta data of the variable field, you cans store information like `title`, `icon`, etc.\n   */\n  meta?: VariableMeta;\n}\n\n/**\n * Variable Field abstract class, which is the base class for `VariableDeclaration` and `Property`\n *\n * - `VariableDeclaration` is used to declare a variable in a block scope.\n * - `Property` is used to declare a property in an object.\n */\nexport abstract class BaseVariableField<VariableMeta = any> extends ASTNode<\n  BaseVariableFieldJSON<VariableMeta>\n> {\n  public flags: ASTNodeFlags = ASTNodeFlags.VariableField;\n\n  protected _type?: BaseType;\n\n  protected _meta: VariableMeta = {} as any;\n\n  protected _initializer?: BaseExpression;\n\n  /**\n   * Parent variable fields, sorted from closest to farthest\n   */\n  get parentFields(): BaseVariableField[] {\n    return getParentFields(this);\n  }\n\n  /**\n   * KeyPath of the variable field, sorted from farthest to closest\n   */\n  get keyPath(): string[] {\n    return [...this.parentFields.reverse().map((_field) => _field.key), this.key];\n  }\n\n  /**\n   * Metadata of the variable field, you cans store information like `title`, `icon`, etc.\n   */\n  get meta(): VariableMeta {\n    return this._meta;\n  }\n\n  /**\n   * Type of the variable field, similar to js code:\n   * `const v: string`\n   */\n  get type(): BaseType {\n    return (this._initializer?.returnType || this._type)!;\n  }\n\n  /**\n   * Initializer of the variable field, similar to js code:\n   * `const v = 'hello'`\n   *\n   * with initializer, the type of field will be inferred from the initializer.\n   */\n  get initializer(): BaseExpression | undefined {\n    return this._initializer;\n  }\n\n  /**\n   * The global unique hash of the field, and will be changed when the field is updated.\n   */\n  get hash(): string {\n    return `[${this._version}]${this.keyPath.join('.')}`;\n  }\n\n  /**\n   * Deserialize the `BaseVariableFieldJSON` to the `BaseVariableField`.\n   * @param json ASTJSON representation of `BaseVariableField`\n   */\n  fromJSON({ type, initializer, meta }: Omit<BaseVariableFieldJSON<VariableMeta>, 'key'>): void {\n    // 类型变化\n    this.updateType(type);\n\n    // 表达式更新\n    this.updateInitializer(initializer);\n\n    // Extra 更新\n    this.updateMeta(meta!);\n  }\n\n  /**\n   * Update the type of the variable field\n   * @param type type ASTJSON representation of Type\n   */\n  updateType(type: BaseVariableFieldJSON['type']) {\n    const nextTypeJson = typeof type === 'string' ? { kind: type } : type;\n    this.updateChildNodeByKey('_type', nextTypeJson);\n  }\n\n  /**\n   * Update the initializer of the variable field\n   * @param nextInitializer initializer ASTJSON representation of Expression\n   */\n  updateInitializer(nextInitializer?: BaseVariableFieldJSON['initializer']) {\n    this.updateChildNodeByKey('_initializer', nextInitializer);\n  }\n\n  /**\n   * Update the meta data of the variable field\n   * @param nextMeta meta data of the variable field\n   */\n  updateMeta(nextMeta: VariableMeta) {\n    if (!shallowEqual(nextMeta, this._meta)) {\n      this._meta = nextMeta;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * Get the variable field by keyPath, similar to js code:\n   * `v.a.b`\n   * @param keyPath\n   * @returns\n   */\n  getByKeyPath(keyPath: string[]): BaseVariableField | undefined {\n    if (this.type?.flags & ASTNodeFlags.DrilldownType) {\n      return this.type.getByKeyPath(keyPath) as BaseVariableField | undefined;\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Subscribe to type change of the variable field\n   * @param observer\n   * @returns\n   */\n  onTypeChange(observer: (type: ASTNode | undefined) => void) {\n    return this.subscribe(observer, { selector: (curr) => curr.type });\n  }\n\n  /**\n   * Serialize the variable field to JSON\n   * @returns ASTNodeJSON representation of `BaseVariableField`\n   */\n  toJSON(): BaseVariableFieldJSON<VariableMeta> {\n    return {\n      key: this.key,\n      type: this.type?.toJSON(),\n      initializer: this.initializer?.toJSON(),\n      meta: this._meta,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/declaration/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { VariableDeclaration, type VariableDeclarationJSON } from './variable-declaration';\nexport {\n  VariableDeclarationList,\n  type VariableDeclarationListJSON,\n  type VariableDeclarationListChangeAction,\n} from './variable-declaration-list';\nexport { type PropertyJSON, Property } from './property';\nexport { BaseVariableField } from './base-variable-field';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/declaration/property.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { BaseVariableField, BaseVariableFieldJSON } from './base-variable-field';\n\n/**\n * ASTNodeJSON representation of the `Property`.\n */\nexport type PropertyJSON<VariableMeta = any> = BaseVariableFieldJSON<VariableMeta>;\n\n/**\n * `Property` is a variable field that represents a property of a `ObjectType`.\n */\nexport class Property<VariableMeta = any> extends BaseVariableField<VariableMeta> {\n  static kind: string = ASTKind.Property;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/declaration/variable-declaration-list.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { GlobalEventActionType } from '../types';\nimport { ASTNode } from '../ast-node';\nimport { type VariableDeclarationJSON, VariableDeclaration } from './variable-declaration';\n\nexport interface VariableDeclarationListJSON<VariableMeta = any> {\n  /**\n   * `declarations` must be of type `VariableDeclaration`, so the business can omit the `kind` field.\n   */\n  declarations?: VariableDeclarationJSON<VariableMeta>[];\n  /**\n   * The starting order number for variables.\n   */\n  startOrder?: number;\n}\n\nexport type VariableDeclarationListChangeAction = GlobalEventActionType<\n  'VariableListChange',\n  {\n    prev: VariableDeclaration[];\n    next: VariableDeclaration[];\n  },\n  VariableDeclarationList\n>;\n\nexport class VariableDeclarationList extends ASTNode<VariableDeclarationListJSON> {\n  static kind: string = ASTKind.VariableDeclarationList;\n\n  /**\n   * Map of variable declarations, keyed by variable name.\n   */\n  declarationTable: Map<string, VariableDeclaration> = new Map();\n\n  /**\n   * Variable declarations, sorted by `order`.\n   */\n  declarations: VariableDeclaration[];\n\n  /**\n   * Deserialize the `VariableDeclarationListJSON` to the `VariableDeclarationList`.\n   * - VariableDeclarationListChangeAction will be dispatched after deserialization.\n   *\n   * @param declarations Variable declarations.\n   * @param startOrder The starting order number for variables. Default is 0.\n   */\n  fromJSON({ declarations, startOrder }: VariableDeclarationListJSON): void {\n    const removedKeys = new Set(this.declarationTable.keys());\n    const prev = [...(this.declarations || [])];\n\n    // Iterate over the new properties.\n    this.declarations = (declarations || []).map(\n      (declaration: VariableDeclarationJSON, idx: number) => {\n        const order = (startOrder || 0) + idx;\n\n        // If the key is not set, reuse the previous key.\n        const declarationKey = declaration.key || this.declarations?.[idx]?.key;\n        const existDeclaration = this.declarationTable.get(declarationKey);\n        if (declarationKey) {\n          removedKeys.delete(declarationKey);\n        }\n\n        if (existDeclaration) {\n          existDeclaration.fromJSON({ order, ...declaration });\n\n          return existDeclaration;\n        } else {\n          const newDeclaration = this.createChildNode({\n            order,\n            ...declaration,\n            kind: ASTKind.VariableDeclaration,\n          }) as VariableDeclaration;\n          this.fireChange();\n\n          this.declarationTable.set(newDeclaration.key, newDeclaration);\n\n          return newDeclaration;\n        }\n      }\n    );\n\n    // Delete variables that no longer exist.\n    removedKeys.forEach((key) => {\n      const declaration = this.declarationTable.get(key);\n      declaration?.dispose();\n      this.declarationTable.delete(key);\n    });\n\n    this.dispatchGlobalEvent<VariableDeclarationListChangeAction>({\n      type: 'VariableListChange',\n      payload: {\n        prev,\n        next: [...this.declarations],\n      },\n    });\n  }\n\n  /**\n   * Serialize the `VariableDeclarationList` to the `VariableDeclarationListJSON`.\n   * @returns ASTJSON representation of `VariableDeclarationList`\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.VariableDeclarationList,\n      declarations: this.declarations.map((_declaration) => _declaration.toJSON()),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/declaration/variable-declaration.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind, GlobalEventActionType, type CreateASTParams } from '../types';\nimport { BaseVariableField, BaseVariableFieldJSON } from './base-variable-field';\n\n/**\n * ASTNodeJSON representation of the `VariableDeclaration`.\n */\nexport type VariableDeclarationJSON<VariableMeta = any> = BaseVariableFieldJSON<VariableMeta> & {\n  /**\n   * Variable sorting order, which is used to sort variables in `scope.outputs.variables`\n   */\n  order?: number;\n};\n\n/**\n * Action type for re-sorting variable declarations.\n */\nexport type ReSortVariableDeclarationsAction = GlobalEventActionType<'ReSortVariableDeclarations'>;\n\n/**\n * `VariableDeclaration` is a variable field that represents a variable declaration.\n */\nexport class VariableDeclaration<VariableMeta = any> extends BaseVariableField<VariableMeta> {\n  static kind: string = ASTKind.VariableDeclaration;\n\n  protected _order: number = 0;\n\n  /**\n   * Variable sorting order, which is used to sort variables in `scope.outputs.variables`\n   */\n  get order(): number {\n    return this._order;\n  }\n\n  constructor(params: CreateASTParams) {\n    super(params);\n  }\n\n  /**\n   * Deserialize the `VariableDeclarationJSON` to the `VariableDeclaration`.\n   */\n  fromJSON({ order, ...rest }: Omit<VariableDeclarationJSON<VariableMeta>, 'key'>): void {\n    // Update order.\n    this.updateOrder(order);\n\n    // Update other information.\n    super.fromJSON(rest as BaseVariableFieldJSON<VariableMeta>);\n  }\n\n  /**\n   * Update the sorting order of the variable declaration.\n   * @param order Variable sorting order. Default is 0.\n   */\n  updateOrder(order: number = 0): void {\n    if (order !== this._order) {\n      this._order = order;\n      this.dispatchGlobalEvent<ReSortVariableDeclarationsAction>({\n        type: 'ReSortVariableDeclarations',\n      });\n      this.fireChange();\n    }\n  }\n\n  /**\n   * Serialize the `VariableDeclaration` to `VariableDeclarationJSON`.\n   * @returns The JSON representation of `VariableDeclaration`.\n   */\n  toJSON(): VariableDeclarationJSON<VariableMeta> {\n    return {\n      ...super.toJSON(),\n      order: this.order,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/base-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  type Observable,\n  distinctUntilChanged,\n  map,\n  switchMap,\n  combineLatest,\n  of,\n  Subject,\n  share,\n} from 'rxjs';\nimport { shallowEqual } from 'fast-equals';\n\nimport { getParentFields } from '../utils/variable-field';\nimport { ASTNodeJSON, type CreateASTParams } from '../types';\nimport { type BaseType } from '../type';\nimport { ASTNodeFlags } from '../flags';\nimport { type BaseVariableField } from '../declaration';\nimport { ASTNode } from '../ast-node';\nimport { subsToDisposable } from '../../utils/toDisposable';\nimport { IVariableTable } from '../../scope/types';\n\ntype ExpressionRefs = (BaseVariableField | undefined)[];\n\n/**\n * Base class for all expressions.\n *\n * All other expressions should extend this class.\n */\nexport abstract class BaseExpression<JSON extends ASTNodeJSON = any> extends ASTNode<JSON> {\n  public flags: ASTNodeFlags = ASTNodeFlags.Expression;\n\n  /**\n   * Get the global variable table, which is used to access referenced variables.\n   */\n  get globalVariableTable(): IVariableTable {\n    return this.scope.variableEngine.globalVariableTable;\n  }\n\n  /**\n   * Parent variable fields, sorted from closest to farthest.\n   */\n  get parentFields(): BaseVariableField[] {\n    return getParentFields(this);\n  }\n\n  /**\n   * Get the variable fields referenced by the expression.\n   *\n   * This method should be implemented by subclasses.\n   * @returns An array of referenced variable fields.\n   */\n  abstract getRefFields(): ExpressionRefs;\n\n  /**\n   * The return type of the expression.\n   */\n  abstract returnType: BaseType | undefined;\n\n  /**\n   * The variable fields referenced by the expression.\n   */\n  protected _refs: ExpressionRefs = [];\n\n  /**\n   * The variable fields referenced by the expression.\n   */\n  get refs(): ExpressionRefs {\n    return this._refs;\n  }\n\n  protected refreshRefs$: Subject<void> = new Subject();\n\n  /**\n   * Refresh the variable references.\n   */\n  refreshRefs() {\n    this.refreshRefs$.next();\n  }\n\n  /**\n   * An observable that emits the referenced variable fields when they change.\n   */\n  refs$: Observable<ExpressionRefs> = this.refreshRefs$.pipe(\n    map(() => this.getRefFields()),\n    distinctUntilChanged<ExpressionRefs>(shallowEqual),\n    switchMap((refs) =>\n      !refs?.length\n        ? of([])\n        : combineLatest(\n            refs.map((ref) =>\n              ref\n                ? (ref.value$ as unknown as Observable<BaseVariableField | undefined>)\n                : of(undefined)\n            )\n          )\n    ),\n    share()\n  );\n\n  constructor(params: CreateASTParams, opts?: any) {\n    super(params, opts);\n\n    this.toDispose.push(\n      subsToDisposable(\n        this.refs$.subscribe((_refs: ExpressionRefs) => {\n          this._refs = _refs;\n          this.fireChange();\n        })\n      )\n    );\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/enumerate-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind, ASTNodeJSON } from '../types';\nimport { ArrayType } from '../type/array';\nimport { BaseType } from '../type';\nimport { BaseExpression } from './base-expression';\n\n/**\n * ASTNodeJSON representation of `EnumerateExpression`\n */\nexport interface EnumerateExpressionJSON {\n  /**\n   * The expression to be enumerated.\n   */\n  enumerateFor: ASTNodeJSON;\n}\n\n/**\n * Represents an enumeration expression, which iterates over a list and returns the type of the enumerated variable.\n */\nexport class EnumerateExpression extends BaseExpression<EnumerateExpressionJSON> {\n  static kind: string = ASTKind.EnumerateExpression;\n\n  protected _enumerateFor: BaseExpression | undefined;\n\n  /**\n   * The expression to be enumerated.\n   */\n  get enumerateFor() {\n    return this._enumerateFor;\n  }\n\n  /**\n   * The return type of the expression.\n   */\n  get returnType(): BaseType | undefined {\n    // The return value of the enumerated expression.\n    const childReturnType = this.enumerateFor?.returnType;\n\n    if (childReturnType?.kind === ASTKind.Array) {\n      // Get the item type of the array.\n      return (childReturnType as ArrayType).items;\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Get the variable fields referenced by the expression.\n   * @returns An empty array, as this expression does not reference any variables.\n   */\n  getRefFields(): [] {\n    return [];\n  }\n\n  /**\n   * Deserializes the `EnumerateExpressionJSON` to the `EnumerateExpression`.\n   * @param json The `EnumerateExpressionJSON` to deserialize.\n   */\n  fromJSON({ enumerateFor: expression }: EnumerateExpressionJSON): void {\n    this.updateChildNodeByKey('_enumerateFor', expression);\n  }\n\n  /**\n   * Serialize the `EnumerateExpression` to `EnumerateExpressionJSON`.\n   * @returns The JSON representation of `EnumerateExpression`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.EnumerateExpression,\n      enumerateFor: this.enumerateFor?.toJSON(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { BaseExpression } from './base-expression';\nexport { EnumerateExpression, type EnumerateExpressionJSON } from './enumerate-expression';\nexport { KeyPathExpression, type KeyPathExpressionJSON } from './keypath-expression';\nexport { LegacyKeyPathExpression } from './legacy-keypath-expression';\nexport { WrapArrayExpression, type WrapArrayExpressionJSON } from './wrap-array-expression';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/keypath-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { distinctUntilChanged } from 'rxjs';\nimport { shallowEqual } from 'fast-equals';\n\nimport { checkRefCycle } from '../utils/expression';\nimport { ASTNodeJSON, ASTKind, CreateASTParams } from '../types';\nimport { BaseType } from '../type';\nimport { type BaseVariableField } from '../declaration';\nimport { subsToDisposable } from '../../utils/toDisposable';\nimport { BaseExpression } from './base-expression';\n\n/**\n * ASTNodeJSON representation of `KeyPathExpression`\n */\nexport interface KeyPathExpressionJSON {\n  /**\n   * The key path of the variable.\n   */\n  keyPath: string[];\n}\n\n/**\n * Represents a key path expression, which is used to reference a variable by its key path.\n *\n * This is the V2 of `KeyPathExpression`, with the following improvements:\n * - `returnType` is copied to a new instance to avoid reference issues.\n * - Circular reference detection is introduced.\n */\nexport class KeyPathExpression<\n  CustomPathJSON extends ASTNodeJSON = KeyPathExpressionJSON\n> extends BaseExpression<CustomPathJSON> {\n  static kind: string = ASTKind.KeyPathExpression;\n\n  protected _keyPath: string[] = [];\n\n  protected _rawPathJson: CustomPathJSON;\n\n  /**\n   * The key path of the variable.\n   */\n  get keyPath(): string[] {\n    return this._keyPath;\n  }\n\n  /**\n   * Get the variable fields referenced by the expression.\n   * @returns An array of referenced variable fields.\n   */\n  getRefFields(): BaseVariableField[] {\n    const ref = this.scope.available.getByKeyPath(this._keyPath);\n\n    // When refreshing references, check for circular references. If a circular reference exists, do not reference the variable.\n    if (checkRefCycle(this, [ref])) {\n      // Prompt that a circular reference exists.\n      console.warn(\n        '[CustomKeyPathExpression] checkRefCycle: Reference Cycle Existed',\n        this.parentFields.map((_field) => _field.key).reverse()\n      );\n      return [];\n    }\n\n    return ref ? [ref] : [];\n  }\n\n  /**\n   * The return type of the expression.\n   *\n   * A new `returnType` node is generated directly, instead of reusing the existing one, to ensure that different key paths do not point to the same field.\n   */\n  _returnType: BaseType;\n\n  /**\n   * The return type of the expression.\n   */\n  get returnType() {\n    return this._returnType;\n  }\n\n  /**\n   * Parse the business-defined path expression into a key path.\n   *\n   * Businesses can quickly customize their own path expressions by modifying this method.\n   * @param json The path expression defined by the business.\n   * @returns The key path.\n   */\n  protected parseToKeyPath(json: CustomPathJSON): string[] {\n    // The default JSON is in KeyPathExpressionJSON format.\n    return (json as unknown as KeyPathExpressionJSON).keyPath;\n  }\n\n  /**\n   * Deserializes the `KeyPathExpressionJSON` to the `KeyPathExpression`.\n   * @param json The `KeyPathExpressionJSON` to deserialize.\n   */\n  fromJSON(json: CustomPathJSON): void {\n    const keyPath = this.parseToKeyPath(json);\n\n    if (!shallowEqual(keyPath, this._keyPath)) {\n      this._keyPath = keyPath;\n      this._rawPathJson = json;\n\n      // After the keyPath is updated, the referenced variables need to be refreshed.\n      this.refreshRefs();\n    }\n  }\n\n  /**\n   * Get the return type JSON by reference.\n   * @param _ref The referenced variable field.\n   * @returns The JSON representation of the return type.\n   */\n  getReturnTypeJSONByRef(_ref: BaseVariableField | undefined): ASTNodeJSON | undefined {\n    return _ref?.type?.toJSON();\n  }\n\n  constructor(params: CreateASTParams, opts: any) {\n    super(params, opts);\n\n    this.toDispose.pushAll([\n      // Can be used when the variable list changes (when there are additions or deletions).\n      this.scope.available.onVariableListChange(() => {\n        this.refreshRefs();\n      }),\n      // When the referable variable pointed to by this._keyPath changes, refresh the reference data.\n      this.scope.available.onAnyVariableChange((_v) => {\n        if (_v.key === this._keyPath[0]) {\n          this.refreshRefs();\n        }\n      }),\n      subsToDisposable(\n        this.refs$\n          .pipe(\n            distinctUntilChanged(\n              (prev, next) => prev === next,\n              (_refs) => _refs?.[0]?.type?.hash\n            )\n          )\n          .subscribe((_type) => {\n            const [ref] = this._refs;\n            this.updateChildNodeByKey('_returnType', this.getReturnTypeJSONByRef(ref));\n          })\n      ),\n    ]);\n  }\n\n  /**\n   * Serialize the `KeyPathExpression` to `KeyPathExpressionJSON`.\n   * @returns The JSON representation of `KeyPathExpression`.\n   */\n  toJSON() {\n    return this._rawPathJson;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/legacy-keypath-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { shallowEqual } from 'fast-equals';\n\nimport { ASTNodeJSON, ASTKind, CreateASTParams } from '../types';\nimport { BaseType } from '../type';\nimport { ASTNodeFlags } from '../flags';\nimport { type BaseVariableField } from '../declaration';\nimport { BaseExpression } from './base-expression';\n\n/**\n * ASTNodeJSON representation of `KeyPathExpression`\n */\nexport interface KeyPathExpressionJSON {\n  /**\n   * The key path of the variable.\n   */\n  keyPath: string[];\n}\n\n/**\n * @deprecated Use `KeyPathExpression` instead.\n * Represents a key path expression, which is used to reference a variable by its key path.\n */\nexport class LegacyKeyPathExpression<\n  CustomPathJSON extends ASTNodeJSON = KeyPathExpressionJSON\n> extends BaseExpression<CustomPathJSON> {\n  static kind: string = ASTKind.KeyPathExpression;\n\n  protected _keyPath: string[] = [];\n\n  protected _rawPathJson: CustomPathJSON;\n\n  /**\n   * The key path of the variable.\n   */\n  get keyPath(): string[] {\n    return this._keyPath;\n  }\n\n  /**\n   * Get the variable fields referenced by the expression.\n   * @returns An array of referenced variable fields.\n   */\n  getRefFields(): BaseVariableField[] {\n    const ref = this.scope.available.getByKeyPath(this._keyPath);\n    return ref ? [ref] : [];\n  }\n\n  /**\n   * The return type of the expression.\n   */\n  get returnType(): BaseType | undefined {\n    const [refNode] = this._refs || [];\n\n    // Get the type of the referenced variable.\n    if (refNode && refNode.flags & ASTNodeFlags.VariableField) {\n      return refNode.type;\n    }\n\n    return;\n  }\n\n  /**\n   * Parse the business-defined path expression into a key path.\n   *\n   * Businesses can quickly customize their own path expressions by modifying this method.\n   * @param json The path expression defined by the business.\n   * @returns The key path.\n   */\n  protected parseToKeyPath(json: CustomPathJSON): string[] {\n    // The default JSON is in KeyPathExpressionJSON format.\n    return (json as unknown as KeyPathExpressionJSON).keyPath;\n  }\n\n  /**\n   * Deserializes the `KeyPathExpressionJSON` to the `KeyPathExpression`.\n   * @param json The `KeyPathExpressionJSON` to deserialize.\n   */\n  fromJSON(json: CustomPathJSON): void {\n    const keyPath = this.parseToKeyPath(json);\n\n    if (!shallowEqual(keyPath, this._keyPath)) {\n      this._keyPath = keyPath;\n      this._rawPathJson = json;\n\n      // After the keyPath is updated, the referenced variables need to be refreshed.\n      this.refreshRefs();\n    }\n  }\n\n  constructor(params: CreateASTParams, opts: any) {\n    super(params, opts);\n\n    this.toDispose.pushAll([\n      // Can be used when the variable list changes (when there are additions or deletions).\n      this.scope.available.onVariableListChange(() => {\n        this.refreshRefs();\n      }),\n      // When the referable variable pointed to by this._keyPath changes, refresh the reference data.\n      this.scope.available.onAnyVariableChange((_v) => {\n        if (_v.key === this._keyPath[0]) {\n          this.refreshRefs();\n        }\n      }),\n    ]);\n  }\n\n  /**\n   * Serialize the `KeyPathExpression` to `KeyPathExpressionJSON`.\n   * @returns The JSON representation of `KeyPathExpression`.\n   */\n  toJSON() {\n    return this._rawPathJson;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/expression/wrap-array-expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { postConstructAST } from '../utils/inversify';\nimport { ASTKind, ASTNodeJSON } from '../types';\nimport { BaseType } from '../type';\nimport { BaseExpression } from './base-expression';\n\n/**\n * ASTNodeJSON representation of `WrapArrayExpression`\n */\nexport interface WrapArrayExpressionJSON {\n  /**\n   * The expression to be wrapped.\n   */\n  wrapFor: ASTNodeJSON;\n}\n\n/**\n * Represents a wrap expression, which wraps an expression with an array.\n */\nexport class WrapArrayExpression extends BaseExpression<WrapArrayExpressionJSON> {\n  static kind: string = ASTKind.WrapArrayExpression;\n\n  protected _wrapFor: BaseExpression | undefined;\n\n  protected _returnType: BaseType | undefined;\n\n  /**\n   * The expression to be wrapped.\n   */\n  get wrapFor() {\n    return this._wrapFor;\n  }\n\n  /**\n   * The return type of the expression.\n   */\n  get returnType(): BaseType | undefined {\n    return this._returnType;\n  }\n\n  /**\n   * Refresh the return type of the expression.\n   */\n  refreshReturnType() {\n    // The return value of the wrapped expression.\n    const childReturnTypeJSON = this.wrapFor?.returnType?.toJSON();\n\n    this.updateChildNodeByKey('_returnType', {\n      kind: ASTKind.Array,\n      items: childReturnTypeJSON,\n    });\n  }\n\n  /**\n   * Get the variable fields referenced by the expression.\n   * @returns An empty array, as this expression does not reference any variables.\n   */\n  getRefFields(): [] {\n    return [];\n  }\n\n  /**\n   * Deserializes the `WrapArrayExpressionJSON` to the `WrapArrayExpression`.\n   * @param json The `WrapArrayExpressionJSON` to deserialize.\n   */\n  fromJSON({ wrapFor: expression }: WrapArrayExpressionJSON): void {\n    this.updateChildNodeByKey('_wrapFor', expression);\n  }\n\n  /**\n   * Serialize the `WrapArrayExpression` to `WrapArrayExpressionJSON`.\n   * @returns The JSON representation of `WrapArrayExpression`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.WrapArrayExpression,\n      wrapFor: this.wrapFor?.toJSON(),\n    };\n  }\n\n  @postConstructAST()\n  protected init() {\n    this.refreshReturnType = this.refreshReturnType.bind(this);\n\n    this.toDispose.push(\n      this.subscribe(this.refreshReturnType, {\n        selector: (curr) => curr.wrapFor?.returnType,\n        triggerOnInit: true,\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/factory.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind, ASTNodeJSON } from './types';\nimport { StringJSON } from './type/string';\nimport { MapJSON } from './type/map';\nimport { ArrayJSON } from './type/array';\nimport { CustomTypeJSON, ObjectJSON, UnionJSON } from './type';\nimport {\n  EnumerateExpressionJSON,\n  KeyPathExpressionJSON,\n  WrapArrayExpressionJSON,\n} from './expression';\nimport { PropertyJSON, VariableDeclarationJSON, VariableDeclarationListJSON } from './declaration';\nimport { ASTNode } from './ast-node';\n\n/**\n * Variable-core ASTNode factories.\n */\nexport namespace ASTFactory {\n  /**\n   * Type-related factories.\n   */\n\n  /**\n   * Creates a `String` type node.\n   */\n  export const createString = (json?: StringJSON) => ({\n    kind: ASTKind.String,\n    ...(json || {}),\n  });\n\n  /**\n   * Creates a `Number` type node.\n   */\n  export const createNumber = () => ({ kind: ASTKind.Number });\n\n  /**\n   * Creates a `Boolean` type node.\n   */\n  export const createBoolean = () => ({ kind: ASTKind.Boolean });\n\n  /**\n   * Creates an `Integer` type node.\n   */\n  export const createInteger = () => ({ kind: ASTKind.Integer });\n\n  /**\n   * Creates an `Object` type node.\n   */\n  export const createObject = (json: ObjectJSON) => ({\n    kind: ASTKind.Object,\n    ...json,\n  });\n\n  /**\n   * Creates an `Array` type node.\n   */\n  export const createArray = (json: ArrayJSON) => ({\n    kind: ASTKind.Array,\n    ...json,\n  });\n\n  /**\n   * Creates a `Map` type node.\n   */\n  export const createMap = (json: MapJSON) => ({\n    kind: ASTKind.Map,\n    ...json,\n  });\n\n  /**\n   * Creates a `Union` type node.\n   */\n  export const createUnion = (json: UnionJSON) => ({\n    kind: ASTKind.Union,\n    ...json,\n  });\n\n  /**\n   * Creates a `CustomType` node.\n   */\n  export const createCustomType = (json: CustomTypeJSON) => ({\n    kind: ASTKind.CustomType,\n    ...json,\n  });\n\n  /**\n   * Declaration-related factories.\n   */\n\n  /**\n   * Creates a `VariableDeclaration` node.\n   */\n  export const createVariableDeclaration = <VariableMeta = any>(\n    json: VariableDeclarationJSON<VariableMeta>\n  ) => ({\n    kind: ASTKind.VariableDeclaration,\n    ...json,\n  });\n\n  /**\n   * Creates a `Property` node.\n   */\n  export const createProperty = <VariableMeta = any>(json: PropertyJSON<VariableMeta>) => ({\n    kind: ASTKind.Property,\n    ...json,\n  });\n\n  /**\n   * Creates a `VariableDeclarationList` node.\n   */\n  export const createVariableDeclarationList = (json: VariableDeclarationListJSON) => ({\n    kind: ASTKind.VariableDeclarationList,\n    ...json,\n  });\n\n  /**\n   * Expression-related factories.\n   */\n\n  /**\n   * Creates an `EnumerateExpression` node.\n   */\n  export const createEnumerateExpression = (json: EnumerateExpressionJSON) => ({\n    kind: ASTKind.EnumerateExpression,\n    ...json,\n  });\n\n  /**\n   * Creates a `KeyPathExpression` node.\n   */\n  export const createKeyPathExpression = (json: KeyPathExpressionJSON) => ({\n    kind: ASTKind.KeyPathExpression,\n    ...json,\n  });\n\n  /**\n   * Creates a `WrapArrayExpression` node.\n   */\n  export const createWrapArrayExpression = (json: WrapArrayExpressionJSON) => ({\n    kind: ASTKind.WrapArrayExpression,\n    ...json,\n  });\n\n  /**\n   * Create by AST Class.\n   */\n\n  /**\n   * Creates Type-Safe ASTNodeJSON object based on the provided AST class.\n   *\n   * @param targetType Target ASTNode class.\n   * @param json The JSON data for the node.\n   * @returns The ASTNode JSON object.\n   */\n  export const create = <JSON extends ASTNodeJSON>(\n    targetType: { kind: string; new (...args: any[]): ASTNode<JSON> },\n    json: JSON\n  ) => ({ kind: targetType.kind, ...json });\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/flags.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * ASTNode flags. Stored in the `flags` property of the `ASTNode`.\n */\nexport enum ASTNodeFlags {\n  /**\n   * None.\n   */\n  None = 0,\n\n  /**\n   * Variable Field.\n   */\n  VariableField = 1 << 0,\n\n  /**\n   * Expression.\n   */\n  Expression = 1 << 2,\n\n  /**\n   * # Variable Type Flags\n   */\n\n  /**\n   *  Basic type.\n   */\n  BasicType = 1 << 3,\n  /**\n   * Drillable variable type.\n   */\n  DrilldownType = 1 << 4,\n  /**\n   * Enumerable variable type.\n   */\n  EnumerateType = 1 << 5,\n  /**\n   * Composite type, currently not in use.\n   */\n  UnionType = 1 << 6,\n\n  /**\n   * Variable type.\n   */\n  VariableType = BasicType | DrilldownType | EnumerateType | UnionType,\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport {\n  type ASTNodeJSON,\n  ASTKind,\n  type GetKindJSON,\n  type GetKindJSONOrKind,\n  type CreateASTParams,\n  type GlobalEventActionType,\n} from './types';\nexport { ASTRegisters } from './ast-registers';\nexport { ASTNode, type ASTNodeRegistry } from './ast-node';\nexport { ASTNodeFlags } from './flags';\n\nexport * from './common';\nexport * from './declaration';\nexport * from './type';\nexport * from './expression';\n\nexport { ASTFactory } from './factory';\nexport { ASTMatch } from './match';\nexport { injectToAST, postConstructAST } from './utils/inversify';\nexport { isMatchAST } from './utils/helpers';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/match.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from './types';\nimport {\n  type StringType,\n  type NumberType,\n  type BooleanType,\n  type IntegerType,\n  type ObjectType,\n  type ArrayType,\n  type MapType,\n  type CustomType,\n} from './type';\nimport { ASTNodeFlags } from './flags';\nimport {\n  type WrapArrayExpression,\n  type EnumerateExpression,\n  type KeyPathExpression,\n} from './expression';\nimport {\n  type BaseVariableField,\n  type Property,\n  type VariableDeclaration,\n  type VariableDeclarationList,\n} from './declaration';\nimport { type ASTNode } from './ast-node';\n\n/**\n * Variable-core ASTNode matchers.\n *\n * - Typescript code inside if statement will be type guarded.\n */\nexport namespace ASTMatch {\n  /**\n   * # Type-related matchers.\n   */\n\n  /**\n   * Check if the node is a `StringType`.\n   */\n  export const isString = (node?: ASTNode): node is StringType => node?.kind === ASTKind.String;\n\n  /**\n   * Check if the node is a `NumberType`.\n   */\n  export const isNumber = (node?: ASTNode): node is NumberType => node?.kind === ASTKind.Number;\n\n  /**\n   * Check if the node is a `BooleanType`.\n   */\n  export const isBoolean = (node?: ASTNode): node is BooleanType => node?.kind === ASTKind.Boolean;\n\n  /**\n   * Check if the node is a `IntegerType`.\n   */\n  export const isInteger = (node?: ASTNode): node is IntegerType => node?.kind === ASTKind.Integer;\n\n  /**\n   * Check if the node is a `ObjectType`.\n   */\n  export const isObject = (node?: ASTNode): node is ObjectType => node?.kind === ASTKind.Object;\n\n  /**\n   * Check if the node is a `ArrayType`.\n   */\n  export const isArray = (node?: ASTNode): node is ArrayType => node?.kind === ASTKind.Array;\n\n  /**\n   * Check if the node is a `MapType`.\n   */\n  export const isMap = (node?: ASTNode): node is MapType => node?.kind === ASTKind.Map;\n\n  /**\n   * Check if the node is a `CustomType`.\n   */\n  export const isCustomType = (node?: ASTNode): node is CustomType =>\n    node?.kind === ASTKind.CustomType;\n\n  /**\n   * # Declaration-related matchers.\n   */\n\n  /**\n   * Check if the node is a `VariableDeclaration`.\n   */\n  export const isVariableDeclaration = <VariableMeta = any>(\n    node?: ASTNode\n  ): node is VariableDeclaration<VariableMeta> => node?.kind === ASTKind.VariableDeclaration;\n\n  /**\n   * Check if the node is a `Property`.\n   */\n  export const isProperty = <VariableMeta = any>(node?: ASTNode): node is Property<VariableMeta> =>\n    node?.kind === ASTKind.Property;\n\n  /**\n   * Check if the node is a `BaseVariableField`.\n   */\n  export const isBaseVariableField = (node?: ASTNode): node is BaseVariableField =>\n    !!(node?.flags || 0 & ASTNodeFlags.VariableField);\n\n  /**\n   * Check if the node is a `VariableDeclarationList`.\n   */\n  export const isVariableDeclarationList = (node?: ASTNode): node is VariableDeclarationList =>\n    node?.kind === ASTKind.VariableDeclarationList;\n\n  /**\n   * # Expression-related matchers.\n   */\n\n  /**\n   * Check if the node is a `EnumerateExpression`.\n   */\n  export const isEnumerateExpression = (node?: ASTNode): node is EnumerateExpression =>\n    node?.kind === ASTKind.EnumerateExpression;\n\n  /**\n   * Check if the node is a `WrapArrayExpression`.\n   */\n  export const isWrapArrayExpression = (node?: ASTNode): node is WrapArrayExpression =>\n    node?.kind === ASTKind.WrapArrayExpression;\n\n  /**\n   * Check if the node is a `KeyPathExpression`.\n   */\n  export const isKeyPathExpression = (node?: ASTNode): node is KeyPathExpression =>\n    node?.kind === ASTKind.KeyPathExpression;\n\n  /**\n   * Check ASTNode Match by ASTClass\n   *\n   * @param node ASTNode to be checked.\n   * @param targetType Target ASTNode class.\n   * @returns Whether the node is of the target type.\n   */\n  export function is<TargetASTNode extends ASTNode>(\n    node?: ASTNode,\n    targetType?: { kind: string; new (...args: any[]): TargetASTNode }\n  ): node is TargetASTNode {\n    return node?.kind === targetType?.kind;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/array.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { parseTypeJsonOrKind } from '../utils/helpers';\nimport { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';\nimport { ASTNodeFlags } from '../flags';\nimport { type BaseVariableField } from '../declaration';\nimport { BaseType } from './base-type';\n\n/**\n * ASTNodeJSON representation of `ArrayType`\n */\nexport interface ArrayJSON {\n  /**\n   * The type of the items in the array.\n   */\n  items?: ASTNodeJSONOrKind;\n}\n\n/**\n * Represents an array type.\n */\nexport class ArrayType extends BaseType<ArrayJSON> {\n  public flags: ASTNodeFlags = ASTNodeFlags.DrilldownType | ASTNodeFlags.EnumerateType;\n\n  static kind: string = ASTKind.Array;\n\n  /**\n   * The type of the items in the array.\n   */\n  items: BaseType;\n\n  /**\n   * Deserializes the `ArrayJSON` to the `ArrayType`.\n   * @param json The `ArrayJSON` to deserialize.\n   */\n  fromJSON({ items }: ArrayJSON): void {\n    this.updateChildNodeByKey('items', parseTypeJsonOrKind(items));\n  }\n\n  /**\n   * Whether the items type can be drilled down.\n   */\n  get canDrilldownItems(): boolean {\n    return !!(this.items?.flags & ASTNodeFlags.DrilldownType);\n  }\n\n  /**\n   * Get a variable field by key path.\n   * @param keyPath The key path to search for.\n   * @returns The variable field if found, otherwise `undefined`.\n   */\n  getByKeyPath(keyPath: string[]): BaseVariableField | undefined {\n    const [curr, ...rest] = keyPath || [];\n\n    if (curr === '0' && this.canDrilldownItems) {\n      // The first item of the array.\n      return this.items.getByKeyPath(rest);\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Check if the current type is equal to the target type.\n   * @param targetTypeJSONOrKind The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {\n    const targetTypeJSON = parseTypeJsonOrKind(targetTypeJSONOrKind);\n    const isSuperEqual = super.isTypeEqual(targetTypeJSONOrKind);\n\n    if (targetTypeJSON?.weak || targetTypeJSON?.kind === ASTKind.Union) {\n      return isSuperEqual;\n    }\n\n    return (\n      targetTypeJSON &&\n      isSuperEqual &&\n      // Weak comparison, only need to compare the Kind.\n      (targetTypeJSON?.weak || this.customStrongEqual(targetTypeJSON))\n    );\n  }\n\n  /**\n   * Array strong comparison.\n   * @param targetTypeJSON The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  protected customStrongEqual(targetTypeJSON: ASTNodeJSON): boolean {\n    if (!this.items) {\n      return !(targetTypeJSON as ArrayJSON)?.items;\n    }\n    return this.items?.isTypeEqual((targetTypeJSON as ArrayJSON).items);\n  }\n\n  /**\n   * Serialize the `ArrayType` to `ArrayJSON`\n   * @returns The JSON representation of `ArrayType`.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.Array,\n      items: this.items?.toJSON(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/base-type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { parseTypeJsonOrKind } from '../utils/helpers';\nimport { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';\nimport { ASTNodeFlags } from '../flags';\nimport { BaseVariableField } from '../declaration';\nimport { ASTNode } from '../ast-node';\nimport { UnionJSON } from './union';\n\n/**\n * Base class for all types.\n *\n * All other types should extend this class.\n */\nexport abstract class BaseType<JSON extends ASTNodeJSON = any> extends ASTNode<JSON> {\n  public flags: number = ASTNodeFlags.BasicType;\n\n  /**\n   * Check if the current type is equal to the target type.\n   * @param targetTypeJSONOrKind The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {\n    const targetTypeJSON = parseTypeJsonOrKind(targetTypeJSONOrKind);\n\n    // If it is a Union type, it is sufficient for one of the subtypes to be equal.\n    if (targetTypeJSON?.kind === ASTKind.Union) {\n      return ((targetTypeJSON as UnionJSON)?.types || [])?.some((_subType) =>\n        this.isTypeEqual(_subType)\n      );\n    }\n\n    return this.kind === targetTypeJSON?.kind;\n  }\n\n  /**\n   * Get a variable field by key path.\n   *\n   * This method should be implemented by drillable types.\n   * @param keyPath The key path to search for.\n   * @returns The variable field if found, otherwise `undefined`.\n   */\n  getByKeyPath(keyPath: string[] = []): BaseVariableField | undefined {\n    throw new Error(`Get By Key Path is not implemented for Type: ${this.kind}`);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/boolean.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { BaseType } from './base-type';\n\n/**\n * Represents a boolean type.\n */\nexport class BooleanType extends BaseType {\n  static kind: string = ASTKind.Boolean;\n\n  /**\n   * Deserializes the `BooleanJSON` to the `BooleanType`.\n   * @param json The `BooleanJSON` to deserialize.\n   */\n  fromJSON(): void {\n    // noop\n  }\n\n  toJSON() {\n    return {};\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/custom-type.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { parseTypeJsonOrKind } from '../utils/helpers';\nimport { ASTKind, ASTNodeJSONOrKind } from '../types';\nimport { type UnionJSON } from './union';\nimport { BaseType } from './base-type';\n\n/**\n * ASTNodeJSON representation of `CustomType`\n */\nexport interface CustomTypeJSON {\n  /**\n   * The name of the custom type.\n   */\n  typeName: string;\n}\n\n/**\n * Represents a custom type.\n */\nexport class CustomType extends BaseType<CustomTypeJSON> {\n  static kind: string = ASTKind.CustomType;\n\n  protected _typeName: string;\n\n  /**\n   * The name of the custom type.\n   */\n  get typeName(): string {\n    return this._typeName;\n  }\n\n  /**\n   * Deserializes the `CustomTypeJSON` to the `CustomType`.\n   * @param json The `CustomTypeJSON` to deserialize.\n   */\n  fromJSON(json: CustomTypeJSON): void {\n    if (this._typeName !== json.typeName) {\n      this._typeName = json.typeName;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * Check if the current type is equal to the target type.\n   * @param targetTypeJSONOrKind The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {\n    const targetTypeJSON = parseTypeJsonOrKind(targetTypeJSONOrKind);\n\n    // If it is a Union type, it is sufficient for one of the subtypes to be equal.\n    if (targetTypeJSON?.kind === ASTKind.Union) {\n      return ((targetTypeJSON as UnionJSON)?.types || [])?.some((_subType) =>\n        this.isTypeEqual(_subType)\n      );\n    }\n\n    return targetTypeJSON?.kind === this.kind && targetTypeJSON?.typeName === this.typeName;\n  }\n\n  toJSON() {\n    return {\n      typeName: this.typeName,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { StringType } from './string';\nexport { IntegerType } from './integer';\nexport { BooleanType } from './boolean';\nexport { NumberType } from './number';\nexport { ArrayType } from './array';\nexport { MapType } from './map';\nexport {\n  type ObjectJSON as ObjectJSON,\n  ObjectType,\n  type ObjectPropertiesChangeAction,\n} from './object';\nexport { BaseType } from './base-type';\nexport { type UnionJSON } from './union';\nexport { CustomType, type CustomTypeJSON } from './custom-type';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/integer.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { ASTNodeFlags } from '../flags';\nimport { BaseType } from './base-type';\n\n/**\n * Represents an integer type.\n */\nexport class IntegerType extends BaseType {\n  public flags: ASTNodeFlags = ASTNodeFlags.BasicType;\n\n  static kind: string = ASTKind.Integer;\n\n  /**\n   * Deserializes the `IntegerJSON` to the `IntegerType`.\n   * @param json The `IntegerJSON` to deserialize.\n   */\n  fromJSON(): void {\n    // noop\n  }\n\n  toJSON() {\n    return {};\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/map.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { parseTypeJsonOrKind } from '../utils/helpers';\nimport { ASTKind, ASTNodeJSON, ASTNodeJSONOrKind } from '../types';\nimport { BaseType } from './base-type';\n\n/**\n * ASTNodeJSON representation of `MapType`\n */\nexport interface MapJSON {\n  /**\n   * The type of the keys in the map.\n   */\n  keyType?: ASTNodeJSONOrKind;\n  /**\n   * The type of the values in the map.\n   */\n  valueType?: ASTNodeJSONOrKind;\n}\n\n/**\n * Represents a map type.\n */\nexport class MapType extends BaseType<MapJSON> {\n  static kind: string = ASTKind.Map;\n\n  /**\n   * The type of the keys in the map.\n   */\n  keyType: BaseType;\n\n  /**\n   * The type of the values in the map.\n   */\n  valueType: BaseType;\n\n  /**\n   * Deserializes the `MapJSON` to the `MapType`.\n   * @param json The `MapJSON` to deserialize.\n   */\n  fromJSON({ keyType = ASTKind.String, valueType }: MapJSON): void {\n    // Key defaults to String.\n    this.updateChildNodeByKey('keyType', parseTypeJsonOrKind(keyType));\n    this.updateChildNodeByKey('valueType', parseTypeJsonOrKind(valueType));\n  }\n\n  /**\n   * Check if the current type is equal to the target type.\n   * @param targetTypeJSONOrKind The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {\n    const targetTypeJSON = parseTypeJsonOrKind(targetTypeJSONOrKind);\n    const isSuperEqual = super.isTypeEqual(targetTypeJSONOrKind);\n\n    if (targetTypeJSON?.weak || targetTypeJSON?.kind === ASTKind.Union) {\n      return isSuperEqual;\n    }\n\n    return (\n      targetTypeJSON &&\n      isSuperEqual &&\n      // Weak comparison, only need to compare the Kind.\n      (targetTypeJSON?.weak || this.customStrongEqual(targetTypeJSON))\n    );\n  }\n\n  /**\n   * Map strong comparison.\n   * @param targetTypeJSON The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  protected customStrongEqual(targetTypeJSON: ASTNodeJSON): boolean {\n    const { keyType = ASTKind.String, valueType } = targetTypeJSON as MapJSON;\n\n    const isValueTypeEqual =\n      (!valueType && !this.valueType) || this.valueType?.isTypeEqual(valueType);\n\n    return isValueTypeEqual && this.keyType?.isTypeEqual(keyType);\n  }\n\n  /**\n   * Serialize the node to a JSON object.\n   * @returns The JSON representation of the node.\n   */\n  toJSON() {\n    return {\n      kind: ASTKind.Map,\n      keyType: this.keyType?.toJSON(),\n      valueType: this.valueType?.toJSON(),\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/number.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { BaseType } from './base-type';\n\n/**\n * Represents a number type.\n */\nexport class NumberType extends BaseType {\n  static kind: string = ASTKind.Number;\n\n  /**\n   * Deserializes the `NumberJSON` to the `NumberType`.\n   * @param json The `NumberJSON` to deserialize.\n   */\n  fromJSON(): void {\n    // noop\n  }\n\n  toJSON() {\n    return {};\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/object.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { xor } from 'lodash-es';\n\nimport { parseTypeJsonOrKind } from '../utils/helpers';\nimport { ASTNodeJSON, ASTKind, ASTNodeJSONOrKind, type GlobalEventActionType } from '../types';\nimport { ASTNodeFlags } from '../flags';\nimport { Property, type PropertyJSON } from '../declaration/property';\nimport { BaseType } from './base-type';\n\n/**\n * ASTNodeJSON representation of `ObjectType`\n */\nexport interface ObjectJSON<VariableMeta = any> {\n  /**\n   * The properties of the object.\n   *\n   * The `properties` of an Object must be of type `Property`, so the business can omit the `kind` field.\n   */\n  properties?: PropertyJSON<VariableMeta>[];\n}\n\n/**\n * Action type for object properties change.\n */\nexport type ObjectPropertiesChangeAction = GlobalEventActionType<\n  'ObjectPropertiesChange',\n  {\n    prev: Property[];\n    next: Property[];\n  },\n  ObjectType\n>;\n\n/**\n * Represents an object type.\n */\nexport class ObjectType extends BaseType<ObjectJSON> {\n  public flags: ASTNodeFlags = ASTNodeFlags.DrilldownType;\n\n  static kind: string = ASTKind.Object;\n\n  /**\n   * A map of property keys to `Property` instances.\n   */\n  propertyTable: Map<string, Property> = new Map();\n\n  /**\n   * An array of `Property` instances.\n   */\n  properties: Property[];\n\n  /**\n   * Deserializes the `ObjectJSON` to the `ObjectType`.\n   * @param json The `ObjectJSON` to deserialize.\n   */\n  fromJSON({ properties }: ObjectJSON): void {\n    const removedKeys = new Set(this.propertyTable.keys());\n    const prev = [...(this.properties || [])];\n\n    // Iterate over the new properties.\n    this.properties = (properties || []).map((property: PropertyJSON) => {\n      const existProperty = this.propertyTable.get(property.key);\n      removedKeys.delete(property.key);\n\n      if (existProperty) {\n        existProperty.fromJSON(property as PropertyJSON);\n\n        return existProperty;\n      } else {\n        const newProperty = this.createChildNode({\n          ...property,\n          kind: ASTKind.Property,\n        }) as Property;\n\n        this.fireChange();\n\n        this.propertyTable.set(property.key, newProperty);\n        // TODO: When a child node is actively destroyed, delete the information in the table.\n\n        return newProperty;\n      }\n    });\n\n    // Delete properties that no longer exist.\n    removedKeys.forEach((key) => {\n      const property = this.propertyTable.get(key);\n      property?.dispose();\n      this.propertyTable.delete(key);\n      this.fireChange();\n    });\n\n    this.dispatchGlobalEvent<ObjectPropertiesChangeAction>({\n      type: 'ObjectPropertiesChange',\n      payload: {\n        prev,\n        next: [...this.properties],\n      },\n    });\n  }\n\n  /**\n   * Serialize the `ObjectType` to `ObjectJSON`.\n   * @returns The JSON representation of `ObjectType`.\n   */\n  toJSON() {\n    return {\n      properties: this.properties.map((_property) => _property.toJSON()),\n    };\n  }\n\n  /**\n   * Get a variable field by key path.\n   * @param keyPath The key path to search for.\n   * @returns The variable field if found, otherwise `undefined`.\n   */\n  getByKeyPath(keyPath: string[]): Property | undefined {\n    const [curr, ...restKeyPath] = keyPath;\n\n    const property = this.propertyTable.get(curr);\n\n    // Found the end of the path.\n    if (!restKeyPath.length) {\n      return property;\n    }\n\n    // Otherwise, continue searching downwards.\n    if (property?.type && property?.type?.flags & ASTNodeFlags.DrilldownType) {\n      return property.type.getByKeyPath(restKeyPath) as Property | undefined;\n    }\n\n    return undefined;\n  }\n\n  /**\n   * Check if the current type is equal to the target type.\n   * @param targetTypeJSONOrKind The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  public isTypeEqual(targetTypeJSONOrKind?: ASTNodeJSONOrKind): boolean {\n    const targetTypeJSON = parseTypeJsonOrKind(targetTypeJSONOrKind);\n    const isSuperEqual = super.isTypeEqual(targetTypeJSONOrKind);\n\n    if (targetTypeJSON?.weak || targetTypeJSON?.kind === ASTKind.Union) {\n      return isSuperEqual;\n    }\n\n    return (\n      targetTypeJSON &&\n      isSuperEqual &&\n      // Weak comparison, only need to compare the Kind.\n      (targetTypeJSON?.weak || this.customStrongEqual(targetTypeJSON))\n    );\n  }\n\n  /**\n   * Object type strong comparison.\n   * @param targetTypeJSON The type to compare with.\n   * @returns `true` if the types are equal, `false` otherwise.\n   */\n  protected customStrongEqual(targetTypeJSON: ASTNodeJSON): boolean {\n    const targetProperties = (targetTypeJSON as ObjectJSON).properties || [];\n\n    const sourcePropertyKeys = Array.from(this.propertyTable.keys());\n    const targetPropertyKeys = targetProperties.map((_target) => _target.key);\n\n    const isKeyStrongEqual = !xor(sourcePropertyKeys, targetPropertyKeys).length;\n\n    return (\n      isKeyStrongEqual &&\n      targetProperties.every((targetProperty) => {\n        const sourceProperty = this.propertyTable.get(targetProperty.key);\n\n        return (\n          sourceProperty &&\n          sourceProperty.key === targetProperty.key &&\n          sourceProperty.type?.isTypeEqual(targetProperty?.type)\n        );\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/string.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTKind } from '../types';\nimport { ASTNodeFlags } from '../flags';\nimport { BaseType } from './base-type';\n\n/**\n * ASTNodeJSON representation of the `StringType`.\n */\nexport interface StringJSON {\n  /**\n   * see https://json-schema.org/understanding-json-schema/reference/type#format\n   */\n  format?: string;\n}\n\nexport class StringType extends BaseType<StringJSON> {\n  public flags: ASTNodeFlags = ASTNodeFlags.BasicType;\n\n  static kind: string = ASTKind.String;\n\n  protected _format?: string;\n\n  /**\n   * see https://json-schema.org/understanding-json-schema/reference/string#format\n   */\n  get format() {\n    return this._format;\n  }\n\n  /**\n   * Deserialize the `StringJSON` to the `StringType`.\n   *\n   * @param json StringJSON representation of the `StringType`.\n   */\n  fromJSON(json?: StringJSON): void {\n    if (json?.format !== this._format) {\n      this._format = json?.format;\n      this.fireChange();\n    }\n  }\n\n  /**\n   * Serialize the `StringType` to `StringJSON`.\n   * @returns The JSON representation of `StringType`.\n   */\n  toJSON() {\n    return {\n      format: this._format,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/type/union.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTNodeJSONOrKind } from '../types';\n\n/**\n * ASTNodeJSON representation of `UnionType`, which union multiple `BaseType`.\n */\nexport interface UnionJSON {\n  types?: ASTNodeJSONOrKind[];\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { type Observer } from 'rxjs';\n\nimport { type Scope } from '../scope';\nimport { type ASTNode } from './ast-node';\n\nexport type ASTKindType = string;\nexport type Identifier = string;\n\n/**\n * ASTNodeJSON is the JSON representation of an ASTNode.\n */\nexport interface ASTNodeJSON {\n  /**\n   * Kind is the type of the AST node.\n   */\n  kind?: ASTKindType;\n\n  /**\n   * Key is the unique identifier of the node.\n   * If not provided, the node will generate a default key value.\n   */\n  key?: Identifier;\n  [key: string]: any;\n}\n\n/**\n * Core AST node types.\n */\nexport enum ASTKind {\n  /**\n   * # Type-related.\n   * - A set of type AST nodes based on JSON types is implemented internally by default.\n   */\n\n  /**\n   * String type.\n   */\n  String = 'String',\n  /**\n   * Number type.\n   */\n  Number = 'Number',\n  /**\n   * Integer type.\n   */\n  Integer = 'Integer',\n  /**\n   * Boolean type.\n   */\n  Boolean = 'Boolean',\n  /**\n   * Object type.\n   */\n  Object = 'Object',\n  /**\n   * Array type.\n   */\n  Array = 'Array',\n  /**\n   * Map type.\n   */\n  Map = 'Map',\n  /**\n   * Union type.\n   * Commonly used for type checking, generally not exposed to the business.\n   */\n  Union = 'Union',\n  /**\n   * Any type.\n   * Commonly used for business logic.\n   */\n  Any = 'Any',\n  /**\n   * Custom type.\n   * For business-defined types.\n   */\n  CustomType = 'CustomType',\n\n  /**\n   * # Declaration-related.\n   */\n\n  /**\n   * Field definition for Object drill-down.\n   */\n  Property = 'Property',\n  /**\n   * Variable declaration.\n   */\n  VariableDeclaration = 'VariableDeclaration',\n  /**\n   * Variable declaration list.\n   */\n  VariableDeclarationList = 'VariableDeclarationList',\n\n  /**\n   * # Expression-related.\n   */\n\n  /**\n   * Access fields on variables through the path system.\n   */\n  KeyPathExpression = 'KeyPathExpression',\n  /**\n   * Iterate over specified data.\n   */\n  EnumerateExpression = 'EnumerateExpression',\n  /**\n   * Wrap with Array Type.\n   */\n  WrapArrayExpression = 'WrapArrayExpression',\n\n  /**\n   * # General-purpose AST nodes.\n   */\n\n  /**\n   * General-purpose List<ASTNode> storage node.\n   */\n  ListNode = 'ListNode',\n  /**\n   * General-purpose data storage node.\n   */\n  DataNode = 'DataNode',\n  /**\n   * General-purpose Map<string, ASTNode> storage node.\n   */\n  MapNode = 'MapNode',\n}\n\nexport interface CreateASTParams {\n  scope: Scope;\n  key?: Identifier;\n  parent?: ASTNode;\n}\n\nexport type ASTNodeJSONOrKind = string | ASTNodeJSON;\n\nexport type ObserverOrNext<T> = Partial<Observer<T>> | ((value: T) => void);\n\nexport interface SubscribeConfig<This, Data> {\n  // Merge all changes within one animationFrame into a single one.\n  debounceAnimation?: boolean;\n  // Respond with a value by default upon subscription.\n  triggerOnInit?: boolean;\n  selector?: (curr: This) => Data;\n}\n\n/**\n * TypeUtils to get the JSON representation of an AST node with a specific kind.\n */\nexport type GetKindJSON<KindType extends string, JSON extends ASTNodeJSON> = {\n  kind: KindType;\n  key?: Identifier;\n} & JSON;\n\n/**\n * TypeUtils to get the JSON representation of an AST node with a specific kind or just the kind string.\n */\nexport type GetKindJSONOrKind<KindType extends string, JSON extends ASTNodeJSON> =\n  | ({\n      kind: KindType;\n      key?: Identifier;\n    } & JSON)\n  | KindType;\n\n/**\n * Global event action type.\n * - Global event might be dispatched from `ASTNode` or `Scope`.\n */\nexport interface GlobalEventActionType<\n  Type = string,\n  Payload = any,\n  AST extends ASTNode = ASTNode\n> {\n  type: Type;\n  payload?: Payload;\n  ast?: AST;\n}\n\nexport type NewASTAction = GlobalEventActionType<'NewAST'>;\nexport type UpdateASTAction = GlobalEventActionType<'UpdateAST'>;\nexport type DisposeASTAction = GlobalEventActionType<'DisposeAST'>;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/utils/expression.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { intersection } from 'lodash-es';\n\nimport { ASTNodeFlags } from '../flags';\nimport { type BaseExpression } from '../expression';\nimport { type BaseVariableField } from '../declaration';\nimport { type ASTNode } from '../ast-node';\nimport { getParentFields } from './variable-field';\nimport { getAllChildren } from './helpers';\n\n/**\n * Get all variables referenced by child ASTs.\n * @param ast The ASTNode to traverse.\n * @returns All variables referenced by child ASTs.\n */\nexport function getAllRefs(ast: ASTNode): BaseVariableField[] {\n  return getAllChildren(ast)\n    .filter((_child) => _child.flags & ASTNodeFlags.Expression)\n    .map((_child) => (_child as BaseExpression).refs)\n    .flat()\n    .filter(Boolean) as BaseVariableField[];\n}\n\n/**\n * Checks for circular references.\n * @param curr The current expression.\n * @param refNode The referenced variable node.\n * @returns Whether a circular reference exists.\n */\nexport function checkRefCycle(\n  curr: BaseExpression,\n  refNodes: (BaseVariableField | undefined)[]\n): boolean {\n  // If there are no circular references in the scope, then it is impossible to have a circular reference.\n  if (\n    intersection(curr.scope.coverScopes, refNodes.map((_ref) => _ref?.scope).filter(Boolean))\n      .length === 0\n  ) {\n    return false;\n  }\n\n  // BFS traversal.\n  const visited = new Set<BaseVariableField>();\n  const queue = [...refNodes];\n\n  while (queue.length) {\n    const currNode = queue.shift()!;\n    visited.add(currNode);\n\n    for (const ref of getAllRefs(currNode).filter((_ref) => !visited.has(_ref))) {\n      queue.push(ref);\n    }\n  }\n\n  // If the referenced variables include the parent variable of the expression, then there is a circular reference.\n  return intersection(Array.from(visited), getParentFields(curr)).length > 0;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/utils/helpers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTNodeJSON, ASTNodeJSONOrKind } from '../types';\nimport { ASTMatch } from '../match';\nimport { ASTNode } from '../ast-node';\n\nexport function updateChildNodeHelper(\n  this: ASTNode,\n  {\n    getChildNode,\n    updateChildNode,\n    removeChildNode,\n    nextJSON,\n  }: {\n    getChildNode: () => ASTNode | undefined;\n    updateChildNode: (nextNode: ASTNode) => void;\n    removeChildNode: () => void;\n    nextJSON?: ASTNodeJSON;\n  }\n): ASTNode | undefined {\n  const currNode: ASTNode | undefined = getChildNode();\n\n  const isNewKind = currNode?.kind !== nextJSON?.kind;\n  // If `nextJSON` does not pass a key value, the key value remains unchanged by default.\n  const isNewKey = nextJSON?.key && nextJSON?.key !== currNode?.key;\n\n  if (isNewKind || isNewKey) {\n    // The previous node needs to be destroyed.\n    if (currNode) {\n      currNode.dispose();\n      removeChildNode();\n    }\n\n    if (nextJSON) {\n      const newNode = this.createChildNode(nextJSON);\n      updateChildNode(newNode);\n      this.fireChange();\n      return newNode;\n    } else {\n      // Also trigger an update when deleting a child node directly.\n      this.fireChange();\n    }\n  } else if (nextJSON) {\n    currNode?.fromJSON(nextJSON);\n  }\n\n  return currNode;\n}\n\nexport function parseTypeJsonOrKind(typeJSONOrKind?: ASTNodeJSONOrKind): ASTNodeJSON | undefined {\n  return typeof typeJSONOrKind === 'string' ? { kind: typeJSONOrKind } : typeJSONOrKind;\n}\n\n// Get all children.\nexport function getAllChildren(ast: ASTNode): ASTNode[] {\n  return [...ast.children, ...ast.children.map((_child) => getAllChildren(_child)).flat()];\n}\n\n/**\n * isMatchAST is same as ASTMatch.is\n * @param node\n * @param targetType\n * @returns\n */\nexport function isMatchAST<TargetASTNode extends ASTNode>(\n  node?: ASTNode,\n  targetType?: { kind: string; new (...args: any[]): TargetASTNode }\n): node is TargetASTNode {\n  return ASTMatch.is(node, targetType);\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/utils/inversify.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\n\nimport { type ASTNode } from '../ast-node';\n\nexport const injectToAST = (serviceIdentifier: interfaces.ServiceIdentifier) =>\n  function (target: any, propertyKey: string) {\n    if (!serviceIdentifier) {\n      throw new Error(\n        `ServiceIdentifier ${serviceIdentifier} in @lazyInject is Empty, it might be caused by file circular dependency, please check it.`\n      );\n    }\n\n    const descriptor = {\n      get() {\n        const container = (this as ASTNode).scope.variableEngine.container;\n        return container.get(serviceIdentifier);\n      },\n      set() {},\n      configurable: true,\n      enumerable: true,\n    } as any;\n\n    // Object.defineProperty(target, propertyKey, descriptor);\n\n    return descriptor;\n  };\n\nexport const POST_CONSTRUCT_AST_SYMBOL = Symbol('post_construct_ast');\n\nexport const postConstructAST = () => (target: any, propertyKey: string) => {\n  // Only run once.\n  if (!Reflect.hasMetadata(POST_CONSTRUCT_AST_SYMBOL, target)) {\n    Reflect.defineMetadata(POST_CONSTRUCT_AST_SYMBOL, propertyKey, target);\n  } else {\n    throw Error('Duplication Post Construct AST');\n  }\n};\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/utils/observable.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/ast/utils/variable-field.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ASTNodeFlags } from '../flags';\nimport { BaseVariableField } from '../declaration';\nimport { ASTNode } from '../ast-node';\n\n/**\n * Parent variable fields, sorted from nearest to farthest.\n */\nexport function getParentFields(ast: ASTNode): BaseVariableField[] {\n  let curr = ast.parent;\n  const res: BaseVariableField[] = [];\n\n  while (curr) {\n    if (curr.flags & ASTNodeFlags.VariableField) {\n      res.push(curr as BaseVariableField);\n    }\n    curr = curr.parent;\n  }\n\n  return res;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { VariableContainerModule } from './variable-container-module';\nexport { VariableEngine } from './variable-engine';\nexport { VariableEngineProvider } from './providers';\n\nexport * from './react';\nexport * from './scope';\nexport * from './ast';\nexport * from './services';\nexport { type Observer } from 'rxjs';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/providers.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { interfaces } from 'inversify';\n\nimport { type VariableEngine } from './variable-engine';\n\n/**\n * A provider for dynamically obtaining the `VariableEngine` instance.\n * This is used to prevent circular dependencies when injecting `VariableEngine`.\n */\nexport const VariableEngineProvider = Symbol('DynamicVariableEngine');\nexport type VariableEngineProvider = () => VariableEngine;\n\n/**\n * A provider for obtaining the Inversify container instance.\n * This allows other parts of the application, like AST nodes, to access the container for dependency injection.\n */\nexport const ContainerProvider = Symbol('ContainerProvider');\nexport type ContainerProvider = () => interfaces.Container;\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/react/context.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\n/* eslint-disable react/prop-types */\n\nimport React, { createContext, useContext } from 'react';\n\nimport { Scope } from '../scope';\n\ninterface ScopeContextProps {\n  scope: Scope;\n}\n\nconst ScopeContext = createContext<ScopeContextProps>(null!);\n\n/**\n * ScopeProvider provides the scope to its children via context.\n */\nexport const ScopeProvider = (\n  props: React.PropsWithChildren<{\n    /**\n     * scope used in the context\n     */\n    scope?: Scope;\n    /**\n     * @deprecated use scope prop instead, this is kept for backward compatibility\n     */\n    value?: ScopeContextProps;\n  }>\n) => {\n  const { scope, value, children } = props;\n\n  const scopeToUse = scope || value?.scope;\n\n  if (!scopeToUse) {\n    throw new Error('[ScopeProvider] scope is required');\n  }\n\n  return <ScopeContext.Provider value={{ scope: scopeToUse }}>{children}</ScopeContext.Provider>;\n};\n\n/**\n * useCurrentScope returns the scope provided by ScopeProvider.\n * @returns\n */\nexport const useCurrentScope = <Strict extends boolean = false>(params?: {\n  /**\n   * whether to throw error when no scope in ScopeProvider is found\n   */\n  strict: Strict;\n}): Strict extends true ? Scope : Scope | undefined => {\n  const { strict = false } = params || {};\n\n  const context = useContext(ScopeContext);\n\n  if (!context) {\n    if (strict) {\n      throw new Error('useCurrentScope must be used within a <ScopeProvider scope={scope}>');\n    }\n    console.warn('useCurrentScope should be used within a <ScopeProvider scope={scope}>');\n  }\n\n  return context?.scope;\n};\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/react/hooks/use-available-variables.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useRefresh, useService } from '@flowgram.ai/core';\n\nimport { useCurrentScope } from '../context';\nimport { VariableEngine } from '../../variable-engine';\nimport { VariableDeclaration } from '../../ast';\n\n/**\n * Get available variable list in the current scope.\n *\n * - If no scope, return global variable list.\n * - The hook is reactive to variable list or any variables change.\n */\nexport function useAvailableVariables(): VariableDeclaration[] {\n  const scope = useCurrentScope();\n  const variableEngine: VariableEngine = useService(VariableEngine);\n\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    // 没有作用域时，监听全局变量表\n    if (!scope) {\n      const disposable = variableEngine.globalVariableTable.onListOrAnyVarChange(() => {\n        refresh();\n      });\n\n      return () => disposable.dispose();\n    }\n\n    const disposable = scope.available.onDataChange(() => {\n      refresh();\n    });\n\n    return () => disposable.dispose();\n  }, []);\n\n  // 没有作用域时，使用全局变量表\n  return scope ? scope.available.variables : variableEngine.globalVariableTable.variables;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/react/hooks/use-output-variables.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/core';\n\nimport { useCurrentScope } from '../context';\nimport { VariableDeclaration } from '../../ast';\n\n/**\n * Get output variable list in the current scope.\n *\n * - The hook is reactive to variable list or any variables change.\n */\nexport function useOutputVariables(): VariableDeclaration[] {\n  const scope = useCurrentScope();\n\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    if (!scope) {\n      throw new Error(\n        '[useOutputVariables]: No scope found, useOutputVariables must be used in <ScopeProvider>'\n      );\n    }\n\n    const disposable = scope.output.onListOrAnyVarChange(() => {\n      refresh();\n    });\n\n    return () => disposable.dispose();\n  }, []);\n\n  return scope?.output.variables || [];\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/react/hooks/use-scope-available.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { useEffect } from 'react';\n\nimport { useRefresh } from '@flowgram.ai/core';\n\nimport { useCurrentScope } from '../context';\nimport { ScopeAvailableData } from '../../scope/datas';\n\n/**\n * Get the available variables in the current scope.\n * 获取作用域的可访问变量\n *\n * @returns the available variables in the current scope\n */\nexport function useScopeAvailable(params?: { autoRefresh?: boolean }): ScopeAvailableData {\n  const { autoRefresh = true } = params || {};\n\n  const scope = useCurrentScope({ strict: true });\n  const refresh = useRefresh();\n\n  useEffect(() => {\n    if (!autoRefresh) {\n      return () => null;\n    }\n\n    const disposable = scope.available.onListOrAnyVarChange(() => {\n      refresh();\n    });\n\n    return () => disposable.dispose();\n  }, [autoRefresh]);\n\n  return scope.available;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/react/index.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ScopeProvider, useCurrentScope } from './context';\nexport { useScopeAvailable } from './hooks/use-scope-available';\nexport { useAvailableVariables } from './hooks/use-available-variables';\nexport { useOutputVariables } from './hooks/use-output-variables';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/datas/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ScopeOutputData } from './scope-output-data';\nexport { ScopeAvailableData } from './scope-available-data';\nexport { ScopeEventData } from './scope-event-data';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/datas/scope-available-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport {\n  Observable,\n  Subject,\n  animationFrameScheduler,\n  debounceTime,\n  distinctUntilChanged,\n  map,\n  merge,\n  share,\n  skip,\n  startWith,\n  switchMap,\n  tap,\n} from 'rxjs';\nimport { flatten } from 'lodash-es';\nimport { shallowEqual } from 'fast-equals';\nimport { Disposable } from '@flowgram.ai/utils';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { IVariableTable } from '../types';\nimport { type Scope } from '../scope';\nimport { subsToDisposable } from '../../utils/toDisposable';\nimport { createMemo } from '../../utils/memo';\nimport { SubscribeConfig } from '../../ast/types';\nimport { ASTNode, BaseVariableField, VariableDeclaration } from '../../ast';\n\n/**\n * Manages the available variables within a scope.\n */\nexport class ScopeAvailableData {\n  protected memo = createMemo();\n\n  /**\n   * The global variable table from the variable engine.\n   */\n  get globalVariableTable(): IVariableTable {\n    return this.scope.variableEngine.globalVariableTable;\n  }\n\n  protected _version: number = 0;\n\n  protected refresh$: Subject<void> = new Subject();\n\n  protected _variables: VariableDeclaration[] = [];\n\n  /**\n   * The current version of the available data, which increments on each change.\n   */\n  get version() {\n    return this._version;\n  }\n\n  protected bumpVersion() {\n    this._version = this._version + 1;\n    if (this._version === Number.MAX_SAFE_INTEGER) {\n      this._version = 0;\n    }\n  }\n\n  /**\n   * Refreshes the list of available variables.\n   * This should be called when the dependencies of the scope change.\n   */\n  refresh(): void {\n    // Do not trigger refresh for a disposed scope.\n    if (this.scope.disposed) {\n      return;\n    }\n    this.refresh$.next();\n  }\n\n  /**\n   * An observable that emits when the list of available variables changes.\n   */\n  protected variables$: Observable<VariableDeclaration[]> = this.refresh$.pipe(\n    // Map to the flattened list of variables from all dependency scopes.\n    map(() => flatten(this.depScopes.map((scope) => scope.output.variables || []))),\n    // Use shallow equality to check if the variable list has changed.\n    distinctUntilChanged<VariableDeclaration[]>(shallowEqual),\n    share()\n  );\n\n  /**\n   * An observable that emits when any variable in the available list changes its value.\n   */\n  protected anyVariableChange$: Observable<VariableDeclaration> = this.variables$.pipe(\n    switchMap((_variables) =>\n      merge(\n        ..._variables.map((_v) =>\n          _v.value$.pipe<any>(\n            // Skip the initial value of the BehaviorSubject.\n            skip(1)\n          )\n        )\n      )\n    ),\n    share()\n  );\n\n  /**\n   * Subscribes to changes in any variable's value in the available list.\n   * @param observer A function to be called with the changed variable.\n   * @returns A disposable to unsubscribe from the changes.\n   */\n  onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void) {\n    return subsToDisposable(this.anyVariableChange$.subscribe(observer));\n  }\n\n  /**\n   * Subscribes to changes in the list of available variables.\n   * @param observer A function to be called with the new list of variables.\n   * @returns A disposable to unsubscribe from the changes.\n   */\n  onVariableListChange(observer: (variables: VariableDeclaration[]) => void) {\n    return subsToDisposable(this.variables$.subscribe(observer));\n  }\n\n  /**\n   * @deprecated\n   */\n  protected onDataChangeEmitter = new Emitter<VariableDeclaration[]>();\n\n  protected onListOrAnyVarChangeEmitter = new Emitter<VariableDeclaration[]>();\n\n  /**\n   * @deprecated use available.onListOrAnyVarChange instead\n   */\n  public onDataChange = this.onDataChangeEmitter.event;\n\n  /**\n   * An event that fires when the variable list changes or any variable's value is updated.\n   */\n  public onListOrAnyVarChange = this.onListOrAnyVarChangeEmitter.event;\n\n  constructor(public readonly scope: Scope) {\n    this.scope.toDispose.pushAll([\n      this.onVariableListChange((_variables) => {\n        this._variables = _variables;\n        this.memo.clear();\n        this.onDataChangeEmitter.fire(this._variables);\n        this.bumpVersion();\n        this.onListOrAnyVarChangeEmitter.fire(this._variables);\n      }),\n      this.onAnyVariableChange(() => {\n        this.onDataChangeEmitter.fire(this._variables);\n        this.bumpVersion();\n        this.onListOrAnyVarChangeEmitter.fire(this._variables);\n      }),\n      Disposable.create(() => {\n        this.refresh$.complete();\n        this.refresh$.unsubscribe();\n      }),\n    ]);\n  }\n\n  /**\n   * Gets the list of available variables.\n   */\n  get variables(): VariableDeclaration[] {\n    return this._variables;\n  }\n\n  /**\n   * Gets the keys of the available variables.\n   */\n  get variableKeys(): string[] {\n    return this.memo('availableKeys', () => this._variables.map((_v) => _v.key));\n  }\n\n  /**\n   * Gets the dependency scopes.\n   */\n  get depScopes(): Scope[] {\n    return this.scope.depScopes;\n  }\n\n  /**\n   * Retrieves a variable field by its key path from the available variables.\n   * @param keyPath The key path to the variable field.\n   * @returns The found `BaseVariableField` or `undefined`.\n   */\n  getByKeyPath(keyPath: string[] = []): BaseVariableField | undefined {\n    // Check if the variable is accessible in the current scope.\n    if (!this.variableKeys.includes(keyPath[0])) {\n      return;\n    }\n    return this.globalVariableTable.getByKeyPath(keyPath);\n  }\n\n  /**\n   * Tracks changes to a variable field by its key path.\n   * This includes changes to its type, value, or any nested properties.\n   * @param keyPath The key path to the variable field to track.\n   * @param cb The callback to execute when the variable changes.\n   * @param opts Configuration options for the subscription.\n   * @returns A disposable to unsubscribe from the tracking.\n   */\n  trackByKeyPath<Data = BaseVariableField | undefined>(\n    keyPath: string[] = [],\n    cb: (variable?: Data) => void,\n    opts?: SubscribeConfig<BaseVariableField | undefined, Data>\n  ): Disposable {\n    const { triggerOnInit = true, debounceAnimation, selector } = opts || {};\n\n    return subsToDisposable(\n      merge(this.anyVariableChange$, this.variables$)\n        .pipe(\n          triggerOnInit ? startWith() : tap(() => null),\n          map(() => {\n            const v = this.getByKeyPath(keyPath);\n            return selector ? selector(v) : (v as any);\n          }),\n          distinctUntilChanged(\n            (a, b) => shallowEqual(a, b),\n            (value) => {\n              if (value instanceof ASTNode) {\n                // If the value is an ASTNode, compare its hash for changes.\n                return value.hash;\n              }\n              return value;\n            }\n          ),\n          // Debounce updates to a single emission per animation frame.\n          debounceAnimation ? debounceTime(0, animationFrameScheduler) : tap(() => null)\n        )\n        .subscribe(cb)\n    );\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/datas/scope-event-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Subject, filter } from 'rxjs';\nimport { Disposable } from '@flowgram.ai/utils';\n\nimport { type Scope } from '../scope';\nimport { subsToDisposable } from '../../utils/toDisposable';\nimport { type GlobalEventActionType } from '../../ast';\n\ntype Observer<ActionType extends GlobalEventActionType = GlobalEventActionType> = (\n  action: ActionType\n) => void;\n\n/**\n * Manages global events within a scope.\n */\nexport class ScopeEventData {\n  event$: Subject<GlobalEventActionType> = new Subject<GlobalEventActionType>();\n\n  /**\n   * Dispatches a global event.\n   * @param action The event action to dispatch.\n   */\n  dispatch<ActionType extends GlobalEventActionType = GlobalEventActionType>(action: ActionType) {\n    if (this.scope.disposed) {\n      return;\n    }\n    this.event$.next(action);\n  }\n\n  /**\n   * Subscribes to all global events.\n   * @param observer The observer function to call with the event action.\n   * @returns A disposable to unsubscribe from the events.\n   */\n  subscribe<ActionType extends GlobalEventActionType = GlobalEventActionType>(\n    observer: Observer<ActionType>\n  ): Disposable {\n    return subsToDisposable(this.event$.subscribe(observer as Observer));\n  }\n\n  /**\n   * Subscribes to a specific type of global event.\n   * @param type The type of the event to subscribe to.\n   * @param observer The observer function to call with the event action.\n   * @returns A disposable to unsubscribe from the event.\n   */\n  on<ActionType extends GlobalEventActionType = GlobalEventActionType>(\n    type: ActionType['type'],\n    observer: Observer<ActionType>\n  ): Disposable {\n    return subsToDisposable(\n      this.event$.pipe(filter((_action) => _action.type === type)).subscribe(observer as Observer)\n    );\n  }\n\n  constructor(public readonly scope: Scope) {\n    scope.toDispose.pushAll([\n      this.subscribe((_action) => {\n        scope.variableEngine.fireGlobalEvent(_action);\n      }),\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/datas/scope-output-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { VariableTable } from '../variable-table';\nimport { IVariableTable } from '../types';\nimport { type Scope } from '../scope';\nimport { type VariableEngine } from '../../variable-engine';\nimport { createMemo } from '../../utils/memo';\nimport { NewASTAction } from '../../ast/types';\nimport { DisposeASTAction } from '../../ast/types';\nimport { ReSortVariableDeclarationsAction } from '../../ast/declaration/variable-declaration';\nimport { ASTKind, type VariableDeclaration } from '../../ast';\n\n/**\n * Manages the output variables of a scope.\n */\nexport class ScopeOutputData {\n  protected variableTable: IVariableTable;\n\n  protected memo = createMemo();\n\n  /**\n   * The variable engine instance.\n   */\n  get variableEngine(): VariableEngine {\n    return this.scope.variableEngine;\n  }\n\n  /**\n   * The global variable table from the variable engine.\n   */\n  get globalVariableTable(): IVariableTable {\n    return this.scope.variableEngine.globalVariableTable;\n  }\n\n  /**\n   * The current version of the output data, which increments on each change.\n   */\n  get version() {\n    return this.variableTable.version;\n  }\n\n  /**\n   * @deprecated use onListOrAnyVarChange instead\n   */\n  get onDataChange() {\n    return this.variableTable.onDataChange.bind(this.variableTable);\n  }\n\n  /**\n   * An event that fires when the list of output variables changes.\n   */\n  get onVariableListChange() {\n    return this.variableTable.onVariableListChange.bind(this.variableTable);\n  }\n\n  /**\n   * An event that fires when any output variable's value changes.\n   */\n  get onAnyVariableChange() {\n    return this.variableTable.onAnyVariableChange.bind(this.variableTable);\n  }\n\n  /**\n   * An event that fires when the output variable list changes or any variable's value is updated.\n   */\n  get onListOrAnyVarChange() {\n    return this.variableTable.onListOrAnyVarChange.bind(this.variableTable);\n  }\n\n  protected _hasChanges = false;\n\n  constructor(public readonly scope: Scope) {\n    // Setup scope variable table based on globalVariableTable\n    this.variableTable = new VariableTable(scope.variableEngine.globalVariableTable);\n\n    this.scope.toDispose.pushAll([\n      // When the root AST node is updated, check if there are any changes.\n      this.scope.ast.subscribe(() => {\n        if (this._hasChanges) {\n          this.memo.clear();\n          this.notifyCoversChange();\n          this.variableTable.fireChange();\n          this._hasChanges = false;\n        }\n      }),\n      this.scope.event.on<DisposeASTAction>('DisposeAST', (_action) => {\n        if (_action.ast?.kind === ASTKind.VariableDeclaration) {\n          this.removeVariableFromTable(_action.ast.key);\n        }\n      }),\n      this.scope.event.on<NewASTAction>('NewAST', (_action) => {\n        if (_action.ast?.kind === ASTKind.VariableDeclaration) {\n          this.addVariableToTable(_action.ast as VariableDeclaration);\n        }\n      }),\n      this.scope.event.on<ReSortVariableDeclarationsAction>('ReSortVariableDeclarations', () => {\n        this._hasChanges = true;\n      }),\n      this.variableTable,\n    ]);\n  }\n\n  /**\n   * The output variable declarations of the scope, sorted by order.\n   */\n  get variables(): VariableDeclaration[] {\n    return this.memo('variables', () =>\n      this.variableTable.variables.sort((a, b) => a.order - b.order)\n    );\n  }\n\n  /**\n   * The keys of the output variables.\n   */\n  get variableKeys(): string[] {\n    return this.memo('variableKeys', () => this.variableTable.variableKeys);\n  }\n\n  protected addVariableToTable(variable: VariableDeclaration) {\n    if (variable.scope !== this.scope) {\n      throw Error('VariableDeclaration must be a ast node in scope');\n    }\n\n    (this.variableTable as VariableTable).addVariableToTable(variable);\n    this._hasChanges = true;\n  }\n\n  protected removeVariableFromTable(key: string) {\n    (this.variableTable as VariableTable).removeVariableFromTable(key);\n    this._hasChanges = true;\n  }\n\n  /**\n   * Retrieves a variable declaration by its key.\n   * @param key The key of the variable.\n   * @returns The `VariableDeclaration` or `undefined` if not found.\n   */\n  getVariableByKey(key: string) {\n    return this.variableTable.getVariableByKey(key);\n  }\n\n  /**\n   * Notifies the covering scopes that the available variables have changed.\n   */\n  notifyCoversChange(): void {\n    this.scope.coverScopes.forEach((scope) => scope.available.refresh());\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { ScopeChain } from './scope-chain';\nexport { Scope } from './scope';\nexport { ScopeOutputData } from './datas';\nexport { type IVariableTable } from './types';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/scope-chain.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable } from 'inversify';\nimport { DisposableCollection, type Event } from '@flowgram.ai/utils';\n\nimport { VariableEngineProvider } from '../providers';\nimport { type Scope } from './scope';\n\n/**\n * Manages the dependency relationships between scopes.\n * This is an abstract class, and specific implementations determine how the scope order is managed.\n */\n@injectable()\nexport abstract class ScopeChain {\n  readonly toDispose: DisposableCollection = new DisposableCollection();\n\n  @inject(VariableEngineProvider) variableEngineProvider: VariableEngineProvider;\n\n  get variableEngine() {\n    return this.variableEngineProvider();\n  }\n\n  constructor() {}\n\n  /**\n   * Refreshes the dependency and coverage relationships for all scopes.\n   */\n  refreshAllChange(): void {\n    this.variableEngine.getAllScopes().forEach((_scope) => {\n      _scope.refreshCovers();\n      _scope.refreshDeps();\n    });\n  }\n\n  /**\n   * Gets the dependency scopes for a given scope.\n   * @param scope The scope to get dependencies for.\n   * @returns An array of dependency scopes.\n   */\n  abstract getDeps(scope: Scope): Scope[];\n\n  /**\n   * Gets the covering scopes for a given scope.\n   * @param scope The scope to get covers for.\n   * @returns An array of covering scopes.\n   */\n  abstract getCovers(scope: Scope): Scope[];\n\n  /**\n   * Sorts all scopes based on their dependency relationships.\n   * @returns A sorted array of all scopes.\n   */\n  abstract sortAll(): Scope[];\n\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  get onDispose(): Event<void> {\n    return this.toDispose.onDispose;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/scope.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { DisposableCollection } from '@flowgram.ai/utils';\n\nimport { type VariableEngine } from '../variable-engine';\nimport { createMemo } from '../utils/memo';\nimport { ASTKind, type ASTNode, type ASTNodeJSON, MapNode } from '../ast';\nimport { ScopeAvailableData, ScopeEventData, ScopeOutputData } from './datas';\n\n/**\n * Interface for the Scope constructor.\n */\nexport interface IScopeConstructor {\n  new (options: {\n    id: string | symbol;\n    variableEngine: VariableEngine;\n    meta?: Record<string, any>;\n  }): Scope;\n}\n\n/**\n * Represents a variable scope, which manages its own set of variables and their lifecycle.\n * - `scope.output` represents the variables declared within this scope.\n * - `scope.available` represents all variables accessible from this scope, including those from parent scopes.\n */\nexport class Scope<ScopeMeta extends Record<string, any> = Record<string, any>> {\n  /**\n   * A unique identifier for the scope.\n   */\n  readonly id: string | symbol;\n\n  /**\n   * The variable engine instance this scope belongs to.\n   */\n  readonly variableEngine: VariableEngine;\n\n  /**\n   * Metadata associated with the scope, which can be extended by higher-level business logic.\n   */\n  readonly meta: ScopeMeta;\n\n  /**\n   * The root AST node for this scope, which is a MapNode.\n   * It stores various data related to the scope, such as `outputs`.\n   */\n  readonly ast: MapNode;\n\n  /**\n   * Manages the available variables for this scope.\n   */\n  readonly available: ScopeAvailableData;\n\n  /**\n   * Manages the output variables for this scope.\n   */\n  readonly output: ScopeOutputData;\n\n  /**\n   * Manages event dispatching and handling for this scope.\n   */\n  readonly event: ScopeEventData;\n\n  /**\n   * A memoization utility for caching computed values.\n   */\n  protected memo = createMemo();\n\n  public toDispose: DisposableCollection = new DisposableCollection();\n\n  constructor(options: { id: string | symbol; variableEngine: VariableEngine; meta?: ScopeMeta }) {\n    this.id = options.id;\n    this.meta = options.meta || ({} as any);\n    this.variableEngine = options.variableEngine;\n\n    this.event = new ScopeEventData(this);\n\n    this.ast = this.variableEngine.astRegisters.createAST(\n      {\n        kind: ASTKind.MapNode,\n        key: String(this.id),\n      },\n      {\n        scope: this,\n      }\n    ) as MapNode;\n\n    this.output = new ScopeOutputData(this);\n    this.available = new ScopeAvailableData(this);\n  }\n\n  /**\n   * Refreshes the covering scopes.\n   */\n  refreshCovers(): void {\n    this.memo.clear('covers');\n  }\n\n  /**\n   * Refreshes the dependency scopes and the available variables.\n   */\n  refreshDeps(): void {\n    this.memo.clear('deps');\n    this.available.refresh();\n  }\n\n  /**\n   * Gets the scopes that this scope depends on.\n   */\n  get depScopes(): Scope[] {\n    return this.memo('deps', () =>\n      this.variableEngine.chain\n        .getDeps(this)\n        .filter((_scope) => Boolean(_scope) && !_scope?.disposed)\n    );\n  }\n\n  /**\n   * Gets the scopes that are covered by this scope.\n   */\n  get coverScopes(): Scope[] {\n    return this.memo('covers', () =>\n      this.variableEngine.chain\n        .getCovers(this)\n        .filter((_scope) => Boolean(_scope) && !_scope?.disposed)\n    );\n  }\n\n  /**\n   * Disposes of the scope and its resources.\n   * This will also trigger updates in dependent and covering scopes.\n   */\n  dispose(): void {\n    this.ast.dispose();\n    this.toDispose.dispose();\n\n    // When a scope is disposed, update its dependent and covering scopes.\n    this.coverScopes.forEach((_scope) => _scope.refreshDeps());\n    this.depScopes.forEach((_scope) => _scope.refreshCovers());\n  }\n\n  onDispose = this.toDispose.onDispose;\n\n  get disposed(): boolean {\n    return this.toDispose.disposed;\n  }\n\n  /**\n   * Sets a variable in the scope with the default key 'outputs'.\n   *\n   * @param json The JSON representation of the AST node to set.\n   * @returns The created or updated AST node.\n   */\n  public setVar<Node extends ASTNode = ASTNode>(json: ASTNodeJSON): Node;\n\n  /**\n   * Sets a variable in the scope with a specified key.\n   *\n   * @param key The key of the variable to set.\n   * @param json The JSON representation of the AST node to set.\n   * @returns The created or updated AST node.\n   */\n  public setVar<Node extends ASTNode = ASTNode>(key: string, json: ASTNodeJSON): Node;\n\n  public setVar<Node extends ASTNode = ASTNode>(\n    arg1: string | ASTNodeJSON,\n    arg2?: ASTNodeJSON\n  ): Node {\n    if (typeof arg1 === 'string' && arg2 !== undefined) {\n      return this.ast.set(arg1, arg2);\n    }\n\n    if (typeof arg1 === 'object' && arg2 === undefined) {\n      return this.ast.set('outputs', arg1);\n    }\n\n    throw new Error('Invalid arguments');\n  }\n\n  /**\n   * Retrieves a variable from the scope by its key.\n   *\n   * @param key The key of the variable to retrieve. Defaults to 'outputs'.\n   * @returns The AST node for the variable, or `undefined` if not found.\n   */\n  public getVar<Node extends ASTNode = ASTNode>(key: string = 'outputs') {\n    return this.ast.get<Node>(key);\n  }\n\n  /**\n   * Clears a variable from the scope by its key.\n   *\n   * @param key The key of the variable to clear. Defaults to 'outputs'.\n   */\n  public clearVar(key: string = 'outputs') {\n    return this.ast.remove(key);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Event, Disposable } from '@flowgram.ai/utils';\n\nimport { BaseVariableField, VariableDeclaration } from '../ast';\nimport { type Scope } from './scope';\n\n/**\n * Parameters for getting all scopes.\n */\nexport interface GetAllScopeParams {\n  /**\n   * Whether to sort the scopes.\n   */\n  sort?: boolean;\n}\n\n/**\n * Action type for scope changes.\n */\nexport interface ScopeChangeAction {\n  type: 'add' | 'delete' | 'update' | 'available';\n  scope: Scope;\n}\n\n/**\n * Interface for a variable table.\n */\nexport interface IVariableTable extends Disposable {\n  /**\n   * The parent variable table.\n   */\n  parentTable?: IVariableTable;\n\n  /**\n   * @deprecated Use `onVariableListChange` or `onAnyVariableChange` instead.\n   */\n  onDataChange: Event<void>;\n\n  /**\n   * The current version of the variable table.\n   */\n  version: number;\n\n  /**\n   * The list of variables in the table.\n   */\n  variables: VariableDeclaration[];\n\n  /**\n   * The keys of the variables in the table.\n   */\n  variableKeys: string[];\n\n  /**\n   * Fires a change event.\n   */\n  fireChange(): void;\n\n  /**\n   * Gets a variable or property by its key path.\n   * @param keyPath The key path to the variable or property.\n   * @returns The found `BaseVariableField` or `undefined`.\n   */\n  getByKeyPath(keyPath: string[]): BaseVariableField | undefined;\n\n  /**\n   * Gets a variable by its key.\n   * @param key The key of the variable.\n   * @returns The found `VariableDeclaration` or `undefined`.\n   */\n  getVariableByKey(key: string): VariableDeclaration | undefined;\n\n  /**\n   * Disposes the variable table.\n   */\n  dispose(): void;\n\n  /**\n   * Subscribes to changes in the variable list.\n   * @param observer The observer function.\n   * @returns A disposable to unsubscribe.\n   */\n  onVariableListChange(observer: (variables: VariableDeclaration[]) => void): Disposable;\n\n  /**\n   * Subscribes to changes in any variable's value.\n   * @param observer The observer function.\n   * @returns A disposable to unsubscribe.\n   */\n  onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void): Disposable;\n\n  /**\n   * Subscribes to both variable list changes and any variable's value changes.\n   * @param observer The observer function.\n   * @returns A disposable to unsubscribe.\n   */\n  onListOrAnyVarChange(observer: () => void): Disposable;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/scope/variable-table.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Observable, Subject, merge, share, skip, switchMap } from 'rxjs';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { subsToDisposable } from '../utils/toDisposable';\nimport { BaseVariableField } from '../ast/declaration/base-variable-field';\nimport { VariableDeclaration } from '../ast';\nimport { IVariableTable } from './types';\n\n/**\n * A class that stores and manages variables in a table-like structure.\n * It provides methods for adding, removing, and retrieving variables, as well as\n * observables for listening to changes in the variable list and individual variables.\n */\nexport class VariableTable implements IVariableTable {\n  protected table: Map<string, VariableDeclaration> = new Map();\n\n  toDispose = new DisposableCollection();\n\n  /**\n   * @deprecated\n   */\n  protected onDataChangeEmitter = new Emitter<void>();\n\n  protected variables$: Subject<VariableDeclaration[]> = new Subject<VariableDeclaration[]>();\n\n  /**\n   * An observable that listens for value changes on any variable within the table.\n   */\n  protected anyVariableChange$: Observable<VariableDeclaration> = this.variables$.pipe(\n    switchMap((_variables) =>\n      merge(\n        ..._variables.map((_v) =>\n          _v.value$.pipe<any>(\n            // Skip the initial value of the BehaviorSubject\n            skip(1)\n          )\n        )\n      )\n    ),\n    share()\n  );\n\n  /**\n   * Subscribes to updates on any variable in the list.\n   * @param observer A function to be called when any variable's value changes.\n   * @returns A disposable object to unsubscribe from the updates.\n   */\n  onAnyVariableChange(observer: (changedVariable: VariableDeclaration) => void) {\n    return subsToDisposable(this.anyVariableChange$.subscribe(observer));\n  }\n\n  /**\n   * Subscribes to changes in the variable list (additions or removals).\n   * @param observer A function to be called when the list of variables changes.\n   * @returns A disposable object to unsubscribe from the updates.\n   */\n  onVariableListChange(observer: (variables: VariableDeclaration[]) => void) {\n    return subsToDisposable(this.variables$.subscribe(observer));\n  }\n\n  /**\n   * Subscribes to both variable list changes and updates to any variable in the list.\n   * @param observer A function to be called when either the list or a variable in it changes.\n   * @returns A disposable collection to unsubscribe from both events.\n   */\n  onListOrAnyVarChange(observer: () => void) {\n    const disposables = new DisposableCollection();\n    disposables.pushAll([this.onVariableListChange(observer), this.onAnyVariableChange(observer)]);\n    return disposables;\n  }\n\n  /**\n   * @deprecated Use onListOrAnyVarChange instead.\n   */\n  public onDataChange = this.onDataChangeEmitter.event;\n\n  protected _version: number = 0;\n\n  /**\n   * Fires change events to notify listeners that the data has been updated.\n   */\n  fireChange() {\n    this.bumpVersion();\n    this.onDataChangeEmitter.fire();\n    this.variables$.next(this.variables);\n    this.parentTable?.fireChange();\n  }\n\n  /**\n   * The current version of the variable table, incremented on each change.\n   */\n  get version(): number {\n    return this._version;\n  }\n\n  /**\n   * Increments the version number, resetting to 0 if it reaches MAX_SAFE_INTEGER.\n   */\n  protected bumpVersion() {\n    this._version = this._version + 1;\n    if (this._version === Number.MAX_SAFE_INTEGER) {\n      this._version = 0;\n    }\n  }\n\n  constructor(\n    /**\n     * An optional parent table. If provided, this table will contain all variables\n     * from the current table.\n     */\n    public parentTable?: IVariableTable\n  ) {\n    this.toDispose.pushAll([\n      this.onDataChangeEmitter,\n      // Activate the share() operator\n      this.onAnyVariableChange(() => {\n        this.bumpVersion();\n      }),\n    ]);\n  }\n\n  /**\n   * An array of all variables in the table.\n   */\n  get variables(): VariableDeclaration[] {\n    return Array.from(this.table.values());\n  }\n\n  /**\n   * An array of all variable keys in the table.\n   */\n  get variableKeys(): string[] {\n    return Array.from(this.table.keys());\n  }\n\n  /**\n   * Retrieves a variable or a nested property field by its key path.\n   * @param keyPath An array of keys representing the path to the desired field.\n   * @returns The found variable or property field, or undefined if not found.\n   */\n  getByKeyPath(keyPath: string[]): BaseVariableField | undefined {\n    const [variableKey, ...propertyKeys] = keyPath || [];\n\n    if (!variableKey) {\n      return;\n    }\n\n    const variable = this.getVariableByKey(variableKey);\n\n    return propertyKeys.length ? variable?.getByKeyPath(propertyKeys) : variable;\n  }\n\n  /**\n   * Retrieves a variable by its key.\n   * @param key The key of the variable to retrieve.\n   * @returns The variable declaration if found, otherwise undefined.\n   */\n  getVariableByKey(key: string) {\n    return this.table.get(key);\n  }\n\n  /**\n   * Adds a variable to the table.\n   * If a parent table exists, the variable is also added to the parent.\n   * @param variable The variable declaration to add.\n   */\n  addVariableToTable(variable: VariableDeclaration) {\n    this.table.set(variable.key, variable);\n    if (this.parentTable) {\n      (this.parentTable as VariableTable).addVariableToTable(variable);\n    }\n  }\n\n  /**\n   * Removes a variable from the table.\n   * If a parent table exists, the variable is also removed from the parent.\n   * @param key The key of the variable to remove.\n   */\n  removeVariableFromTable(key: string) {\n    this.table.delete(key);\n    if (this.parentTable) {\n      (this.parentTable as VariableTable).removeVariableFromTable(key);\n    }\n  }\n\n  /**\n   * Disposes of all resources used by the variable table.\n   */\n  dispose(): void {\n    this.variableKeys.forEach((_key) =>\n      (this.parentTable as VariableTable)?.removeVariableFromTable(_key)\n    );\n    this.parentTable?.fireChange();\n    this.variables$.complete();\n    this.variables$.unsubscribe();\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/services/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { VariableFieldKeyRenameService } from './variable-field-key-rename-service';\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/services/variable-field-key-rename-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { difference } from 'lodash-es';\nimport { inject, injectable, postConstruct, preDestroy } from 'inversify';\nimport { DisposableCollection, Emitter } from '@flowgram.ai/utils';\n\nimport { VariableEngine } from '../variable-engine';\nimport {\n  ASTNode,\n  BaseVariableField,\n  ObjectPropertiesChangeAction,\n  VariableDeclarationListChangeAction,\n} from '../ast';\n\ninterface RenameInfo {\n  before: BaseVariableField;\n  after: BaseVariableField;\n}\n\n/**\n * This service is responsible for detecting when a variable field's key is renamed.\n * It listens for changes in variable declaration lists and object properties, and\n * determines if a change constitutes a rename operation.\n */\n@injectable()\nexport class VariableFieldKeyRenameService {\n  @inject(VariableEngine) variableEngine: VariableEngine;\n\n  toDispose = new DisposableCollection();\n\n  renameEmitter = new Emitter<RenameInfo>();\n\n  /**\n   * Emits events for fields that are disposed of during a list change, but not renamed.\n   * This helps distinguish between a field that was truly removed and one that was renamed.\n   */\n  disposeInListEmitter = new Emitter<BaseVariableField>();\n\n  /**\n   * An event that fires when a variable field key is successfully renamed.\n   */\n  onRename = this.renameEmitter.event;\n\n  /**\n   * An event that fires when a field is removed from a list (and not part of a rename).\n   */\n  onDisposeInList = this.disposeInListEmitter.event;\n\n  /**\n   * Handles changes in a list of fields to detect rename operations.\n   * @param ast The AST node where the change occurred.\n   * @param prev The list of fields before the change.\n   * @param next The list of fields after the change.\n   */\n  handleFieldListChange(ast?: ASTNode, prev?: BaseVariableField[], next?: BaseVariableField[]) {\n    // 1. Check if a rename is possible.\n    if (!ast || !prev?.length || !next?.length) {\n      this.notifyFieldsDispose(prev, next);\n      return;\n    }\n\n    // 2. The lengths of the lists must be the same for a rename.\n    if (prev.length !== next.length) {\n      this.notifyFieldsDispose(prev, next);\n      return;\n    }\n\n    let renameNodeInfo: RenameInfo | null = null;\n    let existFieldChanged = false;\n\n    for (const [index, prevField] of prev.entries()) {\n      const nextField = next[index];\n\n      if (prevField.key !== nextField.key) {\n        // Only one rename is allowed at a time.\n        if (existFieldChanged) {\n          this.notifyFieldsDispose(prev, next);\n          return;\n        }\n        existFieldChanged = true;\n\n        if (prevField.type?.kind === nextField.type?.kind) {\n          renameNodeInfo = { before: prevField, after: nextField };\n        }\n      }\n    }\n\n    if (!renameNodeInfo) {\n      this.notifyFieldsDispose(prev, next);\n      return;\n    }\n\n    this.renameEmitter.fire(renameNodeInfo);\n  }\n\n  /**\n   * Notifies listeners about fields that were removed from a list.\n   * @param prev The list of fields before the change.\n   * @param next The list of fields after the change.\n   */\n  notifyFieldsDispose(prev?: BaseVariableField[], next?: BaseVariableField[]) {\n    const removedFields = difference(prev || [], next || []);\n    removedFields.forEach((_field) => this.disposeInListEmitter.fire(_field));\n  }\n\n  @postConstruct()\n  init() {\n    this.toDispose.pushAll([\n      this.variableEngine.onGlobalEvent<VariableDeclarationListChangeAction>(\n        'VariableListChange',\n        (_action) => {\n          this.handleFieldListChange(_action.ast, _action.payload?.prev, _action.payload?.next);\n        }\n      ),\n      this.variableEngine.onGlobalEvent<ObjectPropertiesChangeAction>(\n        'ObjectPropertiesChange',\n        (_action) => {\n          this.handleFieldListChange(_action.ast, _action.payload?.prev, _action.payload?.next);\n        }\n      ),\n    ]);\n  }\n\n  @preDestroy()\n  dispose() {\n    this.toDispose.dispose();\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/utils/memo.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\ntype KeyType = string | symbol;\n\n/**\n * Create memo manager\n * @returns\n */\nexport const createMemo = (): {\n  <T>(key: KeyType, fn: () => T): T;\n  clear: (key?: KeyType) => void;\n} => {\n  const _memoCache = new Map<KeyType, any>();\n\n  const memo = <T>(key: KeyType, fn: () => T): T => {\n    if (_memoCache.has(key)) {\n      return _memoCache.get(key) as T;\n    }\n    const data = fn();\n    _memoCache.set(key, data);\n    return data as T;\n  };\n\n  const clear = (key?: KeyType) => {\n    if (key) {\n      _memoCache.delete(key);\n    } else {\n      _memoCache.clear();\n    }\n  };\n\n  memo.clear = clear;\n\n  return memo;\n};\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/utils/toDisposable.tsx",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Subscription } from 'rxjs';\nimport { Disposable } from '@flowgram.ai/utils';\n\n/**\n * Convert rxjs subscription to disposable\n * @param subscription - The rxjs subscription\n * @returns The disposable\n */\nexport function subsToDisposable(subscription: Subscription): Disposable {\n  return Disposable.create(() => subscription.unsubscribe());\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/variable-container-module.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { ContainerModule } from 'inversify';\n\nimport { VariableEngine } from './variable-engine';\nimport { VariableFieldKeyRenameService } from './services';\nimport { ContainerProvider, VariableEngineProvider } from './providers';\nimport { ASTRegisters } from './ast';\n\n/**\n * An InversifyJS container module that binds all the necessary services for the variable engine.\n * This module sets up the dependency injection for the core components of the variable engine.\n */\nexport const VariableContainerModule = new ContainerModule((bind) => {\n  bind(VariableEngine).toSelf().inSingletonScope();\n  bind(ASTRegisters).toSelf().inSingletonScope();\n\n  bind(VariableFieldKeyRenameService).toSelf().inSingletonScope();\n\n  // Provide a dynamic provider for VariableEngine to prevent circular dependencies.\n  bind(VariableEngineProvider).toDynamicValue((ctx) => () => ctx.container.get(VariableEngine));\n\n  // Provide a ContainerProvider to allow AST nodes and other components to access the container.\n  bind(ContainerProvider).toDynamicValue((ctx) => () => ctx.container);\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/src/variable-engine.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Subject } from 'rxjs';\nimport { inject, injectable, interfaces, preDestroy } from 'inversify';\nimport { Disposable, DisposableCollection } from '@flowgram.ai/utils';\nimport { Emitter } from '@flowgram.ai/utils';\n\nimport { subsToDisposable } from './utils/toDisposable';\nimport { createMemo } from './utils/memo';\nimport { VariableTable } from './scope/variable-table';\nimport { ScopeChangeAction } from './scope/types';\nimport { IScopeConstructor } from './scope/scope';\nimport { Scope, ScopeChain, type IVariableTable } from './scope';\nimport { ContainerProvider } from './providers';\nimport { ASTRegisters, type GlobalEventActionType } from './ast';\n\n/**\n * The core of the variable engine system.\n * It manages scopes, variables, and events within the system.\n */\n@injectable()\nexport class VariableEngine implements Disposable {\n  protected toDispose = new DisposableCollection();\n\n  protected memo = createMemo();\n\n  protected scopeMap = new Map<string | symbol, Scope>();\n\n  /**\n   * A rxjs subject that emits global events occurring within the variable engine.\n   */\n  globalEvent$: Subject<GlobalEventActionType> = new Subject<GlobalEventActionType>();\n\n  protected onScopeChangeEmitter = new Emitter<ScopeChangeAction>();\n\n  /**\n   * A table containing all global variables.\n   */\n  public globalVariableTable: IVariableTable = new VariableTable();\n\n  /**\n   * An event that fires whenever a scope is added, updated, or deleted.\n   */\n  public onScopeChange = this.onScopeChangeEmitter.event;\n\n  @inject(ContainerProvider) private readonly containerProvider: ContainerProvider;\n\n  /**\n   * The Inversify container instance.\n   */\n  get container(): interfaces.Container {\n    return this.containerProvider();\n  }\n\n  constructor(\n    /**\n     * The scope chain, which manages the dependency relationships between scopes.\n     */\n    @inject(ScopeChain)\n    public readonly chain: ScopeChain,\n    /**\n     * The registry for all AST node types.\n     */\n    @inject(ASTRegisters)\n    public readonly astRegisters: ASTRegisters\n  ) {\n    this.toDispose.pushAll([\n      chain,\n      Disposable.create(() => {\n        // Dispose all scopes\n        this.getAllScopes().forEach((scope) => scope.dispose());\n        this.globalVariableTable.dispose();\n      }),\n    ]);\n  }\n\n  /**\n   * Disposes of all resources used by the variable engine.\n   */\n  @preDestroy()\n  dispose(): void {\n    this.toDispose.dispose();\n  }\n\n  /**\n   * Retrieves a scope by its unique identifier.\n   * @param scopeId The ID of the scope to retrieve.\n   * @returns The scope if found, otherwise undefined.\n   */\n  getScopeById(scopeId: string | symbol): Scope | undefined {\n    return this.scopeMap.get(scopeId);\n  }\n\n  /**\n   * Removes a scope by its unique identifier and disposes of it.\n   * @param scopeId The ID of the scope to remove.\n   */\n  removeScopeById(scopeId: string | symbol): void {\n    this.getScopeById(scopeId)?.dispose();\n  }\n\n  /**\n   * Creates a new scope or retrieves an existing one if the ID and type match.\n   * @param id The unique identifier for the scope.\n   * @param meta Optional metadata for the scope, defined by the user.\n   * @param options Options for creating the scope.\n   * @param options.ScopeConstructor The constructor to use for creating the scope. Defaults to `Scope`.\n   * @returns The created or existing scope.\n   */\n  createScope(\n    id: string | symbol,\n    meta?: Record<string, any>,\n    options: {\n      ScopeConstructor?: IScopeConstructor;\n    } = {}\n  ): Scope {\n    const { ScopeConstructor = Scope } = options;\n\n    let scope = this.getScopeById(id);\n\n    if (!scope) {\n      scope = new ScopeConstructor({ variableEngine: this, meta, id });\n      this.scopeMap.set(id, scope);\n      this.onScopeChangeEmitter.fire({ type: 'add', scope: scope! });\n\n      scope.toDispose.pushAll([\n        scope.ast.subscribe(() => {\n          this.onScopeChangeEmitter.fire({ type: 'update', scope: scope! });\n        }),\n        // Fires when available variables change\n        scope.available.onDataChange(() => {\n          this.onScopeChangeEmitter.fire({ type: 'available', scope: scope! });\n        }),\n      ]);\n      scope.onDispose(() => {\n        this.scopeMap.delete(id);\n        this.onScopeChangeEmitter.fire({ type: 'delete', scope: scope! });\n      });\n    }\n\n    return scope;\n  }\n\n  /**\n   * Retrieves all scopes currently managed by the engine.\n   * @param options Options for retrieving the scopes.\n   * @param options.sort Whether to sort the scopes based on their dependency chain.\n   * @returns An array of all scopes.\n   */\n  getAllScopes({\n    sort,\n  }: {\n    sort?: boolean;\n  } = {}): Scope[] {\n    const allScopes = Array.from(this.scopeMap.values());\n\n    if (sort) {\n      const sortScopes = this.chain.sortAll();\n      const remainScopes = new Set(allScopes);\n      sortScopes.forEach((_scope) => remainScopes.delete(_scope));\n\n      return [...sortScopes, ...Array.from(remainScopes)];\n    }\n\n    return [...allScopes];\n  }\n\n  /**\n   * Fires a global event to be broadcast to all listeners.\n   * @param event The global event to fire.\n   */\n  fireGlobalEvent(event: GlobalEventActionType) {\n    this.globalEvent$.next(event);\n  }\n\n  /**\n   * Subscribes to a specific type of global event.\n   * @param type The type of the event to listen for.\n   * @param observer A function to be called when the event is observed.\n   * @returns A disposable object to unsubscribe from the event.\n   */\n  onGlobalEvent<ActionType extends GlobalEventActionType = GlobalEventActionType>(\n    type: ActionType['type'],\n    observer: (action: ActionType) => void\n  ): Disposable {\n    return subsToDisposable(\n      this.globalEvent$.subscribe((_action) => {\n        if (_action.type === type) {\n          observer(_action as ActionType);\n        }\n      })\n    );\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"include\": [\n    \"./src\"\n  ],\n  \"compilerOptions\": {\n    \"types\": [\n      \"reflect-metadata\"\n    ]\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-core/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__/**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-core/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__mocks__/container.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Container } from 'inversify';\nimport {\n  ASTFactory,\n  ScopeChain,\n  VariableContainerModule,\n} from '@flowgram.ai/variable-core';\nimport { createPlaygroundContainer } from '@flowgram.ai/core';\nimport {\n  FreeLayoutScopeChain,\n  FixedLayoutScopeChain,\n  FlowNodeVariableData,\n  VariableChainConfig,\n  GlobalScope,\n  bindGlobalScope,\n  ScopeChainTransformService,\n} from '../src';\nimport { EntityManager } from '@flowgram.ai/core';\nimport { VariableEngine } from '@flowgram.ai/variable-core';\nimport {\n  FlowDocument,\n  FlowDocumentContainerModule,\n} from '@flowgram.ai/document';\nimport { WorkflowDocumentContainerModule } from '@flowgram.ai/free-layout-core';\n\nexport interface TestConfig extends VariableChainConfig {\n  enableGlobalScope?: boolean;\n  onInit?: (container: Container) => void;\n  runExtraTest?: (container: Container) => void\n}\n\nexport function getContainer(layout: 'free' | 'fixed', config?: TestConfig): Container {\n  const { enableGlobalScope, onInit, runExtraTest, ...layoutConfig } = config || {};\n\n  const container = createPlaygroundContainer() as Container;\n  container.load(VariableContainerModule);\n  container.load(FlowDocumentContainerModule);\n\n  if (layout === 'free') {\n    container.load(WorkflowDocumentContainerModule);\n    // container.get(WorkflowLinesManager).registerContribution(WorkflowSimpleLineContribution);\n    //container.get(WorkflowLinesManager).switchLineType(WorkflowSimpleLineContribution.type);\n  }\n\n  if (layoutConfig) {\n    container.bind(VariableChainConfig).toConstantValue(layoutConfig);\n  }\n  if (layout === 'free') {\n    container.bind(ScopeChain).to(FreeLayoutScopeChain).inSingletonScope();\n  }\n  if (layout === 'fixed') {\n    container.bind(ScopeChain).to(FixedLayoutScopeChain).inSingletonScope();\n  }\n\n  container.bind(ScopeChainTransformService).toSelf().inSingletonScope();\n\n  bindGlobalScope(container.bind.bind(container))\n\n  const entityManager = container.get<EntityManager>(EntityManager);\n  const variableEngine = container.get<VariableEngine>(VariableEngine);\n  const document = container.get<FlowDocument>(FlowDocument);\n\n  if (enableGlobalScope) {\n    // when get global scope, it will auto create it if not exists\n    container.get(GlobalScope).setVar(ASTFactory.createVariableDeclaration({\n      key: 'GlobalScope',\n      type: ASTFactory.createString(),\n    }));\n  }\n\n  /**\n   * 扩展 FlowNodeVariableData\n   */\n  entityManager.registerEntityData(\n    FlowNodeVariableData,\n    () => ({ variableEngine } as any),\n  );\n  document.registerNodeDatas(FlowNodeVariableData);\n\n  onInit?.(container);\n\n  return container;\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__mocks__/fixed-layout-specs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowDocumentJSON } from '@flowgram.ai/document';\n\nexport const fixLayout1: FlowDocumentJSON = {\n  nodes: [\n    {\n      type: 'start',\n      meta: {\n        isStart: true,\n      },\n      id: 'start',\n    },\n    {\n      type: 'getRecords',\n      id: 'getRecords_07e97c55832',\n    },\n    {\n      type: 'loop',\n      id: 'forEach_260a8f85ff2',\n      blocks: [\n        {\n          type: 'createRecord',\n          id: 'createRecord_8f85ff2c11d',\n        },\n        {\n          type: 'dynamicSplit',\n          id: 'exclusiveSplit_ff2c11d0fb4',\n          blocks: [\n            {\n              id: 'branch_f2c11d0fb42',\n            },\n            {\n              id: 'branch_2c11d0fb42c',\n            },\n          ],\n        },\n      ],\n    },\n    {\n      type: 'dynamicSplit',\n      id: 'exclusiveSplit_88dbf2c60ae',\n      meta: {\n        defaultExpanded: true,\n      },\n      blocks: [\n        {\n          id: 'branch_8dbf2c60aee',\n          blocks: [\n            {\n              type: 'dynamicSplit',\n              id: 'exclusiveSplit_a59afaadc9a',\n              blocks: [\n                {\n                  id: 'branch_59afaadc9ac',\n                },\n                {\n                  id: 'branch_9afaadc9acd',\n                  blocks: [\n                    {\n                      type: 'deleteRecords',\n                      id: 'deleteRecords_c32807e97c5',\n                    },\n                  ]\n                },\n              ],\n            },\n          ]\n        },\n        {\n          id: 'branch_dbf2c60aee4',\n          blocks: [\n            {\n              type: 'updateRecords',\n              id: 'updateRecords_7ed2a172c32',\n            },\n          ]\n        },\n      ],\n    },\n    {\n      type: 'end',\n      id: 'end',\n    },\n  ],\n};\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__mocks__/free-layout-specs.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { WorkflowJSON } from \"@flowgram.ai/free-layout-core\";\n\nexport const freeLayout1: WorkflowJSON = {\n  \"nodes\": [\n      {\n          \"id\": \"start_0\",\n          \"type\": \"start\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": -264,\n                  \"y\": -79\n              }\n          },\n          \"data\": {}\n      },\n      {\n          \"id\": \"end_0\",\n          \"type\": \"end\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": 1515,\n                  \"y\": -191\n              }\n          },\n          \"data\": {}\n      },\n      {\n          \"id\": \"base_1\",\n          \"type\": \"base\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": 103.5,\n                  \"y\": -122.5\n              }\n          }\n      },\n      {\n          \"id\": \"base_2\",\n          \"type\": \"base\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": 525.5,\n                  \"y\": -250.5\n              }\n          }\n      },\n      {\n          \"id\": \"loop_1\",\n          \"type\": \"loop\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": 541.5,\n                  \"y\": 42.5\n              },\n              \"canvasPosition\": {\n                  \"x\": 1154.5,\n                  \"y\": 206.5\n              }\n          },\n          \"data\": {\n              \"target\": {\n                  \"meta\": {\n                      \"name\": \"循环输出_\"\n                  },\n                  \"type\": \"Boolean\"\n              }\n          },\n          \"blocks\": [\n              {\n                  \"id\": \"base_in_loop_1\",\n                  \"type\": \"base\",\n                  \"meta\": {\n                      \"position\": {\n                          \"x\": 40,\n                          \"y\": 100\n                      }\n                  }\n              },\n              {\n                  \"id\": \"base_in_loop_2\",\n                  \"type\": \"base\",\n                  \"meta\": {\n                      \"position\": {\n                          \"x\": 62,\n                          \"y\": 299\n                      }\n                  }\n              },\n              {\n                  \"id\": \"base_in_loop_3\",\n                  \"type\": \"base\",\n                  \"meta\": {\n                      \"position\": {\n                          \"x\": 457,\n                          \"y\": 188\n                      }\n                  }\n              }\n          ],\n          \"edges\": [\n              {\n                  \"sourceNodeID\": \"base_in_loop_1\",\n                  \"targetNodeID\": \"base_in_loop_3\"\n              },\n              {\n                  \"sourceNodeID\": \"base_in_loop_2\",\n                  \"targetNodeID\": \"base_in_loop_3\"\n              }\n          ]\n      },\n      {\n          \"id\": \"base_3\",\n          \"type\": \"base\",\n          \"meta\": {\n              \"position\": {\n                  \"x\": 1063.5,\n                  \"y\": -52.987060546875\n              }\n          }\n      }\n  ],\n  \"edges\": [\n      {\n          \"sourceNodeID\": \"base_2\",\n          \"targetNodeID\": \"end_0\"\n      },\n      {\n          \"sourceNodeID\": \"base_3\",\n          \"targetNodeID\": \"end_0\"\n      },\n      {\n          \"sourceNodeID\": \"start_0\",\n          \"targetNodeID\": \"base_1\"\n      },\n      {\n          \"sourceNodeID\": \"base_1\",\n          \"targetNodeID\": \"base_2\"\n      },\n      {\n          \"sourceNodeID\": \"base_1\",\n          \"targetNodeID\": \"loop_1\"\n      },\n      {\n          \"sourceNodeID\": \"loop_1\",\n          \"targetNodeID\": \"base_3\",\n          \"sourcePortID\": \"loop-output\"\n      }\n  ]\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__mocks__/run-fixed-layout-test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\nimport { VariableEngine } from '@flowgram.ai/variable-core';\nimport { ASTKind } from '@flowgram.ai/variable-core';\nimport { FlowDocument, FlowDocumentJSON } from '@flowgram.ai/document';\nimport {\n  FlowNodeVariableData,\n} from '../src';\nimport { TestConfig, getContainer } from './container'\n\nexport const runFixedLayoutTest = (testName:string, spec: FlowDocumentJSON, config?: TestConfig) => {\n  describe(testName, () => {\n    const container = getContainer('fixed', config);\n    const flowDocument = container.get(FlowDocument);\n    const variableEngine = container.get(VariableEngine);\n\n    variableEngine.onScopeChange(action => {\n      const { type, scope } = action;\n\n      // 作用域默认创建一个默认变量\n      if (type === 'add') {\n        scope.ast.set('/', {\n          kind: ASTKind.VariableDeclaration,\n          type: ASTKind.String,\n          key: String(scope.id),\n        });\n      }\n    });\n\n    // 创建一个测试作用域\n    variableEngine.createScope('testScope');\n\n    const traverseVariableDatas = () =>\n      flowDocument\n        .getAllNodes()\n        // TODO 包含 $ 的节点不注册 variableData\n        .filter(_node => !_node.id.startsWith('$'))\n        .map(_node => _node.getData(FlowNodeVariableData))\n        .filter(Boolean);\n\n    // 初始化所有节点的私有作用域\n    const initAllNodePrivate = () => {\n      traverseVariableDatas().forEach(_data => {\n        _data.initPrivate();\n      });\n    };\n\n    // 初始化所有节点的可用变量\n    const printAllNodeAvailableMapping = (_scopeType: 'public' | 'private' = 'public') =>\n      traverseVariableDatas().reduce((acm, _data) => {\n        const scope = _data[_scopeType]!;\n        acm.set(String(scope.id), scope.available.variableKeys);\n\n        return acm;\n      }, new Map<string, string[]>());\n\n    // 初始化所有节点的覆盖作用域\n    const printAllCovers = (_scopeType: 'public' | 'private' = 'public') =>\n      traverseVariableDatas().reduce((acm, _data) => {\n        const scope = _data[_scopeType]!;\n        acm.set(\n          String(scope.id),\n          scope.coverScopes.map(_scope => String(_scope.id)),\n        );\n\n        return acm;\n      }, new Map<string, string[]>());\n\n    flowDocument.fromJSON(spec);\n\n    test('test get Deps', () => {\n      expect(printAllNodeAvailableMapping()).toMatchSnapshot();\n    });\n\n    test('test get Covers', () => {\n      expect(printAllCovers()).toMatchSnapshot();\n    });\n\n    test('test get Deps After Init Private', () => {\n      initAllNodePrivate();\n      expect(printAllNodeAvailableMapping()).toMatchSnapshot();\n    });\n\n    test('test get private scope Deps', () => {\n      expect(printAllNodeAvailableMapping('private')).toMatchSnapshot();\n    });\n\n    test('test get Covers After Init Private', () => {\n      expect(printAllCovers()).toMatchSnapshot();\n    });\n\n    test('test get private scope Covers', () => {\n      expect(printAllCovers('private')).toMatchSnapshot();\n    });\n\n    test('test sort', () => {\n      expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();\n    });\n\n    config?.runExtraTest?.(container);\n  });\n\n\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__mocks__/run-free-layout-test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { describe, expect, test } from 'vitest';\nimport { VariableEngine } from '@flowgram.ai/variable-core';\nimport { ASTKind } from '@flowgram.ai/variable-core';\nimport {\n  FlowNodeVariableData\n} from '../src';\nimport { TestConfig, getContainer } from './container';\nimport { WorkflowDocument, WorkflowJSON } from '@flowgram.ai/free-layout-core';\n\nexport const runFreeLayoutTest = (testName: string, spec: WorkflowJSON, config?: TestConfig) => {\n  describe(testName, async () => {\n    const container = getContainer('free', config);\n    const flowDocument = container.get(WorkflowDocument);\n    const variableEngine = container.get(VariableEngine);\n\n    variableEngine.onScopeChange(action => {\n      const { type, scope } = action;\n\n      // 作用域默认创建一个默认变量\n      if (type === 'add') {\n        scope.ast.set('/', {\n          kind: ASTKind.VariableDeclaration,\n          type: ASTKind.String,\n          key: String(scope.id),\n        });\n      }\n    });\n\n    // 创建一个全局作用域\n    variableEngine.createScope('testScope');\n\n    const traverseVariableDatas = () =>\n      flowDocument\n        .getAllNodes()\n        // TODO 包含 $ 的节点不注册 variableData\n        .filter(_node => !_node.id.startsWith('$'))\n        .map(_node => _node.getData(FlowNodeVariableData))\n        .filter(Boolean);\n\n    // 初始化所有节点的私有作用域\n    const initAllNodePrivate = () => {\n      traverseVariableDatas().forEach(_data => {\n        _data.initPrivate();\n      });\n    };\n\n    // 初始化所有节点的可用变量\n    const printAllNodeAvailableMapping = (_scopeType: 'public' | 'private' = 'public') =>\n      traverseVariableDatas().reduce((acm, _data) => {\n        const scope = _data[_scopeType]!;\n        acm.set(String(scope.id), scope.available.variableKeys);\n\n        return acm;\n      }, new Map<string, string[]>());\n\n    // 初始化所有节点的覆盖作用域\n    const printAllCovers = (_scopeType: 'public' | 'private' = 'public') =>\n      traverseVariableDatas().reduce((acm, _data) => {\n        const scope = _data[_scopeType]!;\n        acm.set(\n          String(scope.id),\n          scope.coverScopes.map(_scope => String(_scope.id)),\n        );\n\n        return acm;\n      }, new Map<string, string[]>());\n\n    await flowDocument.fromJSON(spec);\n\n    test('test get Deps', () => {\n      expect(printAllNodeAvailableMapping()).toMatchSnapshot();\n    });\n\n    test('test get Covers', () => {\n      expect(printAllCovers()).toMatchSnapshot();\n    });\n\n    test('test get Deps After Init Private', () => {\n      initAllNodePrivate();\n      expect(printAllNodeAvailableMapping()).toMatchSnapshot();\n    });\n\n    test('test get private scope Deps', () => {\n      expect(printAllNodeAvailableMapping('private')).toMatchSnapshot();\n    });\n\n    test('test get Covers After Init Private', () => {\n      expect(printAllCovers()).toMatchSnapshot();\n    });\n\n    test('test get private scope Covers', () => {\n      expect(printAllCovers('private')).toMatchSnapshot();\n    });\n\n    test('test sort', () => {\n      expect(variableEngine.getAllScopes({ sort: true }).map(_scope => _scope.id)).toMatchSnapshot();\n    });\n\n    config?.runExtraTest?.(container);\n  });\n\n\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-enable-global-scope.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fix Layout Enable Global Scope > test get Covers 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test get Covers After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"getRecords_07e97c55832_private\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test get Deps 1`] = `\nMap {\n  \"start\" => [\n    \"GlobalScope\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"GlobalScope\",\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n  ],\n  \"end\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test get Deps After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"GlobalScope\",\n    \"start_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac_private\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"end\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"end_private\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test get private scope Covers 1`] = `\nMap {\n  \"start_private\" => [\n    \"start\",\n  ],\n  \"getRecords_07e97c55832_private\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"branch_f2c11d0fb42\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"branch_59afaadc9ac\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"end_private\" => [\n    \"end\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test get private scope Deps 1`] = `\nMap {\n  \"start_private\" => [\n    \"GlobalScope\",\n  ],\n  \"getRecords_07e97c55832_private\" => [\n    \"GlobalScope\",\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"end_private\" => [\n    \"GlobalScope\",\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Enable Global Scope > test sort 1`] = `\n[\n  Symbol(GlobalScope),\n  \"start_private\",\n  \"start\",\n  \"getRecords_07e97c55832\",\n  \"getRecords_07e97c55832_private\",\n  \"forEach_260a8f85ff2\",\n  \"forEach_260a8f85ff2_private\",\n  \"createRecord_8f85ff2c11d\",\n  \"createRecord_8f85ff2c11d_private\",\n  \"exclusiveSplit_ff2c11d0fb4\",\n  \"exclusiveSplit_ff2c11d0fb4_private\",\n  \"branch_f2c11d0fb42\",\n  \"branch_f2c11d0fb42_private\",\n  \"branch_2c11d0fb42c\",\n  \"branch_2c11d0fb42c_private\",\n  \"exclusiveSplit_88dbf2c60ae\",\n  \"exclusiveSplit_88dbf2c60ae_private\",\n  \"branch_8dbf2c60aee\",\n  \"branch_8dbf2c60aee_private\",\n  \"exclusiveSplit_a59afaadc9a\",\n  \"exclusiveSplit_a59afaadc9a_private\",\n  \"branch_59afaadc9ac\",\n  \"branch_59afaadc9ac_private\",\n  \"branch_9afaadc9acd\",\n  \"branch_9afaadc9acd_private\",\n  \"deleteRecords_c32807e97c5\",\n  \"deleteRecords_c32807e97c5_private\",\n  \"branch_dbf2c60aee4\",\n  \"branch_dbf2c60aee4_private\",\n  \"updateRecords_7ed2a172c32\",\n  \"updateRecords_7ed2a172c32_private\",\n  \"end\",\n  \"end_private\",\n  \"testScope\",\n  \"$blockIcon$exclusiveSplit_ff2c11d0fb4\",\n  \"$inlineBlocks$exclusiveSplit_ff2c11d0fb4\",\n  \"$blockOrderIcon$branch_f2c11d0fb42\",\n  \"$blockOrderIcon$branch_2c11d0fb42c\",\n  \"$blockIcon$exclusiveSplit_88dbf2c60ae\",\n  \"$inlineBlocks$exclusiveSplit_88dbf2c60ae\",\n  \"$blockOrderIcon$branch_8dbf2c60aee\",\n  \"$blockIcon$exclusiveSplit_a59afaadc9a\",\n  \"$inlineBlocks$exclusiveSplit_a59afaadc9a\",\n  \"$blockOrderIcon$branch_59afaadc9ac\",\n  \"$blockOrderIcon$branch_9afaadc9acd\",\n  \"$blockOrderIcon$branch_dbf2c60aee4\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-layout-filter-start-end.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fix Layout Filter Start End > test get Covers 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test get Covers After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"getRecords_07e97c55832_private\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test get Deps 1`] = `\nMap {\n  \"start\" => [],\n  \"getRecords_07e97c55832\" => [],\n  \"forEach_260a8f85ff2\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n  ],\n  \"end\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test get Deps After Init Private 1`] = `\nMap {\n  \"start\" => [],\n  \"getRecords_07e97c55832\" => [\n    \"getRecords_07e97c55832_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac_private\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"end\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"end_private\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test get private scope Covers 1`] = `\nMap {\n  \"start_private\" => [\n    \"start\",\n  ],\n  \"getRecords_07e97c55832_private\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"branch_f2c11d0fb42\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"branch_59afaadc9ac\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"end_private\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test get private scope Deps 1`] = `\nMap {\n  \"start_private\" => [],\n  \"getRecords_07e97c55832_private\" => [],\n  \"forEach_260a8f85ff2_private\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"end_private\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Filter Start End > test sort 1`] = `\n[\n  \"start\",\n  \"getRecords_07e97c55832\",\n  \"getRecords_07e97c55832_private\",\n  \"forEach_260a8f85ff2\",\n  \"forEach_260a8f85ff2_private\",\n  \"createRecord_8f85ff2c11d\",\n  \"createRecord_8f85ff2c11d_private\",\n  \"exclusiveSplit_ff2c11d0fb4\",\n  \"exclusiveSplit_ff2c11d0fb4_private\",\n  \"branch_f2c11d0fb42\",\n  \"branch_f2c11d0fb42_private\",\n  \"branch_2c11d0fb42c\",\n  \"branch_2c11d0fb42c_private\",\n  \"exclusiveSplit_88dbf2c60ae\",\n  \"exclusiveSplit_88dbf2c60ae_private\",\n  \"branch_8dbf2c60aee\",\n  \"branch_8dbf2c60aee_private\",\n  \"exclusiveSplit_a59afaadc9a\",\n  \"exclusiveSplit_a59afaadc9a_private\",\n  \"branch_59afaadc9ac\",\n  \"branch_59afaadc9ac_private\",\n  \"branch_9afaadc9acd\",\n  \"branch_9afaadc9acd_private\",\n  \"deleteRecords_c32807e97c5\",\n  \"deleteRecords_c32807e97c5_private\",\n  \"branch_dbf2c60aee4\",\n  \"branch_dbf2c60aee4_private\",\n  \"updateRecords_7ed2a172c32\",\n  \"updateRecords_7ed2a172c32_private\",\n  \"testScope\",\n  \"$blockIcon$exclusiveSplit_ff2c11d0fb4\",\n  \"$inlineBlocks$exclusiveSplit_ff2c11d0fb4\",\n  \"$blockOrderIcon$branch_f2c11d0fb42\",\n  \"$blockOrderIcon$branch_2c11d0fb42c\",\n  \"$blockIcon$exclusiveSplit_88dbf2c60ae\",\n  \"$inlineBlocks$exclusiveSplit_88dbf2c60ae\",\n  \"$blockOrderIcon$branch_8dbf2c60aee\",\n  \"$blockIcon$exclusiveSplit_a59afaadc9a\",\n  \"$inlineBlocks$exclusiveSplit_a59afaadc9a\",\n  \"$blockOrderIcon$branch_59afaadc9ac\",\n  \"$blockOrderIcon$branch_9afaadc9acd\",\n  \"$blockOrderIcon$branch_dbf2c60aee4\",\n  \"end\",\n  \"start_private\",\n  \"end_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-layout-group.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fix Layout Group > test get Covers 1`] = `\nMap {\n  \"start_0\" => [\n    \"node_0\",\n    \"node_1\",\n    \"end_0\",\n  ],\n  \"node_0\" => [\n    \"node_1\",\n    \"end_0\",\n  ],\n  \"node_1\" => [\n    \"end_0\",\n  ],\n  \"end_0\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Group > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"node_0\",\n    \"node_0_private\",\n    \"node_1\",\n    \"node_1_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"node_0\" => [\n    \"node_1\",\n    \"node_1_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"node_1\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"end_0\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Group > test get Deps 1`] = `\nMap {\n  \"start_0\" => [],\n  \"node_0\" => [\n    \"start_0\",\n  ],\n  \"node_1\" => [\n    \"start_0\",\n    \"node_0\",\n  ],\n  \"end_0\" => [\n    \"start_0\",\n    \"node_0\",\n    \"node_1\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Group > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"start_0_private\",\n  ],\n  \"node_0\" => [\n    \"start_0\",\n    \"node_0_private\",\n  ],\n  \"node_1\" => [\n    \"start_0\",\n    \"node_0\",\n    \"node_1_private\",\n  ],\n  \"end_0\" => [\n    \"start_0\",\n    \"node_0\",\n    \"node_1\",\n    \"end_0_private\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Group > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [\n    \"start_0\",\n  ],\n  \"node_0_private\" => [\n    \"node_0\",\n  ],\n  \"node_1_private\" => [\n    \"node_1\",\n  ],\n  \"end_0_private\" => [\n    \"end_0\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Group > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"node_0_private\" => [\n    \"start_0\",\n  ],\n  \"node_1_private\" => [\n    \"start_0\",\n    \"node_0\",\n  ],\n  \"end_0_private\" => [\n    \"start_0\",\n    \"node_0\",\n    \"node_1\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Group > test sort 1`] = `\n[\n  \"start_0_private\",\n  \"start_0\",\n  \"node_0\",\n  \"node_0_private\",\n  \"node_1\",\n  \"node_1_private\",\n  \"end_0\",\n  \"end_0_private\",\n  \"testScope\",\n  \"$group_test$\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-layout-no-config.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fix Layout Without Config > test get Covers 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test get Covers After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"getRecords_07e97c55832_private\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_9afaadc9acd\" => [],\n  \"deleteRecords_c32807e97c5\" => [],\n  \"branch_dbf2c60aee4\" => [],\n  \"updateRecords_7ed2a172c32\" => [],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test get Deps 1`] = `\nMap {\n  \"start\" => [],\n  \"getRecords_07e97c55832\" => [\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n  ],\n  \"end\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test get Deps After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"start_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"start\",\n    \"getRecords_07e97c55832_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac_private\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"end\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"end_private\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test get private scope Covers 1`] = `\nMap {\n  \"start_private\" => [\n    \"start\",\n  ],\n  \"getRecords_07e97c55832_private\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"branch_f2c11d0fb42\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"branch_59afaadc9ac\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"end_private\" => [\n    \"end\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test get private scope Deps 1`] = `\nMap {\n  \"start_private\" => [],\n  \"getRecords_07e97c55832_private\" => [\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"end_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout Without Config > test sort 1`] = `\n[\n  \"start_private\",\n  \"start\",\n  \"getRecords_07e97c55832\",\n  \"getRecords_07e97c55832_private\",\n  \"forEach_260a8f85ff2\",\n  \"forEach_260a8f85ff2_private\",\n  \"createRecord_8f85ff2c11d\",\n  \"createRecord_8f85ff2c11d_private\",\n  \"exclusiveSplit_ff2c11d0fb4\",\n  \"exclusiveSplit_ff2c11d0fb4_private\",\n  \"branch_f2c11d0fb42\",\n  \"branch_f2c11d0fb42_private\",\n  \"branch_2c11d0fb42c\",\n  \"branch_2c11d0fb42c_private\",\n  \"exclusiveSplit_88dbf2c60ae\",\n  \"exclusiveSplit_88dbf2c60ae_private\",\n  \"branch_8dbf2c60aee\",\n  \"branch_8dbf2c60aee_private\",\n  \"exclusiveSplit_a59afaadc9a\",\n  \"exclusiveSplit_a59afaadc9a_private\",\n  \"branch_59afaadc9ac\",\n  \"branch_59afaadc9ac_private\",\n  \"branch_9afaadc9acd\",\n  \"branch_9afaadc9acd_private\",\n  \"deleteRecords_c32807e97c5\",\n  \"deleteRecords_c32807e97c5_private\",\n  \"branch_dbf2c60aee4\",\n  \"branch_dbf2c60aee4_private\",\n  \"updateRecords_7ed2a172c32\",\n  \"updateRecords_7ed2a172c32_private\",\n  \"end\",\n  \"end_private\",\n  \"testScope\",\n  \"$blockIcon$exclusiveSplit_ff2c11d0fb4\",\n  \"$inlineBlocks$exclusiveSplit_ff2c11d0fb4\",\n  \"$blockOrderIcon$branch_f2c11d0fb42\",\n  \"$blockOrderIcon$branch_2c11d0fb42c\",\n  \"$blockIcon$exclusiveSplit_88dbf2c60ae\",\n  \"$inlineBlocks$exclusiveSplit_88dbf2c60ae\",\n  \"$blockOrderIcon$branch_8dbf2c60aee\",\n  \"$blockIcon$exclusiveSplit_a59afaadc9a\",\n  \"$inlineBlocks$exclusiveSplit_a59afaadc9a\",\n  \"$blockOrderIcon$branch_59afaadc9ac\",\n  \"$blockOrderIcon$branch_9afaadc9acd\",\n  \"$blockOrderIcon$branch_dbf2c60aee4\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-layout-transform-empty.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fixed Layout transform empty > test get Covers 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test get Deps 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [],\n  \"base_1_private\" => [],\n  \"base_2_private\" => [],\n  \"loop_1_private\" => [],\n  \"base_in_loop_1_private\" => [],\n  \"base_in_loop_2_private\" => [],\n  \"base_in_loop_3_private\" => [],\n  \"base_3_private\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [],\n  \"base_1_private\" => [],\n  \"base_2_private\" => [],\n  \"loop_1_private\" => [],\n  \"base_in_loop_1_private\" => [],\n  \"base_in_loop_2_private\" => [],\n  \"base_in_loop_3_private\" => [],\n  \"base_3_private\" => [],\n}\n`;\n\nexports[`Variable Fixed Layout transform empty > test sort 1`] = `\n[\n  \"start_0\",\n  \"testScope\",\n  \"end_0\",\n  \"base_1\",\n  \"base_2\",\n  \"loop_1\",\n  \"base_in_loop_1\",\n  \"base_in_loop_2\",\n  \"base_in_loop_3\",\n  \"base_3\",\n  \"start_0_private\",\n  \"end_0_private\",\n  \"base_1_private\",\n  \"base_2_private\",\n  \"loop_1_private\",\n  \"base_in_loop_1_private\",\n  \"base_in_loop_2_private\",\n  \"base_in_loop_3_private\",\n  \"base_3_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-fix-layout.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Fix Layout > test get Covers 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"end\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"end\",\n  ],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout > test get Covers After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"getRecords_07e97c55832\",\n    \"getRecords_07e97c55832_private\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [],\n  \"branch_f2c11d0fb42\" => [\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n    \"end\",\n    \"end_private\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"end\",\n    \"end_private\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"end\",\n    \"end_private\",\n  ],\n  \"end\" => [],\n}\n`;\n\nexports[`Variable Fix Layout > test get Deps 1`] = `\nMap {\n  \"start\" => [],\n  \"getRecords_07e97c55832\" => [\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n  ],\n  \"end\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout > test get Deps After Init Private 1`] = `\nMap {\n  \"start\" => [\n    \"start_private\",\n  ],\n  \"getRecords_07e97c55832\" => [\n    \"start\",\n    \"getRecords_07e97c55832_private\",\n  ],\n  \"forEach_260a8f85ff2\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"createRecord_8f85ff2c11d\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_f2c11d0fb42\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42_private\",\n  ],\n  \"branch_2c11d0fb42c\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"branch_8dbf2c60aee\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_59afaadc9ac\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac_private\",\n  ],\n  \"branch_9afaadc9acd\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"deleteRecords_c32807e97c5\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_dbf2c60aee4\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"updateRecords_7ed2a172c32\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"end\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"end_private\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout > test get private scope Covers 1`] = `\nMap {\n  \"start_private\" => [\n    \"start\",\n  ],\n  \"getRecords_07e97c55832_private\" => [\n    \"getRecords_07e97c55832\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"forEach_260a8f85ff2\",\n    \"createRecord_8f85ff2c11d\",\n    \"createRecord_8f85ff2c11d_private\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42_private\",\n    \"branch_2c11d0fb42c\",\n    \"branch_2c11d0fb42c_private\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"branch_f2c11d0fb42\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"branch_2c11d0fb42c\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac_private\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"branch_59afaadc9ac\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"deleteRecords_c32807e97c5_private\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n    \"updateRecords_7ed2a172c32_private\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"updateRecords_7ed2a172c32\",\n  ],\n  \"end_private\" => [\n    \"end\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout > test get private scope Deps 1`] = `\nMap {\n  \"start_private\" => [],\n  \"getRecords_07e97c55832_private\" => [\n    \"start\",\n  ],\n  \"forEach_260a8f85ff2_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n  ],\n  \"createRecord_8f85ff2c11d_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n  ],\n  \"exclusiveSplit_ff2c11d0fb4_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n  ],\n  \"branch_f2c11d0fb42_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n  ],\n  \"branch_2c11d0fb42c_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"forEach_260a8f85ff2_private\",\n    \"createRecord_8f85ff2c11d\",\n    \"exclusiveSplit_ff2c11d0fb4\",\n    \"exclusiveSplit_ff2c11d0fb4_private\",\n    \"branch_f2c11d0fb42\",\n    \"branch_f2c11d0fb42\",\n  ],\n  \"exclusiveSplit_88dbf2c60ae_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n  ],\n  \"branch_8dbf2c60aee_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n  ],\n  \"exclusiveSplit_a59afaadc9a_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n  ],\n  \"branch_59afaadc9ac_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n  ],\n  \"branch_9afaadc9acd_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n  ],\n  \"deleteRecords_c32807e97c5_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee_private\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"exclusiveSplit_a59afaadc9a_private\",\n    \"branch_59afaadc9ac\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"branch_9afaadc9acd_private\",\n  ],\n  \"branch_dbf2c60aee4_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n  ],\n  \"updateRecords_7ed2a172c32_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae_private\",\n    \"branch_8dbf2c60aee\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"branch_dbf2c60aee4_private\",\n  ],\n  \"end_private\" => [\n    \"start\",\n    \"getRecords_07e97c55832\",\n    \"forEach_260a8f85ff2\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"exclusiveSplit_88dbf2c60ae\",\n    \"branch_8dbf2c60aee\",\n    \"exclusiveSplit_a59afaadc9a\",\n    \"branch_59afaadc9ac\",\n    \"branch_9afaadc9acd\",\n    \"deleteRecords_c32807e97c5\",\n    \"branch_dbf2c60aee4\",\n    \"updateRecords_7ed2a172c32\",\n  ],\n}\n`;\n\nexports[`Variable Fix Layout > test sort 1`] = `\n[\n  \"start_private\",\n  \"start\",\n  \"getRecords_07e97c55832\",\n  \"getRecords_07e97c55832_private\",\n  \"forEach_260a8f85ff2\",\n  \"forEach_260a8f85ff2_private\",\n  \"createRecord_8f85ff2c11d\",\n  \"createRecord_8f85ff2c11d_private\",\n  \"exclusiveSplit_ff2c11d0fb4\",\n  \"exclusiveSplit_ff2c11d0fb4_private\",\n  \"branch_f2c11d0fb42\",\n  \"branch_f2c11d0fb42_private\",\n  \"branch_2c11d0fb42c\",\n  \"branch_2c11d0fb42c_private\",\n  \"exclusiveSplit_88dbf2c60ae\",\n  \"exclusiveSplit_88dbf2c60ae_private\",\n  \"branch_8dbf2c60aee\",\n  \"branch_8dbf2c60aee_private\",\n  \"exclusiveSplit_a59afaadc9a\",\n  \"exclusiveSplit_a59afaadc9a_private\",\n  \"branch_59afaadc9ac\",\n  \"branch_59afaadc9ac_private\",\n  \"branch_9afaadc9acd\",\n  \"branch_9afaadc9acd_private\",\n  \"deleteRecords_c32807e97c5\",\n  \"deleteRecords_c32807e97c5_private\",\n  \"branch_dbf2c60aee4\",\n  \"branch_dbf2c60aee4_private\",\n  \"updateRecords_7ed2a172c32\",\n  \"updateRecords_7ed2a172c32_private\",\n  \"end\",\n  \"end_private\",\n  \"testScope\",\n  \"$blockIcon$exclusiveSplit_ff2c11d0fb4\",\n  \"$inlineBlocks$exclusiveSplit_ff2c11d0fb4\",\n  \"$blockOrderIcon$branch_f2c11d0fb42\",\n  \"$blockOrderIcon$branch_2c11d0fb42c\",\n  \"$blockIcon$exclusiveSplit_88dbf2c60ae\",\n  \"$inlineBlocks$exclusiveSplit_88dbf2c60ae\",\n  \"$blockOrderIcon$branch_8dbf2c60aee\",\n  \"$blockIcon$exclusiveSplit_a59afaadc9a\",\n  \"$inlineBlocks$exclusiveSplit_a59afaadc9a\",\n  \"$blockOrderIcon$branch_59afaadc9ac\",\n  \"$blockOrderIcon$branch_9afaadc9acd\",\n  \"$blockOrderIcon$branch_dbf2c60aee4\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-free-enable-global-scope.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Free Layout Enable Global Scope > test get Covers 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [\n    \"end_0\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_1_private\",\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test get Deps 1`] = `\nMap {\n  \"start_0\" => [\n    \"GlobalScope\",\n  ],\n  \"end_0\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n  ],\n  \"base_2\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_2\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_3\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"GlobalScope\",\n    \"start_0_private\",\n  ],\n  \"end_0\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n    \"end_0_private\",\n  ],\n  \"base_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1_private\",\n  ],\n  \"base_2\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"base_2_private\",\n  ],\n  \"loop_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_1_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2_private\",\n  ],\n  \"base_in_loop_3\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_3\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n    \"base_3_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [\n    \"start_0\",\n  ],\n  \"end_0_private\" => [\n    \"end_0\",\n  ],\n  \"base_1_private\" => [\n    \"base_1\",\n  ],\n  \"base_2_private\" => [\n    \"base_2\",\n  ],\n  \"loop_1_private\" => [\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n    \"loop_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"base_in_loop_1\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"base_in_loop_2\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_3_private\" => [\n    \"base_3\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [\n    \"GlobalScope\",\n  ],\n  \"end_0_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n  ],\n  \"base_2_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3_private\" => [\n    \"GlobalScope\",\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Enable Global Scope > test sort 1`] = `\n[\n  Symbol(GlobalScope),\n  \"testScope\",\n  \"start_0\",\n  \"end_0\",\n  \"base_1\",\n  \"base_2\",\n  \"loop_1\",\n  \"base_in_loop_1\",\n  \"base_in_loop_2\",\n  \"base_in_loop_3\",\n  \"base_3\",\n  \"start_0_private\",\n  \"end_0_private\",\n  \"base_1_private\",\n  \"base_2_private\",\n  \"loop_1_private\",\n  \"base_in_loop_1_private\",\n  \"base_in_loop_2_private\",\n  \"base_in_loop_3_private\",\n  \"base_3_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-free-is-node-children-private.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Free Layout Is Node Children Private > test get Covers 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_in_loop_3\" => [\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_3\" => [\n    \"end_0\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_1_private\",\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_in_loop_3\" => [\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_3\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test get Deps 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1\" => [\n    \"start_0\",\n  ],\n  \"base_2\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_2\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"start_0_private\",\n  ],\n  \"end_0\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n    \"end_0_private\",\n  ],\n  \"base_1\" => [\n    \"start_0\",\n    \"base_1_private\",\n  ],\n  \"base_2\" => [\n    \"start_0\",\n    \"base_1\",\n    \"base_2_private\",\n  ],\n  \"loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_1_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2_private\",\n  ],\n  \"base_in_loop_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n    \"base_3_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [\n    \"start_0\",\n  ],\n  \"end_0_private\" => [\n    \"end_0\",\n  ],\n  \"base_1_private\" => [\n    \"base_1\",\n  ],\n  \"base_2_private\" => [\n    \"base_2\",\n  ],\n  \"loop_1_private\" => [\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n    \"loop_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"base_in_loop_1\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"base_in_loop_2\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_3_private\" => [\n    \"base_3\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1_private\" => [\n    \"start_0\",\n  ],\n  \"base_2_private\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1_private\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout Is Node Children Private > test sort 1`] = `\n[\n  \"testScope\",\n  \"start_0\",\n  \"end_0\",\n  \"base_1\",\n  \"base_2\",\n  \"loop_1\",\n  \"base_in_loop_1\",\n  \"base_in_loop_2\",\n  \"base_in_loop_3\",\n  \"base_3\",\n  \"start_0_private\",\n  \"end_0_private\",\n  \"base_1_private\",\n  \"base_2_private\",\n  \"loop_1_private\",\n  \"base_in_loop_1_private\",\n  \"base_in_loop_2_private\",\n  \"base_in_loop_3_private\",\n  \"base_3_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-free-layout-transform-empty.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Free Layout transform empty > test get Covers 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test get Deps 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [],\n  \"base_1\" => [],\n  \"base_2\" => [],\n  \"loop_1\" => [],\n  \"base_in_loop_1\" => [],\n  \"base_in_loop_2\" => [],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [],\n  \"base_1_private\" => [],\n  \"base_2_private\" => [],\n  \"loop_1_private\" => [],\n  \"base_in_loop_1_private\" => [],\n  \"base_in_loop_2_private\" => [],\n  \"base_in_loop_3_private\" => [],\n  \"base_3_private\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [],\n  \"base_1_private\" => [],\n  \"base_2_private\" => [],\n  \"loop_1_private\" => [],\n  \"base_in_loop_1_private\" => [],\n  \"base_in_loop_2_private\" => [],\n  \"base_in_loop_3_private\" => [],\n  \"base_3_private\" => [],\n}\n`;\n\nexports[`Variable Free Layout transform empty > test sort 1`] = `\n[\n  \"testScope\",\n  \"start_0\",\n  \"end_0\",\n  \"base_1\",\n  \"base_2\",\n  \"loop_1\",\n  \"base_in_loop_1\",\n  \"base_in_loop_2\",\n  \"base_in_loop_3\",\n  \"base_3\",\n  \"start_0_private\",\n  \"end_0_private\",\n  \"base_1_private\",\n  \"base_2_private\",\n  \"loop_1_private\",\n  \"base_in_loop_1_private\",\n  \"base_in_loop_2_private\",\n  \"base_in_loop_3_private\",\n  \"base_3_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/__snapshots__/variable-free-layout.test.ts.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`Variable Free Layout > test get Covers 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"end_0\",\n    \"loop_1\",\n    \"base_3\",\n    \"base_in_loop_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_3\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"end_0\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [\n    \"end_0\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test get Covers After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"base_1\",\n    \"base_1_private\",\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"end_0\" => [],\n  \"base_1\" => [\n    \"base_2\",\n    \"base_2_private\",\n    \"end_0\",\n    \"end_0_private\",\n    \"loop_1\",\n    \"loop_1_private\",\n    \"base_3\",\n    \"base_3_private\",\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_2\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"loop_1\" => [\n    \"base_3\",\n    \"base_3_private\",\n    \"end_0\",\n    \"end_0_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_in_loop_3\" => [],\n  \"base_3\" => [\n    \"end_0\",\n    \"end_0_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test get Deps 1`] = `\nMap {\n  \"start_0\" => [],\n  \"end_0\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1\" => [\n    \"start_0\",\n  ],\n  \"base_2\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_2\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test get Deps After Init Private 1`] = `\nMap {\n  \"start_0\" => [\n    \"start_0_private\",\n  ],\n  \"end_0\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n    \"end_0_private\",\n  ],\n  \"base_1\" => [\n    \"start_0\",\n    \"base_1_private\",\n  ],\n  \"base_2\" => [\n    \"start_0\",\n    \"base_1\",\n    \"base_2_private\",\n  ],\n  \"loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_1\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_1_private\",\n  ],\n  \"base_in_loop_2\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2_private\",\n  ],\n  \"base_in_loop_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n    \"base_in_loop_3_private\",\n  ],\n  \"base_3\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n    \"base_3_private\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test get private scope Covers 1`] = `\nMap {\n  \"start_0_private\" => [\n    \"start_0\",\n  ],\n  \"end_0_private\" => [\n    \"end_0\",\n  ],\n  \"base_1_private\" => [\n    \"base_1\",\n  ],\n  \"base_2_private\" => [\n    \"base_2\",\n  ],\n  \"loop_1_private\" => [\n    \"base_in_loop_1\",\n    \"base_in_loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_2_private\",\n    \"base_in_loop_3\",\n    \"base_in_loop_3_private\",\n    \"loop_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"base_in_loop_1\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"base_in_loop_2\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"base_in_loop_3\",\n  ],\n  \"base_3_private\" => [\n    \"base_3\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test get private scope Deps 1`] = `\nMap {\n  \"start_0_private\" => [],\n  \"end_0_private\" => [\n    \"start_0\",\n    \"loop_1\",\n    \"base_1\",\n    \"base_3\",\n    \"base_2\",\n  ],\n  \"base_1_private\" => [\n    \"start_0\",\n  ],\n  \"base_2_private\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"loop_1_private\" => [\n    \"start_0\",\n    \"base_1\",\n  ],\n  \"base_in_loop_1_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_2_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n  ],\n  \"base_in_loop_3_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1_private\",\n    \"base_in_loop_2\",\n    \"base_in_loop_1\",\n  ],\n  \"base_3_private\" => [\n    \"start_0\",\n    \"base_1\",\n    \"loop_1\",\n  ],\n}\n`;\n\nexports[`Variable Free Layout > test sort 1`] = `\n[\n  \"testScope\",\n  \"start_0\",\n  \"end_0\",\n  \"base_1\",\n  \"base_2\",\n  \"loop_1\",\n  \"base_in_loop_1\",\n  \"base_in_loop_2\",\n  \"base_in_loop_3\",\n  \"base_3\",\n  \"start_0_private\",\n  \"end_0_private\",\n  \"base_1_private\",\n  \"base_2_private\",\n  \"loop_1_private\",\n  \"base_in_loop_1_private\",\n  \"base_in_loop_2_private\",\n  \"base_in_loop_3_private\",\n  \"base_3_private\",\n]\n`;\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-enable-global-scope.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\nimport { fixLayout1 } from '../__mocks__/fixed-layout-specs';\n\nrunFixedLayoutTest('Variable Fix Layout Enable Global Scope', fixLayout1, {\n  enableGlobalScope: true,\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-layout-filter-start-end.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeScope } from '../src/types';\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\nimport { fixLayout1 } from '../__mocks__/fixed-layout-specs';\n\nconst filterStart = (_scope: FlowNodeScope) => !['start'].includes(_scope.meta?.node?.id || '');\n\nconst filterEnd = (_scope: FlowNodeScope) => !['end'].includes(_scope.meta?.node?.id || '');\n\nrunFixedLayoutTest('Variable Fix Layout Filter Start End', fixLayout1, {\n  transformCovers: (scopes) => scopes.filter(filterEnd),\n  transformDeps: (scopes) => scopes.filter(filterStart),\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-layout-group.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\n\nrunFixedLayoutTest(\n  'Variable Fix Layout Group',\n  {\n    nodes: [\n      {\n        id: 'start_0',\n        type: 'start',\n        meta: {\n          isStart: true,\n        },\n        blocks: [],\n      },\n      {\n        id: '$group_test$',\n        type: 'block',\n        blocks: [\n          {\n            id: 'node_0',\n            type: 'noop',\n            blocks: [],\n          },\n          {\n            id: 'node_1',\n            type: 'noop',\n            blocks: [],\n          },\n        ],\n      },\n      {\n        id: 'end_0',\n        type: 'end',\n        blocks: [],\n      },\n    ],\n  },\n  {}\n);\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-layout-no-config.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\nimport { fixLayout1 } from '../__mocks__/fixed-layout-specs';\n\nrunFixedLayoutTest('Variable Fix Layout Without Config', fixLayout1, {});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-layout-transform-empty.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from 'vitest';\n\nimport { ScopeChainTransformService } from '../src';\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\nimport { freeLayout1 } from '../__mocks__/free-layout-specs';\n\nrunFixedLayoutTest('Variable Fixed Layout transform empty', freeLayout1, {\n  onInit(container) {\n    const transformService = container.get(ScopeChainTransformService);\n\n    transformService.registerTransformer('MOCK', {\n      transformCovers: (scopes) => scopes,\n      transformDeps: (scopes) => scopes,\n    });\n\n    // again transformer, prevent duplicated transformerId\n    transformService.registerTransformer('MOCK', {\n      transformCovers: () => [],\n      transformDeps: () => [],\n    });\n    transformService.registerTransformer('MOCK', {\n      transformCovers: () => [],\n      transformDeps: () => [],\n    });\n  },\n  runExtraTest: (container) => {\n    test('check has transformer', () => {\n      const transformService = container.get(ScopeChainTransformService);\n      expect(transformService.hasTransformer('MOCK')).to.be.true;\n      expect(transformService.hasTransformer('VARIABLE_LAYOUT_CONFIG')).to.be.false;\n      expect(transformService.hasTransformer('NOT_EXIST')).to.be.false;\n    });\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-fix-layout.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFixedLayoutTest } from '../__mocks__/run-fixed-layout-test';\nimport { fixLayout1 } from '../__mocks__/fixed-layout-specs';\n\nrunFixedLayoutTest('Variable Fix Layout', fixLayout1, {\n  isNodeChildrenPrivate: (node) =>\n    // 只有循环是 private\n    node.flowNodeType === 'loop',\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-free-enable-global-scope.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';\nimport { freeLayout1 } from '../__mocks__/free-layout-specs';\n\nrunFreeLayoutTest('Variable Free Layout Enable Global Scope', freeLayout1, {\n  enableGlobalScope: true,\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-free-is-node-children-private.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';\nimport { freeLayout1 } from '../__mocks__/free-layout-specs';\n\nrunFreeLayoutTest('Variable Free Layout Is Node Children Private', freeLayout1, {\n  isNodeChildrenPrivate(node) {\n    return false;\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-free-layout-transform-empty.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { test, expect } from 'vitest';\n\nimport { ScopeChainTransformService } from '../src';\nimport { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';\nimport { freeLayout1 } from '../__mocks__/free-layout-specs';\n\nrunFreeLayoutTest('Variable Free Layout transform empty', freeLayout1, {\n  // 模拟清空作用域\n  transformCovers: () => [],\n  transformDeps: () => [],\n  runExtraTest: (container) => {\n    test('check has transformer', () => {\n      const transformService = container.get(ScopeChainTransformService);\n      expect(transformService.hasTransformer('VARIABLE_LAYOUT_CONFIG')).to.be.true;\n    });\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/__tests__/variable-free-layout.test.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { runFreeLayoutTest } from '../__mocks__/run-free-layout-test';\nimport { freeLayout1 } from '../__mocks__/free-layout-specs';\n\nrunFreeLayoutTest('Variable Free Layout', freeLayout1, {});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/eslint.config.js",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst { defineFlatConfig } = require('@flowgram.ai/eslint-config');\n\nmodule.exports = defineFlatConfig({\n  preset: 'web',\n  packageRoot: __dirname,\n  ignorePatterns: ['**/tests__'],\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/package.json",
    "content": "{\n  \"name\": \"@flowgram.ai/variable-layout\",\n  \"version\": \"0.1.8\",\n  \"homepage\": \"https://flowgram.ai/\",\n  \"repository\": \"https://github.com/bytedance/flowgram.ai\",\n  \"license\": \"MIT\",\n  \"exports\": {\n    \"types\": \"./dist/index.d.ts\",\n    \"import\": \"./dist/esm/index.js\",\n    \"require\": \"./dist/index.js\"\n  },\n  \"main\": \"./dist/index.js\",\n  \"module\": \"./dist/esm/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"scripts\": {\n    \"build\": \"npm run build:fast -- --dts-resolve\",\n    \"build:fast\": \"tsup src/index.ts --format cjs,esm --sourcemap --legacy-output\",\n    \"build:watch\": \"npm run build:fast -- --dts-resolve\",\n    \"clean\": \"rimraf dist\",\n    \"test\": \"vitest run\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"ts-check\": \"tsc --noEmit\",\n    \"watch\": \"npm run build:fast -- --dts-resolve --watch --ignore-watch dist\"\n  },\n  \"dependencies\": {\n    \"@flowgram.ai/core\": \"workspace:*\",\n    \"@flowgram.ai/document\": \"workspace:*\",\n    \"@flowgram.ai/free-layout-core\": \"workspace:*\",\n    \"@flowgram.ai/variable-core\": \"workspace:*\",\n    \"inversify\": \"^6.0.1\",\n    \"reflect-metadata\": \"~0.2.2\"\n  },\n  \"devDependencies\": {\n    \"@flowgram.ai/eslint-config\": \"workspace:*\",\n    \"@flowgram.ai/ts-config\": \"workspace:*\",\n    \"@vitest/coverage-v8\": \"^3.2.4\",\n    \"eslint\": \"^9.0.0\",\n    \"tsup\": \"^8.0.1\",\n    \"typescript\": \"^5.8.3\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"publishConfig\": {\n    \"access\": \"public\",\n    \"registry\": \"https://registry.npmjs.org/\"\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/chains/fixed-layout-scope-chain.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, optional } from 'inversify';\nimport { Scope, ScopeChain } from '@flowgram.ai/variable-core';\nimport { FlowDocument, type FlowVirtualTree } from '@flowgram.ai/document';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { VariableChainConfig } from '../variable-chain-config';\nimport { FlowNodeScope, FlowNodeScopeTypeEnum, ScopeChainNode } from '../types';\nimport { ScopeChainTransformService } from '../services/scope-chain-transform-service';\nimport { GlobalScope } from '../scopes/global-scope';\nimport { FlowNodeVariableData } from '../flow-node-variable-data';\n\n/**\n * Scope chain implementation based on `FlowVirtualTree`.\n */\nexport class FixedLayoutScopeChain extends ScopeChain {\n  // By adding { id: string }, custom virtual nodes can be flexibly added\n  tree: FlowVirtualTree<ScopeChainNode> | undefined;\n\n  @inject(ScopeChainTransformService)\n  protected transformService: ScopeChainTransformService;\n\n  constructor(\n    @inject(FlowDocument)\n    protected flowDocument: FlowDocument,\n    @optional()\n    @inject(VariableChainConfig)\n    protected configs?: VariableChainConfig\n  ) {\n    super();\n\n    // Bind the tree in flowDocument\n    this.bindTree(flowDocument.originTree);\n\n    // When originTree changes, trigger changes in dependencies\n    this.toDispose.push(\n      // REFRACTOR: onTreeChange trigger timing needs to be refined\n      flowDocument.originTree.onTreeChange(() => {\n        this.refreshAllChange();\n      })\n    );\n  }\n\n  /**\n   * Binds the scope chain to a `FlowVirtualTree`.\n   * @param tree The `FlowVirtualTree` to bind to.\n   */\n  bindTree(tree: FlowVirtualTree<ScopeChainNode>): void {\n    this.tree = tree;\n  }\n\n  /**\n   * Gets the dependency scopes for a given scope.\n   * @param scope The scope to get dependencies for.\n   * @returns An array of dependency scopes.\n   */\n  getDeps(scope: FlowNodeScope): FlowNodeScope[] {\n    if (!this.tree) {\n      return this.transformService.transformDeps([], { scope });\n    }\n\n    const node = scope.meta.node;\n    if (!node) {\n      return this.transformService.transformDeps([], { scope });\n    }\n\n    const deps: FlowNodeScope[] = [];\n\n    let curr: ScopeChainNode | undefined = node;\n\n    while (curr) {\n      const { parent, pre } = this.tree.getInfo(curr);\n      const currData = this.getVariableData(curr);\n\n      // Contains child nodes and is not a private scope\n\n      if (curr === node) {\n        // public can depend on private\n        if (scope.meta.type === FlowNodeScopeTypeEnum.public && currData?.private) {\n          deps.unshift(currData.private);\n        }\n      } else if (this.hasChildren(curr) && !this.isNodeChildrenPrivate(curr)) {\n        // For nodes with child elements, include the child elements in the dependency scope\n        deps.unshift(\n          ...this.getAllSortedChildScope(curr, {\n            ignoreNodeChildrenPrivate: true,\n          })\n        );\n      }\n\n      // The public of the node can be accessed\n      if (currData && curr !== node) {\n        deps.unshift(currData.public);\n      }\n\n      // Process the previous node\n      if (pre) {\n        curr = pre;\n        continue;\n      }\n\n      // Process the parent node\n      if (parent) {\n        let currParent: ScopeChainNode | undefined = parent;\n        let currParentPre: ScopeChainNode | undefined = this.tree.getPre(currParent);\n\n        while (currParent) {\n          // Both private and public of the parent node can be accessed by child nodes\n          const currParentData = this.getVariableData(currParent);\n          if (currParentData) {\n            deps.unshift(...currParentData.allScopes);\n          }\n\n          // If the current parent has a pre node, stop searching upwards\n          if (currParentPre) {\n            break;\n          }\n\n          currParent = this.tree.getParent(currParent);\n          currParentPre = currParent ? this.tree.getPre(currParent) : undefined;\n        }\n        curr = currParentPre;\n        continue;\n      }\n\n      // If there is no next and no parent, end the loop directly\n      curr = undefined;\n    }\n\n    // If scope is GlobalScope, add globalScope to deps\n    const globalScope = this.variableEngine.getScopeById(GlobalScope.ID);\n    if (globalScope) {\n      deps.unshift(globalScope);\n    }\n\n    return this.transformService.transformDeps(deps, { scope });\n  }\n\n  /**\n   * Gets the covering scopes for a given scope.\n   * @param scope The scope to get covering scopes for.\n   * @returns An array of covering scopes.\n   */\n  getCovers(scope: FlowNodeScope): FlowNodeScope[] {\n    if (!this.tree) {\n      return this.transformService.transformCovers([], { scope });\n    }\n\n    // If scope is GlobalScope, return all scopes except GlobalScope\n    if (GlobalScope.is(scope)) {\n      const scopes = this.variableEngine\n        .getAllScopes({ sort: true })\n        .filter((_scope) => !GlobalScope.is(_scope));\n\n      return this.transformService.transformCovers(scopes, { scope });\n    }\n\n    const node = scope.meta.node;\n    if (!node) {\n      return this.transformService.transformCovers([], { scope });\n    }\n\n    const covers: FlowNodeScope[] = [];\n\n    // If it is a private scope, only child nodes can access it\n    if (scope.meta.type === FlowNodeScopeTypeEnum.private) {\n      covers.push(\n        ...this.getAllSortedChildScope(node, {\n          addNodePrivateScope: true,\n        }).filter((_scope) => _scope !== scope)\n      );\n      return this.transformService.transformCovers(covers, { scope });\n    }\n\n    let curr: ScopeChainNode | undefined = node;\n\n    while (curr) {\n      const { next, parent } = this.tree.getInfo(curr);\n      const currData = this.getVariableData(curr);\n\n      // For nodes with child elements, include the child elements in the covering scope\n      if (curr !== node) {\n        if (this.hasChildren(curr)) {\n          covers.push(\n            ...this.getAllSortedChildScope(curr, {\n              addNodePrivateScope: true,\n            })\n          );\n        } else if (currData) {\n          covers.push(...currData.allScopes);\n        }\n      }\n\n      // Process the next node\n      if (next) {\n        curr = next;\n        continue;\n      }\n\n      if (parent) {\n        let currParent: ScopeChainNode | undefined = parent;\n        let currParentNext: ScopeChainNode | undefined = this.tree.getNext(currParent);\n\n        while (currParent) {\n          // Private scopes cannot be accessed by subsequent nodes\n          if (this.isNodeChildrenPrivate(currParent)) {\n            return this.transformService.transformCovers(covers, { scope });\n          }\n\n          // If the current parent has a next node, stop searching upwards\n          if (currParentNext) {\n            break;\n          }\n\n          currParent = this.tree.getParent(currParent);\n          currParentNext = currParent ? this.tree.getNext(currParent) : undefined;\n        }\n        if (!currParentNext && currParent) {\n          break;\n        }\n\n        curr = currParentNext;\n        continue;\n      }\n\n      // next 和 parent 都没有，直接结束循环\n      curr = undefined;\n    }\n\n    return this.transformService.transformCovers(covers, { scope });\n  }\n\n  /**\n   * Sorts all scopes in the scope chain.\n   * @returns A sorted array of all scopes.\n   */\n  sortAll(): Scope[] {\n    const startNode = this.flowDocument.getAllNodes().find((_node) => _node.isStart);\n    if (!startNode) {\n      return [];\n    }\n\n    const startVariableData = startNode.getData(FlowNodeVariableData);\n    const startPublicScope = startVariableData.public;\n    const deps = this.getDeps(startPublicScope);\n\n    const covers = this.getCovers(startPublicScope).filter(\n      (_scope) => !deps.includes(_scope) && _scope !== startPublicScope\n    );\n\n    return [...deps, startPublicScope, ...covers];\n  }\n\n  /**\n   * Gets the `FlowNodeVariableData` for a given `ScopeChainNode`.\n   * @param node The `ScopeChainNode` to get data for.\n   * @returns The `FlowNodeVariableData` or `undefined` if not found.\n   */\n  private getVariableData(node: ScopeChainNode): FlowNodeVariableData | undefined {\n    if (node.flowNodeType === 'virtualNode') {\n      return;\n    }\n    // TODO Nodes containing $ do not register variableData\n    if (node.id.startsWith('$')) {\n      return;\n    }\n\n    return (node as FlowNodeEntity).getData(FlowNodeVariableData);\n  }\n\n  /**\n   * Checks if the children of a node are private.\n   * @param node The node to check.\n   * @returns `true` if the children are private, `false` otherwise.\n   */\n  private isNodeChildrenPrivate(node?: ScopeChainNode): boolean {\n    if (this.configs?.isNodeChildrenPrivate) {\n      return node ? this.configs?.isNodeChildrenPrivate(node) : false;\n    }\n\n    const isSystemNode = node?.id.startsWith('$');\n    // Fallback: all nodes with children (node id does not start with $) are private scopes\n    return !isSystemNode && this.hasChildren(node);\n  }\n\n  /**\n   * Checks if a node has children.\n   * @param node The node to check.\n   * @returns `true` if the node has children, `false` otherwise.\n   */\n  private hasChildren(node?: ScopeChainNode): boolean {\n    return Boolean(this.tree && node && this.tree.getChildren(node).length > 0);\n  }\n\n  /**\n   * Gets all sorted child scopes of a node.\n   * @param node The node to get child scopes for.\n   * @param options Options for getting child scopes.\n   * @returns An array of sorted child scopes.\n   */\n  private getAllSortedChildScope(\n    node: ScopeChainNode,\n    {\n      ignoreNodeChildrenPrivate,\n      addNodePrivateScope,\n    }: { ignoreNodeChildrenPrivate?: boolean; addNodePrivateScope?: boolean } = {}\n  ): FlowNodeScope[] {\n    const scopes: FlowNodeScope[] = [];\n\n    const variableData = this.getVariableData(node);\n\n    if (variableData) {\n      scopes.push(variableData.public);\n    }\n\n    // For private scopes, the variables of child nodes are not exposed externally\n    // (If the parent node has public variables, they are exposed externally)\n    if (ignoreNodeChildrenPrivate && this.isNodeChildrenPrivate(node)) {\n      return scopes;\n    }\n\n    if (addNodePrivateScope && variableData?.private) {\n      scopes.push(variableData.private);\n    }\n\n    const children = this.tree?.getChildren(node) || [];\n    scopes.push(\n      ...children\n        .map((child) =>\n          this.getAllSortedChildScope(child, { ignoreNodeChildrenPrivate, addNodePrivateScope })\n        )\n        .flat()\n    );\n\n    return scopes;\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/chains/free-layout-scope-chain.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, optional, postConstruct } from 'inversify';\nimport { Scope, ScopeChain } from '@flowgram.ai/variable-core';\nimport { WorkflowNodeLinesData, WorkflowNodeMeta } from '@flowgram.ai/free-layout-core';\nimport {\n  FlowNodeEntity,\n  FlowDocument,\n  FlowVirtualTree,\n  FlowNodeBaseType,\n} from '@flowgram.ai/document';\nimport { EntityManager } from '@flowgram.ai/core';\n\nimport { VariableChainConfig } from '../variable-chain-config';\nimport { FlowNodeScope, FlowNodeScopeTypeEnum } from '../types';\nimport { ScopeChainTransformService } from '../services/scope-chain-transform-service';\nimport { GlobalScope } from '../scopes/global-scope';\nimport { FlowNodeVariableData } from '../flow-node-variable-data';\n\n/**\n * Scope chain implementation for free layout.\n */\nexport class FreeLayoutScopeChain extends ScopeChain {\n  @inject(EntityManager) entityManager: EntityManager;\n\n  @inject(FlowDocument)\n  protected flowDocument: FlowDocument;\n\n  @optional()\n  @inject(VariableChainConfig)\n  protected configs?: VariableChainConfig;\n\n  @inject(ScopeChainTransformService)\n  protected transformService: ScopeChainTransformService;\n\n  /**\n   * The virtual tree of the flow document.\n   */\n  get tree(): FlowVirtualTree<FlowNodeEntity> {\n    return this.flowDocument.originTree;\n  }\n\n  @postConstruct()\n  onInit() {\n    this.toDispose.pushAll([\n      // When the line changes, the scope chain will be updated\n      this.entityManager.onEntityDataChange(({ entityDataType }) => {\n        if (entityDataType === WorkflowNodeLinesData.type) {\n          this.refreshAllChange();\n        }\n      }),\n      // Refresh the scope when the tree changes\n      this.tree.onTreeChange(() => {\n        this.refreshAllChange();\n      }),\n    ]);\n  }\n\n  /**\n   * Gets all input layer nodes for a given node in the same layer, sorted by distance.\n   * @param node The node to get input layer nodes for.\n   * @returns An array of input layer nodes.\n   */\n  protected getAllInputLayerNodes(node: FlowNodeEntity): FlowNodeEntity[] {\n    const currParent = this.getNodeParent(node);\n\n    const result = new Set<FlowNodeEntity>();\n\n    // add by bfs\n    const queue: FlowNodeEntity[] = [node];\n\n    while (queue.length) {\n      const curr = queue.shift()!;\n\n      (curr.lines?.inputNodes || []).forEach((inputNode) => {\n        if (this.getNodeParent(inputNode) === currParent) {\n          if (result.has(inputNode)) {\n            return;\n          }\n          queue.push(inputNode);\n          result.add(inputNode);\n        }\n      });\n    }\n\n    return Array.from(result).reverse();\n  }\n\n  /**\n   * Gets all output layer nodes for a given node in the same layer.\n   * @param curr The node to get output layer nodes for.\n   * @returns An array of output layer nodes.\n   */\n  protected getAllOutputLayerNodes(curr: FlowNodeEntity): FlowNodeEntity[] {\n    const currParent = this.getNodeParent(curr);\n\n    return (curr.lines?.allOutputNodes || []).filter(\n      (_node) => this.getNodeParent(_node) === currParent\n    );\n  }\n\n  /**\n   * Gets the dependency scopes for a given scope.\n   * @param scope The scope to get dependencies for.\n   * @returns An array of dependency scopes.\n   */\n  getDeps(scope: FlowNodeScope): FlowNodeScope[] {\n    const { node } = scope.meta || {};\n    if (!node) {\n      return this.transformService.transformDeps([], { scope });\n    }\n\n    const deps: FlowNodeScope[] = [];\n\n    // 1. find dep nodes\n    let curr: FlowNodeEntity | undefined = node;\n\n    while (curr) {\n      // 2. private scope of parent node can be access\n      const currVarData: FlowNodeVariableData = curr.getData(FlowNodeVariableData);\n      if (currVarData?.private && scope !== currVarData.private) {\n        deps.unshift(currVarData.private);\n      }\n\n      // 3. all public scopes of inputNodes\n      const allInputNodes: FlowNodeEntity[] = this.getAllInputLayerNodes(curr);\n      deps.unshift(\n        ...allInputNodes\n          .map((_node) => [\n            _node.getData(FlowNodeVariableData).public,\n            // 4. all public children of inputNodes\n            ...this.getAllPublicChildScopes(_node),\n          ])\n          .flat()\n          .filter(Boolean)\n      );\n\n      curr = this.getNodeParent(curr);\n    }\n\n    // If scope is GlobalScope, add globalScope to deps\n    const globalScope = this.variableEngine.getScopeById(GlobalScope.ID);\n    if (globalScope) {\n      deps.unshift(globalScope);\n    }\n\n    const uniqDeps = Array.from(new Set(deps));\n    return this.transformService.transformDeps(uniqDeps, { scope });\n  }\n\n  /**\n   * Gets the covering scopes for a given scope.\n   * @param scope The scope to get covering scopes for.\n   * @returns An array of covering scopes.\n   */\n  getCovers(scope: FlowNodeScope): FlowNodeScope[] {\n    // If scope is GlobalScope, return all scopes except GlobalScope\n    if (GlobalScope.is(scope)) {\n      const scopes = this.variableEngine\n        .getAllScopes({ sort: true })\n        .filter((_scope) => !GlobalScope.is(_scope));\n\n      return this.transformService.transformCovers(scopes, { scope });\n    }\n\n    const { node } = scope.meta || {};\n    if (!node) {\n      return this.transformService.transformCovers([], { scope });\n    }\n\n    const isPrivate = scope.meta.type === FlowNodeScopeTypeEnum.private;\n\n    // 1. BFS to find all covered nodes\n    const queue: FlowNodeEntity[] = [];\n\n    if (isPrivate) {\n      // private can only cover its child nodes\n      queue.push(...this.getNodeChildren(node));\n    } else {\n      // Otherwise, cover all nodes of its output lines\n      queue.push(...(this.getAllOutputLayerNodes(node) || []));\n\n      // get all parents\n      let parent = this.getNodeParent(node);\n\n      while (parent) {\n        // if childNodes of parent is private to next nodes, break\n        if (this.isNodeChildrenPrivate(parent)) {\n          break;\n        }\n\n        queue.push(...this.getAllOutputLayerNodes(parent));\n\n        parent = this.getNodeParent(parent);\n      }\n    }\n\n    // 2. Get the public and private scopes of all covered nodes\n    const scopes: FlowNodeScope[] = [];\n\n    while (queue.length) {\n      const _node = queue.shift()!;\n      const variableData: FlowNodeVariableData = _node.getData(FlowNodeVariableData);\n      scopes.push(...variableData.allScopes);\n      const children = _node && this.getNodeChildren(_node);\n\n      if (children?.length) {\n        queue.push(...children);\n      }\n    }\n\n    // 3. If the current scope is private, the public scope of the current node can also be covered\n    const currentVariableData: FlowNodeVariableData = node.getData(FlowNodeVariableData);\n    if (isPrivate && currentVariableData.public) {\n      scopes.push(currentVariableData.public);\n    }\n\n    const uniqScopes = Array.from(new Set(scopes));\n\n    return this.transformService.transformCovers(uniqScopes, { scope });\n  }\n\n  /**\n   * Gets the children of a node.\n   * @param node The node to get children for.\n   * @returns An array of child nodes.\n   */\n  getNodeChildren(node: FlowNodeEntity): FlowNodeEntity[] {\n    if (this.configs?.getNodeChildren) {\n      return this.configs.getNodeChildren?.(node);\n    }\n    const nodeMeta = node.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(node);\n\n    if (subCanvas) {\n      // The sub-canvas itself does not have children\n      if (subCanvas.isCanvas) {\n        return [];\n      } else {\n        return subCanvas.canvasNode.collapsedChildren;\n      }\n    }\n\n    // In some scenarios, the parent-child relationship is expressed through connections, so it needs to be configured at the upper level\n    return this.tree.getChildren(node);\n  }\n\n  /**\n   * Get All children of nodes\n   * @param node\n   * @returns\n   */\n  getAllPublicChildScopes(node: FlowNodeEntity): Scope[] {\n    if (this.isNodeChildrenPrivate(node)) {\n      return [];\n    }\n\n    return this.getNodeChildren(node)\n      .map((_node) => [\n        _node.getData(FlowNodeVariableData).public,\n        ...this.getAllPublicChildScopes(_node),\n      ])\n      .flat();\n  }\n\n  /**\n   * Gets the parent of a node.\n   * @param node The node to get the parent for.\n   * @returns The parent node or `undefined` if not found.\n   */\n  getNodeParent(node: FlowNodeEntity): FlowNodeEntity | undefined {\n    // In some scenarios, the parent-child relationship is expressed through connections, so it needs to be configured at the upper level\n    if (this.configs?.getNodeParent) {\n      return this.configs.getNodeParent(node);\n    }\n    let parent = node.document.originTree.getParent(node);\n\n    // If currentParent is Group, get the parent of parent\n    while (parent?.flowNodeType === FlowNodeBaseType.GROUP) {\n      parent = parent.parent;\n    }\n\n    if (!parent) {\n      return parent;\n    }\n\n    const nodeMeta = parent.getNodeMeta<WorkflowNodeMeta>();\n    const subCanvas = nodeMeta.subCanvas?.(parent);\n    if (subCanvas?.isCanvas) {\n      // Get real parent node by subCanvas Configuration\n      return subCanvas.parentNode;\n    }\n\n    return parent;\n  }\n\n  /**\n   * Checks if the children of a node are private and cannot be accessed by subsequent nodes.\n   * @param node The node to check.\n   * @returns `true` if the children are private, `false` otherwise.\n   */\n  protected isNodeChildrenPrivate(node?: FlowNodeEntity): boolean {\n    if (this.configs?.isNodeChildrenPrivate) {\n      return node ? this.configs?.isNodeChildrenPrivate(node) : false;\n    }\n\n    const isSystemNode = node?.id.startsWith('$');\n\n    // Except system node and group node, everything else is private\n    return !isSystemNode && node?.flowNodeType !== FlowNodeBaseType.GROUP;\n  }\n\n  /**\n   * Sorts all scopes in the scope chain.\n   * @returns An empty array, as this method is not implemented.\n   */\n  sortAll(): Scope[] {\n    // Not implemented yet\n    console.warn('FreeLayoutScopeChain.sortAll is not implemented');\n    return [];\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/flow-node-variable-data.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { BaseVariableField, VariableEngine } from '@flowgram.ai/variable-core';\nimport { type ASTNode, ASTNodeJSON } from '@flowgram.ai/variable-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\nimport { EntityData } from '@flowgram.ai/core';\n\nimport { FlowNodeScope, FlowNodeScopeMeta, FlowNodeScopeTypeEnum } from './types';\n\ninterface Options {\n  variableEngine: VariableEngine;\n}\n\n/**\n * Manages variable data for a flow node, including public and private scopes.\n */\nexport class FlowNodeVariableData extends EntityData {\n  static type: string = 'FlowNodeVariableData';\n\n  declare entity: FlowNodeEntity;\n\n  readonly variableEngine: VariableEngine;\n\n  /**\n   * Private variables can be accessed by public ones, but not the other way around.\n   */\n  protected _private?: FlowNodeScope;\n\n  protected _public: FlowNodeScope;\n\n  /**\n   * The private scope of the node.\n   */\n  get private() {\n    return this._private;\n  }\n\n  /**\n   * The public scope of the node.\n   */\n  get public() {\n    return this._public;\n  }\n\n  /**\n   * Sets a variable in the public AST (Abstract Syntax Tree) with the given key and JSON value.\n   *\n   * @param key - The key under which the variable will be stored.\n   * @param json - The JSON value to store.\n   * @returns The updated AST node.\n   */\n  public setVar(key: string, json: ASTNodeJSON): ASTNode;\n\n  /**\n   * Sets a variable in the public AST (Abstract Syntax Tree) with the default key 'outputs'.\n   *\n   * @param json - The JSON value to store.\n   * @returns The updated AST node.\n   */\n  public setVar(json: ASTNodeJSON): ASTNode;\n\n  public setVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {\n    if (typeof arg1 === 'string' && arg2 !== undefined) {\n      return this.public.ast.set(arg1, arg2);\n    }\n\n    if (typeof arg1 === 'object' && arg2 === undefined) {\n      return this.public.ast.set('outputs', arg1);\n    }\n\n    throw new Error('Invalid arguments');\n  }\n\n  /**\n   * Retrieves a variable from the public AST (Abstract Syntax Tree) by key.\n   *\n   * @param key - The key of the variable to retrieve. Defaults to 'outputs'.\n   * @returns The value of the variable, or undefined if not found.\n   */\n  public getVar(key: string = 'outputs') {\n    return this.public.ast.get(key);\n  }\n\n  /**\n   * Clears a variable from the public AST (Abstract Syntax Tree) by key.\n   *\n   * @param key - The key of the variable to clear. Defaults to 'outputs'.\n   * @returns The updated AST node.\n   */\n  public clearVar(key: string = 'outputs') {\n    return this.public.ast.remove(key);\n  }\n\n  /**\n   * Sets a variable in the private AST (Abstract Syntax Tree) with the given key and JSON value.\n   *\n   * @param key - The key under which the variable will be stored.\n   * @param json - The JSON value to store.\n   * @returns The updated AST node.\n   */\n  public setPrivateVar(key: string, json: ASTNodeJSON): ASTNode;\n\n  /**\n   * Sets a variable in the private AST (Abstract Syntax Tree) with the default key 'outputs'.\n   *\n   * @param json - The JSON value to store.\n   * @returns The updated AST node.\n   */\n  public setPrivateVar(json: ASTNodeJSON): ASTNode;\n\n  public setPrivateVar(arg1: string | ASTNodeJSON, arg2?: ASTNodeJSON): ASTNode {\n    if (typeof arg1 === 'string' && arg2 !== undefined) {\n      return this.initPrivate().ast.set(arg1, arg2);\n    }\n\n    if (typeof arg1 === 'object' && arg2 === undefined) {\n      return this.initPrivate().ast.set('outputs', arg1);\n    }\n\n    throw new Error('Invalid arguments');\n  }\n\n  /**\n   * Retrieves a variable from the private AST (Abstract Syntax Tree) by key.\n   *\n   * @param key - The key of the variable to retrieve. Defaults to 'outputs'.\n   * @returns The value of the variable, or undefined if not found.\n   */\n  public getPrivateVar(key: string = 'outputs') {\n    return this.private?.ast.get(key);\n  }\n\n  /**\n   * Clears a variable from the private AST (Abstract Syntax Tree) by key.\n   *\n   * @param key - The key of the variable to clear. Defaults to 'outputs'.\n   * @returns The updated AST node.\n   */\n  public clearPrivateVar(key: string = 'outputs') {\n    return this.private?.ast.remove(key);\n  }\n\n  /**\n   * An array containing all scopes (public and private) of the node.\n   */\n  get allScopes(): FlowNodeScope[] {\n    const res = [];\n\n    if (this._public) {\n      res.push(this._public);\n    }\n    if (this._private) {\n      res.push(this._private);\n    }\n\n    return res;\n  }\n\n  getDefaultData() {\n    return {};\n  }\n\n  constructor(entity: FlowNodeEntity, readonly opts: Options) {\n    super(entity);\n\n    const { variableEngine } = opts || {};\n    this.variableEngine = variableEngine;\n    this._public = this.variableEngine.createScope(this.entity.id, {\n      node: this.entity,\n      type: FlowNodeScopeTypeEnum.public,\n    } as FlowNodeScopeMeta);\n    this.toDispose.push(this._public);\n  }\n\n  /**\n   * Initializes and returns the private scope for the node.\n   * If the private scope already exists, it returns the existing one.\n   * @returns The private scope of the node.\n   */\n  initPrivate(): FlowNodeScope {\n    if (!this._private) {\n      this._private = this.variableEngine.createScope(`${this.entity.id}_private`, {\n        node: this.entity,\n        type: FlowNodeScopeTypeEnum.private,\n      } as FlowNodeScopeMeta);\n\n      this.variableEngine.chain.refreshAllChange();\n\n      this.toDispose.push(this._private);\n    }\n    return this._private;\n  }\n\n  /**\n   * Find a variable field by key path in the public scope by scope chain.\n   * @param keyPath - The key path of the variable field.\n   * @returns The variable field, or undefined if not found.\n   */\n  getByKeyPath(keyPath: string[]): BaseVariableField | undefined {\n    return this.public.available.getByKeyPath(keyPath);\n  }\n\n  /**\n   * Find a variable field by key path in the private scope by scope chain.\n   * @param keyPath - The key path of the variable field.\n   * @returns The variable field, or undefined if not found.\n   */\n  getByKeyPathInPrivate(keyPath: string[]): BaseVariableField | undefined {\n    return this.private?.available.getByKeyPath(keyPath);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/index.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nexport { FlowNodeVariableData } from './flow-node-variable-data';\nexport { FreeLayoutScopeChain } from './chains/free-layout-scope-chain';\nexport { VariableChainConfig } from './variable-chain-config';\nexport { FixedLayoutScopeChain } from './chains/fixed-layout-scope-chain';\nexport {\n  type FlowNodeScopeMeta,\n  type FlowNodeScope,\n  FlowNodeScopeTypeEnum as FlowNodeScopeType,\n} from './types';\nexport { GlobalScope, bindGlobalScope } from './scopes/global-scope';\nexport { ScopeChainTransformService } from './services/scope-chain-transform-service';\nexport { getNodeScope, getNodePrivateScope } from './utils';\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/scopes/global-scope.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { injectable, interfaces } from 'inversify';\nimport { Scope, VariableEngine } from '@flowgram.ai/variable-core';\n\n/**\n * Global Scope stores all variables that are not scoped to any node.\n *\n * - Variables in Global Scope can be accessed by any node.\n * - Any other scope's variables can not be accessed by Global Scope.\n */\n@injectable()\nexport class GlobalScope extends Scope {\n  static readonly ID = Symbol('GlobalScope');\n\n  /**\n   * Check if the scope is Global Scope.\n   * @param scope\n   * @returns\n   */\n  static is(scope: Scope) {\n    return scope.id === GlobalScope.ID;\n  }\n}\n\nexport const bindGlobalScope = (bind: interfaces.Bind) => {\n  bind(GlobalScope).toDynamicValue((ctx) => {\n    const variableEngine = ctx.container.get(VariableEngine);\n    let scope = variableEngine.getScopeById(GlobalScope.ID) as GlobalScope;\n\n    if (!scope) {\n      scope = variableEngine.createScope(\n        GlobalScope.ID,\n        {},\n        { ScopeConstructor: GlobalScope }\n      ) as GlobalScope;\n      variableEngine.chain.refreshAllChange();\n    }\n\n    return scope;\n  });\n};\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/services/scope-chain-transform-service.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { inject, injectable, optional } from 'inversify';\nimport { Scope, VariableEngine } from '@flowgram.ai/variable-core';\nimport { FlowDocument } from '@flowgram.ai/document';\nimport { lazyInject } from '@flowgram.ai/core';\n\nimport { VariableChainConfig } from '../variable-chain-config';\nimport { FlowNodeScope } from '../types';\n\n/**\n * Context for scope transformers.\n */\nexport interface TransformerContext {\n  /**\n   * The current scope\n   */\n  scope: FlowNodeScope;\n  /**\n   * The flow document.\n   */\n  document: FlowDocument;\n  /**\n   * The variable engine.\n   */\n  variableEngine: VariableEngine;\n}\n\n/**\n * A function that transforms an array of scopes.\n * @param scopes The array of scopes to transform.\n * @param ctx The transformer context.\n * @returns The transformed array of scopes.\n */\nexport type IScopeTransformer = (scopes: Scope[], ctx: TransformerContext) => Scope[];\n\nconst passthrough: IScopeTransformer = (scopes, ctx) => scopes;\n\n/**\n * A service for transforming scope chains.\n */\n@injectable()\nexport class ScopeChainTransformService {\n  protected transformerMap: Map<\n    string,\n    { transformDeps: IScopeTransformer; transformCovers: IScopeTransformer }\n  > = new Map();\n\n  @lazyInject(FlowDocument) document: FlowDocument;\n\n  @lazyInject(VariableEngine) variableEngine: VariableEngine;\n\n  constructor(\n    @optional()\n    @inject(VariableChainConfig)\n    protected configs?: VariableChainConfig\n  ) {\n    if (this.configs?.transformDeps || this.configs?.transformCovers) {\n      this.transformerMap.set('VARIABLE_LAYOUT_CONFIG', {\n        transformDeps: this.configs.transformDeps || passthrough,\n        transformCovers: this.configs.transformCovers || passthrough,\n      });\n    }\n  }\n\n  /**\n   * check if transformer registered\n   * @param transformerId used to identify transformer, prevent duplicated\n   * @returns\n   */\n  hasTransformer(transformerId: string) {\n    return this.transformerMap.has(transformerId);\n  }\n\n  /**\n   * register new transform function\n   * @param transformerId used to identify transformer, prevent duplicated transformer\n   * @param transformer The transformer to register.\n   */\n  registerTransformer(\n    transformerId: string,\n    transformer: {\n      transformDeps: IScopeTransformer;\n      transformCovers: IScopeTransformer;\n    }\n  ) {\n    this.transformerMap.set(transformerId, transformer);\n  }\n\n  /**\n   * Transforms the dependency scopes.\n   * @param scopes The array of scopes to transform.\n   * @param param1 The context for the transformation.\n   * @returns The transformed array of scopes.\n   */\n  transformDeps(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {\n    return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {\n      if (!transformer.transformDeps) {\n        return scopes;\n      }\n\n      scopes = transformer.transformDeps(scopes, {\n        scope,\n        document: this.document,\n        variableEngine: this.variableEngine,\n      });\n      return scopes;\n    }, scopes);\n  }\n\n  /**\n   * Transforms the cover scopes.\n   * @param scopes The array of scopes to transform.\n   * @param param1 The context for the transformation.\n   * @returns The transformed array of scopes.\n   */\n  transformCovers(scopes: Scope[], { scope }: { scope: Scope }): Scope[] {\n    return Array.from(this.transformerMap.values()).reduce((scopes, transformer) => {\n      if (!transformer.transformCovers) {\n        return scopes;\n      }\n\n      scopes = transformer.transformCovers(scopes, {\n        scope,\n        document: this.document,\n        variableEngine: this.variableEngine,\n      });\n      return scopes;\n    }, scopes);\n  }\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/types.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { Scope } from '@flowgram.ai/variable-core';\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\n/**\n * Enum for flow node scope types.\n */\nexport enum FlowNodeScopeTypeEnum {\n  /**\n   * Public scope.\n   */\n  public = 'public',\n  /**\n   * Private scope.\n   */\n  private = 'private',\n}\n\n/**\n * Metadata for a flow node scope.\n */\nexport interface FlowNodeScopeMeta {\n  /**\n   * The flow node entity associated with the scope.\n   */\n  node?: FlowNodeEntity;\n  /**\n   * The type of the scope.\n   */\n  type?: FlowNodeScopeTypeEnum;\n}\n\n/**\n * Represents a virtual node in the scope chain.\n */\nexport interface ScopeVirtualNode {\n  /**\n   * The ID of the virtual node.\n   */\n  id: string;\n  /**\n   * The type of the flow node.\n   */\n  flowNodeType: 'virtualNode';\n}\n\n/**\n * Represents a node in the scope chain, which can be either a flow node entity or a virtual node.\n */\nexport type ScopeChainNode = FlowNodeEntity | ScopeVirtualNode;\n\n/**\n * Represents a scope associated with a flow node.\n */\nexport interface FlowNodeScope extends Scope<FlowNodeScopeMeta> {}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/utils.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { FlowNodeVariableData } from './flow-node-variable-data';\n\n/**\n * Use `node.scope` instead.\n * @deprecated\n * @param node The flow node entity.\n * @returns The public scope of the node.\n */\nexport function getNodeScope(node: FlowNodeEntity) {\n  return node.getData(FlowNodeVariableData).public;\n}\n\n/**\n * Use `node.privateScope` instead.\n * @deprecated\n * @param node The flow node entity.\n * @returns The private scope of the node.\n */\nexport function getNodePrivateScope(node: FlowNodeEntity) {\n  return node.getData(FlowNodeVariableData).initPrivate();\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/src/variable-chain-config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport { FlowNodeEntity } from '@flowgram.ai/document';\n\nimport { type ScopeChainNode } from './types';\nimport { IScopeTransformer } from './services/scope-chain-transform-service';\n\n/**\n * Configuration for the variable chain.\n */\nexport interface VariableChainConfig {\n  /**\n   * The output variables of a node's children cannot be accessed by subsequent nodes.\n   *\n   * @param node\n   * @returns\n   */\n  isNodeChildrenPrivate?: (node: ScopeChainNode) => boolean;\n\n  /**\n   * For fixed layout scenarios: there are a large number of useless nodes between parent and child (such as inlineBlocks, etc., which need to be configured to be skipped)\n   * For free canvas scenarios: in some scenarios, the parent-child relationship between nodes is expressed through connections or other interactive forms, which needs to be configurable\n   */\n  getNodeChildren?: (node: FlowNodeEntity) => FlowNodeEntity[];\n  getNodeParent?: (node: FlowNodeEntity) => FlowNodeEntity | undefined;\n\n  /**\n   * Fine-tune the dependency scope.\n   */\n  transformDeps?: IScopeTransformer;\n\n  /**\n   * Fine-tune the cover scope.\n   */\n  transformCovers?: IScopeTransformer;\n}\n\nexport const VariableChainConfig = Symbol('VariableChainConfig');\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/tsconfig.json",
    "content": "{\n  \"extends\": \"@flowgram.ai/ts-config/tsconfig.flow.path.json\",\n  \"compilerOptions\": {\n  },\n  \"include\": [\"src\", \"__tests__\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/vitest.config.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nconst path = require('path');\nimport { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  build: {\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n  },\n  test: {\n    globals: true,\n    mockReset: false,\n    environment: 'jsdom',\n    setupFiles: [path.resolve(__dirname, './vitest.setup.ts')],\n    include: ['**/?(*.){test,spec}.?(c|m)[jt]s?(x)'],\n    exclude: [\n      '**/__mocks__/**',\n      '**/node_modules/**',\n      '**/dist/**',\n      '**/lib/**', // lib 编译结果忽略掉\n      '**/cypress/**',\n      '**/.{idea,git,cache,output,temp}/**',\n      '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',\n    ],\n  },\n});\n"
  },
  {
    "path": "packages/variable-engine/variable-layout/vitest.setup.ts",
    "content": "/**\n * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates\n * SPDX-License-Identifier: MIT\n */\n\nimport 'reflect-metadata';\n"
  },
  {
    "path": "rush.json",
    "content": "/**\n * This is the main configuration file for Rush.\n * For full documentation, please see https://rushjs.io\n */\n{\n    \"$schema\": \"https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json\",\n    /**\n     * (Required) This specifies the version of the Rush engine to be used in this repo.\n     * Rush's \"version selector\" feature ensures that the globally installed tool will\n     * behave like this release, regardless of which version is installed globally.\n     *\n     * The common/scripts/install-run-rush.js automation script also uses this version.\n     *\n     * NOTE: If you upgrade to a new major version of Rush, you should replace the \"v5\"\n     * path segment in the \"$schema\" field for all your Rush config files.  This will ensure\n     * correct error-underlining and tab-completion for editors such as VS Code.\n     */\n    \"rushVersion\": \"5.150.0\",\n    /**\n     * The next field selects which package manager should be installed and determines its version.\n     * Rush installs its own local copy of the package manager to ensure that your build process\n     * is fully isolated from whatever tools are present in the local environment.\n     *\n     * Specify one of: \"pnpmVersion\", \"npmVersion\", or \"yarnVersion\".  See the Rush documentation\n     * for details about these alternatives.\n     */\n    \"pnpmVersion\": \"10.6.5\",\n    // \"npmVersion\": \"6.14.15\",\n    // \"yarnVersion\": \"1.9.4\",\n    /**\n     * Older releases of the Node.js engine may be missing features required by your system.\n     * Other releases may have bugs.  In particular, the \"latest\" version will not be a\n     * Long Term Support (LTS) version and is likely to have regressions.\n     *\n     * Specify a SemVer range to ensure developers use a Node.js version that is appropriate\n     * for your repo.\n     *\n     * LTS schedule: https://nodejs.org/en/about/releases/\n     * LTS versions: https://nodejs.org/en/download/releases/\n     */\n    \"nodeSupportedVersionRange\": \">=18.20.3 <19.0.0 || >=20.14.0 <23.0.0\",\n    /**\n     * If the version check above fails, Rush will display a message showing the current\n     * node version and the supported version range. You can use this setting to provide\n     * additional instructions that will display below the warning, if there's a specific\n     * tool or script you'd like the user to use to get in line with the expected version.\n     */\n    // \"nodeSupportedVersionInstructions\": \"Run 'nvs use' to switch to the expected node version.\",\n    /**\n     * Odd-numbered major versions of Node.js are experimental.  Even-numbered releases\n     * spend six months in a stabilization period before the first Long Term Support (LTS) version.\n     * For example, 8.9.0 was the first LTS version of Node.js 8.  Pre-LTS versions are not recommended\n     * for production usage because they frequently have bugs.  They may cause Rush itself\n     * to malfunction.\n     *\n     * Rush normally prints a warning if it detects a pre-LTS Node.js version.  If you are testing\n     * pre-LTS versions in preparation for supporting the first LTS version, you can use this setting\n     * to disable Rush's warning.\n     */\n    // \"suppressNodeLtsWarning\": false,\n    /**\n     * Rush normally prints a warning if it detects that the current version is not one published to the\n     * public npmjs.org registry. If you need to block calls to the npm registry, you can use this setting to disable\n     * Rush's check.\n     */\n    // \"suppressRushIsPublicVersionCheck\": false,\n    /**\n     * Large monorepos can become intimidating for newcomers if project folder paths don't follow\n     * a consistent and recognizable pattern.  When the system allows nested folder trees,\n     * we've found that teams will often use subfolders to create islands that isolate\n     * their work from others (\"shipping the org\").  This hinders collaboration and code sharing.\n     *\n     * The Rush developers recommend a \"category folder\" model, where buildable project folders\n     * must always be exactly two levels below the repo root.  The parent folder acts as the category.\n     * This provides a basic facility for grouping related projects (e.g. \"apps\", \"libraries\",\n     * \"tools\", \"prototypes\") while still encouraging teams to organize their projects into\n     * a unified taxonomy.  Limiting to 2 levels seems very restrictive at first, but if you have\n     * 20 categories and 20 projects in each category, this scheme can easily accommodate hundreds\n     * of projects.  In practice, you will find that the folder hierarchy needs to be rebalanced\n     * occasionally, but if that's painful, it's a warning sign that your development style may\n     * discourage refactoring.  Reorganizing the categories should be an enlightening discussion\n     * that brings people together, and maybe also identifies poor coding practices (e.g. file\n     * references that reach into other project's folders without using Node.js module resolution).\n     *\n     * The defaults are projectFolderMinDepth=1 and projectFolderMaxDepth=2.\n     *\n     * To remove these restrictions, you could set projectFolderMinDepth=1\n     * and set projectFolderMaxDepth to a large number.\n     */\n    \"projectFolderMinDepth\": 2,\n    \"projectFolderMaxDepth\": 4,\n    /**\n     * Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early\n     * days there was no standard and hardly any enforcement.  A few large legacy projects are still using\n     * nonstandard package names, and private registries sometimes allow it.  Set \"allowMostlyStandardPackageNames\"\n     * to true to relax Rush's enforcement of package names.  This allows upper case letters and in the future may\n     * relax other rules, however we want to minimize these exceptions.  Many popular tools use certain punctuation\n     * characters as delimiters, based on the assumption that they will never appear in a package name; thus if we relax\n     * the rules too much it is likely to cause very confusing malfunctions.\n     *\n     * The default value is false.\n     */\n    // \"allowMostlyStandardPackageNames\": true,\n    /**\n     * This feature helps you to review and approve new packages before they are introduced\n     * to your monorepo.  For example, you may be concerned about licensing, code quality,\n     * performance, or simply accumulating too many libraries with overlapping functionality.\n     * The approvals are tracked in two config files \"browser-approved-packages.json\"\n     * and \"nonbrowser-approved-packages.json\".  See the Rush documentation for details.\n     */\n    // \"approvedPackagesPolicy\": {\n    //   /**\n    //    * The review categories allow you to say for example \"This library is approved for usage\n    //    * in prototypes, but not in production code.\"\n    //    *\n    //    * Each project can be associated with one review category, by assigning the \"reviewCategory\" field\n    //    * in the \"projects\" section of rush.json.  The approval is then recorded in the files\n    //    * \"common/config/rush/browser-approved-packages.json\" and \"nonbrowser-approved-packages.json\"\n    //    * which are automatically generated during \"rush update\".\n    //    *\n    //    * Designate categories with whatever granularity is appropriate for your review process,\n    //    * or you could just have a single category called \"default\".\n    //    */\n    //   \"reviewCategories\": [\n    //     // Some example categories:\n    //     \"production\", // projects that ship to production\n    //     \"tools\",      // non-shipping projects that are part of the developer toolchain\n    //     \"prototypes\"  // experiments that should mostly be ignored by the review process\n    //   ],\n    //\n    //   /**\n    //    * A list of NPM package scopes that will be excluded from review.\n    //    * We recommend to exclude TypeScript typings (the \"@types\" scope), because\n    //    * if the underlying package was already approved, this would imply that the typings\n    //    * are also approved.\n    //    */\n    //   // \"ignoredNpmScopes\": [\"@types\"]\n    // },\n    /**\n     * If you use Git as your version control system, this section has some additional\n     * optional features you can use.\n     */\n    \"gitPolicy\": {\n        /**\n         * Work at a big company?  Tired of finding Git commits at work with unprofessional Git\n         * emails such as \"beer-lover@my-college.edu\"?  Rush can validate people's Git email address\n         * before they get started.\n         *\n         * Define a list of regular expressions describing allowable e-mail patterns for Git commits.\n         * They are case-insensitive anchored JavaScript RegExps.  Example: \".*@example\\.com\"\n         *\n         * IMPORTANT: Because these are regular expressions encoded as JSON string literals,\n         * RegExp escapes need two backslashes, and ordinary periods should be \"\\\\.\".\n         */\n        // \"allowedEmailRegExps\": [\n        //   \"[^@]+@users\\\\.noreply\\\\.github\\\\.com\",\n        //   \"rush-bot@example\\\\.org\"\n        // ],\n        /**\n         * When Rush reports that the address is malformed, the notice can include an example\n         * of a recommended email.  Make sure it conforms to one of the allowedEmailRegExps\n         * expressions.\n         */\n        // \"sampleEmail\": \"example@users.noreply.github.com\",\n        /**\n         * The commit message to use when committing changes during 'rush publish'.\n         *\n         * For example, if you want to prevent these commits from triggering a CI build,\n         * you might configure your system's trigger to look for a special string such as \"[skip-ci]\"\n         * in the commit message, and then customize Rush's message to contain that string.\n         */\n        // \"versionBumpCommitMessage\": \"Bump versions [skip ci]\",\n        /**\n         * The commit message to use when committing changes during 'rush version'.\n         *\n         * For example, if you want to prevent these commits from triggering a CI build,\n         * you might configure your system's trigger to look for a special string such as \"[skip-ci]\"\n         * in the commit message, and then customize Rush's message to contain that string.\n         */\n        // \"changeLogUpdateCommitMessage\": \"Update changelogs [skip ci]\",\n        /**\n         * The commit message to use when committing changefiles during 'rush change --commit'\n         *\n         * If no commit message is set it will default to 'Rush change'\n         */\n        // \"changefilesCommitMessage\": \"Rush change\"\n    },\n    \"repository\": {\n        /**\n         * The URL of this Git repository, used by \"rush change\" to determine the base branch for your PR.\n         *\n         * The \"rush change\" command needs to determine which files are affected by your PR diff.\n         * If you merged or cherry-picked commits from the main branch into your PR branch, those commits\n         * should be excluded from this diff (since they belong to some other PR).  In order to do that,\n         * Rush needs to know where to find the base branch for your PR.  This information cannot be\n         * determined from Git alone, since the \"pull request\" feature is not a Git concept.  Ideally\n         * Rush would use a vendor-specific protocol to query the information from GitHub, Azure DevOps, etc.\n         * But to keep things simple, \"rush change\" simply assumes that your PR is against the \"main\" branch\n         * of the Git remote indicated by the repository.url setting in rush.json.  If you are working in\n         * a GitHub \"fork\" of the real repo, this setting will be different from the repository URL of your\n         * your PR branch, and in this situation \"rush change\" will also automatically invoke \"git fetch\"\n         * to retrieve the latest activity for the remote main branch.\n         */\n        // \"url\": \"https://github.com/microsoft/rush-example\",\n        /**\n         * The default branch name. This tells \"rush change\" which remote branch to compare against.\n         * The default value is \"main\"\n         */\n        // \"defaultBranch\": \"main\",\n        /**\n         * The default remote. This tells \"rush change\" which remote to compare against if the remote URL is\n         * not set or if a remote matching the provided remote URL is not found.\n         */\n        // \"defaultRemote\": \"origin\"\n    },\n    /**\n     * Event hooks are customized script actions that Rush executes when specific events occur\n     */\n    \"eventHooks\": {\n        /**\n         * A list of shell commands to run before \"rush install\" or \"rush update\" starts installation\n         */\n        \"preRushInstall\": [\n            // \"common/scripts/pre-rush-install.js\"\n        ],\n        /**\n         * A list of shell commands to run after \"rush install\" or \"rush update\" finishes installation\n         */\n        \"postRushInstall\": [],\n        /**\n         * A list of shell commands to run before \"rush build\" or \"rush rebuild\" starts building\n         */\n        \"preRushBuild\": [],\n        /**\n         * A list of shell commands to run after \"rush build\" or \"rush rebuild\" finishes building\n         */\n        \"postRushBuild\": [],\n        /**\n         * A list of shell commands to run before the \"rushx\" command starts\n         */\n        \"preRushx\": [],\n        /**\n         * A list of shell commands to run after the \"rushx\" command finishes\n         */\n        \"postRushx\": []\n    },\n    /**\n     * Installation variants allow you to maintain a parallel set of configuration files that can be\n     * used to build the entire monorepo with an alternate set of dependencies.  For example, suppose\n     * you upgrade all your projects to use a new release of an important framework, but during a transition period\n     * you intend to maintain compatibility with the old release.  In this situation, you probably want your\n     * CI validation to build the entire repo twice: once with the old release, and once with the new release.\n     *\n     * Rush \"installation variants\" correspond to sets of config files located under this folder:\n     *\n     *   common/config/rush/variants/<variant_name>\n     *\n     * The variant folder can contain an alternate common-versions.json file.  Its \"preferredVersions\" field can be used\n     * to select older versions of dependencies (within a loose SemVer range specified in your package.json files).\n     * To install a variant, run \"rush install --variant <variant_name>\".\n     *\n     * For more details and instructions, see this article:  https://rushjs.io/pages/advanced/installation_variants/\n     */\n    \"variants\": [\n        // {\n        //   /**\n        //    * The folder name for this variant.\n        //    */\n        //   \"variantName\": \"old-sdk\",\n        //\n        //   /**\n        //    * An informative description\n        //    */\n        //   \"description\": \"Build this repo using the previous release of the SDK\"\n        // }\n    ],\n    /**\n     * Rush can collect anonymous telemetry about everyday developer activity such as\n     * success/failure of installs, builds, and other operations.  You can use this to identify\n     * problems with your toolchain or Rush itself.  THIS TELEMETRY IS NOT SHARED WITH MICROSOFT.\n     * It is written into JSON files in the common/temp folder.  It's up to you to write scripts\n     * that read these JSON files and do something with them.  These scripts are typically registered\n     * in the \"eventHooks\" section.\n     */\n    // \"telemetryEnabled\": false,\n    /**\n     * Allows creation of hotfix changes. This feature is experimental so it is disabled by default.\n     * If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type\n     * will be used when publishing subsequent changes from the monorepo.\n     */\n    // \"hotfixChangeEnabled\": false,\n    /**\n     * This is an optional, but recommended, list of allowed tags that can be applied to Rush projects\n     * using the \"tags\" setting in this file.  This list is useful for preventing mistakes such as misspelling,\n     * and it also provides a centralized place to document your tags.  If \"allowedProjectTags\" list is\n     * not specified, then any valid tag is allowed.  A tag name must be one or more words\n     * separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits,\n     * \".\", and \"@\" characters.\n     */\n    // \"allowedProjectTags\": [ \"tools\", \"frontend-team\", \"1.0.0-release\" ],\n    /**\n     * (Required) This is the inventory of projects to be managed by Rush.\n     *\n     * Rush does not automatically scan for projects using wildcards, for a few reasons:\n     * 1. Depth-first scans are expensive, particularly when tools need to repeatedly collect the list.\n     * 2. On a caching CI machine, scans can accidentally pick up files left behind from a previous build.\n     * 3. It's useful to have a centralized inventory of all projects and their important metadata.\n     */\n    \"projects\": [\n        // {\n        //   /**\n        //    * The NPM package name of the project (must match package.json)\n        //    */\n        //   \"packageName\": \"my-app\",\n        //\n        //   /**\n        //    * The path to the project folder, relative to the rush.json config file.\n        //    */\n        //   \"projectFolder\": \"apps/my-app\",\n        //\n        //   /**\n        //    * This field is only used if \"subspacesEnabled\" is true in subspaces.json.\n        //    * It specifies the subspace that this project belongs to.  If omitted, then the\n        //    * project belongs to the \"default\" subspace.\n        //    */\n        //   // \"subspaceName\": \"my-subspace\",\n        //\n        //   /**\n        //    * An optional category for usage in the \"browser-approved-packages.json\"\n        //    * and \"nonbrowser-approved-packages.json\" files.  The value must be one of the\n        //    * strings from the \"reviewCategories\" defined above.\n        //    */\n        //   \"reviewCategory\": \"production\",\n        //\n        //   /**\n        //    * A list of Rush project names that are to be installed from NPM\n        //    * instead of linking to the local project.\n        //    *\n        //    * If a project's package.json specifies a dependency that is another Rush project\n        //    * in the monorepo workspace, normally Rush will locally link its folder instead of\n        //    * installing from NPM.  If you are using PNPM workspaces, this is indicated by\n        //    * a SemVer range such as \"workspace:^1.2.3\".  To prevent mistakes, Rush reports\n        //    * an error if the \"workspace:\" protocol is missing.\n        //    *\n        //    * Locally linking ensures that regressions are caught as early as possible and is\n        //    * a key benefit of monorepos.  However there are occasional situations where\n        //    * installing from NPM is needed.  A classic example is a cyclic dependency.\n        //    * Imagine three Rush projects: \"my-toolchain\" depends on \"my-tester\", which depends\n        //    * on \"my-library\".  Suppose that we add \"my-toolchain\" to the \"devDependencies\"\n        //    * of \"my-library\" so it can be built by our toolchain.  This cycle creates\n        //    * a problem -- Rush can't build a project using a not-yet-built dependency.\n        //    * We can solve it by adding \"my-toolchain\" to the \"decoupledLocalDependencies\"\n        //    * of \"my-library\", so it builds using the last published release.  Choose carefully\n        //    * which package to decouple; some choices are much easier to manage than others.\n        //    *\n        //    * (In older Rush releases, this setting was called \"cyclicDependencyProjects\".)\n        //    */\n        //   \"decoupledLocalDependencies\": [\n        //     // \"my-toolchain\"\n        //   ],\n        //\n        //   /**\n        //    * If true, then this project will be ignored by the \"rush check\" command.\n        //    * The default value is false.\n        //    */\n        //   // \"skipRushCheck\": false,\n        //\n        //   /**\n        //    * A flag indicating that changes to this project will be published to npm, which affects\n        //    * the Rush change and publish workflows. The default value is false.\n        //    * NOTE: \"versionPolicyName\" and \"shouldPublish\" are alternatives; you cannot specify them both.\n        //    */\n        //   // \"shouldPublish\": false,\n        //\n        //   /**\n        //    * Facilitates postprocessing of a project's files prior to publishing.\n        //    *\n        //    * If specified, the \"publishFolder\" is the relative path to a subfolder of the project folder.\n        //    * The \"rush publish\" command will publish the subfolder instead of the project folder.  The subfolder\n        //    * must contain its own package.json file, which is typically a build output.\n        //    */\n        //   // \"publishFolder\": \"temp/publish\",\n        //\n        //   /**\n        //    * An optional version policy associated with the project.  Version policies are defined\n        //    * in \"version-policies.json\" file.  See the \"rush publish\" documentation for more info.\n        //    * NOTE: \"versionPolicyName\" and \"shouldPublish\" are alternatives; you cannot specify them both.\n        //    */\n        //   // \"versionPolicyName\": \"\",\n        //\n        //   /**\n        //    * An optional set of custom tags that can be used to select this project.  For example,\n        //    * adding \"my-custom-tag\" will allow this project to be selected by the\n        //    * command \"rush list --only tag:my-custom-tag\".  The tag name must be one or more words\n        //    * separated by hyphens or slashes, where a word may contain lowercase ASCII letters, digits,\n        //    * \".\", and \"@\" characters.\n        //    */\n        //   // \"tags\": [ \"1.0.0-release\", \"frontend-team\" ]\n        // },\n        //\n        // {\n        //   \"packageName\": \"my-controls\",\n        //   \"projectFolder\": \"libraries/my-controls\",\n        //   \"reviewCategory\": \"production\",\n        //   \"tags\": [ \"frontend-team\" ]\n        // },\n        //\n        {\n            \"packageName\": \"@flowgram.ai/e2e-fixed-layout\",\n            \"projectFolder\": \"e2e/fixed-layout\",\n            \"tags\": [\n                \"e2e\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/e2e-free-layout\",\n            \"projectFolder\": \"e2e/free-layout\",\n            \"tags\": [\n                \"e2e\"\n            ]\n        },\n        // eslint 通用配置\n        {\n            \"packageName\": \"@flowgram.ai/eslint-config\",\n            \"projectFolder\": \"config/eslint-config\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"config\"\n            ]\n        },\n        // ts 通用配置\n        {\n            \"packageName\": \"@flowgram.ai/ts-config\",\n            \"projectFolder\": \"config/ts-config\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"config\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/create-app\",\n            \"projectFolder\": \"apps/create-app\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"cli\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/cli\",\n            \"projectFolder\": \"apps/cli\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"cli\"\n            ]\n        },\n        // 官网\n        {\n            \"packageName\": \"@flowgram.ai/docs\",\n            \"projectFolder\": \"apps/docs\",\n            \"tags\": [\n                \"docs\"\n            ]\n        },\n        // demos\n        {\n            \"packageName\": \"@flowgram.ai/demo-fixed-layout\",\n            \"projectFolder\": \"apps/demo-fixed-layout\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ],\n            \"versionPolicyName\": \"appPolicy\"\n        },\n        {\n            \"packageName\": \"@flowgram.ai/utils\",\n            \"projectFolder\": \"packages/common/utils\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/core\",\n            \"projectFolder\": \"packages/canvas-engine/core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/document\",\n            \"projectFolder\": \"packages/canvas-engine/document\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/renderer\",\n            \"projectFolder\": \"packages/canvas-engine/renderer\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/reactive\",\n            \"projectFolder\": \"packages/common/reactive\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/background-plugin\",\n            \"projectFolder\": \"packages/plugins/background-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/test-run-plugin\",\n            \"projectFolder\": \"packages/plugins/test-run-plugin\",\n            \"versionPolicyName\": \"publishPolicy\"\n        },\n        {\n            \"packageName\": \"@flowgram.ai/fixed-layout-core\",\n            \"projectFolder\": \"packages/canvas-engine/fixed-layout-core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-layout-core\",\n            \"projectFolder\": \"packages/canvas-engine/free-layout-core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/editor\",\n            \"projectFolder\": \"packages/client/editor\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/fixed-drag-plugin\",\n            \"projectFolder\": \"packages/plugins/fixed-drag-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/fixed-history-plugin\",\n            \"projectFolder\": \"packages/plugins/fixed-history-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/fixed-layout-editor\",\n            \"projectFolder\": \"packages/client/fixed-layout-editor\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/form\",\n            \"projectFolder\": \"packages/node-engine/form\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/form-core\",\n            \"projectFolder\": \"packages/node-engine/form-core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-history-plugin\",\n            \"projectFolder\": \"packages/plugins/free-history-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-hover-plugin\",\n            \"projectFolder\": \"packages/plugins/free-hover-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-layout-editor\",\n            \"projectFolder\": \"packages/client/free-layout-editor\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-lines-plugin\",\n            \"projectFolder\": \"packages/plugins/free-lines-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-node-panel-plugin\",\n            \"projectFolder\": \"packages/plugins/free-node-panel-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-snap-plugin\",\n            \"projectFolder\": \"packages/plugins/free-snap-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-stack-plugin\",\n            \"projectFolder\": \"packages/plugins/free-stack-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-container-plugin\",\n            \"projectFolder\": \"packages/plugins/free-container-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-group-plugin\",\n            \"projectFolder\": \"packages/plugins/free-group-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/group-plugin\",\n            \"projectFolder\": \"packages/plugins/group-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/history-node-plugin\",\n            \"projectFolder\": \"packages/plugins/history-node-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/minimap-plugin\",\n            \"projectFolder\": \"packages/plugins/minimap-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/export-plugin\",\n            \"projectFolder\": \"packages/plugins/export-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/node\",\n            \"projectFolder\": \"packages/node-engine/node\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/node-core-plugin\",\n            \"projectFolder\": \"packages/plugins/node-core-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/node-variable-plugin\",\n            \"projectFolder\": \"packages/plugins/node-variable-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/playground-react\",\n            \"projectFolder\": \"packages/client/playground-react\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/redux-devtool-plugin\",\n            \"projectFolder\": \"packages/plugins/redux-devtool-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/select-box-plugin\",\n            \"projectFolder\": \"packages/plugins/select-box-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/shortcuts-plugin\",\n            \"projectFolder\": \"packages/plugins/shortcuts-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/variable-core\",\n            \"projectFolder\": \"packages/variable-engine/variable-core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/variable-layout\",\n            \"projectFolder\": \"packages/variable-engine/variable-layout\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/json-schema\",\n            \"projectFolder\": \"packages/variable-engine/json-schema\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/variable-plugin\",\n            \"projectFolder\": \"packages/plugins/variable-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/command\",\n            \"projectFolder\": \"packages/common/command\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/history\",\n            \"projectFolder\": \"packages/common/history\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/history-storage\",\n            \"projectFolder\": \"packages/common/history-storage\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-2\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/materials-plugin\",\n            \"projectFolder\": \"packages/plugins/materials-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/fixed-semi-materials\",\n            \"projectFolder\": \"packages/materials/fixed-semi-materials\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/form-materials\",\n            \"projectFolder\": \"packages/materials/form-materials\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/panel-manager-plugin\",\n            \"projectFolder\": \"packages/plugins/panel-manager-plugin\",\n            \"versionPolicyName\": \"publishPolicy\"\n        },\n        {\n            \"packageName\": \"@flowgram.ai/form-antd-materials\",\n            \"projectFolder\": \"packages/materials/form-antd-materials\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/type-editor\",\n            \"projectFolder\": \"packages/materials/type-editor\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/coze-editor\",\n            \"projectFolder\": \"packages/materials/coze-editor\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/free-auto-layout-plugin\",\n            \"projectFolder\": \"packages/plugins/free-auto-layout-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/i18n-plugin\",\n            \"projectFolder\": \"packages/plugins/i18n-plugin\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/i18n\",\n            \"projectFolder\": \"packages/common/i18n\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-free-layout\",\n            \"projectFolder\": \"apps/demo-free-layout\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ],\n            \"versionPolicyName\": \"appPolicy\"\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-fixed-layout-simple\",\n            \"projectFolder\": \"apps/demo-fixed-layout-simple\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-free-layout-simple\",\n            \"projectFolder\": \"apps/demo-free-layout-simple\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-node-form\",\n            \"projectFolder\": \"apps/demo-node-form\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-materials\",\n            \"projectFolder\": \"apps/demo-materials\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-nextjs\",\n            \"projectFolder\": \"apps/demo-nextjs\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-nextjs-antd\",\n            \"projectFolder\": \"apps/demo-nextjs-antd\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-react-16\",\n            \"projectFolder\": \"apps/demo-react-16\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-vite\",\n            \"projectFolder\": \"apps/demo-vite\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/demo-playground\",\n            \"projectFolder\": \"apps/demo-playground\",\n            \"versionPolicyName\": \"appPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\",\n                \"demo\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/runtime-nodejs\",\n            \"projectFolder\": \"packages/runtime/nodejs\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/runtime-js\",\n            \"projectFolder\": \"packages/runtime/js-core\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        },\n        {\n            \"packageName\": \"@flowgram.ai/runtime-interface\",\n            \"projectFolder\": \"packages/runtime/interface\",\n            \"versionPolicyName\": \"publishPolicy\",\n            \"tags\": [\n                \"level-1\",\n                \"team-flow\"\n            ]\n        }\n    ]\n}\n"
  }
]